diff --git a/custom_nodes/Cheny_custom_nodes/yolov8_person_detect.py b/custom_nodes/Cheny_custom_nodes/yolov8_person_detect.py deleted file mode 100644 index 8648fc7b29216a5efe3b34b5c6b1540297efab7a..0000000000000000000000000000000000000000 --- a/custom_nodes/Cheny_custom_nodes/yolov8_person_detect.py +++ /dev/null @@ -1,188 +0,0 @@ -import copy -import os - -import torch -from PIL import Image -import glob -import numpy as np -from ultralytics import YOLO -import folder_paths - -models_dirs=os.path.join(folder_paths.models_dir,'yolo') -def get_files(models_dir,file_exp_list): - file_list=[] - for exp in file_exp_list: - # extend可将可迭代对象中的元素,逐个添加进当前列表 - file_list.extend(glob.glob(os.path.join(models_dir,'*'+exp))) #glob就是模式匹配文件,支持通配符,每个文件返回一个列表,所以用extend - file_dict={} - for i in range(len(file_list)): - _,filename=os.path.split(file_list[i]) # 将文件路径拆成("C:/models/yolo", "model1.pt") - # 创建文件名到文件路径的映射 - file_dict[filename]=file_list[i] - - return file_dict - -def tensor2pil(image): - if isinstance(image,Image.Image): - return image - else: - if len(image.shape)<3: - image=image.unsqueeze(0) - return Image.fromarray((image[0].cpu().numpy()*255).astype(np.uint8)) - -def pil2tensor(image): - new_image=image.convert('RGB') - image_array=np.array(new_image).astype(np.float32)/255.0 - image_tensor=torch.tensor(image_array) - return image_tensor - -def mask2tensor(mask): - mask=mask.convert('L') - mask_array=np.array(mask).astype(np.float32)/255.0 - mask_tensor=torch.tensor(mask_array) - return mask_tensor - -class Yolov8_person_detect: - CATEGORY="My Nodes/yolov8 person detect" - RETURN_TYPES=("MASK",) - RETURN_NAMES=("back_mask",) - FUNCTION="yolov8_person_detect" - - @classmethod - def INPUT_TYPES(cls): - model_exp=["seg.pt"] - FILES_DICT=get_files(models_dirs,model_exp) - FILE_LIST=list(FILES_DICT.keys()) - - return{ - "required":{ - "back_image":("IMAGE",), - "mask":("MASK",), - "yolo_model":(FILE_LIST,), - }, - "optional":{ - "true_rate":("FLOAT",{ - "default":0.85, - "min":0.01, - "max":1.0, - "step":0.01, - "display":"number" - }), - "img_ratio":("FLOAT",{ - "default":float(2/3), - "min":0.01, - "max":1.0, - "step":0.01, - "display":"number" - }), - "x_ratio":("FLOAT",{ - "default":float(0.5), - "min":0.01, - "max":1.0, - "step":0.01, - "display":"number" - }), - "y_ratio":("FLOAT",{ - "default":float(1/10), - "min":0.01, - "max":1.0, - "step":0.01, - "display":"number" - }) - } - } - - def yolov8_person_detect(self,mask,back_image,yolo_model,true_rate,img_ratio,x_ratio,y_ratio): - back_image=tensor2pil(back_image) - - yolo_model=YOLO(os.path.join(models_dirs,yolo_model)) - result = yolo_model( - back_image, - retina_masks=True, - classes=[0], - conf=true_rate, - verbose=False - )[0] - - if result.masks is not None and len(result.masks)>0: - if result.boxes is not None and len(result.boxes)>0: - if result.boxes.conf[0]>=true_rate: - masks_data=result.masks.data - n_mask=masks_data[0] - n_mask=n_mask.unsqueeze(0) - return(n_mask,) - - # 直接生成mask,按背景 2/3 - # bd_w,bd_h=back_image.size - # n_mask=Image.new('L',(bd_w,bd_h),"black") - # target=img_ratio*max(bd_h,bd_w) - - # new_w,new_h=int(img_ratio*bd_w),int(img_ratio*bd_h) - - # paste_mask=Image.new('L',(new_w,new_h),"white") - - - # x=int(bd_w-new_w) - # y=int(bd_h-new_h) - - # x_trap_padding=int(x*x_ratio) - # x=x-x_trap_padding - - # y_trap_padding=int(y*y_ratio) - # y=y-y_trap_padding - - # n_mask.paste(paste_mask,(x,y)) - # n_mask=mask2tensor(n_mask) - - # if len(n_mask.shape)<3: - # n_mask=n_mask.unsqueeze(0) - # print("Yolov8_person_detect done") - # return (n_mask,) - - - # 利用原图人物mask,生成新mask - mask=tensor2pil(mask).convert('L') - - bd_w,bd_h=back_image.size - - n_mask=Image.new('L',(bd_w,bd_h),"black") - target=img_ratio*max(bd_h,bd_w) - - - m_bbox=mask.getbbox() - - mask=mask.crop(m_bbox) - - m_w,m_h=mask.size - ratio=target/max(m_w,m_h) - new_w,new_h=int(ratio*m_w),int(ratio*m_h) - mask=mask.resize((new_w,new_h),Image.LANCZOS) - - - if new_w>=bd_w or new_h>=bd_h: - raise ValueError(f'缩放图片的长宽超过背景图大小,请下调img_ratio值') - - x=int(bd_w-new_w) - y=int(bd_h-new_h) - - x_trap_padding=int(x*x_ratio) - x=x-x_trap_padding - - y_trap_padding=int(y*y_ratio) - y=y-y_trap_padding - - n_mask.paste(mask,(x,y)) - n_mask=mask2tensor(n_mask) - - if len(n_mask.shape)<3: - n_mask=n_mask.unsqueeze(0) - print("Yolov8_person_detect done") - return (n_mask,) - -NODE_CLASS_MAPPINGS={ - "Yolov8_person_detect":Yolov8_person_detect -} - -NODE_DISPLAY_NAME_MAPPINGS={ - "Yolov8_person_detect":"Yolov8_person_detect(My Node)" -} diff --git a/custom_nodes/ComfyUI-AK-Pack/.gitignore b/custom_nodes/ComfyUI-AK-Pack/.gitignore deleted file mode 100644 index c36686459f7c5ec17033a0d1b3b27a8dc0a7dce3..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -# Python bytecode -__pycache__/ -*.pyc -*.pyo -*.pyd - -# Virtual environment -.venv/ -venv/ -env/ - -# Editor/IDE specific files -.idea/ -.vscode/ - -# Operating System files -.DS_Store -Thumbs.db \ No newline at end of file diff --git a/custom_nodes/ComfyUI-AK-Pack/README.md b/custom_nodes/ComfyUI-AK-Pack/README.md deleted file mode 100644 index a6dfe1e90b9a15069d4f356a5174d73b4d01a4f0..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/README.md +++ /dev/null @@ -1,183 +0,0 @@ -#### Other My Nodes -- 📘 [ComfyUI-AK-Pack](https://github.com/akawana/ComfyUI-AK-Pack) -- 📘 [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) - -# ComfyUI AK Pack -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). - -The most interesting parts of this pack for most users are: -- Multiple Samplers Control -- Project Settings -- AK Base - -The other nodes are also useful, but they are of secondary importance. You should be able to figure them out on your own. - -I have also created example workflows: - ---- -#### UI: Multiple Samplers Control - -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. - -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. - -The panel is integrated into the left toolbar of ComfyUI and can be disabled in Settings. - -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. - -For example: "KSampler, 12, 33, My Detailer, BlaBlaSampler" - will find all the samplers mentioned. - -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. - -| [![](./img/MSamplers.jpg)](./img/MSamplers.jpg) | [![](./img/MSamplersSettings.jpg)](./img/MSamplersSettings.jpg) | [![](./img/MSamplersChooser.jpg)](./img/MSamplersChooser.jpg) | -|---|---|---| - ---- -#### UI: Project Settings - -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. -The panel is embedded into the left toolbar of ComfyUI and can be disabled in the settings. - -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. - -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. - -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. - -| [![](./img/ProjectSettings.jpg)](./img/ProjectSettings.jpg) | [![](./img/ProjectSettingsOptions.jpg)](./img/ProjectSettingsOptions.jpg) | [![](./img/AKProjectSettingsOut.jpg)](./img/AKProjectSettingsOut.jpg) | -|---|---|---| - ---- -#### Node: AK Base - -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. - -In addition, it includes: -- a button to copy a single image to the clipboard, -- a button to copy the entire gallery to the clipboard, -- a dialog that allows you to inspect the results while working in a different part of the workflow. - -In the Properties panel, this node provides a setting called node_list. -This is a text field where you can specify a list of nodes, using either node names or node IDs. - -Once configured, AK Base can automatically adjust the parameters seed, denoise, cfg, and xz_steps based on the image selected in the gallery. -This functionality was implemented specifically to support my 📘 [ComfyUI-AK-XZ-Axis](https://github.com/akawana/ComfyUI-AK-XZ-Axis) nodes. - -| [![](./img/AKBase.jpg)](./img/AKBase.jpg) | [![](./img/AKBaseGallery.jpg)](./img/AKBaseGallery.jpg) | [![](./img/AKBasePIP.jpg)](./img/AKBasePIP.jpg) | -|---|---|---| - ---- -#### Node: Setter & Getter - -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. - -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. - ---- -#### Node: Repeat Group State - -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. - -- Finds groups with names containing the target substring. -- Checks whether any of them are Active. -- If **all** matching groups are disabled -> it disables **its own group**. -- If **any** matching group is active -> it enables **its own group**. - - - ---- -#### Node: IsOneOfGroupsActive - -Checks the state of all groups whose names **contain a specified substring**. - -- If **at least one** matching group is Active -> output is `true`. -- If **all** matching groups are Muted/Bypassed -> output is `false`. - - - ---- -#### Node: AK Index Multiple - -Extracts a specific range from any **List** (images, masks, latents, text etc..) and creates individual outputs for that range. -Optionally replaces missing values with a fallback (`if_none`). - - - ---- -#### Node: AK CLIP Encode Multiple - -Extracts a specific range from any **List** of strings and CLIP encodes individual outputs for that range. - -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. - - - ---- -#### Node: AK KSampler Settings & AK KSampler Settings Out - -Allows you to define sampler settings anywhere in the workflow. -The Out node lets you retrieve these settings wherever they are needed. - -- `Seed`, `Sampler`, `Scheduler`, `Steps`, `Cfg`, `Denoise`, `xz_steps` - -> [!NOTE] -> This node includes an additional field called xz_steps. -> 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. -> 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. - ---- -#### Node: AK Replace Alpha With Color & AK Replace Color With Alpha - -`AK Replace Alpha With Color` Replaces alpha with a color. - -`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. - -> [!NOTE] -> 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. - - - ---- -#### Other nodes - -##### Node: AK Pipe & AK Pipe Loop - -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. - -`AK Pipe Loop` Always outputs the most recently modified connected AK Pipe. - -##### Node: CLIP Text Encode Cached - -Simple node for encoding conditioning with a small caching experiment. - -##### Node: CLIP Text Encode and Combine Cached - -Can combine the input conditioning with the text entered in the node. - - - -##### Node: AK Resize On Boolean - -Resizes the image and optional mask based on a boolean parameter (enable / disable). Useful when working with the **Project Settings** panel. - - - -##### Node: AK Contrast And Saturate Image - -Node for adjusting image contrast and saturation. - -##### Node: Preview Raw Text - -Displays text and can format **JSON**. It also supports displaying a list of texts. - ---- - -# Installation - -From your ComfyUI root directory: - -```bash -cd ComfyUI/custom_nodes -git clone https://github.com/akawana/ComfyUI-Utils-extra.git \ No newline at end of file diff --git a/custom_nodes/ComfyUI-AK-Pack/__init__.py b/custom_nodes/ComfyUI-AK-Pack/__init__.py deleted file mode 100644 index 38354b3463150598e1587788d75e05316ec5dd86..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/__init__.py +++ /dev/null @@ -1,109 +0,0 @@ -WEB_DIRECTORY = "./js" - -from .nodes.AKIndexMultiple import NODE_CLASS_MAPPINGS as AKIndexMultiple_MAPPINGS -from .nodes.AKIndexMultiple import NODE_DISPLAY_NAME_MAPPINGS as AKIndexMultiple_DISPLAY - -from .nodes.AKCLIPEncodeMultiple import NODE_CLASS_MAPPINGS as AKCLIPEncodeMultiple_MAPPINGS -from .nodes.AKCLIPEncodeMultiple import NODE_DISPLAY_NAME_MAPPINGS as AKCLIPEncodeMultiple_DISPLAY - -from .nodes.IsOneOfGroupsActive import NODE_CLASS_MAPPINGS as GROUPCHK_MAPPINGS -from .nodes.IsOneOfGroupsActive import NODE_DISPLAY_NAME_MAPPINGS as GROUPCHK_DISPLAY - -from .nodes.RepeatGroupState import NODE_CLASS_MAPPINGS as RPSTATE_MAPPINGS -from .nodes.RepeatGroupState import NODE_DISPLAY_NAME_MAPPINGS as RPSTATE_DISPLAY - -from .nodes.PreviewRawText import NODE_CLASS_MAPPINGS as PRTSTATE_MAPPINGS -from .nodes.PreviewRawText import NODE_DISPLAY_NAME_MAPPINGS as PRTSTATE_DISPLAY - -from .nodes.CLIPTextEncodeCached import NODE_CLASS_MAPPINGS as CLCSTATE_MAPPINGS -from .nodes.CLIPTextEncodeCached import NODE_DISPLAY_NAME_MAPPINGS as CLCSTATE_DISPLAY - -from .nodes.CLIPTextEncodeAndCombineCached import NODE_CLASS_MAPPINGS as CLCOMBINE_STATE_MAPPINGS -from .nodes.CLIPTextEncodeAndCombineCached import NODE_DISPLAY_NAME_MAPPINGS as CLCOMBINE_STATE_DISPLAY - -from .nodes.AKBase import NODE_CLASS_MAPPINGS as AKBSTATE_MAPPINGS -from .nodes.AKBase import NODE_DISPLAY_NAME_MAPPINGS as AKBSTATE_DISPLAY - -from .nodes.AKPipe import NODE_CLASS_MAPPINGS as AKPIPESTATE_MAPPINGS -from .nodes.AKPipe import NODE_DISPLAY_NAME_MAPPINGS as AKPIPESTATE_DISPLAY - -from .nodes.AKPipeLoop import NODE_CLASS_MAPPINGS as AKPIPEL_STATE_MAPPINGS -from .nodes.AKPipeLoop import NODE_DISPLAY_NAME_MAPPINGS as AKPIPEL_STATE_DISPLAY - -from .nodes.Setter import NODE_CLASS_MAPPINGS as SETTERSTATE_MAPPINGS -from .nodes.Setter import NODE_DISPLAY_NAME_MAPPINGS as SETTERSTATE_DISPLAY - -from .nodes.Getter import NODE_CLASS_MAPPINGS as GETTERSTATE_MAPPINGS -from .nodes.Getter import NODE_DISPLAY_NAME_MAPPINGS as GETTERSTATE_DISPLAY - -from .nodes.AKResizeOnBoolean import NODE_CLASS_MAPPINGS as RESIZESTATE_MAPPINGS -from .nodes.AKResizeOnBoolean import NODE_DISPLAY_NAME_MAPPINGS as RESIZESTATE_DISPLAY - -from .nodes.IsMaskEmpty import NODE_CLASS_MAPPINGS as ISMSTATE_MAPPINGS -from .nodes.IsMaskEmpty import NODE_DISPLAY_NAME_MAPPINGS as ISMSTATE_DISPLAY - -from .nodes.AKContrastAndSaturateImage import NODE_CLASS_MAPPINGS as AKSAT_STATE_MAPPINGS -from .nodes.AKContrastAndSaturateImage import NODE_DISPLAY_NAME_MAPPINGS as AKSAT_STATE_DISPLAY - -from .nodes.AKReplaceAlphaWithColor import NODE_CLASS_MAPPINGS as AKRALPHA_STATE_MAPPINGS -from .nodes.AKReplaceAlphaWithColor import NODE_DISPLAY_NAME_MAPPINGS as AKRALPHA_STATE_DISPLAY - -from .nodes.AKReplaceColorWithAlpha import NODE_CLASS_MAPPINGS as AKRCOLOR_STATE_MAPPINGS -from .nodes.AKReplaceColorWithAlpha import NODE_DISPLAY_NAME_MAPPINGS as AKRCOLOR_STATE_DISPLAY - -from .nodes.AKControlMultipleKSamplers import NODE_CLASS_MAPPINGS as AK_CONTROL_SAMPLERS_COLOR_STATE_MAPPINGS -from .nodes.AKControlMultipleKSamplers import NODE_DISPLAY_NAME_MAPPINGS as AK_CONTROL_SAMPLERS_COLOR_STATE_DISPLAY - -from .nodes.AKKSamplerSettings import NODE_CLASS_MAPPINGS as AKKSamplerSettings_STATE_MAPPINGS -from .nodes.AKKSamplerSettings import NODE_DISPLAY_NAME_MAPPINGS as AKKSamplerSettings_STATE_DISPLAY - -from .nodes.AKProjectSettingsOut import NODE_CLASS_MAPPINGS as AKProjectSettingsOut_STATE_MAPPINGS -from .nodes.AKProjectSettingsOut import NODE_DISPLAY_NAME_MAPPINGS as AKProjectSettingsOut_STATE_DISPLAY - -NODE_CLASS_MAPPINGS = { - **AKIndexMultiple_MAPPINGS, - **AKCLIPEncodeMultiple_MAPPINGS, - **GROUPCHK_MAPPINGS, - **RPSTATE_MAPPINGS, - **PRTSTATE_MAPPINGS, - **CLCSTATE_MAPPINGS, - **CLCOMBINE_STATE_MAPPINGS, - **AKBSTATE_MAPPINGS, - **AKPIPESTATE_MAPPINGS, - **AKPIPEL_STATE_MAPPINGS, - **SETTERSTATE_MAPPINGS, - **GETTERSTATE_MAPPINGS, - **RESIZESTATE_MAPPINGS, - **ISMSTATE_MAPPINGS, - **AKSAT_STATE_MAPPINGS, - **AKRALPHA_STATE_MAPPINGS, - **AKRCOLOR_STATE_MAPPINGS, - **AK_CONTROL_SAMPLERS_COLOR_STATE_MAPPINGS, - **AKKSamplerSettings_STATE_MAPPINGS, - **AKProjectSettingsOut_STATE_MAPPINGS, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - **AKIndexMultiple_DISPLAY, - **AKCLIPEncodeMultiple_DISPLAY, - **GROUPCHK_DISPLAY, - **RPSTATE_DISPLAY, - **PRTSTATE_DISPLAY, - **CLCSTATE_DISPLAY, - **CLCOMBINE_STATE_DISPLAY, - **AKBSTATE_DISPLAY, - **AKPIPESTATE_DISPLAY, - **AKPIPEL_STATE_DISPLAY, - **SETTERSTATE_DISPLAY, - **GETTERSTATE_DISPLAY, - **RESIZESTATE_DISPLAY, - **ISMSTATE_DISPLAY, - **AKSAT_STATE_DISPLAY, - **AKRALPHA_STATE_DISPLAY, - **AKRCOLOR_STATE_DISPLAY, - **AK_CONTROL_SAMPLERS_COLOR_STATE_DISPLAY, - **AKKSamplerSettings_STATE_DISPLAY, - **AKProjectSettingsOut_STATE_DISPLAY, -} - -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/custom_nodes/ComfyUI-AK-Pack/icon.jpg b/custom_nodes/ComfyUI-AK-Pack/icon.jpg deleted file mode 100644 index afe5503b42b9edf670e59a602edae852969de3b9..0000000000000000000000000000000000000000 Binary files a/custom_nodes/ComfyUI-AK-Pack/icon.jpg and /dev/null differ diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKBase.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKBase.py deleted file mode 100644 index 704f74e6d3431f82cad931d330c67a26f8fa17ab..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKBase.py +++ /dev/null @@ -1,248 +0,0 @@ - -# === AKXZ embedded JSON fallback (read from IMAGE tensors) === -import json as _akxz_json -import torch as _akxz_torch - -_AKXZ_MAGIC = b"AKXZ" -_AKXZ_HEADER_LEN = 8 - -def _akxz_float01_to_bytes(x: _akxz_torch.Tensor) -> bytes: - x = _akxz_torch.clamp(x * 255.0 + 0.5, 0, 255).to(_akxz_torch.uint8).cpu() - return x.numpy().tobytes() - -def akxz_extract_image_cfg_list(images: _akxz_torch.Tensor): - if not isinstance(images, _akxz_torch.Tensor) or images.ndim != 4: - return [] - b = int(images.shape[0]) - out = [] - for i in range(b): - row = images[i, 0, :, :3].reshape(-1) - raw = _akxz_float01_to_bytes(row) - if len(raw) < _AKXZ_HEADER_LEN or raw[:4] != _AKXZ_MAGIC: - continue - size = int.from_bytes(raw[4:8], "big") - if size <= 0 or (8 + size) > len(raw): - continue - payload = raw[8:8+size] - try: - obj = _akxz_json.loads(payload.decode("utf-8")) - if isinstance(obj, dict): - out.append(obj) - except Exception: - pass - return out - - -import os -import json - -import numpy as np -from PIL import Image -from PIL.PngImagePlugin import PngInfo - -import folder_paths - - -AKBASE_STATE_FILENAME = "ak_base_state.json" -AKBASE_XZ_CONFIG_FILENAME = "ak_base_xz_config.json" - -AKBASE_A_FILENAME = "ak_base_image_a.png" -AKBASE_B_FILENAME = "ak_base_image_b.png" - -AKBASE_GALLERY_PREFIX = "ak_base_image_xy_" -AKBASE_GALLERY_MAX = 512 - - -def _tensor_to_pil(img_tensor): - arr = img_tensor.detach().cpu().numpy() - arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8) - return Image.fromarray(arr) - - -def _temp_dir(): - d = folder_paths.get_temp_directory() - os.makedirs(d, exist_ok=True) - return d - - -def _temp_path(filename: str) -> str: - return os.path.join(_temp_dir(), filename) - - -def _save_temp_png(img_tensor, filename: str, meta: dict = None) -> None: - img = _tensor_to_pil(img_tensor) - - pnginfo = None - if isinstance(meta, dict) and meta: - pnginfo = PngInfo() - for k, v in meta.items(): - if v is None: - v = "" - pnginfo.add_text(str(k), str(v)) - - img.save(_temp_path(filename), compress_level=4, pnginfo=pnginfo) - -def _safe_remove(filename: str) -> None: - try: - p = _temp_path(filename) - if os.path.isfile(p): - os.remove(p) - except Exception: - pass - - -def _clear_gallery_files() -> None: - d = _temp_dir() - for fn in os.listdir(d): - if fn.startswith(AKBASE_GALLERY_PREFIX) and fn.lower().endswith(".png"): - try: - os.remove(os.path.join(d, fn)) - except Exception: - pass - - -def _clear_compare_files() -> None: - _safe_remove(AKBASE_A_FILENAME) - _safe_remove(AKBASE_B_FILENAME) - - -def _write_state(state: dict, filename: str = None) -> None: - try: - fn = filename if filename else AKBASE_STATE_FILENAME - with open(_temp_path(fn), "w", encoding="utf-8") as f: - json.dump(state, f, ensure_ascii=False) - except Exception: - pass - - -class AKBase: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "a_image": ("IMAGE",), - }, - "optional": { - "b_image": ("IMAGE",), - }, - "hidden": { - "unique_id": "UNIQUE_ID", - }, - } - - RETURN_TYPES = () - FUNCTION = "run" - CATEGORY = "AK" - OUTPUT_NODE = True - - def run( - self, - a_image, - b_image=None, - # ak_settings: str = None, - unique_id=None, - ): - a_n = int(a_image.shape[0]) if hasattr(a_image, "shape") else 1 - b_n = int(b_image.shape[0]) if (b_image is not None and hasattr(b_image, "shape")) else 0 - - ak_base_config = "{}" - - node_id = unique_id if unique_id is not None else getattr(self, "node_id", None) - node_id = str(node_id) if node_id is not None else None - suffix = f"_{node_id}" if node_id is not None else "" - - - xz_fname = f"ak_base_xz_config{suffix}.json" if suffix else AKBASE_XZ_CONFIG_FILENAME - try: - - content = "{}" - cfg_list = [] - try: - if (a_n > 1) or (b_n > 1): - imgs = [] - if a_n > 1: - imgs.extend([a_image[i] for i in range(min(a_n, AKBASE_GALLERY_MAX))]) - if b_n > 1 and b_image is not None: - remaining = AKBASE_GALLERY_MAX - len(imgs) - if remaining > 0: - imgs.extend([b_image[i] for i in range(min(b_n, remaining))]) - if imgs: - cfg_list = akxz_extract_image_cfg_list(_akxz_torch.stack(imgs, dim=0)) - else: - cfg_list = akxz_extract_image_cfg_list(a_image) - except Exception: - cfg_list = [] - - if cfg_list: - content = json.dumps({"image": cfg_list}, ensure_ascii=False) - with open(_temp_path(xz_fname), "w", encoding="utf-8") as f: - f.write(content) - - except Exception: - pass - - if a_n > 1 or b_n > 1: - _clear_compare_files() - if suffix: - _safe_remove(f"ak_base_image_a{suffix}.png") - _safe_remove(f"ak_base_image_b{suffix}.png") - _clear_gallery_files() - - _save_temp_png(a_image[0], f"ak_base_image_a{suffix}.png" if suffix else AKBASE_A_FILENAME) - if b_image is None: - _save_temp_png(a_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME) - else: - _save_temp_png(b_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME) - - imgs = [] - if a_n > 1: - imgs.extend([a_image[i] for i in range(min(a_n, AKBASE_GALLERY_MAX))]) - - if b_n > 1 and b_image is not None: - remaining = AKBASE_GALLERY_MAX - len(imgs) - if remaining > 0: - imgs.extend([b_image[i] for i in range(min(b_n, remaining))]) - - gallery_prefix = f"{AKBASE_GALLERY_PREFIX}{node_id}_" if node_id is not None else AKBASE_GALLERY_PREFIX - for i, t in enumerate(imgs): - _save_temp_png(t, f"{gallery_prefix}{i}.png") - - _write_state( - { - "mode": "gallery", - "count": len(imgs), - "gallery_prefix": gallery_prefix, - }, - filename=(f"ak_base_state{suffix}.json" if suffix else AKBASE_STATE_FILENAME), - ) - - return {"ui": {"ak_base_saved": [True]}, "result": (ak_base_config,)} - - _clear_gallery_files() - if suffix: - _safe_remove(f"ak_base_image_a{suffix}.png") - _safe_remove(f"ak_base_image_b{suffix}.png") - - _save_temp_png(a_image[0], f"ak_base_image_a{suffix}.png" if suffix else AKBASE_A_FILENAME) - - if b_image is None: - _save_temp_png(a_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME) - else: - _save_temp_png(b_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME) - - _write_state( - { - "mode": "compare", - "a": {"filename": (f"ak_base_image_a{suffix}.png" if suffix else AKBASE_A_FILENAME), "type": "temp", "subfolder": ""}, - "b": {"filename": (f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME), "type": "temp", "subfolder": ""}, - "preview_width": 512, - "preview_height": 512, - }, - filename=(f"ak_base_state{suffix}.json" if suffix else AKBASE_STATE_FILENAME), - ) - - return {"ui": {"ak_base_saved": [True]}, "result": (ak_base_config,)} - - -NODE_CLASS_MAPPINGS = {"AK Base": AKBase} -NODE_DISPLAY_NAME_MAPPINGS = {"AK Base": "AK Base"} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKCLIPEncodeMultiple.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKCLIPEncodeMultiple.py deleted file mode 100644 index ce6f7f08f09e572adf2a4e2eb7255241cee59883..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKCLIPEncodeMultiple.py +++ /dev/null @@ -1,243 +0,0 @@ -import zlib - - -class AnyType(str): - def __ne__(self, __value: object) -> bool: - return False - - -ANY_TYPE = AnyType("*") - - -class AKCLIPEncodeMultiple: - empty_cache = {} - # text_cache = {} - - last_items = None - idx_cache = {} - hash_cache = {} - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "str_list": (ANY_TYPE, {"forceInput": False}), - "clip": ("CLIP",), - "starting_index": ("INT", {"default": 0, "min": 0, "step": 1}), - "length": ("INT", {"default": 1, "min": 1, "max": 20, "step": 1}), - }, - "optional": { - "mask_list": ("MASK",), - }, - } - - RETURN_TYPES = ("CONDITIONING",) + ("CONDITIONING",) * 20 - RETURN_NAMES = ("combined_con",) + tuple(f"cond_{i}" for i in range(20)) - - FUNCTION = "execute" - CATEGORY = "AK/conditioning" - OUTPUT_NODE = False - - INPUT_IS_LIST = True - - @classmethod - def _get_empty_cond(cls, clip): - key = id(clip) - cached = cls.empty_cache.get(key) - if cached is not None: - return cached - - tokens = clip.tokenize("") - cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) - empty = [[cond, {"pooled_output": pooled}]] - cls.empty_cache[key] = empty - return empty - - @classmethod - def _encode_text(cls, clip, text): - tokens = clip.tokenize(text) - cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) - return [[cond, {"pooled_output": pooled}]] - - @staticmethod - def _clip_key(clip): - inner = getattr(clip, "cond_stage_model", None) - if inner is not None: - return id(inner) - inner = getattr(clip, "clip", None) or getattr(clip, "model", None) - if inner is not None: - return id(inner) - return id(clip) - - @staticmethod - def _apply_mask_to_cond(base_cond, mask): - if mask is None: - return base_cond - cond_with_mask = [] - for t, data in base_cond: - data_copy = dict(data) if data is not None else {} - data_copy["mask"] = mask - data_copy["mask_strength"] = 1.0 - cond_with_mask.append([t, data_copy]) - return cond_with_mask - - @classmethod - def _compute_hash(cls, items, masks, start, length): - max_bytes = 64 * 1024 - buf = bytearray() - - def add_bytes(b): - nonlocal buf - if not b: - return - remaining = max_bytes - len(buf) - if remaining <= 0: - return - if len(b) > remaining: - buf.extend(b[:remaining]) - else: - buf.extend(b) - - for v in items: - s = "" if v is None else v - add_bytes(s.encode("utf-8", errors="ignore") + b"\n") - if len(buf) >= max_bytes: - break - - if len(buf) < max_bytes and masks: - for m in masks: - if m is None: - add_bytes(b"MNONE\n") - else: - try: - t = m - if isinstance(t, (list, tuple)): - t = t[0] - if hasattr(t, "detach"): - t = t.detach() - arr = t.cpu().contiguous().numpy() - add_bytes(arr.tobytes()) - except Exception: - add_bytes(repr(type(m)).encode("utf-8", errors="ignore")) - if len(buf) >= max_bytes: - break - - add_bytes(f"|start={start}|len={length}".encode("ascii")) - - return zlib.adler32(bytes(buf)) & 0xFFFFFFFF - - def execute(self, clip, str_list, starting_index, length, mask_list=None): - if isinstance(clip, (list, tuple)): - clip_obj = clip[0] - else: - clip_obj = clip - - if isinstance(str_list, (list, tuple)): - if len(str_list) == 1 and isinstance(str_list[0], (list, tuple)): - items = list(str_list[0]) - else: - items = list(str_list) - else: - items = [str_list] - - if not isinstance(items, list) or not all( - (isinstance(v, str) or v is None) for v in items - ): - raise RuntimeError("Require array of strings") - - masks = [] - if mask_list is not None: - if isinstance(mask_list, (list, tuple)): - if len(mask_list) == 1 and isinstance(mask_list[0], (list, tuple)): - masks = list(mask_list[0]) - else: - masks = list(mask_list) - else: - masks = [mask_list] - - if isinstance(starting_index, (list, tuple)): - start_raw = starting_index[0] if starting_index else 0 - else: - start_raw = starting_index - - if isinstance(length, (list, tuple)): - length_raw = length[0] if length else 1 - else: - length_raw = length - - start = max(0, int(start_raw)) - length_val = max(1, min(20, int(length_raw))) - - if AKCLIPEncodeMultiple.last_items is None or len(AKCLIPEncodeMultiple.last_items) != len(items): - AKCLIPEncodeMultiple.last_items = [None] * len(items) - AKCLIPEncodeMultiple.idx_cache.clear() - - clip_id = self._clip_key(clip_obj) - items_copy = list(items) - masks_copy = list(masks) if masks else [] - hval = self._compute_hash(items_copy, masks_copy, start, length_val) - cache_key = (clip_id, hval) - - cached_entry = AKCLIPEncodeMultiple.hash_cache.get(cache_key) - if cached_entry is not None: - combined_cached, per_idx_cached = cached_entry - per_idx_cached = list(per_idx_cached) - if len(per_idx_cached) < 20: - per_idx_cached.extend([None] * (20 - len(per_idx_cached))) - elif len(per_idx_cached) > 20: - per_idx_cached = per_idx_cached[:20] - return (combined_cached,) + tuple(per_idx_cached) - - result = [] - empty_cond = None - combined_cond = None - - for i in range(length_val): - idx = start + i - - cond = None - if 0 <= idx < len(items): - v = items[idx] - - mask_for_idx = None - if 0 <= idx < len(masks): - mask_for_idx = masks[idx] - - if v is None: - if empty_cond is None: - empty_cond = self._get_empty_cond(clip_obj) - base_cond = empty_cond - AKCLIPEncodeMultiple.last_items[idx] = None - else: - prev = AKCLIPEncodeMultiple.last_items[idx] - if v == prev: - cached = AKCLIPEncodeMultiple.idx_cache.get((clip_id, idx)) - if cached is not None: - base_cond = cached - else: - base_cond = self._encode_text(clip_obj, v) - AKCLIPEncodeMultiple.idx_cache[(clip_id, idx)] = base_cond - else: - base_cond = self._encode_text(clip_obj, v) - AKCLIPEncodeMultiple.idx_cache[(clip_id, idx)] = base_cond - AKCLIPEncodeMultiple.last_items[idx] = v - cond = self._apply_mask_to_cond(base_cond, mask_for_idx) - if v is not None and cond is not None: - if combined_cond is None: - combined_cond = [] - for t, data in cond: - data_copy = dict(data) if data is not None else {} - combined_cond.append([t, data_copy]) - - result.append(cond) - - if length_val < 20: - result.extend([None] * (20 - length_val)) - - AKCLIPEncodeMultiple.hash_cache[cache_key] = (combined_cond, list(result)) - - return (combined_cond,) + tuple(result) - - -NODE_CLASS_MAPPINGS = {"AKCLIPEncodeMultiple": AKCLIPEncodeMultiple} -NODE_DISPLAY_NAME_MAPPINGS = {"AKCLIPEncodeMultiple": "AK CLIP Encode Multiple"} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKContrastAndSaturateImage.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKContrastAndSaturateImage.py deleted file mode 100644 index 55229991b4595b503b982c92d36222754744cea0..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKContrastAndSaturateImage.py +++ /dev/null @@ -1,281 +0,0 @@ -import torch -import numpy as np -from PIL import Image - -# Hue band definitions (soft ranges in degrees) -FEATHER_DEG = 15.0 - -RED_RANGES_SOFT = [(315.0, 345.0), (15.0, 45.0)] -YELLOW_RANGES_SOFT = [(15.0, 45.0), (75.0, 105.0)] -GREEN_RANGES_SOFT = [(75.0, 105.0), (135.0, 165.0)] -CYAN_RANGES_SOFT = [(135.0, 165.0), (195.0, 225.0)] -BLUE_RANGES_SOFT = [(195.0, 225.0), (225.0, 285.0)] -MAGENTA_RANGES_SOFT = [(255.0, 285.0), (315.0, 345.0)] - - -def _compute_band_weight(h_deg: np.ndarray, ranges_soft): - """Compute per-pixel weight for a hue band with soft feathered edges.""" - if h_deg.ndim != 2: - raise ValueError("Expected 2D hue array") - - weight_total = np.zeros_like(h_deg, dtype=np.float32) - - for start_soft, end_soft in ranges_soft: - width = end_soft - start_soft - if width <= 0.0: - continue - - feather = min(FEATHER_DEG, width * 0.5) - start_hard = start_soft + feather - end_hard = end_soft - feather - - seg_weight = np.zeros_like(h_deg, dtype=np.float32) - - # Fully inside hard region - inside_hard = (h_deg >= start_hard) & (h_deg <= end_hard) - seg_weight[inside_hard] = 1.0 - - # Soft rising edge - if feather > 0.0: - rising = (h_deg >= start_soft) & (h_deg < start_hard) - seg_weight[rising] = (h_deg[rising] - start_soft) / feather - - falling = (h_deg > end_hard) & (h_deg <= end_soft) - seg_weight[falling] = (end_soft - h_deg[falling]) / feather - - weight_total = np.maximum(weight_total, seg_weight) - - return weight_total - - -def _slider_to_factor(value: int) -> float: - """Convert slider value in [-100, 100] to saturation multiplier.""" - v = float(value) - return max(0.0, 1.0 + v / 100.0) - -def _apply_brightness_contrast_rgb01(frame: np.ndarray, brightness: int, contrast: int) -> np.ndarray: - """Apply brightness and contrast to an RGB image in [0,1] float32 format.""" - out = frame.astype(np.float32, copy=False) - - if brightness != 0: - out = out + (float(brightness) / 100.0) - - if contrast != 0: - factor = 1.0 + (float(contrast) / 100.0) - out = (out - 0.5) * factor + 0.5 - - return np.clip(out, 0.0, 1.0) - - - -class AKContrastAndSaturateImage: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "brightness": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "contrast": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "master": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "reds": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "yellows": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "greens": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "cyans": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "blues": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - "magentas": ( - "INT", - { - "default": 0, - "min": -100, - "max": 100, - "step": 1, - }, - ), - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = "apply" - CATEGORY = "AK/image" - - def apply( - self, - image: torch.Tensor, - brightness: int, - contrast: int, - master: int, - reds: int, - yellows: int, - greens: int, - cyans: int, - blues: int, - magentas: int, - ): - if not isinstance(image, torch.Tensor): - raise TypeError("Expected image as torch.Tensor") - - if image.ndim != 4 or image.shape[-1] != 3: - raise ValueError("Expected image with shape [B, H, W, 3]") - - if ( - brightness == 0 - and contrast == 0 - and master == 0 - and reds == 0 - and yellows == 0 - and greens == 0 - and cyans == 0 - and blues == 0 - and magentas == 0 - ): - return (image,) - - device = image.device - batch_size, h, w, c = image.shape - - img_np = image.detach().cpu().numpy() - out_list = [] - - master_factor = _slider_to_factor(master) - reds_factor = _slider_to_factor(reds) - yellows_factor = _slider_to_factor(yellows) - greens_factor = _slider_to_factor(greens) - cyans_factor = _slider_to_factor(cyans) - blues_factor = _slider_to_factor(blues) - magentas_factor = _slider_to_factor(magentas) - - for i in range(batch_size): - frame = img_np[i] - - if brightness != 0 or contrast != 0: - frame = _apply_brightness_contrast_rgb01(frame, brightness, contrast) - - frame_u8 = (frame * 255.0).clip(0, 255).astype(np.uint8) - - pil_rgb = Image.fromarray(frame_u8, mode="RGB") - pil_hsv = pil_rgb.convert("HSV") - hsv = np.array(pil_hsv, dtype=np.uint8) - - H = hsv[..., 0].astype(np.float32) - S = hsv[..., 1].astype(np.float32) - V = hsv[..., 2] - - h_deg = H * (360.0 / 255.0) - - # Base master factor - S = S * master_factor - - # Per-band masks - reds_w = _compute_band_weight(h_deg, RED_RANGES_SOFT) - yellows_w = _compute_band_weight(h_deg, YELLOW_RANGES_SOFT) - greens_w = _compute_band_weight(h_deg, GREEN_RANGES_SOFT) - cyans_w = _compute_band_weight(h_deg, CYAN_RANGES_SOFT) - blues_w = _compute_band_weight(h_deg, BLUE_RANGES_SOFT) - magentas_w = _compute_band_weight(h_deg, MAGENTA_RANGES_SOFT) - - # Sequential multiplicative adjustments - def apply_band(S_arr, weight, factor): - if factor == 1.0: - return S_arr - band_scale = 1.0 + weight * (factor - 1.0) - return S_arr * band_scale - - S = apply_band(S, reds_w, reds_factor) - S = apply_band(S, yellows_w, yellows_factor) - S = apply_band(S, greens_w, greens_factor) - S = apply_band(S, cyans_w, cyans_factor) - S = apply_band(S, blues_w, blues_factor) - S = apply_band(S, magentas_w, magentas_factor) - - S = np.clip(S, 0.0, 255.0).astype(np.uint8) - - hsv_new = np.stack( - [ - H.astype(np.uint8), - S, - V.astype(np.uint8), - ], - axis=-1, - ) - - pil_hsv_new = Image.fromarray(hsv_new, mode="HSV") - pil_rgb_new = pil_hsv_new.convert("RGB") - rgb_np = np.array(pil_rgb_new).astype(np.float32) / 255.0 - out_list.append(rgb_np) - - out_np = np.stack(out_list, axis=0) - out_tensor = torch.from_numpy(out_np).to(device) - return (out_tensor,) - - -NODE_CLASS_MAPPINGS = { - "AKContrastAndSaturateImage": AKContrastAndSaturateImage, -} -NODE_DISPLAY_NAME_MAPPINGS = { - "AKContrastAndSaturateImage": "AK Contrast & Saturate Image", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKControlMultipleKSamplers.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKControlMultipleKSamplers.py deleted file mode 100644 index caf6591e4ff134c1f684b8bb4624aa67ccb3f058..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKControlMultipleKSamplers.py +++ /dev/null @@ -1,46 +0,0 @@ -import comfy.samplers - -class AKControlMultipleKSamplers: - """ - UI-only control node (no outputs). Frontend JS drives synchronization. - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "choose_ksampler": ([""], {"default": ""}), - - "seed ": ("INT", {"default": 0, "min": 0, "max": 0x7FFFFFFF, "step": 1}), - "steps": ("INT", {"default": 15, "min": 1, "max": 100, "step": 1}), - "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1}), - "sampler_name": ( - comfy.samplers.SAMPLER_NAMES, - {"default": comfy.samplers.SAMPLER_NAMES[0]} - ), - "scheduler": ( - comfy.samplers.SCHEDULER_NAMES, - {"default": comfy.samplers.SCHEDULER_NAMES[0]} - ), - "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - "hidden": { - "_ak_state_json": ("STRING", {"default": "{}", "multiline": True}), - } - } - - RETURN_TYPES = () - FUNCTION = "noop" - CATEGORY = "AK/sampling" - - def noop(self, **kwargs): - return () - - -NODE_CLASS_MAPPINGS = { - "AK Control Multiple KSamplers": AKControlMultipleKSamplers -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AK Control Multiple KSamplers": "AK Control Multiple KSamplers" -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKIndexMultiple.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKIndexMultiple.py deleted file mode 100644 index 691c3099513f5f780cb97857952eb68998cb4312..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKIndexMultiple.py +++ /dev/null @@ -1,72 +0,0 @@ -# nodes/IndexMultiple.py - -class AnyType(str): - def __ne__(self, __value: object) -> bool: - return False - -ANY_TYPE = AnyType("*") - -class AKIndexMultiple: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_any": (ANY_TYPE, {"forceInput": False}), - "starting_index": ("INT", {"default": 0, "min": 0, "step": 1}), - "length": ("INT", {"default": 1, "min": 1, "max": 50, "step": 1}), - }, - "optional": { - "if_none": (ANY_TYPE, {}), - } - } - - RETURN_TYPES = (ANY_TYPE,) * 50 - RETURN_NAMES = tuple(f"item_{i}" for i in range(50)) - - FUNCTION = "execute" - CATEGORY = "AK/utils" - OUTPUT_NODE = False - - INPUT_IS_LIST = True - - def execute(self, input_any, starting_index, length, if_none=None): - if isinstance(input_any, (list, tuple)): - if len(input_any) == 1 and isinstance(input_any[0], (list, tuple)): - items = list(input_any[0]) - else: - items = list(input_any) - else: - items = [input_any] - - start = max(0, int(starting_index[0] if isinstance(starting_index, (list, tuple)) else starting_index)) - length_val = max(1, min(50, int(length[0] if isinstance(length, (list, tuple)) else length))) - -# for i, v in enumerate(items[:10]): -# print(f" [{i}] {v}") - - if isinstance(if_none, (list, tuple)): - fallback = if_none[0] if len(if_none) > 0 else None - else: - fallback = if_none - - # print(fallback) - result = [] - - for i in range(50): - idx = start + i - - if i < length_val and 0 <= idx < len(items): - v = items[idx] - if v is None and fallback is not None: - v = fallback - else: - v = fallback - - result.append(v) - - - return tuple(result) - - -NODE_CLASS_MAPPINGS = {"AKIndexMultiple": AKIndexMultiple} -NODE_DISPLAY_NAME_MAPPINGS = {"AKIndexMultiple": "AK Index Multiple"} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKKSamplerSettings.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKKSamplerSettings.py deleted file mode 100644 index c103398e5ab85aba0db301a52140628c2ff28176..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKKSamplerSettings.py +++ /dev/null @@ -1,152 +0,0 @@ -import json -import zlib -import comfy.samplers - - -class AKKSamplerSettings: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - # Don't name this input exactly "seed" or ComfyUI may auto-randomize it - # depending on global seed behavior, causing this node to look "changed" every run. - "seed_value": ("INT", {"default": 0, "min": 0, "max": 2147483647, "step": 1}), - "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "step": 1}), - "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1}), - "sampler_name": (comfy.samplers.SAMPLER_NAMES, {"default": comfy.samplers.SAMPLER_NAMES[0]}), - "scheduler": (comfy.samplers.SCHEDULER_NAMES, {"default": comfy.samplers.SCHEDULER_NAMES[0]}), - "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "xz_steps": ("INT", {"default": 1, "min": 1, "max": 9999, "step": 1}), - }, - "hidden": { - "node_id": "UNIQUE_ID", - }, - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("sampler_settings",) - FUNCTION = "run" - CATEGORY = "AK/sampling" - - def run( - self, - seed_value: int, - steps: int, - cfg: float, - sampler_name: str, - scheduler: str, - denoise: float, - xz_steps: int, - node_id=None, - ): - from_id = str(node_id) if node_id is not None else "" - seed = int(seed_value) - if seed < 0: - seed = 0 - - data = { - "seed": seed, - "steps": int(steps), - "cfg": float(cfg), - "sampler_name": str(sampler_name), - "scheduler": str(scheduler), - "denoise": float(denoise), - "xz_steps": int(xz_steps), - "from_id": from_id, - } - - payload = ( - f"{data['seed']}|{data['steps']}|{data['cfg']}|{data['sampler_name']}|" - f"{data['scheduler']}|{data['denoise']}|{data['xz_steps']}|{from_id}" - ) - h = zlib.adler32(payload.encode("utf-8")) & 0xFFFFFFFF - data["hash"] = int(h) - - s = json.dumps(data, ensure_ascii=False) - return (s,) - - -class AKKSamplerSettingsOut: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "sampler_settings": ("STRING", {"forceInput": True}), - } - } - - RETURN_TYPES = ( - "INT", - "INT", - "FLOAT", - comfy.samplers.SAMPLER_NAMES, - comfy.samplers.SCHEDULER_NAMES, - "FLOAT", - "INT", - ) - RETURN_NAMES = ( - "seed", - "steps", - "cfg", - "sampler_name", - "scheduler", - "denoise", - "xz_steps", - ) - FUNCTION = "run" - CATEGORY = "AK/sampling" - - def run(self, sampler_settings: str): - data = {} - if isinstance(sampler_settings, str) and sampler_settings.strip(): - try: - data = json.loads(sampler_settings) - if not isinstance(data, dict): - data = {} - except Exception: - data = {} - - seed = int(data.get("seed", 0) or 0) - if seed < 0: - seed = 0 - - steps = int(data.get("steps", 20) or 20) - - try: - cfg = float(data.get("cfg", 7.0)) - except Exception: - cfg = 7.0 - - sampler_name = str(data.get("sampler_name", comfy.samplers.SAMPLER_NAMES[0])) - if sampler_name not in comfy.samplers.SAMPLER_NAMES: - sampler_name = comfy.samplers.SAMPLER_NAMES[0] - - scheduler = str(data.get("scheduler", comfy.samplers.SCHEDULER_NAMES[0])) - if scheduler not in comfy.samplers.SCHEDULER_NAMES: - scheduler = comfy.samplers.SCHEDULER_NAMES[0] - - try: - denoise = float(data.get("denoise", 1.0)) - except Exception: - denoise = 1.0 - - xz_steps_raw = data.get("xz_steps", 1) - try: - xz_steps = int(xz_steps_raw) - except Exception: - xz_steps = 1 - if xz_steps < 1: - xz_steps = 1 - - return (seed, steps, cfg, sampler_name, scheduler, denoise, xz_steps) - - -NODE_CLASS_MAPPINGS = { - "AKKSamplerSettings": AKKSamplerSettings, - "AKKSamplerSettingsOut": AKKSamplerSettingsOut, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AKKSamplerSettings": "AK KSampler Settings", - "AKKSamplerSettingsOut": "AK KSampler Settings Out", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKPipeLoop.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKPipeLoop.py deleted file mode 100644 index 1858bfc9c0481f78c2ee00f5319131ea5ead8f38..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKPipeLoop.py +++ /dev/null @@ -1,184 +0,0 @@ -# AKPipeLoop.py - -class AKPipeLoop: - IDX_HASH = 0 - IDX_MODEL = 1 - IDX_CLIP = 2 - IDX_VAE = 3 - IDX_POS = 4 - IDX_NEG = 5 - IDX_LATENT = 6 - IDX_IMAGE = 7 - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "pipe_in_1": ("AK_PIPE",), - }, - "optional": { - "pipe_in_2": ("AK_PIPE",), - "pipe_in_3": ("AK_PIPE",), - "pipe_in_4": ("AK_PIPE",), - "pipe_in_5": ("AK_PIPE",), - "pipe_in_6": ("AK_PIPE",), - "pipe_in_7": ("AK_PIPE",), - "pipe_in_8": ("AK_PIPE",), - "pipe_in_9": ("AK_PIPE",), - "pipe_in_10": ("AK_PIPE",), - }, - } - - RETURN_TYPES = ( - "AK_PIPE", - "MODEL", - "CLIP", - "VAE", - "CONDITIONING", - "CONDITIONING", - "LATENT", - "IMAGE", - ) - - RETURN_NAMES = ( - "pipe_out", - "model", - "clip", - "vae", - "positive", - "negative", - "latent", - "image", - ) - - FUNCTION = "run" - CATEGORY = "AK/pipe" - OUTPUT_NODE = True - - @classmethod - def IS_CHANGED(cls, **kwargs): - # Force this node to always re-run - return float("nan") - - def __init__(self): - # Stored hashes per input index (0..9 for pipe_in_1..pipe_in_10). - self._stored_hashes = {} - - def _normalize_pipe(self, pipe): - if pipe is None: - return None - - if not isinstance(pipe, tuple): - pipe = tuple(pipe) - - # Old format: 7-tuple without hash -> prepend None hash. - if len(pipe) == 7: - pipe = (None,) + pipe - elif len(pipe) < 7: - pipe = (None,) + pipe + (None,) * (7 - len(pipe)) - elif len(pipe) > 8: - pipe = pipe[:8] - - return pipe - - def _get_hash_from_pipe(self, pipe): - if pipe is None: - return None - if not isinstance(pipe, tuple): - pipe = tuple(pipe) - if len(pipe) == 0: - return None - # If it is old 7-tuple without hash, treat hash as None. - if len(pipe) == 7: - return None - return pipe[self.IDX_HASH] - - def _outputs_from_pipe(self, pipe): - # Assumes pipe is already normalized to at least 8 elements. - model = pipe[self.IDX_MODEL] - clip = pipe[self.IDX_CLIP] - vae = pipe[self.IDX_VAE] - positive = pipe[self.IDX_POS] - negative = pipe[self.IDX_NEG] - latent = pipe[self.IDX_LATENT] - image = pipe[self.IDX_IMAGE] - return (pipe, model, clip, vae, positive, negative, latent, image) - - def run( - self, - pipe_in_1, - pipe_in_2=None, - pipe_in_3=None, - pipe_in_4=None, - pipe_in_5=None, - pipe_in_6=None, - pipe_in_7=None, - pipe_in_8=None, - pipe_in_9=None, - pipe_in_10=None, - ): - inputs = [ - pipe_in_1, - pipe_in_2, - pipe_in_3, - pipe_in_4, - pipe_in_5, - pipe_in_6, - pipe_in_7, - pipe_in_8, - pipe_in_9, - pipe_in_10, - ] - - current_hashes = {} - changed_indices = [] - normalized_pipes = {} - - # First pass: compute hashes for all valid inputs and detect changes - for idx, raw_pipe in enumerate(inputs): - if raw_pipe is None: - continue - - pipe = self._normalize_pipe(raw_pipe) - if pipe is None: - continue - - normalized_pipes[idx] = pipe - h = self._get_hash_from_pipe(pipe) - current_hashes[idx] = h - - prev = self._stored_hashes.get(idx, None) - if idx not in self._stored_hashes or h != prev: - changed_indices.append(idx) - - # Update stored hashes snapshot to current state - self._stored_hashes = current_hashes - - # If there is at least one changed input, use the first one - if changed_indices: - first_idx = changed_indices[0] - pipe = normalized_pipes[first_idx] - return self._outputs_from_pipe(pipe) - - # No hashes changed: choose the last non-None current input - last_pipe = None - for raw_pipe in inputs: - if raw_pipe is not None: - p = self._normalize_pipe(raw_pipe) - if p is not None: - last_pipe = p - - if last_pipe is not None: - return self._outputs_from_pipe(last_pipe) - - # Degenerate empty outputs if nothing valid found - return (None, None, None, None, None, None, None, None) - - -NODE_CLASS_MAPPINGS = { - "AK Pipe Loop": AKPipeLoop, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AK Pipe Loop": "AK Pipe Loop", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKProjectSettingsOut.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKProjectSettingsOut.py deleted file mode 100644 index f6c066dcafa0d055089eb83419939aa53d31391a..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKProjectSettingsOut.py +++ /dev/null @@ -1,178 +0,0 @@ -import json -import io -import base64 -import os - -from PIL import Image -import numpy as np -import torch -import folder_paths - - -_GARBAGE_SUBFOLDER = "garbage" - - -def _is_under_dir(path: str, root: str) -> bool: - try: - path_abs = os.path.abspath(path) - root_abs = os.path.abspath(root) - return os.path.commonpath([path_abs, root_abs]) == root_abs - except Exception: - return False - - -def _safe_join_under(root: str, *parts: str) -> str: - joined = os.path.abspath(os.path.join(root, *[str(p or "") for p in parts])) - if not _is_under_dir(joined, root): - return "" - return joined - - -def _load_image_to_tensor(abs_path: str): - img = Image.open(abs_path).convert("RGB") - np_img = np.array(img).astype(np.float32) / 255.0 - return torch.from_numpy(np_img)[None,] - - -def _find_first_subfolder_abs_path_by_filename(filename: str) -> str: - if not filename: - return "" - - input_dir_abs = os.path.abspath(folder_paths.get_input_directory()) - - target = os.path.basename(str(filename)).strip() - if not target: - return "" - - target_l = target.lower() - garbage_root = os.path.join(input_dir_abs, _GARBAGE_SUBFOLDER) - - for dirpath, dirnames, filenames in os.walk(input_dir_abs, topdown=False): - dirpath_abs = os.path.abspath(dirpath) - - # ignore input root - if dirpath_abs == input_dir_abs: - continue - - # ignore /input/garbage/ (and any nested subfolders) - if _is_under_dir(dirpath_abs, garbage_root): - continue - - for fn in filenames: - if str(fn).lower() == target_l: - return os.path.join(dirpath_abs, "") - - return "" - - -class AKProjectSettingsOut: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "ak_project_settings_json": ("STRING", {"default": "", "multiline": False}), - } - } - - RETURN_TYPES = ( - "STRING", - "STRING", - "INT", - "INT", - "BOOLEAN", - "IMAGE", - "STRING", - "STRING", - ) - - RETURN_NAMES = ( - "output_filename", - "output_subfolder", - "width", - "height", - "do_resize", - "image", - "image_filename", - "image_filepath", - ) - - FUNCTION = "run" - CATEGORY = "AK/settings" - DESCRIPTION = ( - "Outputs the values of fields defined in the Project Settings panel. " - "Additionally, it can resolve and output the image file path for images " - "located in ComfyUI’s native input directory, including nested subfolders." - ) - - def run(self, ak_project_settings_json): - try: - vals = json.loads(ak_project_settings_json or "{}") - except Exception: - vals = {} - - output_filename = str(vals.get("output_filename", "")) - output_subfolder = str(vals.get("output_subfolder", "")) - width = int(vals.get("width", 0) or 0) - height = int(vals.get("height", 0) or 0) - - do_resize_raw = int(vals.get("do_resize", 0) or 0) - do_resize = bool(do_resize_raw == 1) - - image = None - - open_image_filename = str(vals.get("open_image_filename") or "").strip() - open_image_subfolder = str(vals.get("open_image_subfolder") or "").strip() - open_image_type = str(vals.get("open_image_type") or "input").strip() or "input" - - image_filename = os.path.splitext(os.path.basename(open_image_filename))[0] if open_image_filename else "" - - # image_filepath: folder path (abs) where the file is found in input subfolders; ignore /input/garbage/ - image_filepath = "" - try: - if open_image_filename: - image_filepath = _find_first_subfolder_abs_path_by_filename(open_image_filename) or "" - except Exception: - image_filepath = "" - - # Prefer loading by (filename, subfolder, type) from input directory - if open_image_filename and open_image_type == "input": - try: - input_dir_abs = os.path.abspath(folder_paths.get_input_directory()) - abs_path = _safe_join_under(input_dir_abs, open_image_subfolder, open_image_filename) - if abs_path and os.path.isfile(abs_path): - image = _load_image_to_tensor(abs_path) - except Exception: - image = None - - # Backward compatibility: if JSON still contains a data URL, try loading from it - if image is None: - image_data = vals.get("open_image", "") - if isinstance(image_data, str) and image_data.startswith("data:image"): - try: - _, b64 = image_data.split(",", 1) - raw = base64.b64decode(b64) - img = Image.open(io.BytesIO(raw)).convert("RGB") - np_img = np.array(img).astype(np.float32) / 255.0 - image = torch.from_numpy(np_img)[None,] - except Exception: - image = None - - return ( - output_filename, - output_subfolder, - width, - height, - do_resize, - image, - image_filename, - image_filepath, - ) - - -NODE_CLASS_MAPPINGS = { - "AKProjectSettingsOut": AKProjectSettingsOut -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AKProjectSettingsOut": "AK Project Settings Out" -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceAlphaWithColor.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceAlphaWithColor.py deleted file mode 100644 index 4dca889ecc290b008b8bb750ac5efcc941677157..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceAlphaWithColor.py +++ /dev/null @@ -1,201 +0,0 @@ -import torch -import torch.nn.functional as F -import re - -class AKReplaceAlphaWithColor: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "color_pick_mode": (["user_color", "auto_color"], {"default": "user_color"}), - "threshold": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), - "image": ("IMAGE",), - "color_rgb": ("STRING", {"default": "8, 39, 245", "multiline": False}), - "pad_size": ("INT", {"default": 0, "min": 0, "step": 1}), - }, - "optional": { - "mask": ("MASK",), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "replace_alpha" - CATEGORY = "AK/image" - - @staticmethod - def _parse_rgb(s: str): - parts = [p for p in re.split(r"[,\s;]+", (s or "").strip()) if p != ""] - if len(parts) != 3: - return (8, 39, 245) - vals = [] - for p in parts: - try: - v = int(float(p)) - except Exception: - return (8, 39, 245) - vals.append(max(0, min(255, v))) - return tuple(vals) - - @staticmethod - def _auto_pick_color(src_rgb_01: torch.Tensor, threshold_255: int): - if src_rgb_01.dim() != 4 or src_rgb_01.shape[-1] != 3: - return None - - thr = int(threshold_255) - if thr < 0: - thr = 0 - elif thr > 255: - thr = 255 - - pix = src_rgb_01.reshape(-1, 3).clamp(0.0, 1.0) - n = int(pix.shape[0]) - if n <= 0: - return None - - max_samples = 20000 - if n > max_samples: - idx = torch.randperm(n, device=pix.device)[:max_samples] - pix = pix.index_select(0, idx) - - pix255 = (pix * 255.0).round() - - step = 16 - vals = torch.arange(0, 256, step, device=pix.device, dtype=pix255.dtype) - - try: - rr, gg, bb = torch.meshgrid(vals, vals, vals, indexing="ij") - except TypeError: - rr, gg, bb = torch.meshgrid(vals, vals, vals) - - cand = torch.stack([rr.reshape(-1), gg.reshape(-1), bb.reshape(-1)], dim=1) - m = int(cand.shape[0]) - - best_min_d2 = None - best = None - - pix255_t = pix255.t().contiguous() - p2 = (pix255 * pix255).sum(dim=1).view(1, -1) - - chunk = 256 - for s in range(0, m, chunk): - c = cand[s:s + chunk] - c2 = (c * c).sum(dim=1, keepdim=True) - cross = 2.0 * (c @ pix255_t) - d2 = (c2 + p2 - cross).clamp_min(0.0) - min_d2 = d2.min(dim=1).values - - cur_best_val = min_d2.max() - if best_min_d2 is None or cur_best_val > best_min_d2: - best_min_d2 = cur_best_val - best_idx = int(min_d2.argmax().item()) - best = c[best_idx] - - if best is None or best_min_d2 is None: - return None - - thr2 = float(thr * thr) - if float(best_min_d2.item()) <= thr2 + 1e-6: - return None - - return (int(best[0].item()), int(best[1].item()), int(best[2].item())) - - @staticmethod - def _compute_pad(h: int, w: int, pad_size: int): - p = int(pad_size) - if p <= 0: - return (0, 0, 0, 0) - - if w >= h: - add_w = p - add_h = int(round(p * (h / max(1, w)))) - else: - add_h = p - add_w = int(round(p * (w / max(1, h)))) - - left = add_w // 2 - right = add_w - left - top = add_h // 2 - bottom = add_h - top - return (left, right, top, bottom) - - def replace_alpha(self, color_pick_mode, threshold, image, color_rgb, pad_size=0, mask=None): - if image.dim() != 4: - return (image,) - - c = image.shape[-1] - if c < 3: - return (image,) - - src_rgb = image[..., :3].clamp(0.0, 1.0) - - # If a mask is provided but the input image has no alpha channel, do nothing. - if mask is not None and c < 4: - return (image,) - - mode = str(color_pick_mode or "user_color") - thr = int(threshold) if threshold is not None else 0 - - if mode == "auto_color": - picked = self._auto_pick_color(src_rgb, thr) - if picked is None: - picked = self._parse_rgb(color_rgb) - else: - picked = self._parse_rgb(color_rgb) - - color = torch.tensor([picked[0], picked[1], picked[2]], device=image.device, dtype=image.dtype) / 255.0 - color_view = color.view(1, 1, 1, 3).expand_as(src_rgb) - - if mask is not None: - # Mask must match image spatial resolution. - try: - ih = int(image.shape[1]) - iw = int(image.shape[2]) - if mask.dim() == 3: - mh, mw = int(mask.shape[1]), int(mask.shape[2]) - elif mask.dim() == 4: - mh, mw = int(mask.shape[1]), int(mask.shape[2]) - else: - return (image,) - if mh != ih or mw != iw: - return (image,) - except Exception: - return (image,) - m = mask - if m.dim() == 3: - m = m.unsqueeze(-1) - elif m.dim() == 4 and m.shape[-1] != 1: - m = m[..., :1] - m = m.to(device=image.device, dtype=image.dtype).clamp(0.0, 1.0) - out = src_rgb * (1.0 - m) + color_view * m - else: - if c < 4: - out = image - else: - alpha = image[..., 3:4].clamp(0.0, 1.0) - out = src_rgb * alpha + color_view * (1.0 - alpha) - - p = int(pad_size) if pad_size is not None else 0 - if p > 0 and out.dim() == 4 and out.shape[-1] >= 3: - b, h, w, ch = out.shape - left, right, top, bottom = self._compute_pad(h, w, p) - if left or right or top or bottom: - out_nchw = out[..., :3].permute(0, 3, 1, 2).contiguous() - padded = [] - for ci in range(3): - v = float(color[ci].item()) - ch_t = out_nchw[:, ci:ci+1, :, :] - ch_p = F.pad(ch_t, (left, right, top, bottom), mode="constant", value=v) - padded.append(ch_p) - out_nchw = torch.cat(padded, dim=1) - out = out_nchw.permute(0, 2, 3, 1).contiguous() - - return (out,) - - -NODE_CLASS_MAPPINGS = { - "AK Replace Alpha with Color": AKReplaceAlphaWithColor, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AK Replace Alpha with Color": "AK Replace Alpha with Color", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceColorWithAlpha.py b/custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceColorWithAlpha.py deleted file mode 100644 index 4f930b74025f58a903d23fcee423a9a9f4fd0601..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceColorWithAlpha.py +++ /dev/null @@ -1,153 +0,0 @@ -import torch -import re - -class AKReplaceColorWithAlpha: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "crop_size": ("INT", {"default": 0, "min": 0, "step": 1}), - "color_pick_mode": (["user_color", "left_top_pixel", "right_top_pixel", "left_bottom_pixel", "right_bottom_pixel"], {"default": "user_color"}), - "color_rgb": ("STRING", {"default": "8, 39, 245", "multiline": False}), - "threshold": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), - "softness": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "replace_color" - CATEGORY = "AK/image" - - @staticmethod - def _parse_rgb(s: str): - parts = [p for p in re.split(r"[,\s;]+", (s or "").strip()) if p != ""] - if len(parts) != 3: - return (8, 39, 245) - vals = [] - for p in parts: - try: - v = int(float(p)) - except Exception: - return (8, 39, 245) - vals.append(max(0, min(255, v))) - return tuple(vals) - - @staticmethod - def _pick_corner_color(src_rgb: torch.Tensor, mode: str): - # src_rgb: [B, H, W, 3] in 0..1 - _, h, w, _ = src_rgb.shape - if mode == "left_top_pixel": - y, x = 0, 0 - elif mode == "right_top_pixel": - y, x = 0, max(0, w - 1) - elif mode == "left_bottom_pixel": - y, x = max(0, h - 1), 0 - elif mode == "right_bottom_pixel": - y, x = max(0, h - 1), max(0, w - 1) - else: - y, x = 0, 0 - return src_rgb[0, y, x, :].clamp(0.0, 1.0) - - @staticmethod - def _compute_crop(h: int, w: int, crop_size: int): - p = int(crop_size) - if p <= 0: - return (0, 0, 0, 0) - - # Preserve aspect: shrink the larger dimension by p, shrink the other proportionally. - if w >= h: - sub_w = min(p, max(0, w - 1)) - sub_h = int(round(sub_w * (h / max(1, w)))) - else: - sub_h = min(p, max(0, h - 1)) - sub_w = int(round(sub_h * (w / max(1, h)))) - - left = sub_w // 2 - right = sub_w - left - top = sub_h // 2 - bottom = sub_h - top - return (left, right, top, bottom) - - @staticmethod - def _center_crop(img: torch.Tensor, crop_size: int): - # img: [B,H,W,C] - if img is None or img.dim() != 4: - return img - b, h, w, c = img.shape - left, right, top, bottom = AKReplaceColorWithAlpha._compute_crop(h, w, crop_size) - if left == 0 and right == 0 and top == 0 and bottom == 0: - return img - y0 = top - y1 = h - bottom - x0 = left - x1 = w - right - if y1 <= y0 or x1 <= x0: - return img - return img[:, y0:y1, x0:x1, :] - - def replace_color(self, image, crop_size, color_pick_mode, color_rgb, threshold, softness): - if image.dim() != 4: - return (image,) - - cs = int(crop_size) if crop_size is not None else 0 - if cs > 0: - image = self._center_crop(image, cs) - - c = image.shape[-1] - if c < 3: - return (image,) - - src_rgb = image[..., :3].clamp(0.0, 1.0) - - if c >= 4: - src_a = image[..., 3:4].clamp(0.0, 1.0) - else: - src_a = torch.ones_like(image[..., :1]) - - mode = str(color_pick_mode or "user_color") - - if mode == "user_color": - rgb = self._parse_rgb(color_rgb) - target = torch.tensor([rgb[0], rgb[1], rgb[2]], device=image.device, dtype=image.dtype) / 255.0 - else: - target = self._pick_corner_color(src_rgb, mode).to(device=image.device, dtype=image.dtype) - - thr = int(threshold) if threshold is not None else 0 - soft = int(softness) if softness is not None else 0 - if thr < 0: - thr = 0 - elif thr > 255: - thr = 255 - if soft < 0: - soft = 0 - elif soft > 255: - soft = 255 - - t0 = float(thr) / 255.0 - t1 = float(min(255, thr + soft)) / 255.0 - - d2 = (src_rgb - target.view(1, 1, 1, 3)).pow(2).sum(dim=-1, keepdim=True) - - if soft <= 0: - sel = d2 <= (t0 * t0 + 1e-12) - out_a = torch.where(sel, torch.zeros_like(src_a), src_a) - else: - d = torch.sqrt(d2 + 1e-12) - denom = t1 - t0 - if denom <= 1e-8: - denom = 1e-8 - alpha_factor = ((d - t0) / denom).clamp(0.0, 1.0) - out_a = (src_a * alpha_factor).clamp(0.0, 1.0) - - out = torch.cat([src_rgb, out_a], dim=-1) - return (out,) - - -NODE_CLASS_MAPPINGS = { - "AK Replace Color with Alpha": AKReplaceColorWithAlpha, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AK Replace Color with Alpha": "AK Replace Color with Alpha", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeAndCombineCached.py b/custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeAndCombineCached.py deleted file mode 100644 index a42fe037f923ba42452d21034f93104f81d09164..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeAndCombineCached.py +++ /dev/null @@ -1,68 +0,0 @@ -class CLIPTextEncodeAndCombineCached: - _last_text = None - _last_encoded = None - _last_clip_id = None - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "clip": ("CLIP",), - "text": ("STRING", {"multiline": True, "default": ""}), - }, - "optional": { - "conditioning": ("CONDITIONING",), - }, - } - - RETURN_TYPES = ("CONDITIONING",) - RETURN_NAMES = ("conditioning",) - FUNCTION = "execute" - CATEGORY = "AK/conditioning" - OUTPUT_NODE = False - - @classmethod - def _normalize_text(cls, text): - if text is None: - return "" - return str(text).replace("\r\n", "\n").replace("\r", "\n") - - @classmethod - def _has_meaningful_text(cls, text): - return bool(text and text.strip()) - - @classmethod - def _encode_cached(cls, clip, text): - clip_id = id(clip) - if cls._last_clip_id == clip_id and cls._last_text == text and cls._last_encoded is not None: - return cls._last_encoded - - tokens = clip.tokenize(text) - cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) - conditioning = [[cond, {"pooled_output": pooled}]] - - cls._last_clip_id = clip_id - cls._last_text = text - cls._last_encoded = conditioning - - return conditioning - - @classmethod - def execute(cls, clip, text, conditioning=None): - text = cls._normalize_text(text) - - if not cls._has_meaningful_text(text): - if conditioning is not None: - return (conditioning,) - return ([],) - - new_cond = cls._encode_cached(clip, text) - - if conditioning is None: - return (new_cond,) - - return (conditioning + new_cond,) - - -NODE_CLASS_MAPPINGS = {"CLIPTextEncodeAndCombineCached": CLIPTextEncodeAndCombineCached} -NODE_DISPLAY_NAME_MAPPINGS = {"CLIPTextEncodeAndCombineCached": "CLIP Text Encode & Combine (Cached)"} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeCached.py b/custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeCached.py deleted file mode 100644 index b08f3343924f036657f7d300e2c47d772ba20e69..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeCached.py +++ /dev/null @@ -1,50 +0,0 @@ -class CLIPTextEncodeCached: - # Runtime cache: text_hash -> conditioning - _last_text = None - _last_cond = None - _last_clip_id = None - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "clip": ("CLIP",), - "text": ( - "STRING", - {"multiline": True, "default": "", "forceInput": True}, - ), - } - } - - RETURN_TYPES = ("CONDITIONING",) - RETURN_NAMES = ("conditioning",) - FUNCTION = "execute" - CATEGORY = "AK/conditioning" - OUTPUT_NODE = False - - @classmethod - def execute(cls, clip, text): - if text is None: - text = "" - - text = text.replace("\r\n", "\n").replace("\r", "\n") - - clip_id = id(clip) - - if cls._last_clip_id == clip_id and cls._last_text == text and cls._last_cond is not None: - return (cls._last_cond,) - - tokens = clip.tokenize(text) - cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) - conditioning = [[cond, {"pooled_output": pooled}]] - - cls._last_clip_id = clip_id - cls._last_text = text - cls._last_cond = conditioning - - return (conditioning,) - - -NODE_CLASS_MAPPINGS = {"CLIPTextEncodeCached": CLIPTextEncodeCached} - -NODE_DISPLAY_NAME_MAPPINGS = {"CLIPTextEncodeCached": "CLIP Text Encode (Cached)"} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/Getter.py b/custom_nodes/ComfyUI-AK-Pack/nodes/Getter.py deleted file mode 100644 index 49ed03be67243be5c2cf6a0283db31d6dc5f3681..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/Getter.py +++ /dev/null @@ -1,38 +0,0 @@ -class AnyType(str): - def __ne__(self, __value: object) -> bool: - return False - -ANY_TYPE = AnyType("*") - - -class Getter: - @classmethod - def INPUT_TYPES(cls): - return { - "required": {}, - "optional": { - "inp": (ANY_TYPE,), - }, - "hidden": { - "var_name": "STRING", - "unique_id": "UNIQUE_ID", - }, - } - - RETURN_TYPES = (ANY_TYPE,) - RETURN_NAMES = ("OBJ",) - FUNCTION = "get" - CATEGORY = "AK/pipe" - - def get(self, inp=None, var_name="", unique_id=None): - if inp is None: - raise Exception(f"[Getter {unique_id} {var_name}] inp is not connected") - return (inp,) - - -NODE_CLASS_MAPPINGS = { - "Getter": Getter, -} -NODE_DISPLAY_NAME_MAPPINGS = { - "Getter": "Getter", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/IsOneOfGroupsActive.py b/custom_nodes/ComfyUI-AK-Pack/nodes/IsOneOfGroupsActive.py deleted file mode 100644 index 13324ecb7d8398ed84044fce194f4712f886e8d8..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/IsOneOfGroupsActive.py +++ /dev/null @@ -1,43 +0,0 @@ -import math - -class IsOneOfGroupsActive: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "group_name_contains": ( - "STRING", - { - "default": "", - "multiline": False, - }, - ), - "active_state": ( - "BOOLEAN", - { - "default": False, - }, - ), - } - } - - RETURN_TYPES = ("BOOLEAN",) - RETURN_NAMES = ("boolean",) - FUNCTION = "pass_state" - CATEGORY = "AK/logic" - - @classmethod - def IS_CHANGED(cls, group_name_contains, active_state): - return float("nan") - - def pass_state(self, group_name_contains, active_state): - return (active_state,) - - -NODE_CLASS_MAPPINGS = { - "IsOneOfGroupsActive": IsOneOfGroupsActive, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "IsOneOfGroupsActive": "IsOneOfGroupsActive", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/PreviewRawText.py b/custom_nodes/ComfyUI-AK-Pack/nodes/PreviewRawText.py deleted file mode 100644 index d8ad156847a9b2a297178784c8a5579d1ebb7e4f..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/PreviewRawText.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -import json -import logging - -logger = logging.getLogger(__name__) - - -class PreviewRawText: - @classmethod - def INPUT_TYPES(cls): - return { - "required": {}, - "optional": { - "text": ("STRING", {"forceInput": True}), - "list_values": ( - "LIST", - { - "element_type": "STRING", - "forceInput": True, - }, - ), - }, - "hidden": { - "unique_id": "UNIQUE_ID", - }, - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "notify" - OUTPUT_NODE = True - - CATEGORY = "AK/utils" - - @staticmethod - def _pretty_json_if_possible(value: str) -> str: - if value is None: - return "" - s = value.strip() - if not s: - return value - if s[0] not in "{[": - return value - try: - obj = json.loads(s) - except Exception: - return value - try: - return json.dumps(obj, ensure_ascii=False, indent=2) - except Exception: - return value - - def notify(self, text=None, list_values=None, unique_id=None): - base_text = "" if text is None else str(text) - base_text = self._pretty_json_if_possible(base_text) - - def normalize_list_values(value): - if value is None: - return "" - if not isinstance(value, list): - v = "NONE" if value is None else str(value) - return self._pretty_json_if_possible(v) - - lines = [] - - for v in value: - if isinstance(v, list): - for inner in v: - if inner is None: - lines.append("NONE") - else: - lines.append(self._pretty_json_if_possible(str(inner))) - else: - if v is None: - lines.append("NONE") - else: - lines.append(self._pretty_json_if_possible(str(v))) - - return "\n\n".join(lines) - - list_text = normalize_list_values(list_values) - - if base_text and list_text: - final_text = base_text + "\n\n" + list_text - elif list_text: - final_text = list_text - else: - final_text = base_text - - return {"ui": {"text": final_text}, "result": (final_text,)} - - -NODE_CLASS_MAPPINGS = { - "PreviewRawText": PreviewRawText, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "PreviewRawText": "Preview Raw Text", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/RepeatGroupState.py b/custom_nodes/ComfyUI-AK-Pack/nodes/RepeatGroupState.py deleted file mode 100644 index fd30a7c5f0c45470ef6e691a23a68ffd9063b6b9..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/RepeatGroupState.py +++ /dev/null @@ -1,35 +0,0 @@ -class RepeatGroupState: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "group_name_contains": ( - "STRING", - { - "default": "", - "multiline": False, - }, - ), - } - } - - RETURN_TYPES = () - RETURN_NAMES = () - FUNCTION = "do_nothing" - CATEGORY = "AK/logic" - - @classmethod - def IS_CHANGED(cls, group_name_contains): - return float("nan") - - def do_nothing(self, group_name_contains): - return () - - -NODE_CLASS_MAPPINGS = { - "RepeatGroupState": RepeatGroupState, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "RepeatGroupState": "Repeat Group State", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/nodes/Setter.py b/custom_nodes/ComfyUI-AK-Pack/nodes/Setter.py deleted file mode 100644 index 3f5f91fb808d395527431cbe5deb99c6addc0487..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/nodes/Setter.py +++ /dev/null @@ -1,136 +0,0 @@ -import sys, types, bisect - -_STORE_KEY = "ak_var_nodes_store" - -class AnyType(str): - def __ne__(self, __value: object) -> bool: - return False - -ANY_TYPE = AnyType("*") - -def _get_store(): - st = sys.modules.get(_STORE_KEY) - if st is None: - st = types.SimpleNamespace( - last_prompt_obj_id=None, - allowed_ids_by_name={}, - values_by_name={}, - names_sorted=[], - last_name_by_setter_id={} - ) - sys.modules[_STORE_KEY] = st - if not hasattr(st, "last_name_by_setter_id"): - st.last_name_by_setter_id = {} - return st - -def _rebuild_from_prompt(st, prompt): - st.allowed_ids_by_name.clear() - st.names_sorted.clear() - valid_names = set() - - try: - if isinstance(prompt, dict): - for node_id, node in prompt.items(): - if not isinstance(node, dict): - continue - if node.get("class_type") != "Setter": - continue - inputs = node.get("inputs") or {} - name = inputs.get("var_name", "") - if name is None: - name = "" - if not isinstance(name, str): - name = str(name) - name = name.strip() - if not name: - continue - - sid = str(node_id) - old_name = st.last_name_by_setter_id.get(sid) - - if old_name and old_name != name: - if name not in st.values_by_name: - v = st.values_by_name.get(old_name, None) - if v is not None: - st.values_by_name[name] = v - if old_name in st.values_by_name and old_name != name: - del st.values_by_name[old_name] - - st.last_name_by_setter_id[sid] = name - - valid_names.add(name) - if name not in st.allowed_ids_by_name: - st.allowed_ids_by_name[name] = sid - if name not in st.values_by_name: - st.values_by_name[name] = None - bisect.insort(st.names_sorted, name) - - for k in list(st.values_by_name.keys()): - if k not in valid_names: - del st.values_by_name[k] - - for sid, nm in list(st.last_name_by_setter_id.items()): - if nm not in valid_names: - del st.last_name_by_setter_id[sid] - except Exception: - st.allowed_ids_by_name.clear() - st.names_sorted.clear() - -class Setter: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "obj": (ANY_TYPE,), - "var_name": ("STRING", {"default": ""}), - }, - "hidden": { - "prompt": "PROMPT", - "unique_id": "UNIQUE_ID", - } - } - - RETURN_TYPES = (ANY_TYPE,) - RETURN_NAMES = ("OUT",) - FUNCTION = "set" - OUTPUT_NODE = True - CATEGORY = "AK/pipe" - - def set(self, obj, var_name, prompt=None, unique_id=None): - st = _get_store() - - pid = None - try: - pid = id(prompt) - except Exception: - pid = None - - if pid is not None and pid != st.last_prompt_obj_id: - st.last_prompt_obj_id = pid - _rebuild_from_prompt(st, prompt) - - if var_name is None: - var_name = "" - if not isinstance(var_name, str): - var_name = str(var_name) - name = var_name.strip() - if not name: - raise Exception(f"[Setter {unique_id}] var_name is empty") - - if name not in st.allowed_ids_by_name: - st.allowed_ids_by_name[name] = str(unique_id) - st.values_by_name[name] = None - bisect.insort(st.names_sorted, name) - - # value = obj[0] if isinstance(obj, (list, tuple)) else obj - value = obj - st.values_by_name[name] = value - return (value,) - -NODE_CLASS_MAPPINGS = { - "Setter": Setter, -} -NODE_DISPLAY_NAME_MAPPINGS = { - "Setter": "Setter", -} diff --git a/custom_nodes/ComfyUI-AK-Pack/pyproject.toml b/custom_nodes/ComfyUI-AK-Pack/pyproject.toml deleted file mode 100644 index 97d9ef6cec005244169bd2cac3f3ffc67c104e3b..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-AK-Pack/pyproject.toml +++ /dev/null @@ -1,28 +0,0 @@ -[project] -name = "comfyui-ak-pack" -publisher = "akawana2" -version = "3.01" -frontend_version = "3.01" -description = "Utility tools. Index Multiple, CLIP Multiple, Pipe, Getter, Setter." -readme = "README.md" -requires-python = ">=3.10" - - -packages = ["nodes"] - -[project.urls] -Repository = "https://github.com/akawana/ComfyUI-AK-Pack" -Documentation = "https://github.com/akawana/ComfyUI-AK-Pack/wiki" -"Bug Tracker" = "https://github.com/akawana/ComfyUI-AK-Pack/issues" - -[tool.comfy_node] -package = "nodes" -publisher = "akawana2" -display_name = "AK Pack" -summary = "Index Multiple, CLIP Multiple, Pipe, Getter, Setter." -tags = ["utility", "list", "group", "clip"] - -[tool.comfy] -PublisherId = "akawana2" # TODO (fill in Publisher ID from Comfy Registry Website). -DisplayName = "AK Pack" # Display name for the Custom Node. Can be changed later. -Icon = "https://raw.githubusercontent.com/akawana/ComfyUI-Utils-extra/refs/heads/main/icon.jpg" \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/classes/AD_AnyFileList.py b/custom_nodes/ComfyUI-Addoor/classes/AD_AnyFileList.py deleted file mode 100644 index c33ba3bbe0557b07190c6b617d7a7cfb066ae953..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/classes/AD_AnyFileList.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import torch -import numpy as np -from PIL import Image -import csv - -class AD_AnyFileList: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "Directory": ("STRING", {"default": ""}), - "Load_Cap": ("INT", {"default": 100, "min": 1, "max": 1000}), - "Skip_Frame": ("INT", {"default": 0, "min": 0, "max": 100}), - "Filter_By": (["*", "images", "text"],), - "Extension": ("STRING", {"default": "*"}), - "Deep_Search": ("BOOLEAN", {"default": False}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}) - } - } - - RETURN_TYPES = ("STRING", "IMAGE", "STRING", "STRING", "STRING", "STRING", "INT", "STRING") - RETURN_NAMES = ("Directory_Output", "Image_List", "Text_List", "File_Path_List", "File_Name_List", "File_Name_With_Extension_List", "Total_Files", "Merged_Text") - FUNCTION = "process_files" - OUTPUT_NODE = True - OUTPUT_IS_LIST = (False, True, True, True, True, True, False, False) - CATEGORY = "🌻 Addoor/Batch" - - def process_files(self, Directory, Load_Cap, Skip_Frame, Filter_By, Extension, Deep_Search, seed): - file_extensions = { - "images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"], - "text": [".txt", ".csv", ".md", ".json"], - } - - file_paths = [] - if Deep_Search: - for root, _, files in os.walk(Directory): - for file in files: - file_path = os.path.join(root, file) - file_extension = os.path.splitext(file)[1].lower() - if (Filter_By == "*" or file_extension in file_extensions.get(Filter_By, [])) and \ - (Extension == "*" or file.lower().endswith(Extension.lower())): - file_paths.append(file_path) - else: - for file in os.listdir(Directory): - file_path = os.path.join(Directory, file) - if os.path.isfile(file_path): - file_extension = os.path.splitext(file)[1].lower() - if (Filter_By == "*" or file_extension in file_extensions.get(Filter_By, [])) and \ - (Extension == "*" or file.lower().endswith(Extension.lower())): - file_paths.append(file_path) - - file_paths = sorted(file_paths)[Skip_Frame:Skip_Frame + Load_Cap] - - image_list = [] - text_list = [] - file_name_list = [] - file_name_with_extension_list = [] - merged_text = "" - - for file_path in file_paths: - file_extension = os.path.splitext(file_path)[1].lower() - if file_extension in file_extensions["images"]: - try: - img = Image.open(file_path).convert("RGB") - image = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0) - image_list.append(image) - except Exception as e: - print(f"Error loading image '{file_path}': {e}") - elif file_extension in file_extensions["text"]: - try: - if file_extension == '.csv': - with open(file_path, 'r', newline='', encoding='utf-8') as csvfile: - content = '\n'.join([','.join(row) for row in csv.reader(csvfile)]) - else: - with open(file_path, 'r', encoding='utf-8') as txtfile: - content = txtfile.read() - text_list.append(content) - merged_text += content + "\n\n" - except Exception as e: - print(f"Error reading file '{file_path}': {e}") - - file_name_list.append(os.path.splitext(os.path.basename(file_path))[0]) - file_name_with_extension_list.append(os.path.basename(file_path)) - - total_files = len(file_paths) - return (Directory, image_list, text_list, file_paths, file_name_list, file_name_with_extension_list, total_files, merged_text.strip()) - diff --git a/custom_nodes/ComfyUI-Addoor/classes/AD_BatchImageLoadFromDir.py b/custom_nodes/ComfyUI-Addoor/classes/AD_BatchImageLoadFromDir.py deleted file mode 100644 index d7604f2be1545f53faca1f6f7bcc9fd257bbfd98..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/classes/AD_BatchImageLoadFromDir.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import torch -import numpy as np -from PIL import Image - -class AD_BatchImageLoadFromDir: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "Directory": ("STRING", {"default": ""}), - "Load_Cap": ("INT", {"default": 100, "min": 1, "max": 1000}), - "Skip_Frame": ("INT", {"default": 0, "min": 0, "max": 100}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}) - } - } - - RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING", "INT") - RETURN_NAMES = ("Images", "Image_Paths", "Image_Names_suffix", "Image_Names", "Count") - FUNCTION = "load_images" - OUTPUT_NODE = True - OUTPUT_IS_LIST = (True, True, True, True, False) - CATEGORY = "🌻 Addoor/Batch" - - def load_images(self, Directory, Load_Cap, Skip_Frame, seed): - image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp'] - file_paths = [] - for root, dirs, files in os.walk(Directory): - for file in files: - if any(file.lower().endswith(ext) for ext in image_extensions): - file_paths.append(os.path.join(root, file)) - file_paths = sorted(file_paths)[Skip_Frame:Skip_Frame + Load_Cap] - - images = [] - image_paths = [] - image_names_suffix = [] - image_names = [] - - for file_path in file_paths: - try: - img = Image.open(file_path).convert("RGB") - image = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0) - images.append(image) - image_paths.append(file_path) - image_names_suffix.append(os.path.basename(file_path)) - image_names.append(os.path.splitext(os.path.basename(file_path))[0]) - except Exception as e: - print(f"Error loading image '{file_path}': {e}") - - count = len(images) - return (images, image_paths, image_names_suffix, image_names, count) - diff --git a/custom_nodes/ComfyUI-Addoor/classes/AD_LoadImageAdvanced.py b/custom_nodes/ComfyUI-Addoor/classes/AD_LoadImageAdvanced.py deleted file mode 100644 index 25bc80889a69835244ba7b7a40f7b8c9b7ea1504..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/classes/AD_LoadImageAdvanced.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -import hashlib -from PIL import Image, ImageSequence, ImageOps -import numpy as np -import torch -import folder_paths - -class AD_LoadImageAdvanced: - @classmethod - def INPUT_TYPES(s): - input_dir = folder_paths.get_input_directory() - - files = [] - for root, dirs, filenames in os.walk(input_dir): - for filename in filenames: - full_path = os.path.join(root, filename) - relative_path = os.path.relpath(full_path, input_dir) - relative_path = relative_path.replace("\\", "/") - files.append(relative_path) - - return {"required": - {"image": (sorted(files), {"image_upload": True}), - "strip_extension": ("BOOLEAN", {"default": True}), - "rgba": ("BOOLEAN", {"default": False})} - } - - CATEGORY = "🌻 Addoor/Image" - - RETURN_TYPES = ("IMAGE", "MASK", "STRING") - RETURN_NAMES = ("IMAGE", "MASK", "FILENAME") - - FUNCTION = "load_image" - - def load_image(self, image, strip_extension, rgba): - image_path = folder_paths.get_annotated_filepath(image) - img = Image.open(image_path) - output_images = [] - output_masks = [] - for i in ImageSequence.Iterator(img): - i = ImageOps.exif_transpose(i) - if i.mode == 'I': - i = i.point(lambda i: i * (1 / 255)) - if rgba: - image = i.convert("RGBA") - image = np.array(image).astype(np.float32) / 255.0 - image = torch.from_numpy(image)[None,] - else: - image = i.convert("RGB") - image = np.array(image).astype(np.float32) / 255.0 - image = torch.from_numpy(image)[None,] - if 'A' in i.getbands(): - mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 - mask = 1. - torch.from_numpy(mask) - else: - mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") - output_images.append(image) - output_masks.append(mask.unsqueeze(0)) - - if len(output_images) > 1: - output_image = torch.cat(output_images, dim=0) - output_mask = torch.cat(output_masks, dim=0) - else: - output_image = output_images[0] - output_mask = output_masks[0] - - if strip_extension: - filename = os.path.splitext(image_path)[0] - else: - filename = os.path.basename(image_path) - - return (output_image, output_mask, filename,) - - @classmethod - def IS_CHANGED(s, image, strip_extension, rgba): - image_path = folder_paths.get_annotated_filepath(image) - m = hashlib.sha256() - with open(image_path, 'rb') as f: - m.update(f.read()) - return m.digest().hex() - - @classmethod - def VALIDATE_INPUTS(s, image, strip_extension, rgba): - if not folder_paths.exists_annotated_filepath(image): - return "Invalid image file: {}".format(image) - - return True - diff --git a/custom_nodes/ComfyUI-Addoor/classes/AD_PromptReplace.py b/custom_nodes/ComfyUI-Addoor/classes/AD_PromptReplace.py deleted file mode 100644 index 7f0133e20218f59ac6572fb38a645e8376e2dff2..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/classes/AD_PromptReplace.py +++ /dev/null @@ -1,59 +0,0 @@ -import random - -class AD_PromptReplace: - def __init__(self): - self.current_index = None - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "Content": ("STRING", {"default": "", "multiline": True}), - "Match": ("STRING", {"default": ""}), - "Replace": ("STRING", {"default": "", "multiline": True}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - "Increment": ("INT", {"default": 1, "min": 0, "max": 1000}), - } - } - - RETURN_TYPES = ("STRING", "INT") - RETURN_NAMES = ("Replaced_Text", "new_seed") - FUNCTION = "replace_prompt" - CATEGORY = "🌻 Addoor/Utilities" - - def replace_prompt(self, Content: str, Match: str, Replace: str, seed: int, Increment: int): - content_lines = Content.split('\n') - replace_lines = Replace.split('\n') if Replace else [] - - if not content_lines: - content_lines = [""] - if not replace_lines: - replace_lines = [""] - - # Initialize current_index if it's None - if self.current_index is None: - if Increment == 0: - # Random starting point - random.seed(seed) - self.current_index = random.randint(0, len(replace_lines) - 1) - else: - # Start from the beginning - self.current_index = 0 - - # Select a line based on increment - if Increment == 0: - # Random selection using seed - random.seed(seed) - selected_index = random.randint(0, len(replace_lines) - 1) - else: - # Sequential selection based on increment - selected_index = self.current_index - self.current_index = (self.current_index + Increment) % len(replace_lines) - - selected_replace = replace_lines[selected_index] - - # Perform text replacement - replaced_content = Content.replace(Match, selected_replace) if Match else Content - - return (replaced_content, seed) - diff --git a/custom_nodes/ComfyUI-Addoor/example_workflow/RunningHub API.json b/custom_nodes/ComfyUI-Addoor/example_workflow/RunningHub API.json deleted file mode 100644 index a9fced997ae04f69e0f511247fe98ad36fab16e6..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/example_workflow/RunningHub API.json +++ /dev/null @@ -1,500 +0,0 @@ -{ - "last_node_id": 11, - "last_link_id": 8, - "nodes": [ - { - "id": 3, - "type": "RunningHubWorkflowExecutor", - "pos": [ - 1814.84912109375, - 670.4561157226562 - ], - "size": [ - 352.79998779296875, - 318 - ], - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "node_info_list", - "type": "NODEINFOLIST", - "link": 2, - "label": "node_info_list" - }, - { - "name": "api_key", - "type": "STRING", - "link": 1, - "widget": { - "name": "api_key" - }, - "label": "api_key" - }, - { - "name": "workflow_id", - "type": "STRING", - "link": 4, - "widget": { - "name": "workflow_id" - }, - "label": "workflow_id" - } - ], - "outputs": [ - { - "name": "file_urls", - "type": "FILEURL_LIST", - "links": [ - 5 - ], - "label": "file_urls", - "slot_index": 0 - }, - { - "name": "task_id", - "type": "STRING", - "links": null, - "label": "task_id" - }, - { - "name": "msg", - "type": "STRING", - "links": null, - "label": "msg" - }, - { - "name": "promptTips", - "type": "STRING", - "links": null, - "label": "promptTips" - }, - { - "name": "taskStatus", - "type": "STRING", - "links": null, - "label": "taskStatus" - }, - { - "name": "fileType", - "type": "STRING", - "links": null, - "label": "fileType" - }, - { - "name": "code", - "type": "STRING", - "links": null, - "label": "code" - }, - { - "name": "json", - "type": "STRING", - "links": null, - "label": "json" - } - ], - "properties": { - "Node name for S&R": "RunningHubWorkflowExecutor" - }, - "widgets_values": [ - "", - "", - 34390019738778, - "randomize", - 60, - 5 - ] - }, - { - "id": 9, - "type": "RunningHubImageUploader", - "pos": [ - 621.948974609375, - 753.6560668945312 - ], - "size": [ - 315, - 58 - ], - "flags": {}, - "order": 0, - "mode": 0, - "inputs": [ - { - "name": "image", - "type": "IMAGE", - "link": null, - "label": "image" - } - ], - "outputs": [ - { - "name": "filename", - "type": "STRING", - "links": null, - "label": "filename" - } - ], - "properties": { - "Node name for S&R": "RunningHubImageUploader" - }, - "widgets_values": [ - "" - ] - }, - { - "id": 6, - "type": "RunningHubWorkflowID", - "pos": [ - 1438.5477294921875, - 882.5560913085938 - ], - "size": [ - 315, - 58 - ], - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 4 - ], - "slot_index": 0, - "label": "STRING" - } - ], - "properties": { - "Node name for S&R": "RunningHubWorkflowID" - }, - "widgets_values": [ - "1867408429454426113" - ] - }, - { - "id": 4, - "type": "RunningHubAccountStatus", - "pos": [ - 1819.9490966796875, - 1044.7562255859375 - ], - "size": [ - 341, - 54 - ], - "flags": {}, - "order": 5, - "mode": 0, - "inputs": [ - { - "name": "api_key", - "type": "STRING", - "link": 3, - "widget": { - "name": "api_key" - }, - "label": "api_key" - } - ], - "outputs": [ - { - "name": "remain_coins", - "type": "INT", - "links": null, - "label": "remain_coins", - "slot_index": 0 - }, - { - "name": "current_task_counts", - "type": "INT", - "links": null, - "label": "current_task_counts" - } - ], - "properties": { - "Node name for S&R": "RunningHubAccountStatus" - }, - "widgets_values": [ - "" - ] - }, - { - "id": 8, - "type": "RunningHubNodeInfoReplace", - "pos": [ - 1024.0535888671875, - 956.73486328125 - ], - "size": [ - 400, - 338 - ], - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "node_info", - "type": "NODEINFO", - "links": null, - "label": "node_info" - }, - { - "name": "new_seed", - "type": "INT", - "links": null, - "label": "new_seed" - } - ], - "properties": { - "Node name for S&R": "RunningHubNodeInfoReplace" - }, - "widgets_values": [ - "", - "", - "", - "", - "", - "", - 842958099223905, - "randomize", - 1, - "normal" - ] - }, - { - "id": 11, - "type": "PreviewImage", - "pos": [ - 2522.2890625, - 673.9710693359375 - ], - "size": [ - 355.91650390625, - 358.8138427734375 - ], - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 8, - "label": "图像" - } - ], - "outputs": [], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, - { - "id": 5, - "type": "RunningHubFileSaver", - "pos": [ - 2187.75634765625, - 669.653564453125 - ], - "size": [ - 315, - 218 - ], - "flags": {}, - "order": 7, - "mode": 0, - "inputs": [ - { - "name": "file_urls", - "type": "FILEURL_LIST", - "link": 5, - "label": "file_urls" - } - ], - "outputs": [ - { - "name": "output_paths", - "type": "STRING", - "links": [], - "label": "output_paths", - "slot_index": 0 - }, - { - "name": "previews", - "type": "IMAGE", - "links": [ - 8 - ], - "label": "previews", - "slot_index": 1 - }, - { - "name": "file_types", - "type": "STRING", - "links": null, - "label": "file_types" - } - ], - "properties": { - "Node name for S&R": "RunningHubFileSaver" - }, - "widgets_values": [ - "outputs", - "RH_Output", - "_", - 4, - "auto", - false - ] - }, - { - "id": 1, - "type": "RunningHubInit", - "pos": [ - 1439.84912109375, - 768.4561157226562 - ], - "size": [ - 315, - 58 - ], - "flags": { - "collapsed": false - }, - "order": 3, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 1, - 3 - ], - "slot_index": 0, - "label": "STRING" - } - ], - "properties": { - "Node name for S&R": "RunningHubInit" - }, - "widgets_values": [ - "" - ] - }, - { - "id": 2, - "type": "RunningHubNodeInfo", - "pos": [ - 1021.8431396484375, - 672.1781005859375 - ], - "size": [ - 400, - 200 - ], - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [ - { - "name": "previous_node_info", - "type": "NODEINFOLIST", - "link": null, - "shape": 7, - "label": "previous_node_info" - } - ], - "outputs": [ - { - "name": "NODEINFOLIST", - "type": "NODEINFOLIST", - "links": [ - 2 - ], - "slot_index": 0, - "label": "NODEINFOLIST" - } - ], - "properties": { - "Node name for S&R": "RunningHubNodeInfo" - }, - "widgets_values": [ - "6", - "text", - "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,", - "" - ] - } - ], - "links": [ - [ - 1, - 1, - 0, - 3, - 1, - "STRING" - ], - [ - 2, - 2, - 0, - 3, - 0, - "NODEINFOLIST" - ], - [ - 3, - 1, - 0, - 4, - 0, - "STRING" - ], - [ - 4, - 6, - 0, - 3, - 2, - "STRING" - ], - [ - 5, - 3, - 0, - 5, - 0, - "FILEURL_LIST" - ], - [ - 8, - 5, - 1, - 11, - 0, - "IMAGE" - ] - ], - "groups": [], - "config": {}, - "extra": { - "ds": { - "scale": 0.8264462809917354, - "offset": [ - -509.6418520759915, - -255.05204017404154 - ] - } - }, - "version": 0.4 -} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/nodes/AddPaddingAdvanced.py b/custom_nodes/ComfyUI-Addoor/nodes/AddPaddingAdvanced.py deleted file mode 100644 index 281875a21c8d718e4f90160224a12ecbbc718bd3..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/AddPaddingAdvanced.py +++ /dev/null @@ -1,435 +0,0 @@ -""" -@author: ComfyNodePRs -@title: ComfyUI Advanced Padding -@description: Advanced padding node with scaling capabilities -@version: 1.0.0 -@project: https://github.com/ComfyNodePRs/advanced-padding -@author: https://github.com/ComfyNodePRs -""" - -import PIL.Image as Image -import PIL.ImageDraw as ImageDraw -import numpy as np -import torch -import torchvision.transforms as t - - -class AD_PaddingAdvanced: - def __init__(self): - pass - - FUNCTION = "process_image" - CATEGORY = "🌻 Addoor/image" - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "scale_by": ("FLOAT", { - "default": 1.0, - "min": 0.1, - "max": 8.0, - "step": 0.05, - "display": "number" - }), - "upscale_method": (["nearest-exact", "bilinear", "bicubic", "lanczos"], {"default": "lanczos"}), - "left": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "top": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "right": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "bottom": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "color": ("STRING", {"default": "#ffffff"}), - "transparent": ("BOOLEAN", {"default": False}), - }, - "optional": { - "background": ("IMAGE",), - } - } - - RETURN_TYPES = ("IMAGE", "MASK") - RETURN_NAMES = ("image", "mask") - - def add_padding(self, image, left, top, right, bottom, color="#ffffff", transparent=False): - padded_images = [] - image = [self.tensor2pil(img) for img in image] - for img in image: - padded_image = Image.new("RGBA" if transparent else "RGB", - (img.width + left + right, img.height + top + bottom), - (0, 0, 0, 0) if transparent else self.hex_to_tuple(color)) - padded_image.paste(img, (left, top)) - padded_images.append(self.pil2tensor(padded_image)) - return torch.cat(padded_images, dim=0) - - def create_mask(self, image, left, top, right, bottom): - masks = [] - image = [self.tensor2pil(img) for img in image] - for img in image: - shape = (left, top, img.width + left, img.height + top) - mask_image = Image.new("L", (img.width + left + right, img.height + top + bottom), 255) - draw = ImageDraw.Draw(mask_image) - draw.rectangle(shape, fill=0) - masks.append(self.pil2tensor(mask_image)) - return torch.cat(masks, dim=0) - - def scale_image(self, image, scale_by, method): - scaled_images = [] - image = [self.tensor2pil(img) for img in image] - - resampling_methods = { - "nearest-exact": Image.Resampling.NEAREST, - "bilinear": Image.Resampling.BILINEAR, - "bicubic": Image.Resampling.BICUBIC, - "lanczos": Image.Resampling.LANCZOS, - } - - for img in image: - # 计算新尺寸 - new_width = int(img.width * scale_by) - new_height = int(img.height * scale_by) - - # 使用选定的方法进行缩放 - scaled_img = img.resize( - (new_width, new_height), - resampling_methods.get(method, Image.Resampling.LANCZOS) - ) - scaled_images.append(self.pil2tensor(scaled_img)) - - return torch.cat(scaled_images, dim=0) - - def hex_to_tuple(self, color): - if not isinstance(color, str): - raise ValueError("Color must be a hex string") - color = color.strip("#") - return tuple([int(color[i:i + 2], 16) for i in range(0, len(color), 2)]) - - def tensor2pil(self, image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) - - def pil2tensor(self, image): - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - - def composite_with_background(self, image, background): - """将图片居中合成到背景上""" - image_pil = self.tensor2pil(image[0]) # 获取第一帧 - bg_pil = self.tensor2pil(background[0]) - - # 创建新的景图像 - result = bg_pil.copy() - - # 计算居中位置 - x = (bg_pil.width - image_pil.width) // 2 - y = (bg_pil.height - image_pil.height) // 2 - - # 如果前景图比背景大,需要裁剪 - if image_pil.width > bg_pil.width or image_pil.height > bg_pil.height: - # 计算裁剪区域 - crop_left = max(0, (image_pil.width - bg_pil.width) // 2) - crop_top = max(0, (image_pil.height - bg_pil.height) // 2) - crop_right = min(image_pil.width, crop_left + bg_pil.width) - crop_bottom = min(image_pil.height, crop_top + bg_pil.height) - - # 裁剪图片 - image_pil = image_pil.crop((crop_left, crop_top, crop_right, crop_bottom)) - - # 更新粘贴位置 - x = max(0, (bg_pil.width - image_pil.width) // 2) - y = max(0, (bg_pil.height - image_pil.height) // 2) - - # 如果前景图有透明通道,使用alpha通道合成 - if image_pil.mode == 'RGBA': - result.paste(image_pil, (x, y), image_pil) - else: - result.paste(image_pil, (x, y)) - - return self.pil2tensor(result) - - def process_image(self, image, scale_by, upscale_method, left, top, right, bottom, color, transparent, background=None): - # 首先进行缩放 - if scale_by != 1.0: - image = self.scale_image(image, scale_by, upscale_method) - - # 添加padding - padded_image = self.add_padding(image, left, top, right, bottom, color, transparent) - - # 如果有背景图,进行合成 - if background is not None: - result = [] - for i in range(len(padded_image)): - # 处理每一帧 - frame = padded_image[i:i+1] - composited = self.composite_with_background(frame, background) - result.append(composited) - padded_image = torch.cat(result, dim=0) - - # 创建mask - mask = self.create_mask(image, left, top, right, bottom) - - return (padded_image, mask) - - -class AD_ImageConcat: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image1": ("IMAGE",), - "image2": ("IMAGE",), - "direction": (["horizontal", "vertical"], {"default": "horizontal"}), - "match_size": ("BOOLEAN", {"default": True}), - "method": (["lanczos", "bicubic", "bilinear", "nearest"], {"default": "lanczos"}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "concat_images" - CATEGORY = "🌻 Addoor/image" - - def concat_images(self, image1, image2, direction="horizontal", match_size=False, method="lanczos"): - try: - # 转换为 PIL 图像 - img1 = tensor_to_image(image1[0]) - img2 = tensor_to_image(image2[0]) - - # 确保两张图片的模式相同 - if img1.mode != img2.mode: - if 'A' in img1.mode or 'A' in img2.mode: - img1 = img1.convert('RGBA') - img2 = img2.convert('RGBA') - else: - img1 = img1.convert('RGB') - img2 = img2.convert('RGB') - - # 如果需要匹配尺寸 - if match_size: - if direction == "horizontal": - # 横向拼接,匹配高度 - if img1.height != img2.height: - new_height = img2.height - new_width = int(img1.width * (new_height / img1.height)) - img1 = img1.resize( - (new_width, new_height), - get_sampler_by_name(method) - ) - else: # vertical - # 纵向拼接,匹配宽度 - if img1.width != img2.width: - new_width = img2.width - new_height = int(img1.height * (new_width / img1.width)) - img1 = img1.resize( - (new_width, new_height), - get_sampler_by_name(method) - ) - - # 创建新图像 - if direction == "horizontal": - new_width = img1.width + img2.width - new_height = max(img1.height, img2.height) - else: # vertical - new_width = max(img1.width, img2.width) - new_height = img1.height + img2.height - - # 创建新的画布 - mode = img1.mode - new_image = Image.new(mode, (new_width, new_height)) - - # 计算粘贴位置(居中对齐) - if direction == "horizontal": - y1 = (new_height - img1.height) // 2 - y2 = (new_height - img2.height) // 2 - new_image.paste(img1, (0, y1)) - new_image.paste(img2, (img1.width, y2)) - else: # vertical - x1 = (new_width - img1.width) // 2 - x2 = (new_width - img2.width) // 2 - new_image.paste(img1, (x1, 0)) - new_image.paste(img2, (x2, img1.height)) - - # 转换回 tensor - tensor = image_to_tensor(new_image) - tensor = tensor.unsqueeze(0) - tensor = tensor.permute(0, 2, 3, 1) - - return (tensor,) - - except Exception as e: - print(f"Error concatenating images: {str(e)}") - return (image1,) - - -# 添加颜色常量 -COLORS = [ - "white", "black", "red", "green", "blue", "yellow", "purple", "orange", - "gray", "brown", "pink", "cyan", "custom" -] - -# 颜色映射 -color_mapping = { - "white": "#FFFFFF", - "black": "#000000", - "red": "#FF0000", - "green": "#00FF00", - "blue": "#0000FF", - "yellow": "#FFFF00", - "purple": "#800080", - "orange": "#FFA500", - "gray": "#808080", - "brown": "#A52A2A", - "pink": "#FFC0CB", - "cyan": "#00FFFF" -} - -def get_color_values(color_name, color_hex, mapping): - """获取颜色值""" - if color_name == "custom": - return color_hex - return mapping.get(color_name, "#000000") - -# 添加图像处理工具函数 -def get_sampler_by_name(method: str) -> int: - """Get PIL resampling method by name.""" - samplers = { - "lanczos": Image.LANCZOS, - "bicubic": Image.BICUBIC, - "hamming": Image.HAMMING, - "bilinear": Image.BILINEAR, - "box": Image.BOX, - "nearest": Image.NEAREST - } - return samplers.get(method, Image.LANCZOS) - -class AD_ColorImage: - """Create a solid color image with advanced options.""" - - def __init__(self): - pass - - # 预定义颜色映射 - COLOR_PRESETS = { - "white": "#FFFFFF", - "black": "#000000", - "red": "#FF0000", - "green": "#00FF00", - "blue": "#0000FF", - "yellow": "#FFFF00", - "purple": "#800080", - "orange": "#FFA500", - "gray": "#808080", - "custom": "custom" - } - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "width": ("INT", { - "default": 512, - "min": 1, - "max": 8192, - "step": 1 - }), - "height": ("INT", { - "default": 512, - "min": 1, - "max": 8192, - "step": 1 - }), - "color": (list(cls.COLOR_PRESETS.keys()), { - "default": "white" - }), - "hex_color": ("STRING", { - "default": "#FFFFFF", - "multiline": False - }), - "alpha": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - }, - "optional": { - "reference_image": ("IMAGE",), - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = "create_color_image" - CATEGORY = "🌻 Addoor/image" - - def hex_to_rgb(self, hex_color: str) -> tuple: - """Convert hex color to RGB tuple.""" - hex_color = hex_color.lstrip('#') - return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - - def create_color_image( - self, - width: int, - height: int, - color: str, - hex_color: str, - alpha: float, - reference_image = None - ): - try: - # 如果有参考图片,使用其尺寸 - if reference_image is not None: - _, height, width, _ = reference_image.shape - - # 获取颜色值 - if color == "custom": - rgb_color = self.hex_to_rgb(hex_color) - else: - rgb_color = self.hex_to_rgb(self.COLOR_PRESETS[color]) - - # 创建图像 - canvas = Image.new( - "RGBA", - (width, height), - (*rgb_color, int(alpha * 255)) - ) - - # 转换为 tensor - tensor = image_to_tensor(canvas) - tensor = tensor.unsqueeze(0) - tensor = tensor.permute(0, 2, 3, 1) - - return (tensor,) - - except Exception as e: - print(f"Error creating color image: {str(e)}") - canvas = Image.new( - "RGB", - (width, height), - (0, 0, 0) - ) - tensor = image_to_tensor(canvas) - tensor = tensor.unsqueeze(0) - tensor = tensor.permute(0, 2, 3, 1) - return (tensor,) - -def tensor_to_image(tensor): - """Convert tensor to PIL Image.""" - if len(tensor.shape) == 4: - tensor = tensor.squeeze(0) # 移除 batch 维度 - return t.ToPILImage()(tensor.permute(2, 0, 1)) - -def image_to_tensor(image): - """Convert PIL Image to tensor.""" - tensor = t.ToTensor()(image) - return tensor - -NODE_CLASS_MAPPINGS = { - "AD_advanced-padding": AD_PaddingAdvanced, - "AD_image-concat": AD_ImageConcat, - "AD_color-image": AD_ColorImage, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "AD_advanced-padding": "AD Advanced Padding", - "AD_image-concat": "AD Image Concatenation", - "AD_color-image": "AD Color Image", -} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/nodes/AddPaddingBase.py b/custom_nodes/ComfyUI-Addoor/nodes/AddPaddingBase.py deleted file mode 100644 index 553f5ae8360d40772e15ac0bc56e5f554d307dad..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/AddPaddingBase.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -@author: ealkanat -@title: ComfyUI Easy Padding -@description: A simple custom node for creates padding for given image -@version: 1.0.2 -@project: https://github.com/erkana/comfyui_easy_padding -@author: https://github.com/erkana -""" - -import PIL.Image as Image -import PIL.ImageDraw as ImageDraw -import numpy as np -import torch - - -class AddPaddingBase: - def __init__(self): - pass - - FUNCTION = "resize" - CATEGORY = "🌻 Addoor/image" - - def add_padding(self, image, left, top, right, bottom, color="#ffffff", transparent=False): - padded_images = [] - image = [self.tensor2pil(img) for img in image] - for img in image: - padded_image = Image.new("RGBA" if transparent else "RGB", - (img.width + left + right, img.height + top + bottom), - (0, 0, 0, 0) if transparent else self.hex_to_tuple(color)) - padded_image.paste(img, (left, top)) - padded_images.append(self.pil2tensor(padded_image)) - return torch.cat(padded_images, dim=0) - - def create_mask(self, image, left, top, right, bottom): - masks = [] - image = [self.tensor2pil(img) for img in image] - for img in image: - shape = (left, top, img.width + left, img.height + top) - mask_image = Image.new("L", (img.width + left + right, img.height + top + bottom), 255) - draw = ImageDraw.Draw(mask_image) - draw.rectangle(shape, fill=0) - masks.append(self.pil2tensor(mask_image)) - return torch.cat(masks, dim=0) - - def hex_to_float(self, color): - if not isinstance(color, str): - raise ValueError("Color must be a hex string") - color = color.strip("#") - return int(color, 16) / 255.0 - - def hex_to_tuple(self, color): - if not isinstance(color, str): - raise ValueError("Color must be a hex string") - color = color.strip("#") - return tuple([int(color[i:i + 2], 16) for i in range(0, len(color), 2)]) - - # Tensor to PIL - def tensor2pil(self, image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) - - # PIL to Tensor - def pil2tensor(self, image): - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - - - -class AddPadding(AddPaddingBase): - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "left": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "top": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "right": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "bottom": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}), - "color": ("STRING", {"default": "#ffffff"}), - "transparent": ("BOOLEAN", {"default": False}), - }, - } - - RETURN_TYPES = ("IMAGE", "MASK") - - def resize(self, image, left, top, right, bottom, color, transparent): - return (self.add_padding(image, left, top, right, bottom, color, transparent), - self.create_mask(image, left, top, right, bottom),) - - -NODE_CLASS_MAPPINGS = { - "comfyui-easy-padding": AddPadding, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "comfyui-easy-padding": "ComfyUI Easy Padding", -} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-FofrToolkit.py b/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-FofrToolkit.py deleted file mode 100644 index 7bba0b7f80bb72bb04d49c0fbd7dea9f2e072781..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-FofrToolkit.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -@name: "ComfyUI FofrToolkit", -@version: (1,0,0), -@author: "fofr", -@description: "Experimental toolkit for comfyui.", -@project: "https://github.com/fofr/comfyui-fofr-toolkit", -@url: "https://github.com/fofr", -""" - - -class ToolkitIncrementer: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "current_index": ( - "INT", - { - "default": 0, - "min": 0, - "max": 0xFFFFFFFFFFFFFFFF, - "control_after_generate": True, - }, - ), - }, - "optional": { - "max": ( - "INT", - {"default": 10, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}, - ), - }, - } - - RETURN_TYPES = ("INT", "STRING") - RETURN_NAMES = ("INT", "STRING") - FUNCTION = "increment" - CATEGORY = "🌻 Addoor/Utilities" - - def increment(self, current_index, max=0): - if max == 0: - result = current_index - else: - result = current_index % (max + 1) - - return (result, str(result)) - - -class ToolkitWidthAndHeightFromAspectRatio: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "aspect_ratio": ( - [ - "1:1", - "1:2", - "2:1", - "2:3", - "3:2", - "3:4", - "4:3", - "4:5", - "5:4", - "9:16", - "16:9", - "9:21", - "21:9", - ], - {"default": "1:1"}, - ), - "target_size": ("INT", {"default": 1024, "min": 64, "max": 8192}), - }, - "optional": { - "multiple_of": ("INT", {"default": 8, "min": 1, "max": 1024}), - }, - } - - RETURN_TYPES = ("INT", "INT") - RETURN_NAMES = ("width", "height") - FUNCTION = "width_and_height_from_aspect_ratio" - CATEGORY = "🌻 Addoor/Utilities" - - def width_and_height_from_aspect_ratio( - self, aspect_ratio, target_size, multiple_of=8 - ): - w, h = map(int, aspect_ratio.split(":")) - scale = (target_size**2 / (w * h)) ** 0.5 - width = round(w * scale / multiple_of) * multiple_of - height = round(h * scale / multiple_of) * multiple_of - return (width, height) - - -class ToolkitWidthAndHeightForImageScaling: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "target_size": ( - "INT", - {"default": 1024, "min": 64, "max": 8192}, - ), - }, - "optional": { - "multiple_of": ("INT", {"default": 8, "min": 1, "max": 1024}), - }, - } - - RETURN_TYPES = ("INT", "INT") - RETURN_NAMES = ("width", "height") - FUNCTION = "scale_image_to_target" - CATEGORY = "🌻 Addoor/Utilities" - - def scale_image_to_target(self, image, target_size, multiple_of=8): - h, w = image.shape[1:3] - scale = (target_size**2 / (w * h)) ** 0.5 - width = round(w * scale / multiple_of) * multiple_of - height = round(h * scale / multiple_of) * multiple_of - return (width, height) - - -NODE_CLASS_MAPPINGS = { - "Incrementer 🪴": ToolkitIncrementer, - "Width and height from aspect ratio 🪴": ToolkitWidthAndHeightFromAspectRatio, - "Width and height for scaling image to ideal resolution 🪴": ToolkitWidthAndHeightForImageScaling, -} diff --git a/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-imageResize.py b/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-imageResize.py deleted file mode 100644 index 01f9157567a9b1d7d83005a7df1f66c28b904cbe..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-imageResize.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -@author: palant -@title: ComfyUI-imageResize -@description: Custom node for image resizing. -@version: 1.0.0 -@project: https://github.com/palant/image-resize-comfyui -@author: https://github.com/palant -""" - -import torch - -class ImageResize: - def __init__(self): - pass - - - ACTION_TYPE_RESIZE = "resize only" - ACTION_TYPE_CROP = "crop to ratio" - ACTION_TYPE_PAD = "pad to ratio" - RESIZE_MODE_DOWNSCALE = "reduce size only" - RESIZE_MODE_UPSCALE = "increase size only" - RESIZE_MODE_ANY = "any" - RETURN_TYPES = ("IMAGE", "MASK",) - FUNCTION = "resize" - CATEGORY = "🌻 Addoor/image" - - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "pixels": ("IMAGE",), - "action": ([s.ACTION_TYPE_RESIZE, s.ACTION_TYPE_CROP, s.ACTION_TYPE_PAD],), - "smaller_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}), - "larger_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}), - "scale_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}), - "resize_mode": ([s.RESIZE_MODE_DOWNSCALE, s.RESIZE_MODE_UPSCALE, s.RESIZE_MODE_ANY],), - "side_ratio": ("STRING", {"default": "4:3"}), - "crop_pad_position": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - "pad_feathering": ("INT", {"default": 20, "min": 0, "max": 8192, "step": 1}), - }, - "optional": { - "mask_optional": ("MASK",), - }, - } - - - @classmethod - def VALIDATE_INPUTS(s, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, **_): - if side_ratio is not None: - if action != s.ACTION_TYPE_RESIZE and s.parse_side_ratio(side_ratio) is None: - return f"Invalid side ratio: {side_ratio}" - - if smaller_side is not None and larger_side is not None and scale_factor is not None: - if int(smaller_side > 0) + int(larger_side > 0) + int(scale_factor > 0) > 1: - return f"At most one scaling rule (smaller_side, larger_side, scale_factor) should be enabled by setting a non-zero value" - - if scale_factor is not None: - if resize_mode == s.RESIZE_MODE_DOWNSCALE and scale_factor > 1.0: - return f"For resize_mode {s.RESIZE_MODE_DOWNSCALE}, scale_factor should be less than one but got {scale_factor}" - if resize_mode == s.RESIZE_MODE_UPSCALE and scale_factor > 0.0 and scale_factor < 1.0: - return f"For resize_mode {s.RESIZE_MODE_UPSCALE}, scale_factor should be larger than one but got {scale_factor}" - - return True - - - @classmethod - def parse_side_ratio(s, side_ratio): - try: - x, y = map(int, side_ratio.split(":", 1)) - if x < 1 or y < 1: - raise Exception("Ratio factors have to be positive numbers") - return float(x) / float(y) - except: - return None - - - def resize(self, pixels, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, crop_pad_position, pad_feathering, mask_optional=None): - validity = self.VALIDATE_INPUTS(action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio) - if validity is not True: - raise Exception(validity) - - height, width = pixels.shape[1:3] - if mask_optional is None: - mask = torch.zeros(1, height, width, dtype=torch.float32) - else: - mask = mask_optional - if mask.shape[1] != height or mask.shape[2] != width: - mask = torch.nn.functional.interpolate(mask.unsqueeze(0), size=(height, width), mode="bicubic").squeeze(0).clamp(0.0, 1.0) - - crop_x, crop_y, pad_x, pad_y = (0.0, 0.0, 0.0, 0.0) - if action == self.ACTION_TYPE_CROP: - target_ratio = self.parse_side_ratio(side_ratio) - if height * target_ratio < width: - crop_x = width - height * target_ratio - else: - crop_y = height - width / target_ratio - elif action == self.ACTION_TYPE_PAD: - target_ratio = self.parse_side_ratio(side_ratio) - if height * target_ratio > width: - pad_x = height * target_ratio - width - else: - pad_y = width / target_ratio - height - - if smaller_side > 0: - if width + pad_x - crop_x > height + pad_y - crop_y: - scale_factor = float(smaller_side) / (height + pad_y - crop_y) - else: - scale_factor = float(smaller_side) / (width + pad_x - crop_x) - if larger_side > 0: - if width + pad_x - crop_x > height + pad_y - crop_y: - scale_factor = float(larger_side) / (width + pad_x - crop_x) - else: - scale_factor = float(larger_side) / (height + pad_y - crop_y) - - if (resize_mode == self.RESIZE_MODE_DOWNSCALE and scale_factor >= 1.0) or (resize_mode == self.RESIZE_MODE_UPSCALE and scale_factor <= 1.0): - scale_factor = 0.0 - - if scale_factor > 0.0: - 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) - mask = torch.nn.functional.interpolate(mask.unsqueeze(0), scale_factor=scale_factor, mode="bicubic", antialias=True).squeeze(0).clamp(0.0, 1.0) - height, width = pixels.shape[1:3] - - crop_x *= scale_factor - crop_y *= scale_factor - pad_x *= scale_factor - pad_y *= scale_factor - - if crop_x > 0.0 or crop_y > 0.0: - remove_x = (round(crop_x * crop_pad_position), round(crop_x * (1 - crop_pad_position))) if crop_x > 0.0 else (0, 0) - remove_y = (round(crop_y * crop_pad_position), round(crop_y * (1 - crop_pad_position))) if crop_y > 0.0 else (0, 0) - pixels = pixels[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1], :] - mask = mask[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1]] - elif pad_x > 0.0 or pad_y > 0.0: - add_x = (round(pad_x * crop_pad_position), round(pad_x * (1 - crop_pad_position))) if pad_x > 0.0 else (0, 0) - add_y = (round(pad_y * crop_pad_position), round(pad_y * (1 - crop_pad_position))) if pad_y > 0.0 else (0, 0) - - 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) - new_pixels[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0], :] = pixels - pixels = new_pixels - - new_mask = torch.ones(mask.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], dtype=torch.float32) - new_mask[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0]] = mask - mask = new_mask - - if pad_feathering > 0: - for i in range(mask.shape[0]): - for j in range(pad_feathering): - feather_strength = (1 - j / pad_feathering) * (1 - j / pad_feathering) - if add_x[0] > 0 and j < width: - for k in range(height): - mask[i, k, add_x[0] + j] = max(mask[i, k, add_x[0] + j], feather_strength) - if add_x[1] > 0 and j < width: - for k in range(height): - mask[i, k, width + add_x[0] - j - 1] = max(mask[i, k, width + add_x[0] - j - 1], feather_strength) - if add_y[0] > 0 and j < height: - for k in range(width): - mask[i, add_y[0] + j, k] = max(mask[i, add_y[0] + j, k], feather_strength) - if add_y[1] > 0 and j < height: - for k in range(width): - mask[i, height + add_y[0] - j - 1, k] = max(mask[i, height + add_y[0] - j - 1, k], feather_strength) - - return (pixels, mask) - - -NODE_CLASS_MAPPINGS = { - "ImageResize": ImageResize -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "ImageResize": "Image Resize" -} diff --git a/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-textAppend.py b/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-textAppend.py deleted file mode 100644 index 5e9cbfb6c09efd3a30dbfced803a1aa8f5abc8a6..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-textAppend.py +++ /dev/null @@ -1,48 +0,0 @@ -# text_append_node.py - -class TextAppendNode: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "current_text": ("STRING", {"multiline": True}), - "new_text": ("STRING", {"multiline": True}), - "mode": (["append", "prepend"],), - }, - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "process_text" - CATEGORY = "🌻 Addoor/text" - - def process_text(self, current_text, new_text, mode): - current_lines = [line.strip() for line in current_text.split('\n') if line.strip()] - new_lines = [line.strip() for line in new_text.split('\n') if line.strip()] - - result = [] - - # If current_text is empty, return new_text - if not current_lines: - return (new_text,) - - if mode == "append": - for current_line in current_lines: - for new_line in new_lines: - result.append(f"{current_line} {new_line}") - elif mode == "prepend": - for current_line in current_lines: - for new_line in new_lines: - result.append(f"{new_line} {current_line}") - - return ('\n'.join(result),) - -NODE_CLASS_MAPPINGS = { - "TextAppendNode": TextAppendNode -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "TextAppendNode": "Text Append/Prepend" -} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/nodes/__init__.py b/custom_nodes/ComfyUI-Addoor/nodes/__init__.py deleted file mode 100644 index 0d5f9603446a14a5c7206c1bb332b24aede5d7b0..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Addoor Nodes for ComfyUI -Provides nodes for image processing and other utilities -""" - -import os -import glob -import logging -import importlib - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# 首先定义映射字典 -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} - -# 获取当前目录下所有的 .py 文件 -current_dir = os.path.dirname(os.path.abspath(__file__)) -py_files = glob.glob(os.path.join(current_dir, "*.py")) - -# 直接注册所有节点 -for file_path in py_files: - # 跳过 __init__.py - if "__init__.py" in file_path: - continue - - try: - # 获取模块名(不含.py) - module_name = os.path.basename(file_path)[:-3] - # 使用相对导入 - module = importlib.import_module(f".{module_name}", package=__package__) - - # 如果模块有节点映射,则更新 - if hasattr(module, 'NODE_CLASS_MAPPINGS'): - NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) - if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'): - NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) - - logger.info(f"Imported {module_name} successfully") - except ImportError as e: - logger.error(f"Error importing {module_name}: {str(e)}") - except Exception as e: - logger.error(f"Error processing {module_name}: {str(e)}") - -# 如果允许测试节点,导入测试节点 -allow_test_nodes = True -if allow_test_nodes: - try: - from .excluded.experimental_nodes import * - # 更新映射 - if 'NODE_CLASS_MAPPINGS' in locals(): - NODE_CLASS_MAPPINGS.update(locals().get('NODE_CLASS_MAPPINGS', {})) - if 'NODE_DISPLAY_NAME_MAPPINGS' in locals(): - NODE_DISPLAY_NAME_MAPPINGS.update(locals().get('NODE_DISPLAY_NAME_MAPPINGS', {})) - except ModuleNotFoundError: - pass - -logger.debug(f"Registered nodes: {list(NODE_CLASS_MAPPINGS.keys())}") - -WEB_DIRECTORY = "./web" -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/nodes/imagecreatemask.py b/custom_nodes/ComfyUI-Addoor/nodes/imagecreatemask.py deleted file mode 100644 index d1a206d37e096070e54bca1f49da1f9cbb7a1931..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/imagecreatemask.py +++ /dev/null @@ -1,109 +0,0 @@ -from PIL import Image -import numpy as np -import torch -import torchvision.transforms.functional as TF - -def tensor2pil(image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) - -def pil2tensor(image): - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - -def tensor2mask(t: torch.Tensor) -> torch.Tensor: - size = t.size() - if (len(size) < 4): - return t - if size[3] == 1: - return t[:,:,:,0] - elif size[3] == 4: - # Use alpha if available - if torch.min(t[:, :, :, 3]).item() != 1.: - return t[:,:,:,3] - # Convert RGB to grayscale - return TF.rgb_to_grayscale(t.permute(0,3,1,2), num_output_channels=1)[:,0,:,:] - -class image_concat_mask: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image1": ("IMAGE",), - }, - "optional": { - "image2": ("IMAGE",), - "mask": ("MASK",), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask") - FUNCTION = "image_concat_mask" - CATEGORY = "🌻 Addoor/image" - - def image_concat_mask(self, image1, image2=None, mask=None): - processed_images = [] - masks = [] - - for idx, img1 in enumerate(image1): - # Convert tensor to PIL - pil_image1 = tensor2pil(img1) - - # Get first image dimensions - width1, height1 = pil_image1.size - - if image2 is not None and idx < len(image2): - # Use provided second image - pil_image2 = tensor2pil(image2[idx]) - width2, height2 = pil_image2.size - - # Resize image2 to match height of image1 - new_width2 = int(width2 * (height1 / height2)) - pil_image2 = pil_image2.resize((new_width2, height1), Image.Resampling.LANCZOS) - else: - # Create white image with same dimensions as image1 - pil_image2 = Image.new('RGB', (width1, height1), 'white') - new_width2 = width1 - - # Create new image to hold both images side by side - combined_image = Image.new('RGB', (width1 + new_width2, height1)) - - # Paste both images - combined_image.paste(pil_image1, (0, 0)) - combined_image.paste(pil_image2, (width1, 0)) - - # Convert combined image to tensor - combined_tensor = pil2tensor(combined_image) - processed_images.append(combined_tensor) - - # Create mask (0 for left image area, 1 for right image area) - final_mask = torch.zeros((1, height1, width1 + new_width2)) - final_mask[:, :, width1:] = 1.0 # Set right half to 1 - - # If mask is provided, subtract it from the right side - if mask is not None and idx < len(mask): - input_mask = mask[idx] - # Resize input mask to match height1 - pil_input_mask = tensor2pil(input_mask) - pil_input_mask = pil_input_mask.resize((new_width2, height1), Image.Resampling.LANCZOS) - resized_input_mask = pil2tensor(pil_input_mask) - - # Subtract input mask from the right side - final_mask[:, :, width1:] *= (1.0 - resized_input_mask) - - masks.append(final_mask) - - processed_images = torch.cat(processed_images, dim=0) - masks = torch.cat(masks, dim=0) - - return (processed_images, masks) - -NODE_CLASS_MAPPINGS = { - "image concat mask": image_concat_mask -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "image concat mask": "Image Concat with Mask" -} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Addoor/nodes/multiline_string.py b/custom_nodes/ComfyUI-Addoor/nodes/multiline_string.py deleted file mode 100644 index 16a06ae07f5148509570e3a00a39758b7ad52eac..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/nodes/multiline_string.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -This node provides a multiline input field for a string. -Input: - string -Output: - STRING: A copy of 'string'. -""" -class MultilineString: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "string": ("STRING", { - "multiline": True, - "default": "", - }), - }, - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("STRING",) - FUNCTION = "get_string" - OUTPUT_NODE = True - CATEGORY = "🌻 Addoor/text" - - def get_string(self, string): - return (string,) - - -# A dictionary that contains all nodes you want to export with their names -# NOTE: names should be globally unique -NODE_CLASS_MAPPINGS = { - "YANC.MultilineString": MultilineString, -} - -# A dictionary that contains the friendly/humanly readable titles for the nodes -NODE_DISPLAY_NAME_MAPPINGS = { - "YANC.MultilineString": "Multiline String", -} diff --git a/custom_nodes/ComfyUI-Addoor/rp/RunningHubFileSaver.py b/custom_nodes/ComfyUI-Addoor/rp/RunningHubFileSaver.py deleted file mode 100644 index 560441c0846e0e82dff956daef754d86238601a7..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/rp/RunningHubFileSaver.py +++ /dev/null @@ -1,183 +0,0 @@ -import os -import requests -import json -import numpy as np -from PIL import Image -from PIL.PngImagePlugin import PngInfo -import subprocess -import mimetypes -import torch -import logging -import io -import re - -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) - -class RunningHubFileSaverNode: - def __init__(self): - self.compression = 4 - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "file_urls": ("FILEURL_LIST",), - "directory": ("STRING", {"default": "outputs"}), - "filename_prefix": ("STRING", {"default": "RH_Output"}), - "filename_delimiter": ("STRING", {"default": "_"}), - "filename_number_padding": ("INT", {"default": 4, "min": 0, "max": 9, "step": 1}), - "file_extension": (["auto", "png", "jpg", "jpeg", "txt", "json", "mp4"], {"default": "auto"}), - "open_output_directory": ("BOOLEAN", {"default": False}), - }, - } - - RETURN_TYPES = ("STRING", "IMAGE", "STRING") - RETURN_NAMES = ("output_paths", "previews", "file_types") - FUNCTION = "save_files" - CATEGORY = "🌻 Addoor/RHAPI" - - def save_files(self, file_urls, directory, filename_prefix, filename_delimiter, filename_number_padding, file_extension, open_output_directory): - output_paths = [] - previews = [] - file_types = [] - - try: - if not os.path.exists(directory): - os.makedirs(directory) - - for url in file_urls: - if not url: - continue - - result = self.save_single_file( - url, - directory, - filename_prefix, - filename_delimiter, - filename_number_padding, - file_extension - ) - - if result: - path, preview, ftype = result - output_paths.append(path) - previews.append(preview) - file_types.append(ftype) - - if open_output_directory and output_paths: - self.open_directory(directory) - - if not output_paths: - return ("", torch.zeros((1, 1, 1, 3)), "") - - if len(previews) > 1: - combined_preview = torch.cat(previews, dim=0) - else: - combined_preview = previews[0] if previews else torch.zeros((1, 1, 1, 3)) - - return ( - json.dumps(output_paths), - combined_preview, - json.dumps(file_types) - ) - - except Exception as e: - logger.error(f"Error saving files: {str(e)}") - return ("", torch.zeros((1, 1, 1, 3)), "") - - def save_single_file(self, file_url, directory, filename_prefix, filename_delimiter, filename_number_padding, file_extension): - try: - logger.info(f"Attempting to save file from URL: {file_url}") - - if not file_url: - raise ValueError("File URL is empty") - - if not os.path.exists(directory): - logger.info(f"Creating directory: {directory}") - os.makedirs(directory) - - response = requests.get(file_url, timeout=30) - response.raise_for_status() - - content_type = response.headers.get('content-type') - logger.info(f"Content-Type: {content_type}") - - if file_extension == "auto": - file_extension = mimetypes.guess_extension(content_type) - if file_extension: - file_extension = file_extension[1:] # Remove the leading dot - if file_extension == "jpeg": - file_extension = "jpg" - else: - file_extension = "bin" # Default to binary if can't guess - logger.info(f"Auto-detected file extension: {file_extension}") - - file_path = os.path.join(directory, self.generate_filename(directory, filename_prefix, filename_delimiter, filename_number_padding, file_extension)) - logger.info(f"Saving file to: {file_path}") - - preview = None - if file_extension in ['png', 'jpg', 'jpeg']: - try: - img_data = io.BytesIO(response.content) - img = Image.open(img_data) # Reopen the image after verifying - metadata = PngInfo() - metadata.add_text("source_url", file_url) - img.save(file_path, pnginfo=metadata, compress_level=self.compression) - preview = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0) - except Exception as e: - logger.error(f"Error processing image: {str(e)}") - # If image processing fails, save as binary - with open(file_path, 'wb') as f: - f.write(response.content) - preview = torch.zeros((1, 1, 1, 3)) # Placeholder for failed image - elif file_extension in ['txt', 'json']: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(response.text) - preview = torch.zeros((1, 1, 1, 3)) # Placeholder for non-image files - else: - with open(file_path, 'wb') as f: - f.write(response.content) - preview = torch.zeros((1, 1, 1, 3)) # Placeholder for other file types - - logger.info(f"File saved successfully to {file_path}") - - return (file_path, preview, file_extension) - - except requests.exceptions.RequestException as e: - logger.error(f"Error downloading file: {str(e)}") - except IOError as e: - logger.error(f"Error saving file: {str(e)}") - except Exception as e: - logger.error(f"Unexpected error: {str(e)}") - - return ("", torch.zeros((1, 1, 1, 3)), "") - - def generate_filename(self, directory, prefix, delimiter, number_padding, extension): - if number_padding == 0: - return f"{prefix}.{extension}" - - pattern = f"{re.escape(prefix)}{re.escape(delimiter)}(\\d{{{number_padding}}})" - existing_files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))] - existing_counters = [ - int(re.search(pattern, filename).group(1)) - for filename in existing_files - if re.match(pattern, filename) - ] - - counter = max(existing_counters, default=0) + 1 - return f"{prefix}{delimiter}{counter:0{number_padding}}.{extension}" - - @staticmethod - def open_directory(path): - if os.name == 'nt': # Windows - os.startfile(path) - elif os.name == 'posix': # macOS and Linux - try: - subprocess.call(['open', path]) # macOS - except: - try: - subprocess.call(['xdg-open', path]) # Linux - except: - logger.error(f"Could not open directory: {path}") - diff --git a/custom_nodes/ComfyUI-Addoor/rp/RunningHubInit.py b/custom_nodes/ComfyUI-Addoor/rp/RunningHubInit.py deleted file mode 100644 index b64a61223193455255a204b72e6000369910f4dc..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/rp/RunningHubInit.py +++ /dev/null @@ -1,22 +0,0 @@ -from . import validate_api_key - -class RunningHubInitNode: - """Node for initializing RunningHub API connection""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "api_key": ("STRING", {"default": "", "multiline": False}), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "initialize" - CATEGORY = "🌻 Addoor/RHAPI" - - def initialize(self, api_key: str): - if not validate_api_key(api_key): - raise ValueError("Invalid API key format") - return (api_key,) - diff --git a/custom_nodes/ComfyUI-Addoor/rp/RunningHubNodeInfo.py b/custom_nodes/ComfyUI-Addoor/rp/RunningHubNodeInfo.py deleted file mode 100644 index dd40534013b0c532d5989112174577a7d6cd68d4..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/rp/RunningHubNodeInfo.py +++ /dev/null @@ -1,45 +0,0 @@ -class RunningHubNodeInfoNode: - """Node for creating and chaining nodeInfo entries""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "node_id": ("STRING", {"default": ""}), - "field_name": ("STRING", {"default": ""}), - "field_value": ("STRING", {"default": "", "multiline": True}), - "comment": ("STRING", {"default": ""}), - }, - "optional": { - "previous_node_info": ("NODEINFOLIST",), # 用于串联 - } - } - - RETURN_TYPES = ("NODEINFOLIST",) # 改为返回列表类型 - FUNCTION = "create_node_info" - CATEGORY = "🌻 Addoor/RHAPI" - - def create_node_info(self, node_id: str, field_name: str, field_value: str, comment: str, previous_node_info=None): - # 创建当前节点信息 - current_node_info = { - "nodeId": node_id, - "fieldName": field_name, - "fieldValue": field_value, - "comment": comment - } - - # 初始化节点信息列表 - node_info_list = [] - - # 如果有前置节点信息,添加到列表 - if previous_node_info is not None: - if isinstance(previous_node_info, list): - node_info_list.extend(previous_node_info) - else: - node_info_list.append(previous_node_info) - - # 添加当前节点信息 - node_info_list.append(current_node_info) - - return (node_info_list,) - diff --git a/custom_nodes/ComfyUI-Addoor/rp/RunningHubWorkflowExecutor.py b/custom_nodes/ComfyUI-Addoor/rp/RunningHubWorkflowExecutor.py deleted file mode 100644 index fbff5d847a7fb3948b32f7a82d80b4ae3dea2359..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/rp/RunningHubWorkflowExecutor.py +++ /dev/null @@ -1,151 +0,0 @@ -import requests -import json -import time -import logging -from . import validate_api_key, BASE_URL - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -class RunningHubWorkflowExecutorNode: - """Node for executing RunningHub workflows and monitoring task status""" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "api_key": ("STRING", {"default": ""}), - "workflow_id": ("STRING", {"default": ""}), - "node_info_list": ("NODEINFOLIST",), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - "max_attempts": ("INT", {"default": 60, "min": 1, "max": 1000}), - "interval_seconds": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), - } - } - - RETURN_TYPES = ("FILEURL_LIST", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") - RETURN_NAMES = ("file_urls", "task_id", "msg", "promptTips", "taskStatus", "fileType", "code", "json") - FUNCTION = "execute_workflow_and_monitor" - CATEGORY = "🌻 Addoor/RHAPI" - - def execute_workflow_and_monitor(self, api_key: str, workflow_id: str, node_info_list: list, seed: int, max_attempts: int, interval_seconds: float): - if not validate_api_key(api_key): - raise ValueError("Invalid API key") - - # 确保 node_info_list 是列表类型 - if not isinstance(node_info_list, list): - node_info_list = [node_info_list] - - # 移除可能的重复项 - unique_node_info = { - (info.get('nodeId'), info.get('fieldName')): info - for info in node_info_list - }.values() - - workflow_data = { - "workflowId": int(workflow_id), - "apiKey": api_key, - "nodeInfoList": list(unique_node_info), - "seed": seed - } - - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - "User-Agent": "Apifox/1.0.0 (https://apifox.com)", - "Accept": "*/*", - "Host": "www.runninghub.cn", - "Connection": "keep-alive" - } - - workflow_endpoint = f"{BASE_URL}/task/openapi/create" - - try: - logger.info(f"Executing workflow with ID: {workflow_id}") - workflow_response = requests.post( - workflow_endpoint, - headers=headers, - json=workflow_data - ) - workflow_response.raise_for_status() - workflow_result = workflow_response.json() - - if not workflow_result or 'data' not in workflow_result: - raise ValueError("Invalid response from workflow execution") - - task_id = workflow_result.get('data', {}).get('taskId') - if not task_id: - raise ValueError("No task ID returned from workflow execution") - - logger.info(f"Workflow executed. Task ID: {task_id}") - - # Monitor task status - task_data = { - "taskId": task_id, - "apiKey": api_key - } - - task_endpoint = f"{BASE_URL}/task/openapi/outputs" - - for attempt in range(max_attempts): - try: - logger.info(f"Checking task status. Attempt {attempt + 1}/{max_attempts}") - task_response = requests.post( - task_endpoint, - headers=headers, - json=task_data - ) - task_response.raise_for_status() - task_result = task_response.json() - - if not task_result or 'data' not in task_result or not task_result['data']: - logger.warning("Invalid or empty response from task status check") - time.sleep(interval_seconds) - continue - - task_status = task_result['data'][0].get('taskStatus', '') - - if task_status in ['SUCCEDD', 'FAILED']: - logger.info(f"Task completed with status: {task_status}") - break - elif task_status in ['QUEUED', 'RUNNING']: - logger.info(f"Task status: {task_status}. Waiting...") - time.sleep(interval_seconds) - else: - logger.warning(f"Unknown task status: {task_status}") - break - - except requests.exceptions.RequestException as e: - logger.error(f"Error checking task status: {str(e)}") - time.sleep(interval_seconds) - - # Prepare return values - msg = workflow_result.get('msg', '') - prompt_tips = json.dumps(workflow_result.get('data', {}).get('promptTips', '')) - - # 处理多个文件URL - file_urls = [] - if task_result and 'data' in task_result and task_result['data']: - for item in task_result['data']: - if 'fileUrl' in item: - file_urls.append(item['fileUrl']) - - # 如果没有文件URL,添加空字符串 - if not file_urls: - file_urls = [""] - - file_type = task_result['data'][0].get('fileType', '') if task_result and 'data' in task_result and task_result['data'] else '' - code = str(task_result.get('code', '')) if task_result else '' - - return (file_urls, task_id, msg, prompt_tips, task_status, file_type, code, json.dumps(task_result)) - - except requests.exceptions.RequestException as e: - logger.error(f"API request failed: {str(e)}") - return ([""], "", f"Error: {str(e)}", "{}", "ERROR", "", "error", "") - except json.JSONDecodeError as e: - logger.error(f"Failed to parse JSON response: {str(e)}") - return ([""], "", f"Error: Invalid JSON response", "{}", "ERROR", "", "error", "") - except Exception as e: - logger.error(f"Unexpected error: {str(e)}") - return ([""], "", f"Error: {str(e)}", "{}", "ERROR", "", "error", "") - diff --git a/custom_nodes/ComfyUI-Addoor/rp/config.ini b/custom_nodes/ComfyUI-Addoor/rp/config.ini deleted file mode 100644 index 69e966d81176503481bd9ec0471229d1c0a521c5..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Addoor/rp/config.ini +++ /dev/null @@ -1,6 +0,0 @@ -; 密匙请联系微信:8617774(备注密匙)免费开通 - -[License] -COMFYUI_RHAPI_LICENSE_KEY = 1_Is-dRDrw1lbNVdN4vc2k7x4Qkg8hs2 -COMFYUI_RHAPI_USER_ID = user123 -COMFYUI_RHAPI_PHONE = 1234567890 \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/backend/agent_factory.py b/custom_nodes/ComfyUI-Copilot/backend/agent_factory.py deleted file mode 100644 index 2734cf2a0c1008234947c22a55b18516c183dac3..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/agent_factory.py +++ /dev/null @@ -1,98 +0,0 @@ -''' -Author: ai-business-hql qingli.hql@alibaba-inc.com -Date: 2025-07-31 19:38:08 -LastEditors: ai-business-hql ai.bussiness.hql@gmail.com -LastEditTime: 2025-11-20 17:51:33 -FilePath: /comfyui_copilot/backend/agent_factory.py -Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE -''' - -try: - from agents import Agent, OpenAIChatCompletionsModel, ModelSettings, Runner, set_trace_processors, set_tracing_disabled, set_default_openai_api - if not hasattr(__import__('agents'), 'Agent'): - raise ImportError -except Exception: - # Give actionable guidance without crashing obscurely - raise ImportError( - "Detected incorrect or missing 'agents' package. " - "Please uninstall legacy RL 'agents' (and tensorflow/gym if pulled transitively) and install openai-agents. " - "Commands:\n" - " python -m pip uninstall -y agents gym tensorflow\n" - " python -m pip install -U openai-agents\n\n" - "Alternatively, keep both by setting COMFYUI_COPILOT_PREFER_OPENAI_AGENTS=1 so this plugin prefers openai-agents." - ) -from dotenv import dotenv_values -from .utils.globals import LLM_DEFAULT_BASE_URL, LMSTUDIO_DEFAULT_BASE_URL, get_comfyui_copilot_api_key, is_lmstudio_url -from openai import AsyncOpenAI - - -from agents._config import set_default_openai_api -from agents.tracing import set_tracing_disabled -import asyncio -# from .utils.logger import log - -# def load_env_config(): -# """Load environment variables from .env.llm file""" -# from dotenv import load_dotenv - -# env_file_path = os.path.join(os.path.dirname(__file__), '.env.llm') -# if os.path.exists(env_file_path): -# load_dotenv(env_file_path) -# log.info(f"Loaded environment variables from {env_file_path}") -# else: -# log.warning(f"Warning: .env.llm not found at {env_file_path}") - - -# # Load environment configuration -# load_env_config() - -set_default_openai_api("chat_completions") -set_tracing_disabled(False) - - -def create_agent(**kwargs) -> Agent: - # 通过用户配置拿/环境变量 - config = kwargs.pop("config") if "config" in kwargs else {} - # 避免将 None 写入 headers - session_id = (config or {}).get("session_id") - default_headers = {} - if session_id: - default_headers["X-Session-ID"] = session_id - - # Determine base URL and API key - base_url = LLM_DEFAULT_BASE_URL - api_key = get_comfyui_copilot_api_key() or "" - - if config: - if config.get("openai_base_url") and config.get("openai_base_url") != "": - base_url = config.get("openai_base_url") - if config.get("openai_api_key") and config.get("openai_api_key") != "": - api_key = config.get("openai_api_key") - - # Check if this is LMStudio and adjust API key handling - is_lmstudio = is_lmstudio_url(base_url) - if is_lmstudio and not api_key: - # LMStudio typically doesn't require an API key, use a placeholder - api_key = "lmstudio-local" - - client = AsyncOpenAI( - api_key=api_key, - base_url=base_url, - default_headers=default_headers, - ) - - # Determine model with proper precedence: - # 1) Explicit selection from config (model_select from frontend) - # 2) Explicit kwarg 'model' (call-site override) - model_from_config = (config or {}).get("model_select") - model_from_kwargs = kwargs.pop("model", None) - - model_name = model_from_kwargs or model_from_config or "gemini-2.5-flash" - model = OpenAIChatCompletionsModel(model_name, openai_client=client) - - # Safety: ensure no stray 'model' remains in kwargs to avoid duplicate kwarg errors - kwargs.pop("model", None) - - if config.get("max_tokens"): - return Agent(model=model, model_settings=ModelSettings(max_tokens=config.get("max_tokens") or 8192), **kwargs) - return Agent(model=model, **kwargs) \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/backend/controller/conversation_api.py b/custom_nodes/ComfyUI-Copilot/backend/controller/conversation_api.py deleted file mode 100644 index 195cb0f00842384de7aec19dfd97f6387ee0f35f..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/controller/conversation_api.py +++ /dev/null @@ -1,1083 +0,0 @@ -# Copyright (C) 2025 AIDC-AI -# Licensed under the MIT License. - -import json -import asyncio -import time -from typing import Optional, TypedDict, List, Union -import threading -from collections import defaultdict - -from sqlalchemy.orm import identity - -from ..utils.globals import set_language, apply_llm_env_defaults -from ..utils.auth_utils import extract_and_store_api_key -import server -from aiohttp import web -import base64 - -# Import the MCP client function -import os -import shutil - -from ..service.debug_agent import debug_workflow_errors -from ..dao.workflow_table import save_workflow_data, get_workflow_data_by_id, update_workflow_ui_by_id -from ..service.mcp_client import comfyui_agent_invoke -from ..utils.request_context import set_request_context, get_session_id -from ..utils.logger import log -from ..utils.modelscope_gateway import ModelScopeGateway -import folder_paths - - -def get_llm_config_from_headers(request): - """Extract LLM-related configuration from request headers.""" - return { - "openai_api_key": request.headers.get('Openai-Api-Key'), - "openai_base_url": request.headers.get('Openai-Base-Url'), - # Workflow LLM settings (optional, used by tools/agents that need a different LLM) - "workflow_llm_api_key": request.headers.get('Workflow-LLM-Api-Key'), - "workflow_llm_base_url": request.headers.get('Workflow-LLM-Base-Url'), - "workflow_llm_model": request.headers.get('Workflow-LLM-Model'), - } - - -# 全局下载进度存储 -download_progress = {} -download_lock = threading.Lock() - -# 不再使用内存存储会话消息,改为从前端传递历史消息 - -# 在文件开头添加 -STATIC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "public") - -# Define types using TypedDict -class Node(TypedDict): - name: str # 节点名称 - description: str # 节点描述 - image: str # 节点图片url,可为空 - github_url: str # 节点github地址 - from_index: int # 节点在列表中的位置 - to_index: int # 节点在列表中的位置 - -class NodeInfo(TypedDict): - existing_nodes: List[Node] # 已安装的节点 - missing_nodes: List[Node] # 未安装的节点 - -class Workflow(TypedDict, total=False): - id: Optional[int] # 工作流id - name: Optional[str] # 工作流名称 - description: Optional[str] # 工作流描述 - image: Optional[str] # 工作流图片 - workflow: Optional[str] # 工作流 - -class ExtItem(TypedDict): - type: str # 扩展类型 - data: Union[dict, list] # 扩展数据 - -class ChatResponse(TypedDict): - session_id: str # 会话id - text: Optional[str] # 返回文本 - finished: bool # 是否结束 - type: str # 返回的类型 - format: str # 返回的格式 - ext: Optional[List[ExtItem]] # 扩展信息 - -# 下载进度回调类 -class DownloadProgressCallback: - def __init__(self, id: str, filename: str, file_size: int, download_id: str): - self.id = id - self.filename = filename - self.file_size = file_size - self.download_id = download_id - self.progress = 0 - self.status = "downloading" # downloading, completed, failed - self.error_message = None - self.start_time = time.time() - - # 初始化进度记录 - with download_lock: - download_progress[download_id] = { - "id": id, - "filename": filename, - "file_size": file_size, - "progress": 0, - "percentage": 0.0, - "status": "downloading", - "start_time": self.start_time, - "estimated_time": None, - "speed": 0.0, - "error_message": None - } - - def update(self, size: int): - """更新下载进度""" - self.progress += size - current_time = time.time() - elapsed_time = current_time - self.start_time - - # 计算下载速度和预估时间 - if elapsed_time > 0: - speed = self.progress / elapsed_time # bytes per second - if speed > 0 and self.progress < self.file_size: - remaining_bytes = self.file_size - self.progress - estimated_time = remaining_bytes / speed - else: - estimated_time = None - else: - speed = 0.0 - estimated_time = None - - percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0 - - # 更新全局进度 - with download_lock: - if self.download_id in download_progress: - # 直接更新字典的值,而不是调用update方法 - progress_dict = download_progress[self.download_id] - progress_dict["progress"] = self.progress - progress_dict["percentage"] = round(percentage, 2) - progress_dict["speed"] = round(speed, 2) - progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None - - def end(self, success: bool = True, error_message: str = None): - """下载结束回调""" - current_time = time.time() - total_time = current_time - self.start_time - - if success: - self.status = "completed" - # 验证下载完整性 - assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}" - else: - self.status = "failed" - self.error_message = error_message - - # 更新最终状态 - with download_lock: - if self.download_id in download_progress: - # 直接更新字典的值,而不是调用update方法 - progress_dict = download_progress[self.download_id] - progress_dict["status"] = self.status - progress_dict["progress"] = self.file_size if success else self.progress - if self.file_size > 0: - progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100 - else: - progress_dict["percentage"] = 0.0 - progress_dict["total_time"] = round(total_time, 2) - progress_dict["error_message"] = self.error_message - - def fail(self, error_message: str): - """下载失败回调""" - self.end(success=False, error_message=error_message) - -# 生成唯一下载ID -def generate_download_id() -> str: - import uuid - return str(uuid.uuid4()) - -async def upload_to_oss(file_data: bytes, filename: str) -> str: - # TODO: Implement your OSS upload logic here - # For now, save locally and return a placeholder URL - - try: - # Create uploads directory if it doesn't exist - uploads_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'uploads') - os.makedirs(uploads_dir, exist_ok=True) - - # Generate unique filename to avoid conflicts - import uuid - unique_filename = f"{uuid.uuid4().hex}_{filename}" - file_path = os.path.join(uploads_dir, unique_filename) - - # Save file locally - with open(file_path, 'wb') as f: - f.write(file_data) - - # Return a local URL or base64 data URL for now - # In production, replace this with actual OSS URL - base64_data = base64.b64encode(file_data).decode('utf-8') - # Determine MIME type based on file extension - if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')): - mime_type = f"image/{filename.split('.')[-1].lower()}" - if mime_type == "image/jpg": - mime_type = "image/jpeg" - else: - mime_type = "image/jpeg" # Default - - return f"data:{mime_type};base64,{base64_data}" - - except Exception as e: - log.error(f"Error uploading file {filename}: {str(e)}") - # Return original base64 data if upload fails - base64_data = base64.b64encode(file_data).decode('utf-8') - return f"data:image/jpeg;base64,{base64_data}" - -# 关联用户消息和AI响应的checkpoint信息 -def processMessagesWithCheckpoints(messages): - # ... existing code ... - pass - -@server.PromptServer.instance.routes.post("/api/chat/invoke") -async def invoke_chat(request): - log.info("Received invoke_chat request") - - # Extract and store API key from Authorization header - extract_and_store_api_key(request) - - req_json = await request.json() - log.info("Request JSON:", req_json) - - response = web.StreamResponse( - status=200, - reason='OK', - headers={ - 'Content-Type': 'application/json', - 'X-Content-Type-Options': 'nosniff' - } - ) - await response.prepare(request) - - session_id = req_json.get('session_id') - prompt = req_json.get('prompt') - images = req_json.get('images', []) - intent = req_json.get('intent') - ext = req_json.get('ext') - historical_messages = req_json.get('messages', []) - workflow_checkpoint_id = req_json.get('workflow_checkpoint_id') - - # 获取当前语言 - language = request.headers.get('Accept-Language', 'en') - set_language(language) - - # 构建配置信息 - config = { - "session_id": session_id, - "workflow_checkpoint_id": workflow_checkpoint_id, - **get_llm_config_from_headers(request), - "model_select": next((x['data'][0] for x in ext if x['type'] == 'model_select' and x.get('data')), None) - } - # Apply .env-based defaults for LLM-related fields (config > .env > code defaults) - config = apply_llm_env_defaults(config) - - # 设置请求上下文 - 这里建立context隔离 - set_request_context(session_id, workflow_checkpoint_id, config) - - # 图片处理已移至前端,图片信息现在包含在历史消息的OpenAI格式中 - # 保留空的处理逻辑以向后兼容,但实际不再使用 - processed_images = [] - if images and len(images) > 0: - log.info(f"Note: Received {len(images)} images in legacy format, but using OpenAI message format instead") - - # 历史消息已经从前端传递过来,格式为OpenAI格式,直接使用 - log.info(f"-- Received {len(historical_messages)} historical messages") - - # Log workflow checkpoint ID if provided (workflow is now pre-saved before invoke) - if workflow_checkpoint_id: - log.info(f"Using workflow checkpoint ID: {workflow_checkpoint_id} for session {session_id}") - else: - log.info(f"No workflow checkpoint ID provided for session {session_id}") - - # 历史消息已经从前端传递过来,包含了正确格式的OpenAI消息(包括图片) - # 直接使用前端传递的历史消息,无需重新构建当前消息 - openai_messages = historical_messages - - # 不再需要创建用户消息存储到后端,前端负责消息存储 - - try: - # Call the MCP client to get streaming response with historical messages and image support - # Pass OpenAI-formatted messages and processed images to comfyui_agent_invoke - accumulated_text = "" - ext_data = None - finished = True # Default to True - has_sent_response = False - previous_text_length = 0 - - log.info(f"config: {config}") - - # Pass messages in OpenAI format (images are now included in messages) - # Config is now available through request context - async for result in comfyui_agent_invoke(openai_messages, None): - # The MCP client now returns tuples (text, ext_with_finished) where ext_with_finished includes finished status - if isinstance(result, tuple) and len(result) == 2: - text, ext_with_finished = result - if text: - accumulated_text = text # text from MCP is already accumulated - - if ext_with_finished: - # Extract ext data and finished status from the structured response - ext_data = ext_with_finished.get("data") - finished = ext_with_finished.get("finished", True) - log.info(f"-- Received ext data: {ext_data}, finished: {finished}") - else: - # Handle single text chunk (backward compatibility) - text_chunk = result - if text_chunk: - accumulated_text += text_chunk - log.info(f"-- Received text chunk: '{text_chunk}', total length: {len(accumulated_text)}") - - # Send streaming response if we have new text content - # Only send intermediate responses during streaming (not the final one) - if accumulated_text and len(accumulated_text) > previous_text_length: - chat_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=False, # Always false during streaming - type="message", - format="markdown", - ext=None # ext is only sent in final response - ) - - await response.write(json.dumps(chat_response).encode() + b"\n") - previous_text_length = len(accumulated_text) - await asyncio.sleep(0.01) # Small delay for streaming effect - - # Send final response with proper finished logic from MCP client - log.info(f"-- Sending final response: {len(accumulated_text)} chars, ext: {bool(ext_data)}, finished: {finished}") - - final_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=finished, # Use finished status from MCP client - type="message", - format="markdown", - ext=ext_data - ) - - await response.write(json.dumps(final_response).encode() + b"\n") - - # AI响应不再存储到后端,前端负责消息存储 - - except Exception as e: - log.error(f"Error in invoke_chat: {str(e)}") - error_response = ChatResponse( - session_id=session_id, - text=f"I apologize, but an error occurred: {str(e)}", - finished=True, # Always finish on error - type="message", - format="text", - ext=None - ) - await response.write(json.dumps(error_response).encode() + b"\n") - - await response.write_eof() - return response - - -@server.PromptServer.instance.routes.post("/api/save-workflow-checkpoint") -async def save_workflow_checkpoint(request): - """ - Save workflow checkpoint for restore functionality - """ - log.info("Received save-workflow-checkpoint request") - req_json = await request.json() - - try: - session_id = req_json.get('session_id') - workflow_api = req_json.get('workflow_api') # API format workflow - workflow_ui = req_json.get('workflow_ui') # UI format workflow - checkpoint_type = req_json.get('checkpoint_type', 'debug_start') # debug_start, debug_complete, user_message_checkpoint - message_id = req_json.get('message_id') # User message ID for linking (optional) - - if not session_id or not workflow_api: - return web.json_response({ - "success": False, - "message": "Missing required parameters: session_id and workflow_api" - }) - - # Save workflow with checkpoint type in attributes - attributes = { - "checkpoint_type": checkpoint_type, - "timestamp": time.time() - } - - # Set description and additional attributes based on checkpoint type - if checkpoint_type == "user_message_checkpoint" and message_id: - attributes.update({ - "description": f"Workflow checkpoint before user message {message_id}", - "message_id": message_id, - "source": "user_message_pre_invoke" - }) - else: - attributes["description"] = f"Workflow checkpoint: {checkpoint_type}" - - version_id = save_workflow_data( - session_id=session_id, - workflow_data=workflow_api, - workflow_data_ui=workflow_ui, - attributes=attributes - ) - - log.info(f"Workflow checkpoint saved with version ID: {version_id}") - - # Return response format based on checkpoint type - response_data = { - "version_id": version_id, - "checkpoint_type": checkpoint_type - } - - if checkpoint_type == "user_message_checkpoint" and message_id: - response_data.update({ - "checkpoint_id": version_id, # Add checkpoint_id alias for user message checkpoints - "message_id": message_id - }) - - return web.json_response({ - "success": True, - "data": response_data, - "message": f"Workflow checkpoint saved successfully" - }) - - except Exception as e: - log.error(f"Error saving workflow checkpoint: {str(e)}") - return web.json_response({ - "success": False, - "message": f"Failed to save workflow checkpoint: {str(e)}" - }) - - - -@server.PromptServer.instance.routes.get("/api/restore-workflow-checkpoint") -async def restore_workflow_checkpoint(request): - """ - Restore workflow checkpoint by version ID - """ - log.info("Received restore-workflow-checkpoint request") - - try: - version_id = request.query.get('version_id') - - if not version_id: - return web.json_response({ - "success": False, - "message": "Missing required parameter: version_id" - }) - - try: - version_id = int(version_id) - except ValueError: - return web.json_response({ - "success": False, - "message": "Invalid version_id format" - }) - - # Get workflow data by version ID - workflow_version = get_workflow_data_by_id(version_id) - - if not workflow_version: - return web.json_response({ - "success": False, - "message": f"Workflow version {version_id} not found" - }) - - log.info(f"Restored workflow checkpoint version ID: {version_id}") - - return web.json_response({ - "success": True, - "data": { - "version_id": version_id, - "workflow_data": workflow_version.get('workflow_data'), - "workflow_data_ui": workflow_version.get('workflow_data_ui'), - "attributes": workflow_version.get('attributes'), - "created_at": workflow_version.get('created_at') - }, - "message": f"Workflow checkpoint restored successfully" - }) - - except Exception as e: - log.error(f"Error restoring workflow checkpoint: {str(e)}") - return web.json_response({ - "success": False, - "message": f"Failed to restore workflow checkpoint: {str(e)}" - }) - - -@server.PromptServer.instance.routes.post("/api/debug-agent") -async def invoke_debug(request): - """ - Debug agent endpoint for analyzing ComfyUI workflow errors - """ - log.info("Received debug-agent request") - - # Extract and store API key from Authorization header - extract_and_store_api_key(request) - - req_json = await request.json() - - response = web.StreamResponse( - status=200, - reason='OK', - headers={ - 'Content-Type': 'application/json', - 'X-Content-Type-Options': 'nosniff' - } - ) - await response.prepare(request) - - session_id = req_json.get('session_id') - workflow_data = req_json.get('workflow_data') - - # Get configuration from headers (OpenAI settings) - config = { - "session_id": session_id, - "model": "gemini-2.5-flash", # Default model for debug agents - **get_llm_config_from_headers(request), - } - # Apply .env-based defaults for LLM-related fields (config > .env > code defaults) - config = apply_llm_env_defaults(config) - - # 获取当前语言 - language = request.headers.get('Accept-Language', 'en') - set_language(language) - - # 设置请求上下文 - 为debug请求建立context隔离 - set_request_context(session_id, None, config) - - log.info(f"Debug agent config: {config}") - log.info(f"Session ID: {session_id}") - log.info(f"Workflow nodes: {list(workflow_data.keys()) if workflow_data else 'None'}") - - try: - # Call the debug agent with streaming response - accumulated_text = "" - final_ext_data = None - finished = False - - async for result in debug_workflow_errors(workflow_data): - # Stream the response - if isinstance(result, tuple) and len(result) == 2: - text, ext = result - if text: - accumulated_text = text # text is already accumulated from debug agent - - # Handle new ext format from debug_agent matching mcp-client - if ext: - if isinstance(ext, dict) and "data" in ext and "finished" in ext: - # New format: {"data": ext, "finished": finished} - final_ext_data = ext["data"] - finished = ext["finished"] - - # 检查是否包含需要实时发送的workflow_update或param_update - has_realtime_ext = False - if final_ext_data: - for ext_item in final_ext_data: - if ext_item.get("type") in ["workflow_update", "param_update"]: - has_realtime_ext = True - break - - # 如果包含实时ext数据或者已完成,则发送响应 - if has_realtime_ext or finished: - chat_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=finished, - type="message", - format="markdown", - ext=final_ext_data # 发送ext数据 - ) - await response.write(json.dumps(chat_response).encode() + b"\n") - elif not finished: - # 只有文本更新,不发送ext数据 - chat_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=False, - type="message", - format="markdown", - ext=None - ) - await response.write(json.dumps(chat_response).encode() + b"\n") - else: - # Legacy format: direct ext data (for backward compatibility) - final_ext_data = ext - finished = False - - # Create streaming response - chat_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=False, - type="message", - format="markdown", - ext=ext - ) - await response.write(json.dumps(chat_response).encode() + b"\n") - else: - # No ext data, just text streaming - chat_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=False, - type="message", - format="markdown", - ext=None - ) - await response.write(json.dumps(chat_response).encode() + b"\n") - - await asyncio.sleep(0.01) # Small delay for streaming effect - - # Send final response - final_response = ChatResponse( - session_id=session_id, - text=accumulated_text, - finished=True, - type="message", - format="markdown", - ext=final_ext_data if final_ext_data else [{"type": "debug_complete", "data": {"status": "completed"}}] - ) - - # Save workflow checkpoint after debug completion if we have workflow_data - if workflow_data and accumulated_text: - try: - current_session_id = get_session_id() - checkpoint_id = save_workflow_data( - session_id=current_session_id, - workflow_data=workflow_data, - workflow_data_ui=None, # UI format not available in debug agent - attributes={ - "checkpoint_type": "debug_complete", - "description": "Workflow state after debug completion", - "timestamp": time.time() - } - ) - - # Add checkpoint info to ext data - if final_response["ext"]: - final_response["ext"].append({ - "type": "debug_checkpoint", - "data": { - "checkpoint_id": checkpoint_id, - "checkpoint_type": "debug_complete" - } - }) - else: - final_response["ext"] = [{ - "type": "debug_checkpoint", - "data": { - "checkpoint_id": checkpoint_id, - "checkpoint_type": "debug_complete" - } - }] - - log.info(f"Debug completion checkpoint saved with ID: {checkpoint_id}") - except Exception as checkpoint_error: - log.error(f"Failed to save debug completion checkpoint: {checkpoint_error}") - - await response.write(json.dumps(final_response).encode() + b"\n") - log.info("Debug agent processing complete") - - except Exception as e: - log.error(f"Error in debug agent: {str(e)}") - import traceback - traceback.print_exc() - - error_response = ChatResponse( - session_id=session_id, - text=f"❌ Debug agent error: {str(e)}", - finished=True, - type="message", - format="text", - ext=[{"type": "error", "data": {"error": str(e)}}] - ) - await response.write(json.dumps(error_response).encode() + b"\n") - - await response.write_eof() - return response - - -@server.PromptServer.instance.routes.post("/api/update-workflow-ui") -async def update_workflow_ui(request): - """ - Update workflow_data_ui field for a specific checkpoint without affecting other fields - """ - log.info("Received update-workflow-ui request") - req_json = await request.json() - - try: - checkpoint_id = req_json.get('checkpoint_id') - workflow_data_ui = req_json.get('workflow_data_ui') - - if not checkpoint_id or not workflow_data_ui: - return web.json_response({ - "success": False, - "message": "Missing required parameters: checkpoint_id and workflow_data_ui" - }) - - try: - checkpoint_id = int(checkpoint_id) - except ValueError: - return web.json_response({ - "success": False, - "message": "Invalid checkpoint_id format" - }) - - # Update only the workflow_data_ui field - success = update_workflow_ui_by_id(checkpoint_id, workflow_data_ui) - - if success: - log.info(f"Successfully updated workflow_data_ui for checkpoint ID: {checkpoint_id}") - return web.json_response({ - "success": True, - "message": f"Workflow UI data updated successfully for checkpoint {checkpoint_id}" - }) - else: - return web.json_response({ - "success": False, - "message": f"Checkpoint {checkpoint_id} not found" - }) - - except Exception as e: - log.error(f"Error updating workflow UI data: {str(e)}") - return web.json_response({ - "success": False, - "message": f"Failed to update workflow UI data: {str(e)}" - }) - -@server.PromptServer.instance.routes.post("/api/download-model") -async def download_model(request): - """ - Download model from ModelScope using SDK - """ - log.info("Received download-model request") - req_json = await request.json() - - try: - id = req_json.get('id') - model_id = req_json.get('model_id') - model_type = req_json.get('model_type') - dest_dir = req_json.get('dest_dir') - - # 验证必需参数 - if not id: - return web.json_response({ - "success": False, - "message": "Missing required parameter: id" - }) - - if not model_id: - return web.json_response({ - "success": False, - "message": "Missing required parameter: model_id" - }) - - if not model_type: - return web.json_response({ - "success": False, - "message": "Missing required parameter: model_type" - }) - - log.info(f"Downloading model: {model_id} (type: {model_type})") - - # 生成下载ID - download_id = generate_download_id() - - # 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型 - resolved_dest_dir = None - if dest_dir: - resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}")) - else: - try: - model_type_paths = folder_paths.get_folder_paths(model_type) - resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir, model_type) - except Exception: - resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type) - - # 创建进度回调 - progress_callback = DownloadProgressCallback( - id=id, - filename=f"{model_id}.{model_type}", - file_size=0, # 实际大小会在下载过程中获取 - download_id=download_id - ) - - # 启动下载任务(异步执行) - async def download_task(): - try: - # 调用下载方法 - 使用snapshot_download - from modelscope import snapshot_download - - # 创建进度回调包装器 - 实现ModelScope期望的接口 - class ProgressWrapper: - """Factory that returns a per-file progress object with update/end.""" - def __init__(self, download_id: str): - self.download_id = download_id - - def __call__(self, file_name: str, file_size: int): - # Create a per-file progress tracker expected by ModelScope - download_id = self.download_id - - class _PerFileProgress: - def __init__(self, fname: str, fsize: int): - self.file_name = fname - self.file_size = max(int(fsize or 0), 0) - self.progress = 0 - self.last_update_time = time.time() - self.last_downloaded = 0 - with download_lock: - if download_id in download_progress: - # If unknown size, keep 0 to avoid div-by-zero - download_progress[download_id]["file_size"] = self.file_size - - def update(self, size: int): - try: - self.progress += int(size or 0) - now = time.time() - # Update global progress - with download_lock: - if download_id in download_progress: - dp = download_progress[download_id] - dp["progress"] = self.progress - if self.file_size > 0: - dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2) - # speed - elapsed = max(now - self.last_update_time, 1e-6) - speed = (self.progress - self.last_downloaded) / elapsed - dp["speed"] = round(speed, 2) - self.last_update_time = now - self.last_downloaded = self.progress - except Exception as e: - log.error(f"Error in progress update: {e}") - - def end(self): - # Called by modelscope when a file finishes - with download_lock: - if download_id in download_progress: - dp = download_progress[download_id] - if self.file_size > 0: - dp["progress"] = self.file_size - dp["percentage"] = 100.0 - - return _PerFileProgress(file_name, file_size) - - progress_wrapper = ProgressWrapper(download_id) - - # 添加调试日志 - log.info(f"Starting download with progress wrapper: {download_id}") - - # 在线程中执行阻塞的下载,避免阻塞事件循环 - from functools import partial - local_dir = await asyncio.to_thread( - partial( - snapshot_download, - model_id=model_id, - cache_dir=resolved_dest_dir, - progress_callbacks=[progress_wrapper] - ) - ) - - # 下载完成 - progress_callback.end(success=True) - log.info(f"Model downloaded successfully to: {local_dir}") - - # 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir) - try: - moved_count = 0 - allowed_exts = { - ".safetensors", ".ckpt", ".pt", ".pth", ".bin" - # ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx" - } - for root, dirs, files in os.walk(local_dir): - for name in files: - ext = os.path.splitext(name)[1].lower() - if ext in allowed_exts: - src_path = os.path.join(root, name) - target_dir = resolved_dest_dir - os.makedirs(target_dir, exist_ok=True) - target_path = os.path.join(target_dir, name) - # 如果已经在目标目录则跳过 - if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir): - continue - # 处理重名情况:自动追加 _1, _2 ... - if os.path.exists(target_path): - base, ext_real = os.path.splitext(name) - idx = 1 - while True: - candidate = f"{base}_{idx}{ext_real}" - candidate_path = os.path.join(target_dir, candidate) - if not os.path.exists(candidate_path): - target_path = candidate_path - break - idx += 1 - shutil.move(src_path, target_path) - moved_count += 1 - log.info(f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}") - except Exception as move_err: - log.error(f"Post-download move failed: {move_err}") - - except Exception as e: - progress_callback.fail(str(e)) - log.error(f"Download failed: {str(e)}") - - # 启动异步下载任务 - asyncio.create_task(download_task()) - - return web.json_response({ - "success": True, - "data": { - "download_id": download_id, - "id": id, - "model_id": model_id, - "model_type": model_type, - "dest_dir": resolved_dest_dir, - "status": "started" - }, - "message": f"Download started for model '{model_id}'" - }) - - except ImportError as e: - log.error(f"ModelScope SDK not installed: {str(e)}") - return web.json_response({ - "success": False, - "message": "ModelScope SDK not installed. Please install with: pip install modelscope" - }) - - except Exception as e: - log.error(f"Error starting download: {str(e)}") - import traceback - traceback.print_exc() - return web.json_response({ - "success": False, - "message": f"Failed to start download: {str(e)}" - }) - -@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}") -async def get_download_progress(request): - """ - Get download progress by download ID - """ - download_id = request.match_info.get('download_id') - - if not download_id: - return web.json_response({ - "success": False, - "message": "Missing download_id parameter" - }) - - with download_lock: - progress_info = download_progress.get(download_id) - - if not progress_info: - return web.json_response({ - "success": False, - "message": f"Download ID {download_id} not found" - }) - - return web.json_response({ - "success": True, - "data": progress_info, - "message": "Download progress retrieved successfully" - }) - -@server.PromptServer.instance.routes.get("/api/download-progress") -async def list_downloads(request): - """ - List all active downloads - """ - with download_lock: - downloads = list(download_progress.keys()) - - return web.json_response({ - "success": True, - "data": { - "downloads": downloads, - "count": len(downloads) - }, - "message": "Download list retrieved successfully" - }) - -@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}") -async def clear_download_progress(request): - """ - Clear download progress record (for cleanup) - """ - download_id = request.match_info.get('download_id') - - if not download_id: - return web.json_response({ - "success": False, - "message": "Missing download_id parameter" - }) - - with download_lock: - if download_id in download_progress: - del download_progress[download_id] - return web.json_response({ - "success": True, - "message": f"Download progress {download_id} cleared successfully" - }) - else: - return web.json_response({ - "success": False, - "message": f"Download ID {download_id} not found" - }) - -@server.PromptServer.instance.routes.get("/api/model-searchs") -async def model_suggests(request): - """ - Get model search list by keyword - """ - log.info("Received model-search request") - try: - keyword = request.query.get('keyword') - - if not keyword: - return web.json_response({ - "success": False, - "message": "Missing required parameter: keyword" - }) - - # 创建ModelScope网关实例 - gateway = ModelScopeGateway() - - suggests = gateway.search(name=keyword) - - list = suggests["data"] if suggests.get("data") else [] - - return web.json_response({ - "success": True, - "data": { - "searchs": list, - "total": len(list) - }, - "message": f"Get searchs successfully" - }) - - except ImportError as e: - log.error(f"ModelScope SDK not installed: {str(e)}") - return web.json_response({ - "success": False, - "message": "ModelScope SDK not installed. Please install with: pip install modelscope" - }) - - except Exception as e: - log.error(f"Error get model searchs: {str(e)}") - import traceback - traceback.print_exc() - return web.json_response({ - "success": False, - "message": f"Get model searchs failed: {str(e)}" - }) - -@server.PromptServer.instance.routes.get("/api/model-paths") -async def model_paths(request): - """ - Get model paths by type - """ - log.info("Received model-paths request") - try: - model_paths = list(folder_paths.folder_names_and_paths.keys()) - return web.json_response({ - "success": True, - "data": { - "paths": model_paths, - }, - "message": f"Get paths successfully" - }) - - except Exception as e: - log.error(f"Error get model path: {str(e)}") - import traceback - traceback.print_exc() - return web.json_response({ - "success": False, - "message": f"Get model failed: {str(e)}" - }) \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/backend/controller/expert_api.py b/custom_nodes/ComfyUI-Copilot/backend/controller/expert_api.py deleted file mode 100644 index 3be69edc3502497100704f39722be346d0b2d80f..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/controller/expert_api.py +++ /dev/null @@ -1,192 +0,0 @@ -from server import PromptServer -from aiohttp import web -from typing import Dict, Any -import logging -from ..dao.expert_table import ( - create_rewrite_expert, - get_rewrite_expert, - list_rewrite_experts, - update_rewrite_expert_by_id, - delete_rewrite_expert_by_id -) - -# 配置日志 -logger = logging.getLogger(__name__) - -def validate_expert_data(data: Dict[str, Any]) -> tuple[bool, str, Dict[str, Any]]: - """验证专家数据""" - if not data: - return False, "请求数据不能为空", {} - - # 验证必填字段 - if 'name' not in data or not data['name']: - return False, "名称不能为空", {} - - # 验证字段类型和长度 - if not isinstance(data['name'], str) or len(data['name']) > 255: - return False, "名称必须是字符串且长度不超过255个字符", {} - - # 验证可选字段 - validated_data = { - 'name': data['name'].strip(), - 'description': data.get('description'), - 'content': data.get('content') - } - - return True, "", validated_data - -@PromptServer.instance.routes.post("/api/expert/experts") -async def create_expert(request): - """创建新的专家记录""" - try: - data = await request.json() - if not data: - return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400) - - # 验证数据 - is_valid, error_msg, validated_data = validate_expert_data(data) - if not is_valid: - return web.json_response({"success": False, "message": error_msg, "data": None}, status=400) - - # 创建专家记录 - expert_id = create_rewrite_expert( - name=validated_data['name'], - description=validated_data['description'], - content=validated_data['content'] - ) - - logger.info(f"成功创建专家记录,ID: {expert_id}") - - return web.json_response({"success": True, "message": "创建成功", "data": {"id": expert_id}}, status=200) - - except Exception as e: - logger.error(f"创建专家记录失败: {str(e)}") - return web.json_response({"success": False, "message": f"创建失败: {str(e)}", "data": None}, status=500) - -@PromptServer.instance.routes.get("/api/expert/experts") -async def get_experts(request): - """获取所有专家记录列表""" - try: - experts = list_rewrite_experts() - - logger.info(f"成功获取专家记录列表,共 {len(experts)} 条") - - return web.json_response({"success": True, "message": "获取列表成功", "data": {"total": len(experts), "experts": experts}}, status=200) - - except Exception as e: - logger.error(f"获取专家记录列表失败: {str(e)}") - return web.json_response({"success": False, "message": f"获取列表失败: {str(e)}", "data": None}, status=500) - -@PromptServer.instance.routes.get("/api/expert/experts/{expert_id}") -async def get_expert_by_id(request): - """根据ID获取专家记录""" - try: - expert_id = int(request.match_info['expert_id']) - expert = get_rewrite_expert(expert_id) - - if not expert: - return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404) - - logger.info(f"成功获取专家记录,ID: {expert_id}") - - return web.json_response({"success": True, "message": "获取记录成功", "data": expert}, status=200) - - except Exception as e: - logger.error(f"获取专家记录失败,ID: {expert_id}, 错误: {str(e)}") - return web.json_response({"success": False, "message": f"获取记录失败: {str(e)}", "data": None}, status=500) - -@PromptServer.instance.routes.put("/api/expert/experts/{expert_id}") -async def update_expert(request): - """更新专家记录""" - try: - expert_id = int(request.match_info['expert_id']) - data = await request.json() - if not data: - return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400) - - # 验证数据 - is_valid, error_msg, validated_data = validate_expert_data(data) - if not is_valid: - return web.json_response({"success": False, "message": error_msg, "data": None}, status=400) - - # 更新专家记录 - success = update_rewrite_expert_by_id( - expert_id=expert_id, - name=validated_data.get('name'), - description=validated_data.get('description'), - content=validated_data.get('content') - ) - - if not success: - return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404) - - logger.info(f"成功更新专家记录,ID: {expert_id}") - - return web.json_response({"success": True, "message": "更新成功", "data": {"id": expert_id}}, status=200) - - except Exception as e: - logger.error(f"更新专家记录失败,ID: {expert_id}, 错误: {str(e)}") - return web.json_response({"success": False, "message": f"更新失败: {str(e)}", "data": None}, status=500) - -@PromptServer.instance.routes.delete("/api/expert/experts/{expert_id}") -async def delete_expert(request): - """删除专家记录""" - try: - expert_id = int(request.match_info['expert_id']) - success = delete_rewrite_expert_by_id(expert_id) - - if not success: - return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404) - - logger.info(f"成功删除专家记录,ID: {expert_id}") - - return web.json_response({"success": True, "message": "删除成功", "data": {"id": expert_id}}, status=200) - - except Exception as e: - logger.error(f"删除专家记录失败,ID: {expert_id}, 错误: {str(e)}") - return web.json_response({"success": False, "message": f"删除失败: {str(e)}", "data": None}, status=500) - -@PromptServer.instance.routes.patch("/api/expert/experts/{expert_id}") -async def partial_update_expert(request): - """部分更新专家记录""" - try: - expert_id = int(request.match_info['expert_id']) - data = await request.json() - if not data: - return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400) - - # 验证字段 - update_data = {} - if 'name' in data: - if not isinstance(data['name'], str) or len(data['name']) > 255: - return web.json_response({"success": False, "message": "名称必须是字符串且长度不超过255个字符", "data": None}, status=400) - update_data['name'] = data['name'].strip() - - if 'description' in data: - update_data['description'] = data['description'] - - if 'content' in data: - update_data['content'] = data['content'] - - if not update_data: - return web.json_response({"success": False, "message": "没有提供要更新的字段", "data": None}, status=400) - - # 更新专家记录 - success = update_rewrite_expert_by_id( - expert_id=expert_id, - **update_data - ) - - if not success: - return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404) - - logger.info(f"成功部分更新专家记录,ID: {expert_id}") - - return web.json_response({"success": True, "message": "更新成功", "data": {"id": expert_id}}, status=200) - - except Exception as e: - logger.error(f"部分更新专家记录失败,ID: {expert_id}, 错误: {str(e)}") - return web.json_response({"success": False, "message": f"更新失败: {str(e)}", "data": None}, status=500) - -# 路由配置函数 -# 所有路由已通过装饰器自动注册到 ComfyUI 的 PromptServer diff --git a/custom_nodes/ComfyUI-Copilot/backend/core.py b/custom_nodes/ComfyUI-Copilot/backend/core.py deleted file mode 100644 index 65c71c30033b87936364fb18ff48507f56a441b4..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/core.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -from agents._config import set_default_openai_api -from agents.tracing import set_tracing_disabled -# from .utils.logger import log - -# def load_env_config(): -# """Load environment variables from .env.llm file""" -# from dotenv import load_dotenv - -# env_file_path = os.path.join(os.path.dirname(__file__), '.env.llm') -# if os.path.exists(env_file_path): -# load_dotenv(env_file_path) -# log.info(f"Loaded environment variables from {env_file_path}") -# else: -# log.warning(f"Warning: .env.llm not found at {env_file_path}") - - -# # Load environment configuration -# load_env_config() - -set_default_openai_api("chat_completions") -set_tracing_disabled(True) \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/backend/dao/__init__.py b/custom_nodes/ComfyUI-Copilot/backend/dao/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/custom_nodes/ComfyUI-Copilot/backend/dao/workflow_table.py b/custom_nodes/ComfyUI-Copilot/backend/dao/workflow_table.py deleted file mode 100644 index a1bd7827ff96accfd6dbb3ac27d93e931bdc1253..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/dao/workflow_table.py +++ /dev/null @@ -1,183 +0,0 @@ -import os -import json -from typing import Dict, Any, Optional -from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker -from datetime import datetime - -# 创建数据库基类 -Base = declarative_base() - -# 定义workflow_version表模型 -class WorkflowVersion(Base): - __tablename__ = 'workflow_version' - - id = Column(Integer, primary_key=True, autoincrement=True) - session_id = Column(String(255), nullable=False) - workflow_data = Column(Text, nullable=False) # JSON字符串 api格式 - workflow_data_ui = Column(Text, nullable=True) # JSON字符串 ui格式 - attributes = Column(Text, nullable=True) # JSON字符串,存储额外属性 - created_at = Column(DateTime, default=datetime.utcnow) - - def to_dict(self): - return { - 'id': self.id, - 'session_id': self.session_id, - 'workflow_data': json.loads(self.workflow_data) if self.workflow_data else None, - 'attributes': json.loads(self.attributes) if self.attributes else None, - 'created_at': self.created_at.isoformat() if self.created_at else None - } - -class DatabaseManager: - """数据库管理器""" - - def __init__(self, db_path: str = None): - if db_path is None: - # 默认数据库路径 - current_dir = os.path.dirname(os.path.abspath(__file__)) - db_dir = os.path.join(current_dir, '..', 'data') - os.makedirs(db_dir, exist_ok=True) - db_path = os.path.join(db_dir, 'workflow_debug.db') - - self.db_path = db_path - self.engine = create_engine(f'sqlite:///{db_path}', echo=False) - self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) - - # 创建表 - Base.metadata.create_all(bind=self.engine) - - def get_session(self): - """获取数据库会话""" - return self.SessionLocal() - - 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: - """保存工作流版本,返回新版本的ID""" - session = self.get_session() - try: - workflow_version = WorkflowVersion( - session_id=session_id, - workflow_data=json.dumps(workflow_data), - workflow_data_ui=json.dumps(workflow_data_ui) if workflow_data_ui else None, - attributes=json.dumps(attributes) if attributes else None - ) - session.add(workflow_version) - session.commit() - session.refresh(workflow_version) - return workflow_version.id - except Exception as e: - session.rollback() - raise e - finally: - session.close() - - def get_current_workflow_data(self, session_id: str) -> Optional[Dict[str, Any]]: - """获取当前session的最新工作流数据(最大ID版本)""" - session = self.get_session() - try: - latest_version = session.query(WorkflowVersion)\ - .filter(WorkflowVersion.session_id == session_id)\ - .order_by(WorkflowVersion.id.desc())\ - .first() - - if latest_version: - return json.loads(latest_version.workflow_data) - return None - finally: - session.close() - - def get_current_workflow_data_ui(self, session_id: str) -> Optional[Dict[str, Any]]: - """获取当前session的最新工作流数据(最大ID版本)""" - session = self.get_session() - try: - latest_version = session.query(WorkflowVersion)\ - .filter(WorkflowVersion.session_id == session_id)\ - .order_by(WorkflowVersion.id.desc())\ - .first() - - if latest_version: - return json.loads(latest_version.workflow_data_ui) - return None - finally: - session.close() - - def get_workflow_version_by_id(self, version_id: int) -> Optional[Dict[str, Any]]: - """根据版本ID获取工作流数据""" - session = self.get_session() - try: - version = session.query(WorkflowVersion)\ - .filter(WorkflowVersion.id == version_id)\ - .first() - - if version: - result = version.to_dict() - # 添加UI格式的工作流数据 - if version.workflow_data_ui: - result['workflow_data_ui'] = json.loads(version.workflow_data_ui) - return result - return None - finally: - session.close() - - def update_workflow_version(self, version_id: int, workflow_data: Dict[str, Any], attributes: Optional[Dict[str, Any]] = None) -> bool: - """更新指定版本的工作流数据""" - session = self.get_session() - try: - version = session.query(WorkflowVersion)\ - .filter(WorkflowVersion.id == version_id)\ - .first() - - if version: - version.workflow_data = json.dumps(workflow_data) - if attributes: - version.attributes = json.dumps(attributes) - session.commit() - return True - return False - except Exception as e: - session.rollback() - raise e - finally: - session.close() - - def update_workflow_ui(self, version_id: int, workflow_data_ui: Dict[str, Any]) -> bool: - """只更新指定版本的workflow_data_ui字段,不影响其他字段""" - session = self.get_session() - try: - version = session.query(WorkflowVersion)\ - .filter(WorkflowVersion.id == version_id)\ - .first() - - if version: - version.workflow_data_ui = json.dumps(workflow_data_ui) - session.commit() - return True - return False - except Exception as e: - session.rollback() - raise e - finally: - session.close() - -# 全局数据库管理器实例 -db_manager = DatabaseManager() - -def get_workflow_data(session_id: str) -> Optional[Dict[str, Any]]: - """获取当前session的工作流数据的便捷函数""" - return db_manager.get_current_workflow_data(session_id) - -def get_workflow_data_ui(session_id: str) -> Optional[Dict[str, Any]]: - """获取当前session的工作流数据的便捷函数""" - return db_manager.get_current_workflow_data_ui(session_id) - -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: - """保存工作流数据的便捷函数""" - return db_manager.save_workflow_version(session_id, workflow_data, workflow_data_ui, attributes) - -def get_workflow_data_by_id(version_id: int) -> Optional[Dict[str, Any]]: - """根据版本ID获取工作流数据的便捷函数""" - return db_manager.get_workflow_version_by_id(version_id) - -def update_workflow_ui_by_id(version_id: int, workflow_data_ui: Dict[str, Any]) -> bool: - """只更新指定版本的workflow_data_ui字段的便捷函数""" - return db_manager.update_workflow_ui(version_id, workflow_data_ui) \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/backend/logs/comfyui_copilot.log b/custom_nodes/ComfyUI-Copilot/backend/logs/comfyui_copilot.log deleted file mode 100644 index 91b2569a61dd8c629464f88b58e2c9d5473e7e9a..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/logs/comfyui_copilot.log +++ /dev/null @@ -1,1077 +0,0 @@ -2025-12-02 06:57:53 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-02 06:58:14 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 06:58:14 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 1 -2025-12-02 06:58:14 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 06:58:14 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2025-12-02 06:58:14 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 06:58:14 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 06:58:14 | INFO | conversation_api.py:invoke_chat:272 | -- Received 1 historical messages -2025-12-02 06:58:14 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 1 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 06:58:14 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 1, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 06:58:14 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 1 -2025-12-02 06:58:14 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 06:58:14 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 1 -2025-12-02 06:58:14 | INFO | message_memory.py:message_memory_optimize:57 | [Memory] New session detected, creating record -2025-12-02 06:58:14 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 1, messages: [{'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}] -2025-12-02 06:58:27 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 1 messages -2025-12-02 06:58:27 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 06:58:27 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 06:58:28 | ERROR | mcp_client.py:process_stream_events:453 | Unexpected streaming error: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-02 06:58:28 | ERROR | mcp_client.py:process_stream_events:454 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2025-12-02 06:58:28 | ERROR | mcp_client.py:comfyui_agent_invoke:503 | Non-retryable streaming error or max retries reached: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-02 06:58:28 | ERROR | mcp_client.py:comfyui_agent_invoke:504 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 460, in comfyui_agent_invoke - async for stream_data in process_stream_events(result): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 455, in process_stream_events - raise e - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2025-12-02 06:58:28 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2025-12-02 06:58:28 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 06:58:28 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 06:58:28 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 0 chars, ext: False, finished: True -2025-12-02 07:17:05 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:17:05 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 2 -2025-12-02 07:17:05 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 07:17:05 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2025-12-02 07:17:05 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:17:05 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 07:17:05 | INFO | conversation_api.py:invoke_chat:272 | -- Received 3 historical messages -2025-12-02 07:17:05 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 2 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:17:05 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 2, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 07:17:05 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 3 -2025-12-02 07:17:05 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:17:05 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 3 -2025-12-02 07:17:05 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 0, summary length: 0 -2025-12-02 07:17:05 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 3 (threshold: 8) -2025-12-02 07:17:05 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-02 07:17:05 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 3, messages: [{'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '{"session_id":"bb8e7985-667a-4ba4-b1de-dabcc12ab4b7","text":"","finished":true,"type":"message","format":"markdown","ext":null,"message_id":"48fc9c20-a56d-412c-906e-1af6c20d2de0"}'}, {'role': 'user', 'content': 'yes'}] -2025-12-02 07:17:09 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 3 messages -2025-12-02 07:17:09 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 07:17:09 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 07:17:27 | ERROR | mcp_client.py:process_stream_events:453 | Unexpected streaming error: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-02 07:17:27 | ERROR | mcp_client.py:process_stream_events:454 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2025-12-02 07:17:27 | ERROR | mcp_client.py:comfyui_agent_invoke:503 | Non-retryable streaming error or max retries reached: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-02 07:17:27 | ERROR | mcp_client.py:comfyui_agent_invoke:504 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 460, in comfyui_agent_invoke - async for stream_data in process_stream_events(result): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 455, in process_stream_events - raise e - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2025-12-02 07:17:27 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2025-12-02 07:17:27 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 07:17:27 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 07:17:27 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 0 chars, ext: False, finished: True -2025-12-02 07:17:52 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:17:52 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 3 -2025-12-02 07:17:52 | INFO | conversation_api.py:invoke_debug:499 | Received debug-agent request -2025-12-02 07:17:52 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2025-12-02 07:17:52 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:17:52 | INFO | conversation_api.py:invoke_debug:535 | Debug agent config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'model': 'gemini-2.5-flash', 'openai_api_key': None, 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0'} -2025-12-02 07:17:52 | INFO | conversation_api.py:invoke_debug:536 | Session ID: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:17:52 | INFO | conversation_api.py:invoke_debug:537 | Workflow nodes: ['1', '2', '3', '4', '5', '6', '8', '11', '12', '14', '15', '20', '21', '26', '27'] -2025-12-02 07:17:52 | INFO | debug_agent.py:debug_workflow_errors:215 | Saving workflow data for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:17:52 | INFO | debug_agent.py:debug_workflow_errors:221 | Workflow saved with version ID: 4 -2025-12-02 07:17:52 | INFO | debug_agent.py:debug_workflow_errors:505 | -- Starting workflow validation process for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:17:52 | INFO | debug_agent.py:debug_workflow_errors:512 | === Debug Coordinator starting === -2025-12-02 07:17:52 | INFO | debug_agent.py:debug_workflow_errors:539 | Handoff to: ComfyUI-Debug-Coordinator -2025-12-02 07:17:54 | ERROR | debug_agent.py:debug_workflow_errors:700 | Error in debug_workflow_errors: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-02 07:17:54 | ERROR | conversation_api.py:invoke_debug:663 | Failed to save debug completion checkpoint: 'dict' object has no attribute 'append' -2025-12-02 07:17:54 | INFO | conversation_api.py:invoke_debug:666 | Debug agent processing complete -2025-12-02 07:18:02 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:18:02 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 6 -2025-12-02 07:18:02 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 07:18:02 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2025-12-02 07:18:02 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:18:02 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 07:18:02 | INFO | conversation_api.py:invoke_chat:272 | -- Received 7 historical messages -2025-12-02 07:18:02 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 6 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:18:02 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 6, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 07:18:02 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 7 -2025-12-02 07:18:02 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:18:02 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 7 -2025-12-02 07:18:02 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 0, summary length: 0 -2025-12-02 07:18:02 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 7 (threshold: 8) -2025-12-02 07:18:02 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-02 07:18:02 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 7, messages: [{'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '{"session_id":"bb8e7985-667a-4ba4-b1de-dabcc12ab4b7","text":"","finished":true,"type":"message","format":"markdown","ext":null,"message_id":"48fc9c20-a56d-412c-906e-1af6c20d2de0"}'}, {'role': 'user', 'content': 'yes'}, {'role': 'assistant', 'content': '{"session_id":"bb8e7985-667a-4ba4-b1de-dabcc12ab4b7","text":"","finished":true,"type":"message","format":"markdown","ext":null,"message_id":"690e57f6-30ac-4394-ae6c-e4b3e481d4e5"}'}, {'role': 'assistant', 'content': 'Would you like me to help you debug the current workflow on the canvas?'}, {'role': 'assistant', 'content': '\n\n× Error occurred during debugging: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}] -2025-12-02 07:19:29 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-02 07:19:38 | INFO | conversation_api.py:model_paths:1065 | Received model-paths request -2025-12-02 07:19:54 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:19:54 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 7 -2025-12-02 07:19:54 | INFO | conversation_api.py:invoke_debug:499 | Received debug-agent request -2025-12-02 07:19:54 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2025-12-02 07:19:54 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:19:54 | INFO | conversation_api.py:invoke_debug:535 | Debug agent config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'model': 'gemini-2.5-flash', 'openai_api_key': None, 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0'} -2025-12-02 07:19:54 | INFO | conversation_api.py:invoke_debug:536 | Session ID: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:19:54 | INFO | conversation_api.py:invoke_debug:537 | Workflow nodes: ['1', '2', '3', '4', '5', '6', '8', '11', '12', '14', '15', '20', '21', '26', '27'] -2025-12-02 07:19:54 | INFO | debug_agent.py:debug_workflow_errors:215 | Saving workflow data for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:19:54 | INFO | debug_agent.py:debug_workflow_errors:221 | Workflow saved with version ID: 8 -2025-12-02 07:19:54 | INFO | debug_agent.py:debug_workflow_errors:505 | -- Starting workflow validation process for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:19:54 | INFO | debug_agent.py:debug_workflow_errors:512 | === Debug Coordinator starting === -2025-12-02 07:19:54 | INFO | debug_agent.py:debug_workflow_errors:539 | Handoff to: ComfyUI-Debug-Coordinator -2025-12-02 07:20:03 | ERROR | debug_agent.py:debug_workflow_errors:700 | Error in debug_workflow_errors: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-02 07:20:03 | ERROR | conversation_api.py:invoke_debug:663 | Failed to save debug completion checkpoint: 'dict' object has no attribute 'append' -2025-12-02 07:20:03 | INFO | conversation_api.py:invoke_debug:666 | Debug agent processing complete -2025-12-02 07:22:58 | INFO | conversation_api.py:model_paths:1065 | Received model-paths request -2025-12-02 07:23:03 | ERROR | mcp_client.py:comfyui_agent_invoke:684 | Error in comfyui_agent_invoke: unhandled errors in a TaskGroup (1 sub-exception) -2025-12-02 07:23:03 | ERROR | mcp_client.py:comfyui_agent_invoke:685 | Traceback: + Exception Group Traceback (most recent call last): - | File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 143, in comfyui_agent_invoke - | async with mcp_server, bing_server: - | File "/data/envs/Cheny/lib/python3.11/site-packages/agents/mcp/server.py", line 242, in __aenter__ - | await self.connect() - | File "/data/envs/Cheny/lib/python3.11/site-packages/agents/mcp/server.py", line 267, in connect - | transport = await self.exit_stack.enter_async_context(self.create_streams()) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 650, in enter_async_context - | result = await _enter(cm) - | ^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 210, in __aenter__ - | return await anext(self.gen) - | ^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/mcp/client/sse.py", line 55, in sse_client - | async with anyio.create_task_group() as tg: - | File "/data/envs/Cheny/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__ - | raise BaseExceptionGroup( - | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) - +-+---------------- 1 ---------------- - | Traceback (most recent call last): - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions - | yield - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 394, in handle_async_request - | resp = await self._pool.handle_async_request(req) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 256, in handle_async_request - | raise exc from None - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 236, in handle_async_request - | response = await connection.handle_async_request( - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http_proxy.py", line 316, in handle_async_request - | stream = await stream.start_tls(**kwargs) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http11.py", line 376, in start_tls - | return await self._stream.start_tls(ssl_context, server_hostname, timeout) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_backends/anyio.py", line 67, in start_tls - | with map_exceptions(exc_map): - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 158, in __exit__ - | self.gen.throw(typ, value, traceback) - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions - | raise to_exc(exc) from exc - | httpcore.ConnectTimeout - | - | The above exception was the direct cause of the following exception: - | - | Traceback (most recent call last): - | File "/data/envs/Cheny/lib/python3.11/site-packages/mcp/client/sse.py", line 61, in sse_client - | async with aconnect_sse( - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 210, in __aenter__ - | return await anext(self.gen) - | ^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx_sse/_api.py", line 74, in aconnect_sse - | async with client.stream(method, url, headers=headers, **kwargs) as response: - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 210, in __aenter__ - | return await anext(self.gen) - | ^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1583, in stream - | response = await self.send( - | ^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1629, in send - | response = await self._send_handling_auth( - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1657, in _send_handling_auth - | response = await self._send_handling_redirects( - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1694, in _send_handling_redirects - | response = await self._send_single_request(request) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1730, in _send_single_request - | response = await transport.handle_async_request(request) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 393, in handle_async_request - | with map_httpcore_exceptions(): - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 158, in __exit__ - | self.gen.throw(typ, value, traceback) - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions - | raise mapped_exc(message) from exc - | httpx.ConnectTimeout - +------------------------------------ - -2025-12-02 07:23:03 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 07:23:03 | ERROR | conversation_api.py:invoke_chat:351 | Error in invoke_chat: Cannot write to closing transport -2025-12-02 07:24:43 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:24:43 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 10 -2025-12-02 07:24:43 | INFO | conversation_api.py:invoke_debug:499 | Received debug-agent request -2025-12-02 07:24:43 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 07:24:43 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:24:43 | INFO | conversation_api.py:invoke_debug:535 | Debug agent config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'model': 'gemini-2.5-flash', 'openai_api_key': None, 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0'} -2025-12-02 07:24:43 | INFO | conversation_api.py:invoke_debug:536 | Session ID: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:24:43 | INFO | conversation_api.py:invoke_debug:537 | Workflow nodes: ['1', '2', '3', '4', '5', '6', '8', '11', '12', '14', '15', '20', '21', '26', '27'] -2025-12-02 07:24:43 | INFO | debug_agent.py:debug_workflow_errors:215 | Saving workflow data for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:24:43 | INFO | debug_agent.py:debug_workflow_errors:221 | Workflow saved with version ID: 11 -2025-12-02 07:24:44 | INFO | debug_agent.py:debug_workflow_errors:505 | -- Starting workflow validation process for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:24:44 | INFO | debug_agent.py:debug_workflow_errors:512 | === Debug Coordinator starting === -2025-12-02 07:24:44 | INFO | debug_agent.py:debug_workflow_errors:539 | Handoff to: ComfyUI-Debug-Coordinator -2025-12-02 07:26:08 | ERROR | conversation_api.py:invoke_debug:669 | Error in debug agent: Cannot write to closing transport -2025-12-02 07:26:08 | INFO | debug_agent.py:run_workflow:35 | Run workflow for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:26:08 | INFO | debug_agent.py:run_workflow:50 | {'success': True, 'prompt_id': '02ea8a60-6ba3-4c6d-be1a-9f247ddd534e', 'number': 0, 'node_errors': {}} -2025-12-02 07:26:10 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:26:10 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 12 -2025-12-02 07:26:10 | INFO | conversation_api.py:invoke_debug:499 | Received debug-agent request -2025-12-02 07:26:10 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 07:26:10 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:26:10 | INFO | conversation_api.py:invoke_debug:535 | Debug agent config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'model': 'gemini-2.5-flash', 'openai_api_key': None, 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0'} -2025-12-02 07:26:10 | INFO | conversation_api.py:invoke_debug:536 | Session ID: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:26:10 | INFO | conversation_api.py:invoke_debug:537 | Workflow nodes: ['1', '2', '3', '4', '5', '6', '8', '11', '12', '14', '15', '20', '21', '26', '27'] -2025-12-02 07:26:10 | INFO | debug_agent.py:debug_workflow_errors:215 | Saving workflow data for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:26:10 | INFO | debug_agent.py:debug_workflow_errors:221 | Workflow saved with version ID: 13 -2025-12-02 07:26:10 | INFO | debug_agent.py:debug_workflow_errors:505 | -- Starting workflow validation process for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:26:10 | INFO | debug_agent.py:debug_workflow_errors:512 | === Debug Coordinator starting === -2025-12-02 07:26:10 | INFO | debug_agent.py:debug_workflow_errors:539 | Handoff to: ComfyUI-Debug-Coordinator -2025-12-02 07:26:23 | INFO | conversation_api.py:model_paths:1065 | Received model-paths request -2025-12-02 07:26:47 | INFO | debug_agent.py:debug_workflow_errors:567 | -- Tool called: run_workflow -2025-12-02 07:26:47 | INFO | debug_agent.py:run_workflow:35 | Run workflow for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:26:47 | INFO | debug_agent.py:run_workflow:50 | {'success': True, 'prompt_id': '9bfe0dc7-2b40-4be8-926c-00589816020c', 'number': 1, 'node_errors': {}} -2025-12-02 07:26:57 | INFO | debug_agent.py:debug_workflow_errors:643 | -=== Debug process complete === -2025-12-02 07:26:57 | INFO | debug_agent.py:debug_workflow_errors:661 | Debug completion checkpoint saved with ID: 14 -2025-12-02 07:26:57 | INFO | conversation_api.py:invoke_debug:661 | Debug completion checkpoint saved with ID: 15 -2025-12-02 07:26:57 | INFO | conversation_api.py:invoke_debug:666 | Debug agent processing complete -2025-12-02 07:26:57 | INFO | conversation_api.py:update_workflow_ui:692 | Received update-workflow-ui request -2025-12-02 07:26:57 | INFO | conversation_api.py:update_workflow_ui:717 | Successfully updated workflow_data_ui for checkpoint ID: 14 -2025-12-02 07:27:31 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:27:31 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 16 -2025-12-02 07:27:31 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 07:27:31 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 07:27:31 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:27:31 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 07:27:31 | INFO | conversation_api.py:invoke_chat:272 | -- Received 12 historical messages -2025-12-02 07:27:31 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 16 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:27:31 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 16, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 07:27:31 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 12 -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 12 -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 0, summary length: 0 -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 12 (threshold: 8) -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:91 | [Memory] Compression triggered! -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:98 | [Memory] Compressing messages from index 0 to 8 -2025-12-02 07:27:31 | INFO | message_memory.py:message_memory_optimize:99 | [Memory] Messages to compress: 8 -2025-12-02 07:27:31 | INFO | summary_agent.py:generate_summary:100 | Generating summary with model: gemini-2.5-flash -2025-12-02 07:27:35 | INFO | summary_agent.py:generate_summary:132 | Generated summary: The user repeatedly asked for an explanation and official usage guide for the "ConditioningZeroOut" ... -2025-12-02 07:27:35 | INFO | message_memory.py:message_memory_optimize:107 | [Memory] Generated new summary: The user repeatedly asked for an explanation and official usage guide for the "ConditioningZeroOut" ... -2025-12-02 07:27:35 | INFO | message_memory.py:message_memory_optimize:116 | [Memory] Updated index to: 8 -2025-12-02 07:27:35 | INFO | message_memory.py:message_memory_optimize:120 | [Memory] Returning 5 optimized messages -2025-12-02 07:27:35 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 5, messages: [{'role': 'user', 'content': '[Context from previous conversation]: The user repeatedly asked for an explanation and official usage guide for the "ConditioningZeroOut" node, specifically requesting the response in Chinese. The assistant offered to debug the current workflow, but an authorization error (401) occurred, indicating a missing or invalid API key. The user\'s initial request regarding the "ConditioningZeroOut" node remains unaddressed.'}, {'role': 'assistant', 'content': '\n\n× Error occurred during debugging: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key.'}, {'role': 'assistant', 'content': 'I encountered an error while analyzing your workflow: BodyStreamBuffer was aborted'}, {'role': 'assistant', 'content': '▸ **Switching to ComfyUI-Debug-Coordinator**\n\n我将验证您的 ComfyUI 工作流并修复任何错误。让我先运行工作流验证来检查当前状态。\n\n⚙ *ComfyUI-Debug-Coordinator is using run_workflow...*\n\n\n\n● *Tool execution completed*\n\n```\n{"success": true, "prompt_id": "9bfe0dc7-2b40-4be8-926c-00589816020c", "number": 1, "node_errors": {}}\n```\n\n✅ **验证完成!**\n\n您的 ComfyUI 工作流验证成功,没有发现任何错误!\n\n**验证结果:**\n- 状态: 成功\n- 节点错误: 无\n- 提示 ID: 9bfe0dc7-2b40-4be8-926c-00589816020c\n\n您的工作流结构良好,所有节点参数都正确,连接也没有问题。工作流已经可以正常使用了!\n\n如果您还有其他工作流需要验证或者遇到了新的问题,请随时告诉我。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}] -2025-12-02 07:27:40 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 5 messages -2025-12-02 07:27:40 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 07:27:40 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 07:27:42 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-02 07:27:43 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"ConditioningZeroOut\",\"nodes_class_name\":\"ConditioningZeroOut\",\"nodes_extension\":\"ComfyUI\",\"nodes_category\":\"advanced/conditioning\",\"nodes_description\":{\"EN\":\"This node zeroes out specific elements within the conditioning data structure, effectively neutralizing their influence in subsequent processing steps. It's designed for advanced conditioning operations where direct manipulation of the conditioning's internal representation is required.\",\"ZH\":\"\"},\"extension_github_url\":\"https://github.com/comfyanonymous/ComfyUI\",\"nodes_tips\":{\"EN\":\"Use the `ConditioningZeroOut` node when you need to neutralize specific parts of your conditioning data, such as the `pooled_output`, to achieve more controlled and precise conditioning effects. Combine this node with other conditioning nodes to create complex conditioning setups, allowing for greater experimentation and fine-tuning of your AI models.\",\"ZH\":\"\"},\"nodes_debug_tips\":{\"EN\":\"default_nodes_debug_tips\"}},\"error\":null}","annotations":null,"meta":null} -2025-12-02 07:27:43 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-02 07:27:43 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-02 07:27:45 | INFO | conversation_api.py:update_workflow_ui:692 | Received update-workflow-ui request -2025-12-02 07:27:45 | INFO | conversation_api.py:update_workflow_ui:717 | Successfully updated workflow_data_ui for checkpoint ID: 14 -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-02 07:27:46 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 07:27:46 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 07:27:46 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 496 chars, ext: False, finished: True -2025-12-02 07:27:53 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:27:53 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 17 -2025-12-02 07:27:53 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 07:27:53 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 07:27:53 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:27:53 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 07:27:53 | INFO | conversation_api.py:invoke_chat:272 | -- Received 13 historical messages -2025-12-02 07:27:53 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 17 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:27:53 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 17, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 07:27:53 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 13 -2025-12-02 07:27:53 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:27:53 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 13 -2025-12-02 07:27:53 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 8, summary length: 380 -2025-12-02 07:27:53 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 5 (threshold: 8) -2025-12-02 07:27:53 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-02 07:27:53 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 6, messages: [{'role': 'user', 'content': '[Context from previous conversation]: The user repeatedly asked for an explanation and official usage guide for the "ConditioningZeroOut" node, specifically requesting the response in Chinese. The assistant offered to debug the current workflow, but an authorization error (401) occurred, indicating a missing or invalid API key. The user\'s initial request regarding the "ConditioningZeroOut" node remains unaddressed.'}, {'role': 'assistant', 'content': '\n\n× Error occurred during debugging: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key.'}, {'role': 'assistant', 'content': 'I encountered an error while analyzing your workflow: BodyStreamBuffer was aborted'}, {'role': 'assistant', 'content': '▸ **Switching to ComfyUI-Debug-Coordinator**\n\n我将验证您的 ComfyUI 工作流并修复任何错误。让我先运行工作流验证来检查当前状态。\n\n⚙ *ComfyUI-Debug-Coordinator is using run_workflow...*\n\n\n\n● *Tool execution completed*\n\n```\n{"success": true, "prompt_id": "9bfe0dc7-2b40-4be8-926c-00589816020c", "number": 1, "node_errors": {}}\n```\n\n✅ **验证完成!**\n\n您的 ComfyUI 工作流验证成功,没有发现任何错误!\n\n**验证结果:**\n- 状态: 成功\n- 节点错误: 无\n- 提示 ID: 9bfe0dc7-2b40-4be8-926c-00589816020c\n\n您的工作流结构良好,所有节点参数都正确,连接也没有问题。工作流已经可以正常使用了!\n\n如果您还有其他工作流需要验证或者遇到了新的问题,请随时告诉我。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}] -2025-12-02 07:27:59 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 6 messages -2025-12-02 07:27:59 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 07:27:59 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 07:28:08 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-02 07:28:09 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"ConditioningZeroOut\",\"nodes_class_name\":\"ConditioningZeroOut\",\"nodes_extension\":\"ComfyUI\",\"nodes_category\":\"advanced/conditioning\",\"nodes_description\":{\"EN\":\"This node zeroes out specific elements within the conditioning data structure, effectively neutralizing their influence in subsequent processing steps. It's designed for advanced conditioning operations where direct manipulation of the conditioning's internal representation is required.\",\"ZH\":\"\"},\"extension_github_url\":\"https://github.com/comfyanonymous/ComfyUI\",\"nodes_tips\":{\"EN\":\"Use the `ConditioningZeroOut` node when you need to neutralize specific parts of your conditioning data, such as the `pooled_output`, to achieve more controlled and precise conditioning effects. Combine this node with other conditioning nodes to create complex conditioning setups, allowing for greater experimentation and fine-tuning of your AI models.\",\"ZH\":\"\"},\"nodes_debug_tips\":{\"EN\":\"default_nodes_debug_tips\"}},\"error\":null}","annotations":null,"meta":null} -2025-12-02 07:28:09 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-02 07:28:09 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-02 07:28:15 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 07:28:15 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 07:28:15 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 1358 chars, ext: False, finished: True -2025-12-02 07:31:31 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 07:31:31 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 18 -2025-12-02 07:31:31 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 07:31:31 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 07:31:31 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 07:31:31 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 07:31:31 | INFO | conversation_api.py:invoke_chat:272 | -- Received 15 historical messages -2025-12-02 07:31:31 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 18 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:31:31 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 18, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 07:31:31 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 15 -2025-12-02 07:31:31 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 07:31:31 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 15 -2025-12-02 07:31:31 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 8, summary length: 380 -2025-12-02 07:31:31 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 7 (threshold: 8) -2025-12-02 07:31:31 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-02 07:31:31 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 8, messages: [{'role': 'user', 'content': '[Context from previous conversation]: The user repeatedly asked for an explanation and official usage guide for the "ConditioningZeroOut" node, specifically requesting the response in Chinese. The assistant offered to debug the current workflow, but an authorization error (401) occurred, indicating a missing or invalid API key. The user\'s initial request regarding the "ConditioningZeroOut" node remains unaddressed.'}, {'role': 'assistant', 'content': '\n\n× Error occurred during debugging: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key.'}, {'role': 'assistant', 'content': 'I encountered an error while analyzing your workflow: BodyStreamBuffer was aborted'}, {'role': 'assistant', 'content': '▸ **Switching to ComfyUI-Debug-Coordinator**\n\n我将验证您的 ComfyUI 工作流并修复任何错误。让我先运行工作流验证来检查当前状态。\n\n⚙ *ComfyUI-Debug-Coordinator is using run_workflow...*\n\n\n\n● *Tool execution completed*\n\n```\n{"success": true, "prompt_id": "9bfe0dc7-2b40-4be8-926c-00589816020c", "number": 1, "node_errors": {}}\n```\n\n✅ **验证完成!**\n\n您的 ComfyUI 工作流验证成功,没有发现任何错误!\n\n**验证结果:**\n- 状态: 成功\n- 节点错误: 无\n- 提示 ID: 9bfe0dc7-2b40-4be8-926c-00589816020c\n\n您的工作流结构良好,所有节点参数都正确,连接也没有问题。工作流已经可以正常使用了!\n\n如果您还有其他工作流需要验证或者遇到了新的问题,请随时告诉我。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ConditioningZeroOut node work? I need its official usage guide.'}, {'role': 'assistant', 'content': "好的,关于 `ConditioningZeroOut` 节点,以下是其详细信息和使用指南:\n\n#### 节点名称\nConditioningZeroOut\n\n#### 节点类别\n高级/条件 (advanced/conditioning)\n\n#### 扩展来源\nComfyUI 官方扩展\n\n#### 节点描述 (英文原文及翻译)\n\n* **英文原文:** This node zeroes out specific elements within the conditioning data structure, effectively neutralizing their influence in subsequent processing steps. It's designed for advanced conditioning operations where direct manipulation of the conditioning's internal representation is required.\n* **中文翻译:** 此节点用于将条件数据结构中的特定元素清零,从而在后续处理步骤中有效地消除其影响。它专为需要直接操作条件内部表示的高级条件操作而设计。\n\n#### 使用技巧 (英文原文及翻译)\n\n* **英文原文:** Use the `ConditioningZeroOut` node when you need to neutralize specific parts of your conditioning data, such as the `pooled_output`, to achieve more controlled and precise conditioning effects. Combine this node with other conditioning nodes to create complex conditioning setups, allowing for greater experimentation and fine-tuning of your AI models.\n* **中文翻译:** 当您需要中和条件数据中的特定部分(例如 `pooled_output`)以实现更受控和精确的条件效果时,请使用 `ConditioningZeroOut` 节点。将此节点与其他条件节点结合使用,可以创建复杂的条件设置,从而能够对您的 AI 模型进行更深入的实验和微调。\n\n---\n\n**总结:**\n\n`ConditioningZeroOut` 节点的主要作用是**精确地控制和调整条件数据(conditioning data)**。在某些高级生成任务中,我们可能不希望某些条件信息(例如文本编码器生成的 `pooled_output`)影响最终的生成结果。通过将这些特定元素清零,可以有效地“禁用”它们的影响,从而实现更精细的控制。\n\n例如,如果您想在不完全移除所有条件信息的情况下,减弱或消除文本提示中某些特定部分的视觉影响,这个节点会非常有用。它可以帮助您在生成过程中进行更多实验,以找到最佳的条件组合和效果。"}, {'role': 'user', 'content': '如何查看条件零化节点的输出'}] -2025-12-02 07:31:39 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 8 messages -2025-12-02 07:31:39 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 07:31:39 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 07:32:31 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2025-12-02 07:32:31 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 07:32:31 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 07:32:31 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 1093 chars, ext: False, finished: True -2025-12-02 07:38:17 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-02 08:33:14 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 08:33:14 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 19 -2025-12-02 08:33:14 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 08:33:14 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 08:33:14 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 08:33:14 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 08:33:14 | INFO | conversation_api.py:invoke_chat:272 | -- Received 18 historical messages -2025-12-02 08:33:14 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 19 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 08:33:14 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 19, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 08:33:14 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 18 -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 18 -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 8, summary length: 380 -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 10 (threshold: 8) -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:91 | [Memory] Compression triggered! -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:98 | [Memory] Compressing messages from index 8 to 14 -2025-12-02 08:33:14 | INFO | message_memory.py:message_memory_optimize:99 | [Memory] Messages to compress: 6 -2025-12-02 08:33:14 | INFO | summary_agent.py:generate_summary:100 | Generating summary with model: gemini-2.5-flash -2025-12-02 08:33:21 | INFO | summary_agent.py:generate_summary:132 | Generated summary: The user repeatedly requested an explanation and official usage guide for the "ConditioningZeroOut" ... -2025-12-02 08:33:21 | INFO | message_memory.py:message_memory_optimize:107 | [Memory] Generated new summary: The user repeatedly requested an explanation and official usage guide for the "ConditioningZeroOut" ... -2025-12-02 08:33:21 | INFO | message_memory.py:message_memory_optimize:116 | [Memory] Updated index to: 14 -2025-12-02 08:33:21 | INFO | message_memory.py:message_memory_optimize:120 | [Memory] Returning 5 optimized messages -2025-12-02 08:33:21 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 5, messages: [{'role': 'user', 'content': '[Context from previous conversation]: The user repeatedly requested an explanation and official usage guide for the "ConditioningZeroOut" node in Chinese. The assistant initially faced persistent 401 authorization errors during debugging attempts. Subsequently, the `ComfyUI-Debug-Coordinator` successfully validated the user\'s workflow without errors. Following this, the assistant finally provided the requested detailed explanation for the "ConditioningZeroOut" node in Chinese. It described the node as an official ComfyUI advanced/conditioning extension that zeroes out specific elements, like `pooled_output`, within conditioning data to neutralize their influence, enabling precise control and fine-tuning in generative AI models.'}, {'role': 'user', 'content': '如何查看条件零化节点的输出'}, {'role': 'assistant', 'content': '`ConditioningZeroOut` 节点的输出类型是 `CONDITIONING` (条件)。这种类型的数据不是图像,因此不能直接通过像 `Preview Image` (图像预览) 或 `Save Image` (保存图像) 这样的节点来查看其内容。\n\n要“查看”`ConditioningZeroOut` 节点的输出,您需要理解它的作用和它如何影响下游节点。\n\n以下是几种理解和间接“查看”其输出的方法:\n\n1. **观察对 KSampler 的影响:**\n `CONDITIONING` 数据主要用于 `KSampler`(或其他采样器)节点,它告诉采样器如何根据文本提示生成图像。\n * 将 `ConditioningZeroOut` 的输出连接到 `KSampler` 的 `positive` 或 `negative` conditioning 输入。\n * 运行工作流并生成图像。\n * **对比实验:** 运行两次工作流:一次使用 `ConditioningZeroOut` 节点(例如,将其输出连接到 `KSampler`),另一次则不使用该节点,直接将原始的 `CLIPTextEncode` 输出连接到 `KSampler`。通过比较两次生成的图像,您就可以观察到 `ConditioningZeroOut` 对最终图像生成效果的影响。这是最直接、最有效的方法来理解它的作用。\n\n2. **使用专门的调试节点(如果有):**\n 某些自定义扩展包可能会提供专门用于调试或打印 `CONDITIONING` 数据的节点。这些节点通常会将条件数据的内部结构或特定部分转换为文本形式输出,以便您可以查看。但这不是 ComfyUI 内置的标准功能。您可以在 ComfyUI 管理器中搜索相关的调试节点。\n\n3. **理解其内部机制:**\n `ConditioningZeroOut` 节点如其名,会将其接收到的 `CONDITIONING` 数据中的某些特定元素(通常是 `pooled_output` 或其他内部张量)清零。这意味着它会“移除”或“中和”这些特定元素对后续生成过程的影响。您通过观察图像变化来验证这种“移除”是否达到了预期效果。\n\n**简而言之:** 您无法像查看图像一样直接“看到”`ConditioningZeroOut` 的输出。您需要通过观察连接到它的下游节点(特别是 `KSampler`)所生成的图像来间接评估它的效果。通过对比有无该节点时生成的图像差异,您就能理解它具体做了什么。'}, {'role': 'assistant', 'content': 'Would you like me to help you debug the current workflow on the canvas?'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the SetNode node work? I need its official usage guide.'}] -2025-12-02 08:33:25 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 5 messages -2025-12-02 08:33:25 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 08:33:25 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 08:33:29 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-02 08:33:29 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"error\":\"未找到节点: SetNode. Try to use search on Internet tool to find more information.\",\"data\":null}","annotations":null,"meta":null} -2025-12-02 08:33:29 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-02 08:33:29 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=0, ext=None -2025-12-02 08:33:33 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'search_node' was called -2025-12-02 08:33:35 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"answer\":\"The following nodes are relevant to your query:\\n### 1.[FloatNode]\\nComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\\n#### input_nodes\\n**name**: \\\"number\\\"\\n**other_meta**: \\\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"FLOAT\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 2.[FloatNode]\\nComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\\n#### input_nodes\\n**name**: \\\"number\\\"\\n**other_meta**: \\\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"FLOAT\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 3.[FloatNode]\\nComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\\n#### input_nodes\\n**name**: \\\"number\\\"\\n**other_meta**: \\\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"FLOAT\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 4.[FloatNode]\\nComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\\n#### input_nodes\\n**name**: \\\"number\\\"\\n**other_meta**: \\\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"FLOAT\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 5.[StateResetNode]\\nThese nodes are for real-time applications of ComfyUI.\\n#### input_nodes\\n**name**: \\\"always_execute\\\"\\n**other_meta**: \\\"{\\\"default\\\": true, \\\"tooltip\\\": \\\"When enabled, the node updates every execution\\\"}\\\"\\n**comfy_dtype**: \\\"BOOLEAN\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n**name**: \\\"trigger\\\"\\n**other_meta**: \\\"{\\\"default\\\": false, \\\"tooltip\\\": \\\"Set to True to reset all states\\\"}\\\"\\n**comfy_dtype**: \\\"BOOLEAN\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"BOOLEAN\\\"\\n**comfy_dtype**: \\\"BOOLEAN\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 6.[FloatNode]\\nComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\\n#### input_nodes\\n**name**: \\\"number\\\"\\n**other_meta**: \\\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"FLOAT\\\"\\n**comfy_dtype**: \\\"FLOAT\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 7.[StateResetNode]\\nThese nodes are for real-time applications of ComfyUI.\\n#### input_nodes\\n**name**: \\\"always_execute\\\"\\n**other_meta**: \\\"{\\\"default\\\": true, \\\"tooltip\\\": \\\"When enabled, the node updates every execution\\\"}\\\"\\n**comfy_dtype**: \\\"BOOLEAN\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n**name**: \\\"trigger\\\"\\n**other_meta**: \\\"{\\\"default\\\": false, \\\"tooltip\\\": \\\"Set to True to reset all states\\\"}\\\"\\n**comfy_dtype**: \\\"BOOLEAN\\\"\\n**is_required**: \\\"True\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n#### output_nodes\\n**name**: \\\"BOOLEAN\\\"\\n**comfy_dtype**: \\\"BOOLEAN\\\"\\n**python_dtype**: \\\"xxx\\\"\\n**documentation**: \\\"xxx\\\"\\n\\n### 8.[Nodes for use with real-time applications of ComfyUI]\\nNodes for use with real-time applications of ComfyUI enhance interactive experiences by enabling dynamic updates and seamless integration within the ComfyUI framework, optimizing performance for real-time user interactions.\\n#### input_nodes\\n\\n#### output_nodes\\n\\n\\n\",\"data\":[{\"nodes_name\":\"FloatNode\",\"nodes_class_name\":\"FloatNode\",\"nodes_extension\":\"comfyui-fairlab\",\"nodes_category\":\"Fair/string\",\"nodes_inputs\":[{\"name\":\"number\",\"other_meta\":\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\",\"comfy_dtype\":\"FLOAT\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"FLOAT\",\"comfy_dtype\":\"FLOAT\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"extension_github_stars\":2,\"nodes_description\":{\"EN\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"7de18228f12afad78fd919525fd0ff4f\",\"version\":\"1.0.43\",\"id\":103474,\"gmt_create\":\"2025-09-23T03:35:31.041401\",\"gmt_modified\":\"2025-09-23T08:04:20.095364\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"FloatNode\",\"nodes_class_name\":\"FloatNode\",\"nodes_extension\":\"comfyui-fairlab\",\"nodes_category\":\"Fair/string\",\"nodes_inputs\":[{\"name\":\"number\",\"other_meta\":\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\",\"comfy_dtype\":\"FLOAT\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"FLOAT\",\"comfy_dtype\":\"FLOAT\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"extension_github_stars\":2,\"nodes_description\":{\"EN\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"8cb5b73c19d5d43ea4f5ba71d6340aad\",\"version\":null,\"id\":105258,\"gmt_create\":\"2025-09-24T13:56:11.269910\",\"gmt_modified\":\"2025-09-24T13:56:11.269914\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"FloatNode\",\"nodes_class_name\":\"FloatNode\",\"nodes_extension\":\"comfyui-fairlab\",\"nodes_category\":\"Fair/string\",\"nodes_inputs\":[{\"name\":\"number\",\"other_meta\":\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\",\"comfy_dtype\":\"FLOAT\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"FLOAT\",\"comfy_dtype\":\"FLOAT\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"extension_github_stars\":2,\"nodes_description\":{\"EN\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"8db549bace1326cd9ee3398a32f5ad13\",\"version\":null,\"id\":105385,\"gmt_create\":\"2025-09-24T13:57:56.926326\",\"gmt_modified\":\"2025-09-24T13:57:56.926329\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"FloatNode\",\"nodes_class_name\":\"FloatNode\",\"nodes_extension\":\"comfyui-fairlab\",\"nodes_category\":\"Fair/string\",\"nodes_inputs\":[{\"name\":\"number\",\"other_meta\":\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\",\"comfy_dtype\":\"FLOAT\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"FLOAT\",\"comfy_dtype\":\"FLOAT\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"extension_github_stars\":2,\"nodes_description\":{\"EN\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"8f9c47c957c0b3d105bf194cdff78fb2\",\"version\":null,\"id\":105615,\"gmt_create\":\"2025-09-24T14:01:29.023394\",\"gmt_modified\":\"2025-09-24T14:01:29.023398\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"StateResetNode\",\"nodes_class_name\":\"StateResetNode\",\"nodes_extension\":\"comfyui_realtimenodes\",\"nodes_category\":\"real-time/control/utility\",\"nodes_inputs\":[{\"name\":\"always_execute\",\"other_meta\":\"{\\\"default\\\": true, \\\"tooltip\\\": \\\"When enabled, the node updates every execution\\\"}\",\"comfy_dtype\":\"BOOLEAN\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"},{\"name\":\"trigger\",\"other_meta\":\"{\\\"default\\\": false, \\\"tooltip\\\": \\\"Set to True to reset all states\\\"}\",\"comfy_dtype\":\"BOOLEAN\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"BOOLEAN\",\"comfy_dtype\":\"BOOLEAN\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/ryanontheinside/ComfyUI_RealTimeNodes\",\"extension_github_stars\":62,\"nodes_description\":{\"EN\":\"These nodes are for real-time applications of ComfyUI.\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"913e90862baa8502a6dfb5c39107e61e\",\"version\":null,\"id\":105815,\"gmt_create\":\"2025-09-24T14:05:00.584912\",\"gmt_modified\":\"2025-09-24T14:05:00.584916\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"FloatNode\",\"nodes_class_name\":\"FloatNode\",\"nodes_extension\":\"comfyui-fairlab\",\"nodes_category\":\"Fair/string\",\"nodes_inputs\":[{\"name\":\"number\",\"other_meta\":\"{\\\"multiline\\\": false, \\\"defaultInput\\\": false}\",\"comfy_dtype\":\"FLOAT\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"FLOAT\",\"comfy_dtype\":\"FLOAT\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"extension_github_stars\":2,\"nodes_description\":{\"EN\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"99f16d2524fdb70e36ee43d240b60794\",\"version\":null,\"id\":106832,\"gmt_create\":\"2025-09-24T14:19:53.441648\",\"gmt_modified\":\"2025-09-24T14:19:53.441653\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"StateResetNode\",\"nodes_class_name\":\"StateResetNode\",\"nodes_extension\":\"comfyui_realtimenodes\",\"nodes_category\":\"real-time/control/utility\",\"nodes_inputs\":[{\"name\":\"always_execute\",\"other_meta\":\"{\\\"default\\\": true, \\\"tooltip\\\": \\\"When enabled, the node updates every execution\\\"}\",\"comfy_dtype\":\"BOOLEAN\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"},{\"name\":\"trigger\",\"other_meta\":\"{\\\"default\\\": false, \\\"tooltip\\\": \\\"Set to True to reset all states\\\"}\",\"comfy_dtype\":\"BOOLEAN\",\"is_required\":\"True\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"nodes_outputs\":[{\"name\":\"BOOLEAN\",\"comfy_dtype\":\"BOOLEAN\",\"python_dtype\":\"xxx\",\"documentation\":\"xxx\"}],\"update_time\":\"2025-08-26T14:14:57.372738\",\"extension_github_url\":\"https://github.com/ryanontheinside/ComfyUI_RealTimeNodes\",\"extension_github_stars\":62,\"nodes_description\":{\"EN\":\"These nodes are for real-time applications of ComfyUI.\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"\",\"source_from\":\"core\",\"ds\":\"20250918\",\"unique_id\":\"b8f1053e258cf8656be2f23e8587cc05\",\"version\":null,\"id\":110444,\"gmt_create\":\"2025-09-24T15:22:13.461801\",\"gmt_modified\":\"2025-09-24T15:22:13.461805\",\"attributes\":null,\"deleted\":0},{\"nodes_name\":\"Nodes for use with real-time applications of ComfyUI\",\"nodes_class_name\":\"Nodes for use with real-time applications of ComfyUI\",\"nodes_extension\":\"ComfyUI_RealtimeNodes\",\"nodes_category\":\"\",\"nodes_inputs\":[],\"nodes_outputs\":[],\"update_time\":\"2025-09-16T18:21:35.647242\",\"extension_github_url\":\"https://github.com/ryanontheinside/ComfyUI_RealtimeNodes\",\"extension_github_stars\":30,\"nodes_description\":{\"EN\":\"Nodes for use with real-time applications of ComfyUI enhance interactive experiences by enabling dynamic updates and seamless integration within the ComfyUI framework, optimizing performance for real-time user interactions.\"},\"nodes_tips\":{},\"nodes_debug_tips\":{},\"nodes_relation\":\"D Type Converter, Fps Monitor, Fast Webcam Capture, Float Control, Float Sequence, Int Control, Int Sequence, Integer Motion Controller, Lazy Condition, Load Image From Path, Motion Controller, Multiline Text, Quick Shape Mask, Roi Node, Similarity Filter, String Control, String Sequence, Text Renderer, Yolo Similarity Compare\",\"source_from\":\"runcomfy\",\"ds\":\"20250918\",\"unique_id\":\"fbb9b5407178a47c043b1a2c4ea0f25f\",\"version\":null,\"id\":138302,\"gmt_create\":\"2025-09-25T10:14:56.454435\",\"gmt_modified\":\"2025-09-25T10:14:56.454440\",\"attributes\":null,\"deleted\":0}],\"ext\":[{\"type\":\"node\",\"data\":[{\"id\":103474,\"unique_id\":\"7de18228f12afad78fd919525fd0ff4f\",\"name\":\"FloatNode\",\"name_oral\":\"FloatNode\",\"description\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\",\"github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"github_stars\":2,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":105258,\"unique_id\":\"8cb5b73c19d5d43ea4f5ba71d6340aad\",\"name\":\"FloatNode\",\"name_oral\":\"FloatNode\",\"description\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\",\"github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"github_stars\":2,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":105385,\"unique_id\":\"8db549bace1326cd9ee3398a32f5ad13\",\"name\":\"FloatNode\",\"name_oral\":\"FloatNode\",\"description\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\",\"github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"github_stars\":2,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":105615,\"unique_id\":\"8f9c47c957c0b3d105bf194cdff78fb2\",\"name\":\"FloatNode\",\"name_oral\":\"FloatNode\",\"description\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\",\"github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"github_stars\":2,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":105815,\"unique_id\":\"913e90862baa8502a6dfb5c39107e61e\",\"name\":\"StateResetNode\",\"name_oral\":\"StateResetNode\",\"description\":\"These nodes are for real-time applications of ComfyUI.\",\"github_url\":\"https://github.com/ryanontheinside/ComfyUI_RealTimeNodes\",\"github_stars\":62,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":106832,\"unique_id\":\"99f16d2524fdb70e36ee43d240b60794\",\"name\":\"FloatNode\",\"name_oral\":\"FloatNode\",\"description\":\"ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...\",\"github_url\":\"https://github.com/yanhuifair/ComfyUI-FairLab\",\"github_stars\":2,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":110444,\"unique_id\":\"b8f1053e258cf8656be2f23e8587cc05\",\"name\":\"StateResetNode\",\"name_oral\":\"StateResetNode\",\"description\":\"These nodes are for real-time applications of ComfyUI.\",\"github_url\":\"https://github.com/ryanontheinside/ComfyUI_RealTimeNodes\",\"github_stars\":62,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":138302,\"unique_id\":\"fbb9b5407178a47c043b1a2c4ea0f25f\",\"name\":\"Nodes for use with real-time applications of ComfyUI\",\"name_oral\":\"Nodes for use with real-time applications of ComfyUI\",\"description\":\"Nodes for use with real-time applications of ComfyUI enhance interactive experiences by enabling dynamic updates and seamless integration within the ComfyUI framework, optimizing performance for real-time user interactions.\",\"github_url\":\"https://github.com/ryanontheinside/ComfyUI_RealtimeNodes\",\"github_stars\":30,\"from_index\":0,\"to_index\":0,\"is_existed\":true}]}]}","annotations":null,"meta":null} -2025-12-02 08:33:35 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'search_node' -2025-12-02 08:33:35 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'search_node': data=8, ext=[{'type': 'node', 'data': [{'id': 103474, 'unique_id': '7de18228f12afad78fd919525fd0ff4f', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105258, 'unique_id': '8cb5b73c19d5d43ea4f5ba71d6340aad', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105385, 'unique_id': '8db549bace1326cd9ee3398a32f5ad13', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105615, 'unique_id': '8f9c47c957c0b3d105bf194cdff78fb2', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105815, 'unique_id': '913e90862baa8502a6dfb5c39107e61e', 'name': 'StateResetNode', 'name_oral': 'StateResetNode', 'description': 'These nodes are for real-time applications of ComfyUI.', 'github_url': 'https://github.com/ryanontheinside/ComfyUI_RealTimeNodes', 'github_stars': 62, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 106832, 'unique_id': '99f16d2524fdb70e36ee43d240b60794', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 110444, 'unique_id': 'b8f1053e258cf8656be2f23e8587cc05', 'name': 'StateResetNode', 'name_oral': 'StateResetNode', 'description': 'These nodes are for real-time applications of ComfyUI.', 'github_url': 'https://github.com/ryanontheinside/ComfyUI_RealTimeNodes', 'github_stars': 62, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 138302, 'unique_id': 'fbb9b5407178a47c043b1a2c4ea0f25f', 'name': 'Nodes for use with real-time applications of ComfyUI', 'name_oral': 'Nodes for use with real-time applications of ComfyUI', 'description': 'Nodes for use with real-time applications of ComfyUI enhance interactive experiences by enabling dynamic updates and seamless integration within the ComfyUI framework, optimizing performance for real-time user interactions.', 'github_url': 'https://github.com/ryanontheinside/ComfyUI_RealtimeNodes', 'github_stars': 30, 'from_index': 0, 'to_index': 0, 'is_existed': True}]}] -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 2 -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: False -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 0 -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: search_node -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: True -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:538 | - Ext types: ['node'] -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: The following nodes are relevant to your query: -### 1.[FloatNode] -ComfyUI extension with patches and... -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 08:33:42 | INFO | mcp_client.py:comfyui_agent_invoke:647 | Using ext from search_node -2025-12-02 08:33:42 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: [{'type': 'node', 'data': [{'id': 103474, 'unique_id': '7de18228f12afad78fd919525fd0ff4f', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105258, 'unique_id': '8cb5b73c19d5d43ea4f5ba71d6340aad', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105385, 'unique_id': '8db549bace1326cd9ee3398a32f5ad13', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105615, 'unique_id': '8f9c47c957c0b3d105bf194cdff78fb2', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 105815, 'unique_id': '913e90862baa8502a6dfb5c39107e61e', 'name': 'StateResetNode', 'name_oral': 'StateResetNode', 'description': 'These nodes are for real-time applications of ComfyUI.', 'github_url': 'https://github.com/ryanontheinside/ComfyUI_RealTimeNodes', 'github_stars': 62, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 106832, 'unique_id': '99f16d2524fdb70e36ee43d240b60794', 'name': 'FloatNode', 'name_oral': 'FloatNode', 'description': 'ComfyUI extension with patches and nodes. Patches: Preview device, Extension device, Temperature, Memory estimation, Optimizations, Easy generic inputs, Easy multiple inputs. NODES: Image nodes, Image/Area nodes, Image/Blip nodes, Image/Face nodes, Sampling nodes, Patch nodes, Primitive nodes, Primitive/Conversion nodes, Primitive/Process nodes, Workflow nodes, etc...', 'github_url': 'https://github.com/yanhuifair/ComfyUI-FairLab', 'github_stars': 2, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 110444, 'unique_id': 'b8f1053e258cf8656be2f23e8587cc05', 'name': 'StateResetNode', 'name_oral': 'StateResetNode', 'description': 'These nodes are for real-time applications of ComfyUI.', 'github_url': 'https://github.com/ryanontheinside/ComfyUI_RealTimeNodes', 'github_stars': 62, 'from_index': 0, 'to_index': 0, 'is_existed': True}, {'id': 138302, 'unique_id': 'fbb9b5407178a47c043b1a2c4ea0f25f', 'name': 'Nodes for use with real-time applications of ComfyUI', 'name_oral': 'Nodes for use with real-time applications of ComfyUI', 'description': 'Nodes for use with real-time applications of ComfyUI enhance interactive experiences by enabling dynamic updates and seamless integration within the ComfyUI framework, optimizing performance for real-time user interactions.', 'github_url': 'https://github.com/ryanontheinside/ComfyUI_RealtimeNodes', 'github_stars': 30, 'from_index': 0, 'to_index': 0, 'is_existed': True}]}], finished: True -2025-12-02 08:33:42 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 748 chars, ext: True, finished: True -2025-12-02 08:46:12 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 08:46:12 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 20 -2025-12-02 08:48:55 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 08:48:55 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 21 -2025-12-02 08:48:55 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 08:48:55 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 08:48:55 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 08:48:55 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 08:48:55 | INFO | conversation_api.py:invoke_chat:272 | -- Received 21 historical messages -2025-12-02 08:48:55 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 21 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 08:48:55 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 21, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 08:48:55 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 21 -2025-12-02 08:48:55 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 08:48:55 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 21 -2025-12-02 08:48:55 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 14, summary length: 699 -2025-12-02 08:48:55 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 7 (threshold: 8) -2025-12-02 08:48:55 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-02 08:48:55 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 8, messages: [{'role': 'user', 'content': '[Context from previous conversation]: The user repeatedly requested an explanation and official usage guide for the "ConditioningZeroOut" node in Chinese. The assistant initially faced persistent 401 authorization errors during debugging attempts. Subsequently, the `ComfyUI-Debug-Coordinator` successfully validated the user\'s workflow without errors. Following this, the assistant finally provided the requested detailed explanation for the "ConditioningZeroOut" node in Chinese. It described the node as an official ComfyUI advanced/conditioning extension that zeroes out specific elements, like `pooled_output`, within conditioning data to neutralize their influence, enabling precise control and fine-tuning in generative AI models.'}, {'role': 'user', 'content': '如何查看条件零化节点的输出'}, {'role': 'assistant', 'content': '`ConditioningZeroOut` 节点的输出类型是 `CONDITIONING` (条件)。这种类型的数据不是图像,因此不能直接通过像 `Preview Image` (图像预览) 或 `Save Image` (保存图像) 这样的节点来查看其内容。\n\n要“查看”`ConditioningZeroOut` 节点的输出,您需要理解它的作用和它如何影响下游节点。\n\n以下是几种理解和间接“查看”其输出的方法:\n\n1. **观察对 KSampler 的影响:**\n `CONDITIONING` 数据主要用于 `KSampler`(或其他采样器)节点,它告诉采样器如何根据文本提示生成图像。\n * 将 `ConditioningZeroOut` 的输出连接到 `KSampler` 的 `positive` 或 `negative` conditioning 输入。\n * 运行工作流并生成图像。\n * **对比实验:** 运行两次工作流:一次使用 `ConditioningZeroOut` 节点(例如,将其输出连接到 `KSampler`),另一次则不使用该节点,直接将原始的 `CLIPTextEncode` 输出连接到 `KSampler`。通过比较两次生成的图像,您就可以观察到 `ConditioningZeroOut` 对最终图像生成效果的影响。这是最直接、最有效的方法来理解它的作用。\n\n2. **使用专门的调试节点(如果有):**\n 某些自定义扩展包可能会提供专门用于调试或打印 `CONDITIONING` 数据的节点。这些节点通常会将条件数据的内部结构或特定部分转换为文本形式输出,以便您可以查看。但这不是 ComfyUI 内置的标准功能。您可以在 ComfyUI 管理器中搜索相关的调试节点。\n\n3. **理解其内部机制:**\n `ConditioningZeroOut` 节点如其名,会将其接收到的 `CONDITIONING` 数据中的某些特定元素(通常是 `pooled_output` 或其他内部张量)清零。这意味着它会“移除”或“中和”这些特定元素对后续生成过程的影响。您通过观察图像变化来验证这种“移除”是否达到了预期效果。\n\n**简而言之:** 您无法像查看图像一样直接“看到”`ConditioningZeroOut` 的输出。您需要通过观察连接到它的下游节点(特别是 `KSampler`)所生成的图像来间接评估它的效果。通过对比有无该节点时生成的图像差异,您就能理解它具体做了什么。'}, {'role': 'assistant', 'content': 'Would you like me to help you debug the current workflow on the canvas?'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the SetNode node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '看起来您想了解 `SetNode` 节点如何工作以及它的官方使用指南。\n\n我尝试查找名为 `SetNode` 的节点,但系统显示“未找到节点: SetNode”。这可能是因为:\n\n1. 您输入的节点名称有误。\n2. `SetNode` 是某个特定的自定义节点,系统当前没有收录。\n3. 它可能是一个较少使用的节点,或者您误解了其名称。\n\n为了更好地帮助您,请问:\n\n* 您是在哪里看到或听说了 `SetNode` 这个节点?\n* 您希望这个节点实现什么功能?例如,是设置变量、设置参数、还是其他什么操作?\n\n在您提供更多信息之前,我将尝试使用更广泛的搜索来查找与“SetNode”相关的功能节点,或者检查是否存在拼写错误。\n\n根据我的查询,在 ComfyUI 的官方或常用扩展节点中,并没有找到一个确切名为 `SetNode` 的节点。\n\n我在之前的搜索中尝试查找了与“设置”、“节点”、“变量”、“数据”或“参数”相关的节点,发现了一些可能与“设置”某种数据类型相关的节点,例如 `FloatNode`(用于设置浮点数)和 `StateResetNode`(用于重置实时应用的状态)。但是,这些都不是您提到的通用的 `SetNode`。\n\n为了能更准确地帮助您,能否请您提供更多关于 `SetNode` 的信息?例如:\n\n* 您是在哪里了解到 `SetNode` 这个名称的?是在某个教程、视频、还是某个特定工作流中看到的?\n* 您期望 `SetNode` 节点具体实现什么功能?它应该设置哪种类型的数据(例如,文本、数字、图像、模型等),或者它应该控制工作流中的哪个方面?\n\n如果您能提供更多上下文或您希望实现的功能描述,我将能够更精确地为您查找相关节点或提供替代方案。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the DifferentialDiffusion node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}] -2025-12-02 08:49:03 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 8 messages -2025-12-02 08:49:03 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 08:49:03 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 08:49:11 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-02 08:49:12 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"Differential Diffusion\",\"nodes_class_name\":\"DifferentialDiffusion\",\"nodes_extension\":\"\",\"nodes_category\":\"_for_testing\",\"nodes_description\":{},\"extension_github_url\":\"\",\"nodes_tips\":{},\"nodes_debug_tips\":{}},\"error\":null}","annotations":null,"meta":null} -2025-12-02 08:49:12 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-02 08:49:12 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-02 08:49:14 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-02 08:49:14 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"error\":\"未找到节点: FaceDetector. Try to use search on Internet tool to find more information.\",\"data\":null}","annotations":null,"meta":null} -2025-12-02 08:49:14 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-02 08:49:14 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=0, ext=None -2025-12-02 08:49:23 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'bing_search' was called -2025-12-02 08:49:25 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"[\n {\n \"id\": \"result_1764665365038_0\",\n \"title\": \"有玩过ComfyUI和sd的人来说下会sd的情况下还有必要学 ...\",\n \"link\": \"https://www.zhihu.com/question/648533380\",\n \"snippet\": \"ComfyUI 将工作流程分解为可重新排列的元素,以便您可以轻松创建自己的元素。 安装 ComfyUI 您需要一个安装 ComfyUI 才能实践本指南。 本地安装请参见 安装指南。 Think Diffusion 提供 …\"\n },\n {\n \"id\": \"result_1764665365039_1\",\n \"title\": \"全网最全comfyui工作流保姆级教程来啦!comfyui工作流 ...\",\n \"link\": \"https://www.zhihu.com/tardis/bd/art/902286718\",\n \"snippet\": \"2025年4月22日 · 1. WebUI : 优点:界面友好,插件丰富,新手小白基本也能秒上手 缺点:吃显存,对配置要求较高,出图较慢 2. ComfyUI : 优点:性能好,速度快,支持工作流的导入导 …\"\n },\n {\n \"id\": \"result_1764665365039_2\",\n \"title\": \"Comfyui前景如何? - 知乎\",\n \"link\": \"https://www.zhihu.com/question/14481050317\",\n \"snippet\": \"Comfyui前景如何? Comfyui会不会变成独立岗位,未来岗位需求量大不大? ,有没有从业者分享下具体工作内容,还是说只掌握一门Comfyui技能不足以找到工作? 显示全部 关注者 79 被浏览\"\n },\n {\n \"id\": \"result_1764665365040_3\",\n \"title\": \"comfyUI和SD webUI都有哪些差别呢? - 知乎\",\n \"link\": \"https://www.zhihu.com/question/632788243?write\",\n \"snippet\": \"ComfyUI缺点:工作流对于普通用户有一定的门槛,容易看得一头雾水。 之前ComfyUI的安装比较繁琐,随着秋叶一键包加入对ComfyUI的支持,越来越多的中国AI绘画爱好者也加入 …\"\n },\n {\n \"id\": \"result_1764665365040_4\",\n \"title\": \"comfyUI不自动释放显存导致的爆显存,怎么办? - 知乎\",\n \"link\": \"https://www.zhihu.com/question/642660960\",\n \"snippet\": \"comfyUI不自动释放显存导致的爆显存,怎么办? 最近突然发现的一个情况 原本运行正常的工作流突然开始爆显存,观察任务管理器发现一个节点运行完之后显存不会自动释放,导致下一个节 …\"\n }\n]","annotations":null,"meta":null} -2025-12-02 08:49:25 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'bing_search' -2025-12-02 08:49:25 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'bing_search': data=5, ext=None -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 2 -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: False -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 0 -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: bing_search -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 5 -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-02 08:49:33 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 08:49:33 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 08:49:33 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 1831 chars, ext: False, finished: True -2025-12-02 08:57:33 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 08:57:33 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 22 -2025-12-02 08:57:33 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 08:57:33 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 08:57:33 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 08:57:33 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 08:57:33 | INFO | conversation_api.py:invoke_chat:272 | -- Received 23 historical messages -2025-12-02 08:57:33 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 22 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 08:57:33 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 22, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 08:57:33 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 23 -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 23 -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 14, summary length: 699 -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 9 (threshold: 8) -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:91 | [Memory] Compression triggered! -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:98 | [Memory] Compressing messages from index 14 to 19 -2025-12-02 08:57:33 | INFO | message_memory.py:message_memory_optimize:99 | [Memory] Messages to compress: 5 -2025-12-02 08:57:33 | INFO | summary_agent.py:generate_summary:100 | Generating summary with model: gemini-2.5-flash -2025-12-02 08:57:53 | INFO | summary_agent.py:generate_summary:132 | Generated summary: The conversation continued from the user's previous request for an explanation and official usage gu... -2025-12-02 08:57:53 | INFO | message_memory.py:message_memory_optimize:107 | [Memory] Generated new summary: The conversation continued from the user's previous request for an explanation and official usage gu... -2025-12-02 08:57:53 | INFO | message_memory.py:message_memory_optimize:116 | [Memory] Updated index to: 19 -2025-12-02 08:57:53 | INFO | message_memory.py:message_memory_optimize:120 | [Memory] Returning 5 optimized messages -2025-12-02 08:57:53 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 5, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation continued from the user's previous request for an explanation and official usage guide for the `ConditioningZeroOut` node in Chinese. After the `ComfyUI-Debug-Coordinator` successfully validated the workflow, the assistant provided a detailed explanation, describing the node as an official ComfyUI extension that zeroes out elements like `pooled_output` in conditioning data for precise control in generative AI.\n\nSubsequently, the user inquired how to view the `ConditioningZeroOut` node's output. The assistant clarified that its `CONDITIONING` output is not directly viewable as an image. It advised users to indirectly assess its effect by connecting it to a `KSampler` node, generating and comparing images with and without the node, or by understanding its internal mechanism of neutralizing specific conditioning elements. The assistant also mentioned the possibility of specialized debugging nodes. Following this, the assistant offered to debug the user's workflow.\n\nThe user then asked for information and an official guide for a `SetNode` in Chinese. The assistant reported that `SetNode` could not be found, suggesting potential naming errors or that it might be a custom or less common node. It requested further details about where the user encountered `SetNode` and its expected functionality to provide accurate assistance."}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the DifferentialDiffusion node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '### DifferentialDiffusion 节点工作原理及官方使用指南\n\n根据查询结果,`DifferentialDiffusion` 节点的信息如下:\n\n* **节点名称:** Differential Diffusion\n* **节点类名:** DifferentialDiffusion\n* **所属扩展:** (未指定,但类别显示为 `_for_testing`,这可能意味着它是一个内部测试或不公开的节点,或需要特定的扩展)\n* **节点类别:** `_for_testing` (用于测试)\n* **节点描述:** (未提供详细描述)\n* **GitHub 地址:** (未提供)\n\n从提供的信息来看,`DifferentialDiffusion` 节点目前没有详细的官方描述或使用指南。其类别显示为 `_for_testing`,这可能意味着它是一个内部开发或测试中的节点,不作为常规的生产节点对外提供详细文档。\n\n由于缺乏具体的描述,我无法提供详细的工作原理、输入输出或使用方法。如果这是您在某个特定工作流或教程中遇到的节点,请提供更多上下文信息,例如工作流的来源、该节点在工作流中的作用等,我将尽力为您查找相关信息。\n\n---\n\n### FaceDetector 节点工作原理及官方使用指南\n\n很抱歉,我尝试查找名为 `FaceDetector` 的节点,但系统显示“未找到节点: FaceDetector”。\n\n这可能有以下几个原因:\n\n1. **节点名称拼写错误:** 请检查您输入的节点名称是否准确无误。\n2. **自定义节点:** `FaceDetector` 可能是一个来自特定 ComfyUI 扩展包的自定义节点,而非 ComfyUI 内置的官方节点。\n3. **非主流或已废弃节点:** 该节点可能不是一个广泛使用的节点,或者已经被其他功能更强大、更稳定的节点所取代。\n\n为了更好地帮助您,我将尝试通过互联网搜索来获取关于 `FaceDetector` 节点的更多信息。\n\n根据我的进一步搜索,关于 ComfyUI 的“FaceDetector”节点,我未能找到官方的使用指南或明确的节点信息。必应搜索结果也未直接指向名为“FaceDetector”的独立节点。\n\n这可能意味着:\n\n1. **节点名称不准确:** 您使用的节点名称可能与实际的节点类名有所不同。\n2. **它是某个扩展的一部分:** 面部检测功能通常会作为某个更大型的自定义节点包(例如 ControlNet 预处理器、人脸修复相关的扩展等)的一部分出现,而非一个独立的通用“FaceDetector”节点。这些节点通常会有特定的名称,如 `InsightFace`、`Face_Detailer`、`ControlNetPreProcessor-Face` 等。\n3. **较不常用或内部节点:** 像 `DifferentialDiffusion` 一样,它可能是一个不常用或测试中的节点。\n\n**如何实现人脸检测功能?**\n\n在 ComfyUI 中,通常通过以下方式实现人脸检测功能:\n\n* **ControlNet 预处理器:** 许多 ControlNet 预处理器都包含人脸检测功能,例如 `ControlNetPreProcessor-OpenPose` 或专门用于人脸识别/姿态的预处理器。它们会输出包含人脸关键点或边界框的蒙版/图像。\n* **图像修复/细节增强节点:** 一些专注于图像局部细节(尤其是人脸)的节点,例如 `FaceDetailer`(通常来自某些自定义扩展),会内置人脸检测功能,以便对检测到的人脸区域进行特定的处理(如放大、修复)。\n* **专门的人脸相关扩展:** 有些社区开发的 ComfyUI 扩展专注于人脸处理,其中可能会包含用于人脸检测的节点。\n\n**为了更好地帮助您,请您提供更多信息:**\n\n* 您是在哪里看到或听说了 `FaceDetector` 这个节点?\n* 您希望通过这个节点实现什么具体功能?例如,您是想:\n * 检测图片中的人脸位置并获取其边界框?\n * 对人脸区域进行局部重绘或修复?\n * 使用人脸信息进行姿态控制或图像生成?\n\n如果您能提供更多上下文,我将能够更准确地为您推荐相应的节点或工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}] -2025-12-02 08:58:08 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 5 messages -2025-12-02 08:58:08 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 08:58:08 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 08:58:30 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2025-12-02 08:58:30 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 08:58:30 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 08:58:30 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 1005 chars, ext: False, finished: True -2025-12-02 09:20:33 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-02 09:20:33 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 23 -2025-12-02 09:20:33 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-02 09:20:33 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-02 09:20:33 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-02 09:20:33 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-02 09:20:33 | INFO | conversation_api.py:invoke_chat:272 | -- Received 25 historical messages -2025-12-02 09:20:33 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 23 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 09:20:33 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 23, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-02 09:20:33 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 25 -2025-12-02 09:20:33 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-02 09:20:33 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 25 -2025-12-02 09:20:33 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 19, summary length: 1357 -2025-12-02 09:20:33 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 6 (threshold: 8) -2025-12-02 09:20:33 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-02 09:20:33 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 7, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation continued from the user's previous request for an explanation and official usage guide for the `ConditioningZeroOut` node in Chinese. After the `ComfyUI-Debug-Coordinator` successfully validated the workflow, the assistant provided a detailed explanation, describing the node as an official ComfyUI extension that zeroes out elements like `pooled_output` in conditioning data for precise control in generative AI.\n\nSubsequently, the user inquired how to view the `ConditioningZeroOut` node's output. The assistant clarified that its `CONDITIONING` output is not directly viewable as an image. It advised users to indirectly assess its effect by connecting it to a `KSampler` node, generating and comparing images with and without the node, or by understanding its internal mechanism of neutralizing specific conditioning elements. The assistant also mentioned the possibility of specialized debugging nodes. Following this, the assistant offered to debug the user's workflow.\n\nThe user then asked for information and an official guide for a `SetNode` in Chinese. The assistant reported that `SetNode` could not be found, suggesting potential naming errors or that it might be a custom or less common node. It requested further details about where the user encountered `SetNode` and its expected functionality to provide accurate assistance."}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the DifferentialDiffusion node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '### DifferentialDiffusion 节点工作原理及官方使用指南\n\n根据查询结果,`DifferentialDiffusion` 节点的信息如下:\n\n* **节点名称:** Differential Diffusion\n* **节点类名:** DifferentialDiffusion\n* **所属扩展:** (未指定,但类别显示为 `_for_testing`,这可能意味着它是一个内部测试或不公开的节点,或需要特定的扩展)\n* **节点类别:** `_for_testing` (用于测试)\n* **节点描述:** (未提供详细描述)\n* **GitHub 地址:** (未提供)\n\n从提供的信息来看,`DifferentialDiffusion` 节点目前没有详细的官方描述或使用指南。其类别显示为 `_for_testing`,这可能意味着它是一个内部开发或测试中的节点,不作为常规的生产节点对外提供详细文档。\n\n由于缺乏具体的描述,我无法提供详细的工作原理、输入输出或使用方法。如果这是您在某个特定工作流或教程中遇到的节点,请提供更多上下文信息,例如工作流的来源、该节点在工作流中的作用等,我将尽力为您查找相关信息。\n\n---\n\n### FaceDetector 节点工作原理及官方使用指南\n\n很抱歉,我尝试查找名为 `FaceDetector` 的节点,但系统显示“未找到节点: FaceDetector”。\n\n这可能有以下几个原因:\n\n1. **节点名称拼写错误:** 请检查您输入的节点名称是否准确无误。\n2. **自定义节点:** `FaceDetector` 可能是一个来自特定 ComfyUI 扩展包的自定义节点,而非 ComfyUI 内置的官方节点。\n3. **非主流或已废弃节点:** 该节点可能不是一个广泛使用的节点,或者已经被其他功能更强大、更稳定的节点所取代。\n\n为了更好地帮助您,我将尝试通过互联网搜索来获取关于 `FaceDetector` 节点的更多信息。\n\n根据我的进一步搜索,关于 ComfyUI 的“FaceDetector”节点,我未能找到官方的使用指南或明确的节点信息。必应搜索结果也未直接指向名为“FaceDetector”的独立节点。\n\n这可能意味着:\n\n1. **节点名称不准确:** 您使用的节点名称可能与实际的节点类名有所不同。\n2. **它是某个扩展的一部分:** 面部检测功能通常会作为某个更大型的自定义节点包(例如 ControlNet 预处理器、人脸修复相关的扩展等)的一部分出现,而非一个独立的通用“FaceDetector”节点。这些节点通常会有特定的名称,如 `InsightFace`、`Face_Detailer`、`ControlNetPreProcessor-Face` 等。\n3. **较不常用或内部节点:** 像 `DifferentialDiffusion` 一样,它可能是一个不常用或测试中的节点。\n\n**如何实现人脸检测功能?**\n\n在 ComfyUI 中,通常通过以下方式实现人脸检测功能:\n\n* **ControlNet 预处理器:** 许多 ControlNet 预处理器都包含人脸检测功能,例如 `ControlNetPreProcessor-OpenPose` 或专门用于人脸识别/姿态的预处理器。它们会输出包含人脸关键点或边界框的蒙版/图像。\n* **图像修复/细节增强节点:** 一些专注于图像局部细节(尤其是人脸)的节点,例如 `FaceDetailer`(通常来自某些自定义扩展),会内置人脸检测功能,以便对检测到的人脸区域进行特定的处理(如放大、修复)。\n* **专门的人脸相关扩展:** 有些社区开发的 ComfyUI 扩展专注于人脸处理,其中可能会包含用于人脸检测的节点。\n\n**为了更好地帮助您,请您提供更多信息:**\n\n* 您是在哪里看到或听说了 `FaceDetector` 这个节点?\n* 您希望通过这个节点实现什么具体功能?例如,您是想:\n * 检测图片中的人脸位置并获取其边界框?\n * 对人脸区域进行局部重绘或修复?\n * 使用人脸信息进行姿态控制或图像生成?\n\n如果您能提供更多上下文,我将能够更准确地为您推荐相应的节点或工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '### FaceDetector 节点工作原理及官方使用指南\n\n很抱歉,我再次尝试查找名为 `FaceDetector` 的节点,但系统仍然显示“未找到节点: FaceDetector”,并且通过搜索也没有发现ComfyUI官方或主流扩展中存在一个名为“FaceDetector”的独立节点及其官方使用指南。\n\n这再次确认了以下几种可能性:\n\n1. **节点名称不准确:** 您提供的节点名称可能与实际的节点类名有所不同。请仔细检查您看到或使用的节点名称,确保拼写完全正确。\n2. **自定义或小众扩展节点:** `FaceDetector` 很可能是一个来自特定、非主流或个人开发的 ComfyUI 扩展包的自定义节点,而不是 ComfyUI 内置的节点。这类节点通常没有官方的通用文档,其使用指南可能仅存在于该扩展的GitHub页面或相关社区论坛中。\n3. **功能集成在其他节点中:** 在 ComfyUI 中,人脸检测功能通常不会作为一个独立的通用“FaceDetector”节点存在,而是集成在其他更具体的节点中。例如:\n * **ControlNet 预处理器:** 许多 ControlNet 预处理器,如用于姿态识别的 OpenPose 或其他面部关键点预处理器,会内置人脸检测功能以提取面部信息。\n * **人脸修复/细节增强节点:** 一些专门用于人脸修复或细节增强的自定义节点(如某些 `Face Detailer` 节点)会在内部进行人脸检测,以精确地作用于人脸区域。\n * **第三方插件:** 某些 ComfyUI 插件会提供特定的人脸处理功能,其中可能包含隐式或显式的人脸检测模块。\n\n**为了能够更好地帮助您,请您提供更多上下文信息:**\n\n* 您是在哪里看到或听说了 `FaceDetector` 这个节点的?例如,某个特定的工作流文件、教程、视频,或是某个ComfyUI扩展的说明?\n* 您希望通过这个节点实现什么样的具体功能?例如,您是想:\n * 检测图片中的人脸位置并获取其边界框?\n * 生成人脸的蒙版?\n * 对人脸区域进行局部重绘、修复或风格化?\n * 利用人脸信息来控制图像生成?\n\n如果您能提供这些信息,我将能够更准确地判断该节点的功能,并为您寻找替代方案或提供更具体的帮助。'}, {'role': 'user', 'content': '如何查看正面条件的输出是啥'}] -2025-12-02 09:20:37 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 7 messages -2025-12-02 09:20:37 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-02 09:20:37 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-02 09:21:07 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2025-12-02 09:21:07 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-02 09:21:07 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-02 09:21:07 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 616 chars, ext: False, finished: True -2025-12-04 02:50:54 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-04 02:50:55 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-04 02:50:55 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 24 -2025-12-04 02:50:55 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-04 02:50:55 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-04 02:50:55 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-04 02:50:55 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-04 02:50:55 | INFO | conversation_api.py:invoke_chat:272 | -- Received 27 historical messages -2025-12-04 02:50:55 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 24 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-04 02:50:55 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 24, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-04 02:50:55 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 27 -2025-12-04 02:50:55 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-04 02:50:55 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 27 -2025-12-04 02:50:55 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 19, summary length: 1357 -2025-12-04 02:50:55 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 8 (threshold: 8) -2025-12-04 02:50:55 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-04 02:50:55 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 9, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation continued from the user's previous request for an explanation and official usage guide for the `ConditioningZeroOut` node in Chinese. After the `ComfyUI-Debug-Coordinator` successfully validated the workflow, the assistant provided a detailed explanation, describing the node as an official ComfyUI extension that zeroes out elements like `pooled_output` in conditioning data for precise control in generative AI.\n\nSubsequently, the user inquired how to view the `ConditioningZeroOut` node's output. The assistant clarified that its `CONDITIONING` output is not directly viewable as an image. It advised users to indirectly assess its effect by connecting it to a `KSampler` node, generating and comparing images with and without the node, or by understanding its internal mechanism of neutralizing specific conditioning elements. The assistant also mentioned the possibility of specialized debugging nodes. Following this, the assistant offered to debug the user's workflow.\n\nThe user then asked for information and an official guide for a `SetNode` in Chinese. The assistant reported that `SetNode` could not be found, suggesting potential naming errors or that it might be a custom or less common node. It requested further details about where the user encountered `SetNode` and its expected functionality to provide accurate assistance."}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the DifferentialDiffusion node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '### DifferentialDiffusion 节点工作原理及官方使用指南\n\n根据查询结果,`DifferentialDiffusion` 节点的信息如下:\n\n* **节点名称:** Differential Diffusion\n* **节点类名:** DifferentialDiffusion\n* **所属扩展:** (未指定,但类别显示为 `_for_testing`,这可能意味着它是一个内部测试或不公开的节点,或需要特定的扩展)\n* **节点类别:** `_for_testing` (用于测试)\n* **节点描述:** (未提供详细描述)\n* **GitHub 地址:** (未提供)\n\n从提供的信息来看,`DifferentialDiffusion` 节点目前没有详细的官方描述或使用指南。其类别显示为 `_for_testing`,这可能意味着它是一个内部开发或测试中的节点,不作为常规的生产节点对外提供详细文档。\n\n由于缺乏具体的描述,我无法提供详细的工作原理、输入输出或使用方法。如果这是您在某个特定工作流或教程中遇到的节点,请提供更多上下文信息,例如工作流的来源、该节点在工作流中的作用等,我将尽力为您查找相关信息。\n\n---\n\n### FaceDetector 节点工作原理及官方使用指南\n\n很抱歉,我尝试查找名为 `FaceDetector` 的节点,但系统显示“未找到节点: FaceDetector”。\n\n这可能有以下几个原因:\n\n1. **节点名称拼写错误:** 请检查您输入的节点名称是否准确无误。\n2. **自定义节点:** `FaceDetector` 可能是一个来自特定 ComfyUI 扩展包的自定义节点,而非 ComfyUI 内置的官方节点。\n3. **非主流或已废弃节点:** 该节点可能不是一个广泛使用的节点,或者已经被其他功能更强大、更稳定的节点所取代。\n\n为了更好地帮助您,我将尝试通过互联网搜索来获取关于 `FaceDetector` 节点的更多信息。\n\n根据我的进一步搜索,关于 ComfyUI 的“FaceDetector”节点,我未能找到官方的使用指南或明确的节点信息。必应搜索结果也未直接指向名为“FaceDetector”的独立节点。\n\n这可能意味着:\n\n1. **节点名称不准确:** 您使用的节点名称可能与实际的节点类名有所不同。\n2. **它是某个扩展的一部分:** 面部检测功能通常会作为某个更大型的自定义节点包(例如 ControlNet 预处理器、人脸修复相关的扩展等)的一部分出现,而非一个独立的通用“FaceDetector”节点。这些节点通常会有特定的名称,如 `InsightFace`、`Face_Detailer`、`ControlNetPreProcessor-Face` 等。\n3. **较不常用或内部节点:** 像 `DifferentialDiffusion` 一样,它可能是一个不常用或测试中的节点。\n\n**如何实现人脸检测功能?**\n\n在 ComfyUI 中,通常通过以下方式实现人脸检测功能:\n\n* **ControlNet 预处理器:** 许多 ControlNet 预处理器都包含人脸检测功能,例如 `ControlNetPreProcessor-OpenPose` 或专门用于人脸识别/姿态的预处理器。它们会输出包含人脸关键点或边界框的蒙版/图像。\n* **图像修复/细节增强节点:** 一些专注于图像局部细节(尤其是人脸)的节点,例如 `FaceDetailer`(通常来自某些自定义扩展),会内置人脸检测功能,以便对检测到的人脸区域进行特定的处理(如放大、修复)。\n* **专门的人脸相关扩展:** 有些社区开发的 ComfyUI 扩展专注于人脸处理,其中可能会包含用于人脸检测的节点。\n\n**为了更好地帮助您,请您提供更多信息:**\n\n* 您是在哪里看到或听说了 `FaceDetector` 这个节点?\n* 您希望通过这个节点实现什么具体功能?例如,您是想:\n * 检测图片中的人脸位置并获取其边界框?\n * 对人脸区域进行局部重绘或修复?\n * 使用人脸信息进行姿态控制或图像生成?\n\n如果您能提供更多上下文,我将能够更准确地为您推荐相应的节点或工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceDetector node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '### FaceDetector 节点工作原理及官方使用指南\n\n很抱歉,我再次尝试查找名为 `FaceDetector` 的节点,但系统仍然显示“未找到节点: FaceDetector”,并且通过搜索也没有发现ComfyUI官方或主流扩展中存在一个名为“FaceDetector”的独立节点及其官方使用指南。\n\n这再次确认了以下几种可能性:\n\n1. **节点名称不准确:** 您提供的节点名称可能与实际的节点类名有所不同。请仔细检查您看到或使用的节点名称,确保拼写完全正确。\n2. **自定义或小众扩展节点:** `FaceDetector` 很可能是一个来自特定、非主流或个人开发的 ComfyUI 扩展包的自定义节点,而不是 ComfyUI 内置的节点。这类节点通常没有官方的通用文档,其使用指南可能仅存在于该扩展的GitHub页面或相关社区论坛中。\n3. **功能集成在其他节点中:** 在 ComfyUI 中,人脸检测功能通常不会作为一个独立的通用“FaceDetector”节点存在,而是集成在其他更具体的节点中。例如:\n * **ControlNet 预处理器:** 许多 ControlNet 预处理器,如用于姿态识别的 OpenPose 或其他面部关键点预处理器,会内置人脸检测功能以提取面部信息。\n * **人脸修复/细节增强节点:** 一些专门用于人脸修复或细节增强的自定义节点(如某些 `Face Detailer` 节点)会在内部进行人脸检测,以精确地作用于人脸区域。\n * **第三方插件:** 某些 ComfyUI 插件会提供特定的人脸处理功能,其中可能包含隐式或显式的人脸检测模块。\n\n**为了能够更好地帮助您,请您提供更多上下文信息:**\n\n* 您是在哪里看到或听说了 `FaceDetector` 这个节点的?例如,某个特定的工作流文件、教程、视频,或是某个ComfyUI扩展的说明?\n* 您希望通过这个节点实现什么样的具体功能?例如,您是想:\n * 检测图片中的人脸位置并获取其边界框?\n * 生成人脸的蒙版?\n * 对人脸区域进行局部重绘、修复或风格化?\n * 利用人脸信息来控制图像生成?\n\n如果您能提供这些信息,我将能够更准确地判断该节点的功能,并为您寻找替代方案或提供更具体的帮助。'}, {'role': 'user', 'content': '如何查看正面条件的输出是啥'}, {'role': 'assistant', 'content': '正面条件(Positive Conditioning)的输出并非是图片,也不是可以直接“查看”的视觉内容。它是一种复杂的、由模型(如 CLIP)编码的内部数据结构,包含了对图像生成方向的文本描述信息。\n\n您不能直接像查看图片一样去“看”正面条件的输出。它的作用是作为 `KSampler` 等生成模型的输入,指导模型生成符合这些条件描述的图像。\n\n要“查看”正面条件输出的效果,您需要:\n\n1. **将其连接到 `KSampler` 节点:** 这是最直接的方法。将正面条件连接到 `KSampler` 的 `positive` 输入端。\n2. **生成图像并观察效果:** 运行工作流生成一张图片,然后通过观察生成的图片是否符合您在正面条件中输入的文本描述来判断其效果。\n3. **对比实验:**\n * 尝试使用不同的正面提示词生成图片,对比它们之间的差异。\n * 在其他条件相同的情况下,移除或更改正面条件,看看对生成结果的影响。\n\n有一些特殊的调试节点(通常是第三方插件提供的)可能能够“可视化”条件数据的一些内部特征(例如将其转换为某种表示形式或显示其维度信息),但这并不是直接查看其“内容”,更多的是为了高级调试或理解。\n\n如果您想更深入地理解某个特定节点(例如生成正面条件的 CLIP Text Encode 节点)的输出,或者对工作流中正面条件的使用有疑问,请告诉我,我可以帮助您分析工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerUtility: LoadJoyCaption2Model node work? I need its official usage guide.'}] -2025-12-04 02:51:11 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 9 messages -2025-12-04 02:51:11 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-04 02:51:11 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-04 02:51:16 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-04 02:51:16 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"LayerUtility: LoadJoyCaption2Model\",\"nodes_class_name\":\"LayerUtility: LoadJoyCaption2Model\",\"nodes_extension\":\"ComfyUI_LayerStyle_Advance\",\"nodes_category\":\"__dzNodes/LayerUtility\",\"nodes_description\":{\"EN\":\"Facilitates loading and configuring advanced language models for generating detailed image captions. This node helps AI artists leverage sophisticated language models to create contextually rich captions for images.\"},\"extension_github_url\":\"https://github.com/chflame163/ComfyUI_LayerStyle_Advance\",\"nodes_tips\":{\"EN\":\"Ensure that your system has a compatible NVIDIA GPU installed for faster processing. Experiment with different 'llm_model' options to suit your artistic style. Consider trade-offs between precision and performance when selecting 'dtype'.\"},\"nodes_debug_tips\":{}},\"error\":null}","annotations":null,"meta":null} -2025-12-04 02:51:16 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-04 02:51:16 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-04 02:51:24 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-04 02:51:24 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-04 02:51:24 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 1967 chars, ext: False, finished: True -2025-12-04 06:56:40 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-04 06:56:57 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-04 06:56:57 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 25 -2025-12-04 06:56:57 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-04 06:56:57 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2025-12-04 06:56:57 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-04 06:56:57 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-04 06:56:57 | INFO | conversation_api.py:invoke_chat:272 | -- Received 1 historical messages -2025-12-04 06:56:57 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 25 for session 50454351-f3d4-4d50-9c53-0447a6f629be -2025-12-04 06:56:57 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': '50454351-f3d4-4d50-9c53-0447a6f629be', 'workflow_checkpoint_id': 25, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-04 06:56:57 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 1 -2025-12-04 06:56:57 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: 50454351-f3d4-4d50-9c53-0447a6f629be -2025-12-04 06:56:57 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 1 -2025-12-04 06:56:57 | INFO | message_memory.py:message_memory_optimize:57 | [Memory] New session detected, creating record -2025-12-04 06:56:57 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 1, messages: [{'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerUtility: JoyCaptionBeta1 node work? I need its official usage guide.'}] -2025-12-04 06:57:02 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 1 messages -2025-12-04 06:57:02 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-04 06:57:02 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-04 06:57:04 | ERROR | mcp_client.py:process_stream_events:453 | Unexpected streaming error: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-04 06:57:04 | ERROR | mcp_client.py:process_stream_events:454 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2025-12-04 06:57:04 | ERROR | mcp_client.py:comfyui_agent_invoke:503 | Non-retryable streaming error or max retries reached: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2025-12-04 06:57:04 | ERROR | mcp_client.py:comfyui_agent_invoke:504 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 460, in comfyui_agent_invoke - async for stream_data in process_stream_events(result): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 455, in process_stream_events - raise e - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2025-12-04 06:57:04 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2025-12-04 06:57:04 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-04 06:57:04 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-04 06:57:04 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 0 chars, ext: False, finished: True -2025-12-15 10:10:43 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-19 10:45:47 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-19 10:45:52 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-19 10:45:52 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 26 -2025-12-19 10:45:52 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-19 10:45:52 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-19 10:45:52 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-19 10:45:52 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-19 10:45:52 | INFO | conversation_api.py:invoke_chat:272 | -- Received 29 historical messages -2025-12-19 10:45:52 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 26 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-19 10:45:52 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 26, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-19 10:45:52 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 29 -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 29 -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 19, summary length: 1357 -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 10 (threshold: 8) -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:91 | [Memory] Compression triggered! -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:98 | [Memory] Compressing messages from index 19 to 25 -2025-12-19 10:45:52 | INFO | message_memory.py:message_memory_optimize:99 | [Memory] Messages to compress: 6 -2025-12-19 10:45:52 | INFO | summary_agent.py:generate_summary:100 | Generating summary with model: gemini-2.5-flash -2025-12-19 10:45:59 | INFO | summary_agent.py:generate_summary:132 | Generated summary: The conversation continued with a detailed explanation and usage guide for the `ConditioningZeroOut`... -2025-12-19 10:45:59 | INFO | message_memory.py:message_memory_optimize:107 | [Memory] Generated new summary: The conversation continued with a detailed explanation and usage guide for the `ConditioningZeroOut`... -2025-12-19 10:45:59 | INFO | message_memory.py:message_memory_optimize:116 | [Memory] Updated index to: 25 -2025-12-19 10:45:59 | INFO | message_memory.py:message_memory_optimize:120 | [Memory] Returning 5 optimized messages -2025-12-19 10:45:59 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 5, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation continued with a detailed explanation and usage guide for the `ConditioningZeroOut` node in Chinese, following a workflow validation. The user inquired about viewing its output, to which the assistant clarified that `CONDITIONING` output isn't directly viewable, suggesting indirect assessment via `KSampler` or understanding its internal mechanism. The assistant also offered workflow debugging. An inquiry for `SetNode` found no results, prompting the assistant to ask for more context.\n\nSubsequently, the user inquired about the `DifferentialDiffusion` node. The assistant identified it as an undocumented, `_for_testing` node, requesting more context. The user then made multiple inquiries about the `FaceDetector` node. The assistant repeatedly stated it could not find an official node by that name, suggesting it might be a custom, misnamed, or integrated function within other nodes (e.g., ControlNet preprocessors, face detailers). The assistant outlined common methods for face detection and requested specific user goals. The latest user query was about how to view the positive conditioning output."}, {'role': 'assistant', 'content': '正面条件(Positive Conditioning)的输出并非是图片,也不是可以直接“查看”的视觉内容。它是一种复杂的、由模型(如 CLIP)编码的内部数据结构,包含了对图像生成方向的文本描述信息。\n\n您不能直接像查看图片一样去“看”正面条件的输出。它的作用是作为 `KSampler` 等生成模型的输入,指导模型生成符合这些条件描述的图像。\n\n要“查看”正面条件输出的效果,您需要:\n\n1. **将其连接到 `KSampler` 节点:** 这是最直接的方法。将正面条件连接到 `KSampler` 的 `positive` 输入端。\n2. **生成图像并观察效果:** 运行工作流生成一张图片,然后通过观察生成的图片是否符合您在正面条件中输入的文本描述来判断其效果。\n3. **对比实验:**\n * 尝试使用不同的正面提示词生成图片,对比它们之间的差异。\n * 在其他条件相同的情况下,移除或更改正面条件,看看对生成结果的影响。\n\n有一些特殊的调试节点(通常是第三方插件提供的)可能能够“可视化”条件数据的一些内部特征(例如将其转换为某种表示形式或显示其维度信息),但这并不是直接查看其“内容”,更多的是为了高级调试或理解。\n\n如果您想更深入地理解某个特定节点(例如生成正面条件的 CLIP Text Encode 节点)的输出,或者对工作流中正面条件的使用有疑问,请告诉我,我可以帮助您分析工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerUtility: LoadJoyCaption2Model node work? I need its official usage guide.'}, {'role': 'assistant', 'content': "好的,关于 `LayerUtility: LoadJoyCaption2Model` 节点的工作原理和使用指南,我已经为您找到了详细信息。\n\n### LayerUtility: LoadJoyCaption2Model 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerUtility: LoadJoyCaption2Model`\n* **节点类名:** `LayerUtility: LoadJoyCaption2Model`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerUtility`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerUtility: LoadJoyCaption2Model` 节点主要用于**加载和配置先进的语言模型,以生成详细的图像描述(captions)**。\n\n这个节点的核心功能是帮助 AI 艺术家利用复杂的语言模型来为图像创建具有丰富上下文的描述。简而言之,它**负责将一个预训练的图像描述(captioning)模型加载到 ComfyUI 工作流中**,使得后续的节点可以使用这个模型来分析图像并生成相应的文本描述。\n\n它通过集成高级的语言模型技术,能够更好地理解图像内容,并生成比传统图像描述更精准、更具表现力的文本。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 加载 `JoyCaption2` 或类似的图像描述模型。\n* 为后续的图像理解、自动化提示词生成或图像分析任务提供支持。\n\n**b. 参数说明(推测,通常此类加载节点会有模型选择和精度设置):**\n\n* **`llm_model` (语言模型):** 选择要加载的特定图像描述模型。不同的模型可能在描述风格、细节丰富度或处理特定类型图像的能力上有所不同。\n* **`dtype` (数据类型/精度):** 设置模型加载时的精度(例如 `fp16`、`bf16`、`fp32`)。通常,较低的精度(如 `fp16`)可以减少内存占用并提高运行速度,但可能会略微影响描述的准确性;较高的精度(如 `fp32`)则提供更准确的结果,但需要更多的 VRAM 和更长的处理时间。\n\n**c. 使用提示:**\n\n* **GPU 兼容性:** “确保您的系统安装了兼容的 NVIDIA GPU,以实现更快的处理速度。” 这表明该模型可能需要 GPU 加速才能高效运行。\n* **模型选择:** “尝试使用不同的 'llm_model' 选项来适应您的艺术风格。” 这意味着您可以通过切换不同的模型来获取不同风格或重点的图像描述。\n* **精度权衡:** “在选择 'dtype' 时,请权衡精度和性能。” 根据您的硬件条件和对描述准确性的要求,选择合适的精度设置。\n\n**d. 调试提示:**\n\n* 目前没有提供具体的调试提示,但通常加载模型时常见的调试点包括:\n * **模型文件路径是否正确:** 确保模型文件已放置在扩展指定的正确目录下。\n * **内存不足:** 如果出现内存错误,尝试降低 `dtype` 精度或清理 VRAM。\n * **兼容性问题:** 检查 ComfyUI 版本和扩展版本是否兼容。\n\n#### 4. 在工作流中的位置\n\n该节点通常会出现在工作流的**前端**或**图像分析部分**。它负责加载模型,然后将加载好的模型对象传递给下一个节点,例如一个 `Captioner` 或 `Image to Text` 类型的节点,后者会实际使用这个模型来生成图像描述。\n\n**示例工作流片段(概念性):**\n\n```\n[图片加载节点] -> [LayerUtility: LoadJoyCaption2Model] -> [Captioning处理节点] -> [文本输出节点]\n```\n\n#### 总结\n\n`LayerUtility: LoadJoyCaption2Model` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个关键的组件,用于在 ComfyUI 环境中启用先进的图像描述功能。通过正确配置和连接,它可以帮助您为图像自动生成高质量的文本描述。"}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the easy imageColorMatch node work? I need its official usage guide.'}] -2025-12-19 10:46:14 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 5 messages -2025-12-19 10:46:14 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-19 10:46:14 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-19 10:46:18 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-19 10:46:18 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"Image Color Match\",\"nodes_class_name\":\"easy imageColorMatch\",\"nodes_extension\":\"ComfyUI-Easy-Use\",\"nodes_category\":\"EasyUse/Image\",\"nodes_description\":{\"EN\":\"This node facilitates color matching between a reference image and a target image using various methods, allowing for enhanced visual consistency in image processing tasks.\"},\"extension_github_url\":\"https://github.com/yolain/ComfyUI-Easy-Use\",\"nodes_tips\":{},\"nodes_debug_tips\":{\"EN\":\"{\\\"_eval_score\\\": \\\"40/40\\\", \\\"_has_unknown\\\": false, \\\"_error\\\": null}\"}},\"error\":null}","annotations":null,"meta":null} -2025-12-19 10:46:18 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-19 10:46:18 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-19 10:46:26 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-19 10:46:26 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-19 10:46:26 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 2270 chars, ext: False, finished: True -2025-12-19 10:53:00 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-19 10:53:00 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 27 -2025-12-19 10:53:01 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-19 10:53:01 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-19 10:53:01 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-19 10:53:01 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-19 10:53:01 | INFO | conversation_api.py:invoke_chat:272 | -- Received 31 historical messages -2025-12-19 10:53:01 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 27 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-19 10:53:01 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 27, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-19 10:53:01 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 31 -2025-12-19 10:53:01 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-19 10:53:01 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 31 -2025-12-19 10:53:01 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 25, summary length: 1127 -2025-12-19 10:53:01 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 6 (threshold: 8) -2025-12-19 10:53:01 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-19 10:53:01 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 7, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation continued with a detailed explanation and usage guide for the `ConditioningZeroOut` node in Chinese, following a workflow validation. The user inquired about viewing its output, to which the assistant clarified that `CONDITIONING` output isn't directly viewable, suggesting indirect assessment via `KSampler` or understanding its internal mechanism. The assistant also offered workflow debugging. An inquiry for `SetNode` found no results, prompting the assistant to ask for more context.\n\nSubsequently, the user inquired about the `DifferentialDiffusion` node. The assistant identified it as an undocumented, `_for_testing` node, requesting more context. The user then made multiple inquiries about the `FaceDetector` node. The assistant repeatedly stated it could not find an official node by that name, suggesting it might be a custom, misnamed, or integrated function within other nodes (e.g., ControlNet preprocessors, face detailers). The assistant outlined common methods for face detection and requested specific user goals. The latest user query was about how to view the positive conditioning output."}, {'role': 'assistant', 'content': '正面条件(Positive Conditioning)的输出并非是图片,也不是可以直接“查看”的视觉内容。它是一种复杂的、由模型(如 CLIP)编码的内部数据结构,包含了对图像生成方向的文本描述信息。\n\n您不能直接像查看图片一样去“看”正面条件的输出。它的作用是作为 `KSampler` 等生成模型的输入,指导模型生成符合这些条件描述的图像。\n\n要“查看”正面条件输出的效果,您需要:\n\n1. **将其连接到 `KSampler` 节点:** 这是最直接的方法。将正面条件连接到 `KSampler` 的 `positive` 输入端。\n2. **生成图像并观察效果:** 运行工作流生成一张图片,然后通过观察生成的图片是否符合您在正面条件中输入的文本描述来判断其效果。\n3. **对比实验:**\n * 尝试使用不同的正面提示词生成图片,对比它们之间的差异。\n * 在其他条件相同的情况下,移除或更改正面条件,看看对生成结果的影响。\n\n有一些特殊的调试节点(通常是第三方插件提供的)可能能够“可视化”条件数据的一些内部特征(例如将其转换为某种表示形式或显示其维度信息),但这并不是直接查看其“内容”,更多的是为了高级调试或理解。\n\n如果您想更深入地理解某个特定节点(例如生成正面条件的 CLIP Text Encode 节点)的输出,或者对工作流中正面条件的使用有疑问,请告诉我,我可以帮助您分析工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerUtility: LoadJoyCaption2Model node work? I need its official usage guide.'}, {'role': 'assistant', 'content': "好的,关于 `LayerUtility: LoadJoyCaption2Model` 节点的工作原理和使用指南,我已经为您找到了详细信息。\n\n### LayerUtility: LoadJoyCaption2Model 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerUtility: LoadJoyCaption2Model`\n* **节点类名:** `LayerUtility: LoadJoyCaption2Model`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerUtility`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerUtility: LoadJoyCaption2Model` 节点主要用于**加载和配置先进的语言模型,以生成详细的图像描述(captions)**。\n\n这个节点的核心功能是帮助 AI 艺术家利用复杂的语言模型来为图像创建具有丰富上下文的描述。简而言之,它**负责将一个预训练的图像描述(captioning)模型加载到 ComfyUI 工作流中**,使得后续的节点可以使用这个模型来分析图像并生成相应的文本描述。\n\n它通过集成高级的语言模型技术,能够更好地理解图像内容,并生成比传统图像描述更精准、更具表现力的文本。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 加载 `JoyCaption2` 或类似的图像描述模型。\n* 为后续的图像理解、自动化提示词生成或图像分析任务提供支持。\n\n**b. 参数说明(推测,通常此类加载节点会有模型选择和精度设置):**\n\n* **`llm_model` (语言模型):** 选择要加载的特定图像描述模型。不同的模型可能在描述风格、细节丰富度或处理特定类型图像的能力上有所不同。\n* **`dtype` (数据类型/精度):** 设置模型加载时的精度(例如 `fp16`、`bf16`、`fp32`)。通常,较低的精度(如 `fp16`)可以减少内存占用并提高运行速度,但可能会略微影响描述的准确性;较高的精度(如 `fp32`)则提供更准确的结果,但需要更多的 VRAM 和更长的处理时间。\n\n**c. 使用提示:**\n\n* **GPU 兼容性:** “确保您的系统安装了兼容的 NVIDIA GPU,以实现更快的处理速度。” 这表明该模型可能需要 GPU 加速才能高效运行。\n* **模型选择:** “尝试使用不同的 'llm_model' 选项来适应您的艺术风格。” 这意味着您可以通过切换不同的模型来获取不同风格或重点的图像描述。\n* **精度权衡:** “在选择 'dtype' 时,请权衡精度和性能。” 根据您的硬件条件和对描述准确性的要求,选择合适的精度设置。\n\n**d. 调试提示:**\n\n* 目前没有提供具体的调试提示,但通常加载模型时常见的调试点包括:\n * **模型文件路径是否正确:** 确保模型文件已放置在扩展指定的正确目录下。\n * **内存不足:** 如果出现内存错误,尝试降低 `dtype` 精度或清理 VRAM。\n * **兼容性问题:** 检查 ComfyUI 版本和扩展版本是否兼容。\n\n#### 4. 在工作流中的位置\n\n该节点通常会出现在工作流的**前端**或**图像分析部分**。它负责加载模型,然后将加载好的模型对象传递给下一个节点,例如一个 `Captioner` 或 `Image to Text` 类型的节点,后者会实际使用这个模型来生成图像描述。\n\n**示例工作流片段(概念性):**\n\n```\n[图片加载节点] -> [LayerUtility: LoadJoyCaption2Model] -> [Captioning处理节点] -> [文本输出节点]\n```\n\n#### 总结\n\n`LayerUtility: LoadJoyCaption2Model` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个关键的组件,用于在 ComfyUI 环境中启用先进的图像描述功能。通过正确配置和连接,它可以帮助您为图像自动生成高质量的文本描述。"}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the easy imageColorMatch node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `easy imageColorMatch` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### easy imageColorMatch 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `Image Color Match` (图像色彩匹配)\n* **节点类名:** `easy imageColorMatch`\n* **所属扩展:** `ComfyUI-Easy-Use`\n* **节点类别:** `EasyUse/Image`\n* **扩展 GitHub 地址:** [https://github.com/yolain/ComfyUI-Easy-Use](https://github.com/yolain/ComfyUI-Easy-Use)\n\n#### 2. 工作原理与功能描述\n\n`easy imageColorMatch` 节点的主要功能是**在参考图像和目标图像之间进行色彩匹配,采用多种方法,以增强图像处理任务中的视觉一致性。**\n\n简而言之,这个节点允许您将一张图像(目标图像)的颜色风格调整为与另一张图像(参考图像)的颜色风格相匹配。这在许多场景中都非常有用,例如:\n\n* **统一多张图像的色彩风格:** 当您有多张来自不同来源或在不同光照下拍摄的图像时,可以使用此节点使它们的颜色看起来更协调一致。\n* **图像风格迁移:** 将一张图像的特定色彩情绪或调色板应用到另一张图像上。\n* **修复颜色偏差:** 调整颜色有偏差的图像,使其色彩更自然或符合预期。\n\n它通过分析参考图像的颜色分布(例如,平均颜色、色调、饱和度、亮度等),然后将这些颜色特性应用到目标图像上,从而实现颜色匹配。\n\n#### 3. 使用指南与操作建议\n\n该节点通常需要两个主要的图像输入:\n\n* **参考图像 (Reference Image):** 这张图像的颜色风格将作为目标图像调整的依据。它定义了您希望目标图像具有的色彩调性。\n* **目标图像 (Target Image):** 这张图像的颜色将被修改,以匹配参考图像的颜色风格。\n\n**a. 核心参数 (推测,基于通用色彩匹配节点功能):**\n\n虽然 `explain_node` 的返回结果中没有详细列出参数,但通常这类节点会包含以下关键参数来控制色彩匹配的方式:\n\n* **`method` (匹配方法):** 这是最重要的参数,用于选择颜色匹配的算法。常见的选项可能包括:\n * **`Mean and Std` (均值和标准差匹配):** 调整目标图像的每个颜色通道的平均值和标准差,使其与参考图像匹配。这是一种比较基础但有效的方法。\n * **`Histogram Matching` (直方图匹配):** 尝试使目标图像的颜色直方图与参考图像的颜色直方图匹配。这种方法通常能产生更精细的颜色匹配效果。\n * **`LAB Color Space` (LAB 色彩空间匹配):** 在 LAB 色彩空间中进行匹配,可以更好地分离亮度和颜色信息,有时能提供更自然的匹配。\n * **`Adain` (自适应实例归一化):** 一种更先进的风格迁移方法,通常用于将图像的风格(包括颜色和纹理)从一张图像迁移到另一张。\n* **`adjust_brightness` / `preserve_luminance` (调整亮度/保留亮度):** 可能允许您在进行颜色匹配时是否也调整亮度,或者尝试保留目标图像原有的亮度信息。\n* **`color_space` (色彩空间):** 选择在哪个色彩空间进行匹配(例如 RGB, LAB, HSV)。不同的色彩空间对颜色的处理方式不同,可能会影响最终效果。\n\n**b. 使用提示:**\n\n* **选择合适的参考图像:** 参考图像的质量和颜色风格直接影响最终的匹配效果。选择一张具有您期望颜色风格的清晰图像作为参考。\n* **尝试不同的匹配方法:** 没有一种方法适用于所有情况。根据您的图像内容和期望效果,尝试不同的 `method` 选项。\n* **观察细节:** 颜色匹配可能会对图像的某些区域产生不自然的影响,尤其是在颜色差异很大的图像之间进行匹配时。仔细观察匹配后的图像,必要时进行微调或尝试其他方法。\n* **与其他图像处理节点结合:** `easy imageColorMatch` 可以与其他节点(如图像去噪、锐化、调色等)结合使用,以创建更复杂的图像处理工作流。\n\n#### 4. 在工作流中的位置\n\n`easy imageColorMatch` 节点通常放置在图像加载之后,以及任何需要统一色彩风格的图像处理操作之前。\n\n**示例工作流片段(概念性):**\n\n```\n[加载参考图像节点] ----->\n |\n[加载目标图像节点] -----> [easy imageColorMatch] -----> [后续图像处理/保存节点]\n```\n\n#### 总结\n\n`easy imageColorMatch` 节点是 `ComfyUI-Easy-Use` 扩展中一个非常实用的工具,它通过灵活的色彩匹配方法,帮助您轻松地在不同图像之间实现视觉色彩一致性,是图像后期处理和风格统一的强大助手。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerMask: BiRefNetUltraV2 node work? I need its official usage guide.'}] -2025-12-19 10:53:05 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 7 messages -2025-12-19 10:53:05 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-19 10:53:05 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-19 10:53:09 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-19 10:53:09 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"LayerMask: BiRefNetUltraV2\",\"nodes_class_name\":\"LayerMask: BiRefNetUltraV2\",\"nodes_extension\":\"ComfyUI_LayerStyle_Advance\",\"nodes_category\":\"__dzNodes/LayerMask\",\"nodes_description\":{\"EN\":\"The LayerMask: BiRefNetUltraV2 node is designed to provide advanced image processing capabilities by leveraging the BiRefNet model.\"},\"extension_github_url\":\"https://github.com/chflame163/ComfyUI_LayerStyle_Advance\",\"nodes_tips\":{\"EN\":\"* Ensure that the selected model is compatible with your specific task requirements to achieve optimal results. * Experiment with different models to find the one that best suits your artistic style and the complexity of the images you are working with.\"},\"nodes_debug_tips\":{\"EN\":\"Model not found: Verify that the model file is correctly placed in the designated directory and that its name matches the expected input. Incompatible model format: Ensure that the model is in the ONNX format and compatible with the BiRefNet architecture.\"}},\"error\":null}","annotations":null,"meta":null} -2025-12-19 10:53:09 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-19 10:53:09 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-19 10:53:19 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-19 10:53:19 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-19 10:53:19 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 2170 chars, ext: False, finished: True -2025-12-22 07:40:02 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-30 06:44:13 | INFO | llm_api.py:list_models:36 | Received list_models request -2025-12-30 06:44:15 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-30 06:44:15 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 28 -2025-12-30 06:44:17 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-30 06:44:17 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-30 06:44:17 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-30 06:44:17 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-30 06:44:17 | INFO | conversation_api.py:invoke_chat:272 | -- Received 33 historical messages -2025-12-30 06:44:17 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 28 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:44:17 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 28, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-30 06:44:17 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 33 -2025-12-30 06:44:17 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:44:17 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 33 -2025-12-30 06:44:17 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 25, summary length: 1127 -2025-12-30 06:44:17 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 8 (threshold: 8) -2025-12-30 06:44:17 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-30 06:44:17 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 9, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation continued with a detailed explanation and usage guide for the `ConditioningZeroOut` node in Chinese, following a workflow validation. The user inquired about viewing its output, to which the assistant clarified that `CONDITIONING` output isn't directly viewable, suggesting indirect assessment via `KSampler` or understanding its internal mechanism. The assistant also offered workflow debugging. An inquiry for `SetNode` found no results, prompting the assistant to ask for more context.\n\nSubsequently, the user inquired about the `DifferentialDiffusion` node. The assistant identified it as an undocumented, `_for_testing` node, requesting more context. The user then made multiple inquiries about the `FaceDetector` node. The assistant repeatedly stated it could not find an official node by that name, suggesting it might be a custom, misnamed, or integrated function within other nodes (e.g., ControlNet preprocessors, face detailers). The assistant outlined common methods for face detection and requested specific user goals. The latest user query was about how to view the positive conditioning output."}, {'role': 'assistant', 'content': '正面条件(Positive Conditioning)的输出并非是图片,也不是可以直接“查看”的视觉内容。它是一种复杂的、由模型(如 CLIP)编码的内部数据结构,包含了对图像生成方向的文本描述信息。\n\n您不能直接像查看图片一样去“看”正面条件的输出。它的作用是作为 `KSampler` 等生成模型的输入,指导模型生成符合这些条件描述的图像。\n\n要“查看”正面条件输出的效果,您需要:\n\n1. **将其连接到 `KSampler` 节点:** 这是最直接的方法。将正面条件连接到 `KSampler` 的 `positive` 输入端。\n2. **生成图像并观察效果:** 运行工作流生成一张图片,然后通过观察生成的图片是否符合您在正面条件中输入的文本描述来判断其效果。\n3. **对比实验:**\n * 尝试使用不同的正面提示词生成图片,对比它们之间的差异。\n * 在其他条件相同的情况下,移除或更改正面条件,看看对生成结果的影响。\n\n有一些特殊的调试节点(通常是第三方插件提供的)可能能够“可视化”条件数据的一些内部特征(例如将其转换为某种表示形式或显示其维度信息),但这并不是直接查看其“内容”,更多的是为了高级调试或理解。\n\n如果您想更深入地理解某个特定节点(例如生成正面条件的 CLIP Text Encode 节点)的输出,或者对工作流中正面条件的使用有疑问,请告诉我,我可以帮助您分析工作流。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerUtility: LoadJoyCaption2Model node work? I need its official usage guide.'}, {'role': 'assistant', 'content': "好的,关于 `LayerUtility: LoadJoyCaption2Model` 节点的工作原理和使用指南,我已经为您找到了详细信息。\n\n### LayerUtility: LoadJoyCaption2Model 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerUtility: LoadJoyCaption2Model`\n* **节点类名:** `LayerUtility: LoadJoyCaption2Model`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerUtility`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerUtility: LoadJoyCaption2Model` 节点主要用于**加载和配置先进的语言模型,以生成详细的图像描述(captions)**。\n\n这个节点的核心功能是帮助 AI 艺术家利用复杂的语言模型来为图像创建具有丰富上下文的描述。简而言之,它**负责将一个预训练的图像描述(captioning)模型加载到 ComfyUI 工作流中**,使得后续的节点可以使用这个模型来分析图像并生成相应的文本描述。\n\n它通过集成高级的语言模型技术,能够更好地理解图像内容,并生成比传统图像描述更精准、更具表现力的文本。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 加载 `JoyCaption2` 或类似的图像描述模型。\n* 为后续的图像理解、自动化提示词生成或图像分析任务提供支持。\n\n**b. 参数说明(推测,通常此类加载节点会有模型选择和精度设置):**\n\n* **`llm_model` (语言模型):** 选择要加载的特定图像描述模型。不同的模型可能在描述风格、细节丰富度或处理特定类型图像的能力上有所不同。\n* **`dtype` (数据类型/精度):** 设置模型加载时的精度(例如 `fp16`、`bf16`、`fp32`)。通常,较低的精度(如 `fp16`)可以减少内存占用并提高运行速度,但可能会略微影响描述的准确性;较高的精度(如 `fp32`)则提供更准确的结果,但需要更多的 VRAM 和更长的处理时间。\n\n**c. 使用提示:**\n\n* **GPU 兼容性:** “确保您的系统安装了兼容的 NVIDIA GPU,以实现更快的处理速度。” 这表明该模型可能需要 GPU 加速才能高效运行。\n* **模型选择:** “尝试使用不同的 'llm_model' 选项来适应您的艺术风格。” 这意味着您可以通过切换不同的模型来获取不同风格或重点的图像描述。\n* **精度权衡:** “在选择 'dtype' 时,请权衡精度和性能。” 根据您的硬件条件和对描述准确性的要求,选择合适的精度设置。\n\n**d. 调试提示:**\n\n* 目前没有提供具体的调试提示,但通常加载模型时常见的调试点包括:\n * **模型文件路径是否正确:** 确保模型文件已放置在扩展指定的正确目录下。\n * **内存不足:** 如果出现内存错误,尝试降低 `dtype` 精度或清理 VRAM。\n * **兼容性问题:** 检查 ComfyUI 版本和扩展版本是否兼容。\n\n#### 4. 在工作流中的位置\n\n该节点通常会出现在工作流的**前端**或**图像分析部分**。它负责加载模型,然后将加载好的模型对象传递给下一个节点,例如一个 `Captioner` 或 `Image to Text` 类型的节点,后者会实际使用这个模型来生成图像描述。\n\n**示例工作流片段(概念性):**\n\n```\n[图片加载节点] -> [LayerUtility: LoadJoyCaption2Model] -> [Captioning处理节点] -> [文本输出节点]\n```\n\n#### 总结\n\n`LayerUtility: LoadJoyCaption2Model` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个关键的组件,用于在 ComfyUI 环境中启用先进的图像描述功能。通过正确配置和连接,它可以帮助您为图像自动生成高质量的文本描述。"}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the easy imageColorMatch node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `easy imageColorMatch` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### easy imageColorMatch 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `Image Color Match` (图像色彩匹配)\n* **节点类名:** `easy imageColorMatch`\n* **所属扩展:** `ComfyUI-Easy-Use`\n* **节点类别:** `EasyUse/Image`\n* **扩展 GitHub 地址:** [https://github.com/yolain/ComfyUI-Easy-Use](https://github.com/yolain/ComfyUI-Easy-Use)\n\n#### 2. 工作原理与功能描述\n\n`easy imageColorMatch` 节点的主要功能是**在参考图像和目标图像之间进行色彩匹配,采用多种方法,以增强图像处理任务中的视觉一致性。**\n\n简而言之,这个节点允许您将一张图像(目标图像)的颜色风格调整为与另一张图像(参考图像)的颜色风格相匹配。这在许多场景中都非常有用,例如:\n\n* **统一多张图像的色彩风格:** 当您有多张来自不同来源或在不同光照下拍摄的图像时,可以使用此节点使它们的颜色看起来更协调一致。\n* **图像风格迁移:** 将一张图像的特定色彩情绪或调色板应用到另一张图像上。\n* **修复颜色偏差:** 调整颜色有偏差的图像,使其色彩更自然或符合预期。\n\n它通过分析参考图像的颜色分布(例如,平均颜色、色调、饱和度、亮度等),然后将这些颜色特性应用到目标图像上,从而实现颜色匹配。\n\n#### 3. 使用指南与操作建议\n\n该节点通常需要两个主要的图像输入:\n\n* **参考图像 (Reference Image):** 这张图像的颜色风格将作为目标图像调整的依据。它定义了您希望目标图像具有的色彩调性。\n* **目标图像 (Target Image):** 这张图像的颜色将被修改,以匹配参考图像的颜色风格。\n\n**a. 核心参数 (推测,基于通用色彩匹配节点功能):**\n\n虽然 `explain_node` 的返回结果中没有详细列出参数,但通常这类节点会包含以下关键参数来控制色彩匹配的方式:\n\n* **`method` (匹配方法):** 这是最重要的参数,用于选择颜色匹配的算法。常见的选项可能包括:\n * **`Mean and Std` (均值和标准差匹配):** 调整目标图像的每个颜色通道的平均值和标准差,使其与参考图像匹配。这是一种比较基础但有效的方法。\n * **`Histogram Matching` (直方图匹配):** 尝试使目标图像的颜色直方图与参考图像的颜色直方图匹配。这种方法通常能产生更精细的颜色匹配效果。\n * **`LAB Color Space` (LAB 色彩空间匹配):** 在 LAB 色彩空间中进行匹配,可以更好地分离亮度和颜色信息,有时能提供更自然的匹配。\n * **`Adain` (自适应实例归一化):** 一种更先进的风格迁移方法,通常用于将图像的风格(包括颜色和纹理)从一张图像迁移到另一张。\n* **`adjust_brightness` / `preserve_luminance` (调整亮度/保留亮度):** 可能允许您在进行颜色匹配时是否也调整亮度,或者尝试保留目标图像原有的亮度信息。\n* **`color_space` (色彩空间):** 选择在哪个色彩空间进行匹配(例如 RGB, LAB, HSV)。不同的色彩空间对颜色的处理方式不同,可能会影响最终效果。\n\n**b. 使用提示:**\n\n* **选择合适的参考图像:** 参考图像的质量和颜色风格直接影响最终的匹配效果。选择一张具有您期望颜色风格的清晰图像作为参考。\n* **尝试不同的匹配方法:** 没有一种方法适用于所有情况。根据您的图像内容和期望效果,尝试不同的 `method` 选项。\n* **观察细节:** 颜色匹配可能会对图像的某些区域产生不自然的影响,尤其是在颜色差异很大的图像之间进行匹配时。仔细观察匹配后的图像,必要时进行微调或尝试其他方法。\n* **与其他图像处理节点结合:** `easy imageColorMatch` 可以与其他节点(如图像去噪、锐化、调色等)结合使用,以创建更复杂的图像处理工作流。\n\n#### 4. 在工作流中的位置\n\n`easy imageColorMatch` 节点通常放置在图像加载之后,以及任何需要统一色彩风格的图像处理操作之前。\n\n**示例工作流片段(概念性):**\n\n```\n[加载参考图像节点] ----->\n |\n[加载目标图像节点] -----> [easy imageColorMatch] -----> [后续图像处理/保存节点]\n```\n\n#### 总结\n\n`easy imageColorMatch` 节点是 `ComfyUI-Easy-Use` 扩展中一个非常实用的工具,它通过灵活的色彩匹配方法,帮助您轻松地在不同图像之间实现视觉色彩一致性,是图像后期处理和风格统一的强大助手。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerMask: BiRefNetUltraV2 node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `LayerMask: BiRefNetUltraV2` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### LayerMask: BiRefNetUltraV2 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerMask: BiRefNetUltraV2`\n* **节点类名:** `LayerMask: BiRefNetUltraV2`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerMask`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerMask: BiRefNetUltraV2` 节点旨在通过利用 **BiRefNet 模型**,提供先进的图像处理能力。\n\n简而言之,这个节点是一个**图像分割(Segmentation)或抠图(Masking)的工具**。它集成了一个名为 BiRefNet 的深度学习模型。BiRefNet 模型通常用于图像的精确前景/背景分割,尤其擅长处理复杂的图像内容,生成高质量的遮罩(mask)。\n\n当您将图像输入到此节点时,它会利用内部加载的 BiRefNet 模型对图像进行分析,并输出一个二进制遮罩,该遮罩精确地 delineates 出图像中某个特定对象或区域的轮廓。这个遮罩可以用于后续的图层操作、局部编辑或其他图像合成任务。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 从图像中自动生成高精度的前景/背景遮罩。\n* 实现精确的图像分割和抠图,以便进行图层分离、背景替换、局部调整等操作。\n\n**b. 参数说明:**\n\n根据现有信息,`explain_node` 工具没有详细列出此节点的具体输入参数。但根据其功能和 BiRefNet 模型的一般用法,我们可以推断出其核心输入和可能的输出:\n\n* **输入:**\n * **`image` (图像):** 需要进行分割或抠图的原始图像。\n * **`model` (模型):** 通常这类节点会有一个参数用于选择或指定要使用的 BiRefNet 模型版本或检查点。\n* **输出:**\n * **`mask` (遮罩):** 一个黑白图像,其中白色区域表示前景(或感兴趣区域),黑色区域表示背景。\n\n**c. 使用提示:**\n\n* **模型兼容性:** “确保所选模型与您的特定任务要求兼容,以实现最佳结果。” 这意味着您可能需要根据需要处理的图像类型(例如人像、物体、通用场景)选择合适的 BiRefNet 模型。不同的模型可能在训练数据和泛化能力上有所差异。\n* **尝试不同模型:** “尝试使用不同的模型,以找到最适合您的艺术风格和您正在处理的图像复杂度的模型。” 鼓励用户探索不同的模型选项,以获得最佳的遮罩效果。\n* **输入图像质量:** 输入图像的质量会直接影响遮罩的生成精度。清晰、对比度适中的图像通常能产生更好的遮罩。\n\n#### 4. 调试提示\n\n* **模型未找到:** “验证模型文件是否已正确放置在指定目录中,并且其名称与预期输入匹配。” 如果节点报错“模型未找到”,请检查您的 ComfyUI_LayerStyle_Advance 扩展的模型文件夹,确保 BiRefNet 模型文件(通常是 `onnx` 格式)存在且命名正确。\n* **模型格式不兼容:** “确保模型是 ONNX 格式,并且与 BiRefNet 架构兼容。” BiRefNet 模型通常以 ONNX 格式发布。如果模型文件格式不正确或与节点期望的 BiRefNet 架构不符,节点将无法加载和使用该模型。\n\n#### 5. 在工作流中的位置\n\n`LayerMask: BiRefNetUltraV2` 节点通常放置在图像加载之后,以及任何需要进行精确图层分离或遮罩应用的节点之前。它会接收原始图像作为输入,并输出一个遮罩,该遮罩可以作为另一个节点的输入,例如 `Apply Mask`、`Image Composite` 或其他图层混合节点。\n\n**示例工作流片段(概念性):**\n\n```\n[加载图像节点] -----> [LayerMask: BiRefNetUltraV2] -----> [遮罩应用/合成节点] -----> [后续处理/保存节点]\n```\n\n#### 总结\n\n`LayerMask: BiRefNetUltraV2` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个强大的工具,用于借助 BiRefNet 模型进行高级图像分割和遮罩生成。通过正确配置和应用,它能帮助您精确地分离图像元素,为复杂的图像编辑和合成任务提供基础。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}] -2025-12-30 06:44:25 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 9 messages -2025-12-30 06:44:25 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-30 06:44:25 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-30 06:44:31 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-30 06:44:32 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"Scale Image to Total Pixels\",\"nodes_class_name\":\"ImageScaleToTotalPixels\",\"nodes_extension\":\"Comfy\",\"nodes_category\":\"image/upscaling\",\"nodes_description\":{\"EN\":\"This node is designed to upscale an image to a specified total pixel count, allowing for various upscale methods. It calculates the necessary dimensions based on the desired megapixels and applies the chosen upscale method to achieve the target resolution.\"},\"extension_github_url\":\"git@github.com:comfyanonymous/ComfyUI\",\"nodes_tips\":{},\"nodes_debug_tips\":{\"EN\":\"{\\\"_eval_score\\\": \\\"40/40\\\", \\\"_has_unknown\\\": false, \\\"_error\\\": null}\"}},\"error\":null}","annotations":null,"meta":null} -2025-12-30 06:44:32 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-30 06:44:32 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-30 06:44:39 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-30 06:44:39 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-30 06:44:39 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 2135 chars, ext: False, finished: True -2025-12-30 06:44:51 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-30 06:44:51 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 29 -2025-12-30 06:44:51 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-30 06:44:51 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-30 06:44:51 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-30 06:44:51 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-30 06:44:51 | INFO | conversation_api.py:invoke_chat:272 | -- Received 34 historical messages -2025-12-30 06:44:51 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 29 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:44:51 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 29, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-30 06:44:51 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 34 -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 34 -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 25, summary length: 1127 -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 9 (threshold: 8) -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:91 | [Memory] Compression triggered! -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:98 | [Memory] Compressing messages from index 25 to 30 -2025-12-30 06:44:51 | INFO | message_memory.py:message_memory_optimize:99 | [Memory] Messages to compress: 5 -2025-12-30 06:44:51 | INFO | summary_agent.py:generate_summary:100 | Generating summary with model: gemini-2.5-flash -2025-12-30 06:45:01 | INFO | summary_agent.py:generate_summary:132 | Generated summary: The conversation began with an explanation of `ConditioningZeroOut` and clarification that `CONDITIO... -2025-12-30 06:45:01 | INFO | message_memory.py:message_memory_optimize:107 | [Memory] Generated new summary: The conversation began with an explanation of `ConditioningZeroOut` and clarification that `CONDITIO... -2025-12-30 06:45:01 | INFO | message_memory.py:message_memory_optimize:116 | [Memory] Updated index to: 30 -2025-12-30 06:45:01 | INFO | message_memory.py:message_memory_optimize:120 | [Memory] Returning 5 optimized messages -2025-12-30 06:45:01 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 5, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation began with an explanation of `ConditioningZeroOut` and clarification that `CONDITIONING` output isn't directly viewable, suggesting indirect assessment via `KSampler`. Inquiries about `SetNode` and `DifferentialDiffusion` led to requests for more context, with the latter identified as a testing node. Multiple queries for `FaceDetector` received repeated responses that no official node was found, suggesting custom or integrated functions, and common detection methods were outlined.\n\nFollowing this, the assistant clarified that positive conditioning output is complex internal data, not directly viewable. Its effect is assessed by connecting it to `KSampler` to generate images and comparing results, with debug nodes potentially offering internal visualization.\nThe user then inquired about the `LayerUtility: LoadJoyCaption2Model` node. The assistant explained it's from `ComfyUI_LayerStyle_Advance`, loading advanced language models (e.g., `JoyCaption2`) for detailed image captioning, with `llm_model` and `dtype` as key parameters.\nSubsequently, the user asked for `easy imageColorMatch`. The assistant described it as a `ComfyUI-Easy-Use` node that performs color matching between reference and target images for visual consistency. It uses various methods (e.g., Mean/Std, Histogram) and parameters like `method` and `color_space`."}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerMask: BiRefNetUltraV2 node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `LayerMask: BiRefNetUltraV2` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### LayerMask: BiRefNetUltraV2 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerMask: BiRefNetUltraV2`\n* **节点类名:** `LayerMask: BiRefNetUltraV2`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerMask`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerMask: BiRefNetUltraV2` 节点旨在通过利用 **BiRefNet 模型**,提供先进的图像处理能力。\n\n简而言之,这个节点是一个**图像分割(Segmentation)或抠图(Masking)的工具**。它集成了一个名为 BiRefNet 的深度学习模型。BiRefNet 模型通常用于图像的精确前景/背景分割,尤其擅长处理复杂的图像内容,生成高质量的遮罩(mask)。\n\n当您将图像输入到此节点时,它会利用内部加载的 BiRefNet 模型对图像进行分析,并输出一个二进制遮罩,该遮罩精确地 delineates 出图像中某个特定对象或区域的轮廓。这个遮罩可以用于后续的图层操作、局部编辑或其他图像合成任务。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 从图像中自动生成高精度的前景/背景遮罩。\n* 实现精确的图像分割和抠图,以便进行图层分离、背景替换、局部调整等操作。\n\n**b. 参数说明:**\n\n根据现有信息,`explain_node` 工具没有详细列出此节点的具体输入参数。但根据其功能和 BiRefNet 模型的一般用法,我们可以推断出其核心输入和可能的输出:\n\n* **输入:**\n * **`image` (图像):** 需要进行分割或抠图的原始图像。\n * **`model` (模型):** 通常这类节点会有一个参数用于选择或指定要使用的 BiRefNet 模型版本或检查点。\n* **输出:**\n * **`mask` (遮罩):** 一个黑白图像,其中白色区域表示前景(或感兴趣区域),黑色区域表示背景。\n\n**c. 使用提示:**\n\n* **模型兼容性:** “确保所选模型与您的特定任务要求兼容,以实现最佳结果。” 这意味着您可能需要根据需要处理的图像类型(例如人像、物体、通用场景)选择合适的 BiRefNet 模型。不同的模型可能在训练数据和泛化能力上有所差异。\n* **尝试不同模型:** “尝试使用不同的模型,以找到最适合您的艺术风格和您正在处理的图像复杂度的模型。” 鼓励用户探索不同的模型选项,以获得最佳的遮罩效果。\n* **输入图像质量:** 输入图像的质量会直接影响遮罩的生成精度。清晰、对比度适中的图像通常能产生更好的遮罩。\n\n#### 4. 调试提示\n\n* **模型未找到:** “验证模型文件是否已正确放置在指定目录中,并且其名称与预期输入匹配。” 如果节点报错“模型未找到”,请检查您的 ComfyUI_LayerStyle_Advance 扩展的模型文件夹,确保 BiRefNet 模型文件(通常是 `onnx` 格式)存在且命名正确。\n* **模型格式不兼容:** “确保模型是 ONNX 格式,并且与 BiRefNet 架构兼容。” BiRefNet 模型通常以 ONNX 格式发布。如果模型文件格式不正确或与节点期望的 BiRefNet 架构不符,节点将无法加载和使用该模型。\n\n#### 5. 在工作流中的位置\n\n`LayerMask: BiRefNetUltraV2` 节点通常放置在图像加载之后,以及任何需要进行精确图层分离或遮罩应用的节点之前。它会接收原始图像作为输入,并输出一个遮罩,该遮罩可以作为另一个节点的输入,例如 `Apply Mask`、`Image Composite` 或其他图层混合节点。\n\n**示例工作流片段(概念性):**\n\n```\n[加载图像节点] -----> [LayerMask: BiRefNetUltraV2] -----> [遮罩应用/合成节点] -----> [后续处理/保存节点]\n```\n\n#### 总结\n\n`LayerMask: BiRefNetUltraV2` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个强大的工具,用于借助 BiRefNet 模型进行高级图像分割和遮罩生成。通过正确配置和应用,它能帮助您精确地分离图像元素,为复杂的图像编辑和合成任务提供基础。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}] -2025-12-30 06:45:05 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 5 messages -2025-12-30 06:45:05 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-30 06:45:05 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-30 06:45:10 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-30 06:45:10 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"Scale Image to Total Pixels\",\"nodes_class_name\":\"ImageScaleToTotalPixels\",\"nodes_extension\":\"Comfy\",\"nodes_category\":\"image/upscaling\",\"nodes_description\":{\"EN\":\"This node is designed to upscale an image to a specified total pixel count, allowing for various upscale methods. It calculates the necessary dimensions based on the desired megapixels and applies the chosen upscale method to achieve the target resolution.\"},\"extension_github_url\":\"git@github.com:comfyanonymous/ComfyUI\",\"nodes_tips\":{},\"nodes_debug_tips\":{\"EN\":\"{\\\"_eval_score\\\": \\\"40/40\\\", \\\"_has_unknown\\\": false, \\\"_error\\\": null}\"}},\"error\":null}","annotations":null,"meta":null} -2025-12-30 06:45:10 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-30 06:45:10 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-30 06:45:20 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-30 06:45:20 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-30 06:45:20 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 2338 chars, ext: False, finished: True -2025-12-30 06:45:54 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-30 06:45:54 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 30 -2025-12-30 06:45:54 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-30 06:45:54 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-30 06:45:54 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-30 06:45:54 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-30 06:45:54 | INFO | conversation_api.py:invoke_chat:272 | -- Received 36 historical messages -2025-12-30 06:45:54 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 30 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:45:54 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 30, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-30 06:45:54 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 36 -2025-12-30 06:45:54 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:45:54 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 36 -2025-12-30 06:45:54 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 30, summary length: 1360 -2025-12-30 06:45:54 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 6 (threshold: 8) -2025-12-30 06:45:54 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-30 06:45:54 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 7, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation began with an explanation of `ConditioningZeroOut` and clarification that `CONDITIONING` output isn't directly viewable, suggesting indirect assessment via `KSampler`. Inquiries about `SetNode` and `DifferentialDiffusion` led to requests for more context, with the latter identified as a testing node. Multiple queries for `FaceDetector` received repeated responses that no official node was found, suggesting custom or integrated functions, and common detection methods were outlined.\n\nFollowing this, the assistant clarified that positive conditioning output is complex internal data, not directly viewable. Its effect is assessed by connecting it to `KSampler` to generate images and comparing results, with debug nodes potentially offering internal visualization.\nThe user then inquired about the `LayerUtility: LoadJoyCaption2Model` node. The assistant explained it's from `ComfyUI_LayerStyle_Advance`, loading advanced language models (e.g., `JoyCaption2`) for detailed image captioning, with `llm_model` and `dtype` as key parameters.\nSubsequently, the user asked for `easy imageColorMatch`. The assistant described it as a `ComfyUI-Easy-Use` node that performs color matching between reference and target images for visual consistency. It uses various methods (e.g., Mean/Std, Histogram) and parameters like `method` and `color_space`."}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerMask: BiRefNetUltraV2 node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `LayerMask: BiRefNetUltraV2` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### LayerMask: BiRefNetUltraV2 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerMask: BiRefNetUltraV2`\n* **节点类名:** `LayerMask: BiRefNetUltraV2`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerMask`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerMask: BiRefNetUltraV2` 节点旨在通过利用 **BiRefNet 模型**,提供先进的图像处理能力。\n\n简而言之,这个节点是一个**图像分割(Segmentation)或抠图(Masking)的工具**。它集成了一个名为 BiRefNet 的深度学习模型。BiRefNet 模型通常用于图像的精确前景/背景分割,尤其擅长处理复杂的图像内容,生成高质量的遮罩(mask)。\n\n当您将图像输入到此节点时,它会利用内部加载的 BiRefNet 模型对图像进行分析,并输出一个二进制遮罩,该遮罩精确地 delineates 出图像中某个特定对象或区域的轮廓。这个遮罩可以用于后续的图层操作、局部编辑或其他图像合成任务。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 从图像中自动生成高精度的前景/背景遮罩。\n* 实现精确的图像分割和抠图,以便进行图层分离、背景替换、局部调整等操作。\n\n**b. 参数说明:**\n\n根据现有信息,`explain_node` 工具没有详细列出此节点的具体输入参数。但根据其功能和 BiRefNet 模型的一般用法,我们可以推断出其核心输入和可能的输出:\n\n* **输入:**\n * **`image` (图像):** 需要进行分割或抠图的原始图像。\n * **`model` (模型):** 通常这类节点会有一个参数用于选择或指定要使用的 BiRefNet 模型版本或检查点。\n* **输出:**\n * **`mask` (遮罩):** 一个黑白图像,其中白色区域表示前景(或感兴趣区域),黑色区域表示背景。\n\n**c. 使用提示:**\n\n* **模型兼容性:** “确保所选模型与您的特定任务要求兼容,以实现最佳结果。” 这意味着您可能需要根据需要处理的图像类型(例如人像、物体、通用场景)选择合适的 BiRefNet 模型。不同的模型可能在训练数据和泛化能力上有所差异。\n* **尝试不同模型:** “尝试使用不同的模型,以找到最适合您的艺术风格和您正在处理的图像复杂度的模型。” 鼓励用户探索不同的模型选项,以获得最佳的遮罩效果。\n* **输入图像质量:** 输入图像的质量会直接影响遮罩的生成精度。清晰、对比度适中的图像通常能产生更好的遮罩。\n\n#### 4. 调试提示\n\n* **模型未找到:** “验证模型文件是否已正确放置在指定目录中,并且其名称与预期输入匹配。” 如果节点报错“模型未找到”,请检查您的 ComfyUI_LayerStyle_Advance 扩展的模型文件夹,确保 BiRefNet 模型文件(通常是 `onnx` 格式)存在且命名正确。\n* **模型格式不兼容:** “确保模型是 ONNX 格式,并且与 BiRefNet 架构兼容。” BiRefNet 模型通常以 ONNX 格式发布。如果模型文件格式不正确或与节点期望的 BiRefNet 架构不符,节点将无法加载和使用该模型。\n\n#### 5. 在工作流中的位置\n\n`LayerMask: BiRefNetUltraV2` 节点通常放置在图像加载之后,以及任何需要进行精确图层分离或遮罩应用的节点之前。它会接收原始图像作为输入,并输出一个遮罩,该遮罩可以作为另一个节点的输入,例如 `Apply Mask`、`Image Composite` 或其他图层混合节点。\n\n**示例工作流片段(概念性):**\n\n```\n[加载图像节点] -----> [LayerMask: BiRefNetUltraV2] -----> [遮罩应用/合成节点] -----> [后续处理/保存节点]\n```\n\n#### 总结\n\n`LayerMask: BiRefNetUltraV2` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个强大的工具,用于借助 BiRefNet 模型进行高级图像分割和遮罩生成。通过正确配置和应用,它能帮助您精确地分离图像元素,为复杂的图像编辑和合成任务提供基础。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `ImageScaleToTotalPixels` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### ImageScaleToTotalPixels 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `Scale Image to Total Pixels` (将图像缩放至总像素数)\n* **节点类名:** `ImageScaleToTotalPixels`\n* **所属扩展:** `ComfyUI` (核心节点)\n* **节点类别:** `image/upscaling` (图像/放大)\n* **扩展 GitHub 地址:** [https://github.com/comfyanonymous/ComfyUI](https://github.com/comfyanonymous/ComfyUI)\n\n#### 2. 工作原理与功能描述\n\n`ImageScaleToTotalPixels` 节点旨在将图像放大(upscale)到指定的总像素数量。\n\n**核心功能:**\n它允许用户设定一个目标总像素数(通常以百万像素,即 MP 为单位),然后根据这个目标计算出图像所需的新尺寸(宽度和高度),并应用所选的放大方法来达到目标分辨率。\n\n**具体流程:**\n\n1. **输入图像:** 接收一张原始图像。\n2. **设定目标像素:** 用户通过参数输入期望的图像总像素数。例如,如果设定为 2MP,则表示最终图像的总像素数将接近 2,000,000 像素。\n3. **计算新尺寸:** 节点会根据原始图像的宽高比和目标总像素数,智能地计算出新的宽度和高度,以保持图像比例不变。\n4. **应用放大方法:** 节点提供多种图像放大(插值)算法供选择,例如最近邻、双线性、双三次、Lanczos 等。用户可以选择最适合其需求的方法。\n5. **输出结果:** 输出一张经过放大处理,且总像素数达到或接近目标值的图像。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* **统一输出分辨率:** 当您需要确保所有输出图像都具有大致相同的总像素数时非常有用,例如在批量处理或为特定平台准备图像时。\n* **控制文件大小:** 通过控制总像素数,可以间接控制图像的文件大小(尽管压缩算法也会影响)。\n* **放大图像以提高细节:** 在保持宽高比的同时,将图像放大到更高分辨率,为后续的细节增强或修复节点(如 ESRGAN、RealESRGAN)做准备。\n\n**b. 参数说明 (根据功能推断):**\n\n虽然 `explain_node` 工具没有详细列出此节点的具体输入参数名称,但根据其功能描述,我们可以推断出其主要参数:\n\n* **`image` (图像):** 必选输入,您需要处理的原始图像。\n* **`total_pixels` (总像素数 / Megapixels):** 用户期望的输出图像的总像素数,通常以百万像素(MP)为单位。例如,输入 `2` 可能表示 2 百万像素。\n* **`upscale_method` (放大方法):** 用于图像插值的算法选择。常见的选项可能包括:\n * `bilinear` (双线性)\n * `bicubic` (双三次)\n * `nearest-neighbour` (最近邻)\n * `lanczos` (Lanczos)\n * 某些高级放大模型(如果节点集成了)。\n* **`keep_ratio` (保持比例):** 通常这类节点会有一个选项来决定是否保持原始图像的宽高比。`ImageScaleToTotalPixels` 节点的设计就是为了保持比例进行计算。\n\n**c. 使用提示:**\n\n* **理解总像素数:** 1 百万像素 (MP) = 1,000,000 像素。例如,一张 1024x1024 像素的图像总像素数约为 1.05MP。如果您想将图像放大到类似 4K 屏幕的尺寸(约 8.3MP),则可以设置相应的 `total_pixels` 值。\n* **选择合适的放大方法:** 不同的插值方法会在放大图像时产生不同的视觉效果。\n * **`nearest-neighbour`** 速度最快,但会产生锯齿状边缘。\n * **`bilinear`** 和 **`bicubic`** 提供更平滑的过渡,其中 `bicubic` 通常在细节保留和平滑度之间取得更好的平衡。\n * **`lanczos`** 通常能提供锐利的边缘和较好的细节保留,但可能引入少量振铃效应。\n* **与高级放大器结合:** 这个节点常用于在将图像传递给如 `Upscale Model Loader` 和 `Image Upscale With Model` 等更强大的超分辨率模型之前,进行初步的尺寸调整。\n\n#### 4. 在工作流中的位置\n\n`ImageScaleToTotalPixels` 节点通常放置在图像加载或生成之后,以及任何需要更高分辨率图像作为输入的节点之前。它接收图像并输出一个尺寸经过调整的新图像。\n\n**示例工作流片段(概念性):**\n\n```\n[图像加载/生成节点] -----> [ImageScaleToTotalPixels (设定目标MP, 选择放大方法)] -----> [超分辨率模型/细节增强节点] -----> [保存/预览节点]\n```'}, {'role': 'user', 'content': "Reply in zh-CN language: Show me the technical specifications for the GetNode node's inputs and outputs."}] -2025-12-30 06:46:01 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2025-12-30 06:46:01 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 31 -2025-12-30 06:46:01 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2025-12-30 06:46:01 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: a10a9b0ea1bc... -2025-12-30 06:46:01 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2025-12-30 06:46:01 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2025-12-30 06:46:01 | INFO | conversation_api.py:invoke_chat:272 | -- Received 37 historical messages -2025-12-30 06:46:01 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 31 for session bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:46:01 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': 'bb8e7985-667a-4ba4-b1de-dabcc12ab4b7', 'workflow_checkpoint_id': 31, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2025-12-30 06:46:01 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 37 -2025-12-30 06:46:01 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: bb8e7985-667a-4ba4-b1de-dabcc12ab4b7 -2025-12-30 06:46:01 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 37 -2025-12-30 06:46:01 | INFO | message_memory.py:message_memory_optimize:71 | [Memory] Existing session - index: 30, summary length: 1360 -2025-12-30 06:46:01 | INFO | message_memory.py:message_memory_optimize:83 | [Memory] Uncompressed messages: 7 (threshold: 8) -2025-12-30 06:46:01 | INFO | message_memory.py:message_memory_optimize:87 | [Memory] No compression needed -2025-12-30 06:46:01 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 8, messages: [{'role': 'user', 'content': "[Context from previous conversation]: The conversation began with an explanation of `ConditioningZeroOut` and clarification that `CONDITIONING` output isn't directly viewable, suggesting indirect assessment via `KSampler`. Inquiries about `SetNode` and `DifferentialDiffusion` led to requests for more context, with the latter identified as a testing node. Multiple queries for `FaceDetector` received repeated responses that no official node was found, suggesting custom or integrated functions, and common detection methods were outlined.\n\nFollowing this, the assistant clarified that positive conditioning output is complex internal data, not directly viewable. Its effect is assessed by connecting it to `KSampler` to generate images and comparing results, with debug nodes potentially offering internal visualization.\nThe user then inquired about the `LayerUtility: LoadJoyCaption2Model` node. The assistant explained it's from `ComfyUI_LayerStyle_Advance`, loading advanced language models (e.g., `JoyCaption2`) for detailed image captioning, with `llm_model` and `dtype` as key parameters.\nSubsequently, the user asked for `easy imageColorMatch`. The assistant described it as a `ComfyUI-Easy-Use` node that performs color matching between reference and target images for visual consistency. It uses various methods (e.g., Mean/Std, Histogram) and parameters like `method` and `color_space`."}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the LayerMask: BiRefNetUltraV2 node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `LayerMask: BiRefNetUltraV2` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### LayerMask: BiRefNetUltraV2 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `LayerMask: BiRefNetUltraV2`\n* **节点类名:** `LayerMask: BiRefNetUltraV2`\n* **所属扩展:** `ComfyUI_LayerStyle_Advance`\n* **节点类别:** `__dzNodes/LayerMask`\n* **扩展 GitHub 地址:** [https://github.com/chflame163/ComfyUI_LayerStyle_Advance](https://github.com/chflame163/ComfyUI_LayerStyle_Advance)\n\n#### 2. 工作原理与功能描述\n\n`LayerMask: BiRefNetUltraV2` 节点旨在通过利用 **BiRefNet 模型**,提供先进的图像处理能力。\n\n简而言之,这个节点是一个**图像分割(Segmentation)或抠图(Masking)的工具**。它集成了一个名为 BiRefNet 的深度学习模型。BiRefNet 模型通常用于图像的精确前景/背景分割,尤其擅长处理复杂的图像内容,生成高质量的遮罩(mask)。\n\n当您将图像输入到此节点时,它会利用内部加载的 BiRefNet 模型对图像进行分析,并输出一个二进制遮罩,该遮罩精确地 delineates 出图像中某个特定对象或区域的轮廓。这个遮罩可以用于后续的图层操作、局部编辑或其他图像合成任务。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* 从图像中自动生成高精度的前景/背景遮罩。\n* 实现精确的图像分割和抠图,以便进行图层分离、背景替换、局部调整等操作。\n\n**b. 参数说明:**\n\n根据现有信息,`explain_node` 工具没有详细列出此节点的具体输入参数。但根据其功能和 BiRefNet 模型的一般用法,我们可以推断出其核心输入和可能的输出:\n\n* **输入:**\n * **`image` (图像):** 需要进行分割或抠图的原始图像。\n * **`model` (模型):** 通常这类节点会有一个参数用于选择或指定要使用的 BiRefNet 模型版本或检查点。\n* **输出:**\n * **`mask` (遮罩):** 一个黑白图像,其中白色区域表示前景(或感兴趣区域),黑色区域表示背景。\n\n**c. 使用提示:**\n\n* **模型兼容性:** “确保所选模型与您的特定任务要求兼容,以实现最佳结果。” 这意味着您可能需要根据需要处理的图像类型(例如人像、物体、通用场景)选择合适的 BiRefNet 模型。不同的模型可能在训练数据和泛化能力上有所差异。\n* **尝试不同模型:** “尝试使用不同的模型,以找到最适合您的艺术风格和您正在处理的图像复杂度的模型。” 鼓励用户探索不同的模型选项,以获得最佳的遮罩效果。\n* **输入图像质量:** 输入图像的质量会直接影响遮罩的生成精度。清晰、对比度适中的图像通常能产生更好的遮罩。\n\n#### 4. 调试提示\n\n* **模型未找到:** “验证模型文件是否已正确放置在指定目录中,并且其名称与预期输入匹配。” 如果节点报错“模型未找到”,请检查您的 ComfyUI_LayerStyle_Advance 扩展的模型文件夹,确保 BiRefNet 模型文件(通常是 `onnx` 格式)存在且命名正确。\n* **模型格式不兼容:** “确保模型是 ONNX 格式,并且与 BiRefNet 架构兼容。” BiRefNet 模型通常以 ONNX 格式发布。如果模型文件格式不正确或与节点期望的 BiRefNet 架构不符,节点将无法加载和使用该模型。\n\n#### 5. 在工作流中的位置\n\n`LayerMask: BiRefNetUltraV2` 节点通常放置在图像加载之后,以及任何需要进行精确图层分离或遮罩应用的节点之前。它会接收原始图像作为输入,并输出一个遮罩,该遮罩可以作为另一个节点的输入,例如 `Apply Mask`、`Image Composite` 或其他图层混合节点。\n\n**示例工作流片段(概念性):**\n\n```\n[加载图像节点] -----> [LayerMask: BiRefNetUltraV2] -----> [遮罩应用/合成节点] -----> [后续处理/保存节点]\n```\n\n#### 总结\n\n`LayerMask: BiRefNetUltraV2` 节点是 `ComfyUI_LayerStyle_Advance` 扩展中一个强大的工具,用于借助 BiRefNet 模型进行高级图像分割和遮罩生成。通过正确配置和应用,它能帮助您精确地分离图像元素,为复杂的图像编辑和合成任务提供基础。'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}, {'role': 'user', 'content': 'Reply in zh-CN language: How does the ImageScaleToTotalPixels node work? I need its official usage guide.'}, {'role': 'assistant', 'content': '好的,关于 `ImageScaleToTotalPixels` 节点的工作原理和官方使用指南,我已经为您找到详细信息。\n\n### ImageScaleToTotalPixels 节点工作原理及官方使用指南\n\n#### 1. 节点名称与归属\n\n* **节点名称:** `Scale Image to Total Pixels` (将图像缩放至总像素数)\n* **节点类名:** `ImageScaleToTotalPixels`\n* **所属扩展:** `ComfyUI` (核心节点)\n* **节点类别:** `image/upscaling` (图像/放大)\n* **扩展 GitHub 地址:** [https://github.com/comfyanonymous/ComfyUI](https://github.com/comfyanonymous/ComfyUI)\n\n#### 2. 工作原理与功能描述\n\n`ImageScaleToTotalPixels` 节点旨在将图像放大(upscale)到指定的总像素数量。\n\n**核心功能:**\n它允许用户设定一个目标总像素数(通常以百万像素,即 MP 为单位),然后根据这个目标计算出图像所需的新尺寸(宽度和高度),并应用所选的放大方法来达到目标分辨率。\n\n**具体流程:**\n\n1. **输入图像:** 接收一张原始图像。\n2. **设定目标像素:** 用户通过参数输入期望的图像总像素数。例如,如果设定为 2MP,则表示最终图像的总像素数将接近 2,000,000 像素。\n3. **计算新尺寸:** 节点会根据原始图像的宽高比和目标总像素数,智能地计算出新的宽度和高度,以保持图像比例不变。\n4. **应用放大方法:** 节点提供多种图像放大(插值)算法供选择,例如最近邻、双线性、双三次、Lanczos 等。用户可以选择最适合其需求的方法。\n5. **输出结果:** 输出一张经过放大处理,且总像素数达到或接近目标值的图像。\n\n#### 3. 使用指南与操作建议\n\n**a. 核心用途:**\n\n* **统一输出分辨率:** 当您需要确保所有输出图像都具有大致相同的总像素数时非常有用,例如在批量处理或为特定平台准备图像时。\n* **控制文件大小:** 通过控制总像素数,可以间接控制图像的文件大小(尽管压缩算法也会影响)。\n* **放大图像以提高细节:** 在保持宽高比的同时,将图像放大到更高分辨率,为后续的细节增强或修复节点(如 ESRGAN、RealESRGAN)做准备。\n\n**b. 参数说明 (根据功能推断):**\n\n虽然 `explain_node` 工具没有详细列出此节点的具体输入参数名称,但根据其功能描述,我们可以推断出其主要参数:\n\n* **`image` (图像):** 必选输入,您需要处理的原始图像。\n* **`total_pixels` (总像素数 / Megapixels):** 用户期望的输出图像的总像素数,通常以百万像素(MP)为单位。例如,输入 `2` 可能表示 2 百万像素。\n* **`upscale_method` (放大方法):** 用于图像插值的算法选择。常见的选项可能包括:\n * `bilinear` (双线性)\n * `bicubic` (双三次)\n * `nearest-neighbour` (最近邻)\n * `lanczos` (Lanczos)\n * 某些高级放大模型(如果节点集成了)。\n* **`keep_ratio` (保持比例):** 通常这类节点会有一个选项来决定是否保持原始图像的宽高比。`ImageScaleToTotalPixels` 节点的设计就是为了保持比例进行计算。\n\n**c. 使用提示:**\n\n* **理解总像素数:** 1 百万像素 (MP) = 1,000,000 像素。例如,一张 1024x1024 像素的图像总像素数约为 1.05MP。如果您想将图像放大到类似 4K 屏幕的尺寸(约 8.3MP),则可以设置相应的 `total_pixels` 值。\n* **选择合适的放大方法:** 不同的插值方法会在放大图像时产生不同的视觉效果。\n * **`nearest-neighbour`** 速度最快,但会产生锯齿状边缘。\n * **`bilinear`** 和 **`bicubic`** 提供更平滑的过渡,其中 `bicubic` 通常在细节保留和平滑度之间取得更好的平衡。\n * **`lanczos`** 通常能提供锐利的边缘和较好的细节保留,但可能引入少量振铃效应。\n* **与高级放大器结合:** 这个节点常用于在将图像传递给如 `Upscale Model Loader` 和 `Image Upscale With Model` 等更强大的超分辨率模型之前,进行初步的尺寸调整。\n\n#### 4. 在工作流中的位置\n\n`ImageScaleToTotalPixels` 节点通常放置在图像加载或生成之后,以及任何需要更高分辨率图像作为输入的节点之前。它接收图像并输出一个尺寸经过调整的新图像。\n\n**示例工作流片段(概念性):**\n\n```\n[图像加载/生成节点] -----> [ImageScaleToTotalPixels (设定目标MP, 选择放大方法)] -----> [超分辨率模型/细节增强节点] -----> [保存/预览节点]\n```'}, {'role': 'user', 'content': "Reply in zh-CN language: Show me the technical specifications for the GetNode node's inputs and outputs."}, {'role': 'user', 'content': "Reply in zh-CN language: Show me the technical specifications for the ImageScaleToTotalPixels node's inputs and outputs."}] -2025-12-30 06:46:06 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 8 messages -2025-12-30 06:46:06 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2025-12-30 06:46:06 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2025-12-30 06:46:11 | INFO | mcp_client.py:process_stream_events:377 | -- Tool 'explain_node' was called -2025-12-30 06:46:11 | INFO | mcp_client.py:process_stream_events:383 | -- Tool output: {"type":"text","text":"{\"data\":{\"nodes_name\":\"ImageScaleToTotalPixels\",\"nodes_class_name\":\"ImageScaleToTotalPixels\",\"nodes_extension\":\"ComfyUI\",\"nodes_category\":\"image/upscaling\",\"nodes_description\":{\"EN\":\"The ImageScaleToTotalPixels node is designed for resizing images to a specified total number of pixels while maintaining the aspect ratio. It provides various methods for upscaling the image to achieve the desired pixel count.\",\"ZH\":\"\"},\"nodes_inputs\":[{\"desc\":\"The input image to be upscaled to the specified total number of pixels.\",\"name\":\"image\",\"Comfy_dtype\":\"IMAGE\",\"is_required\":\"True\",\"python_dtype\":\"torch.Tensor\"},{\"desc\":\"The method used for upscaling the image. It affects the quality and characteristics of the upscaled image.\",\"name\":\"upscale_method\",\"Comfy_dtype\":\"COMBO[STRING]\",\"is_required\":\"True\",\"python_dtype\":\"str\"},{\"desc\":\"The target size of the image in megapixels. This determines the total number of pixels in the upscaled image.\",\"name\":\"megapixels\",\"Comfy_dtype\":\"FLOAT\",\"is_required\":\"True\",\"python_dtype\":\"float\"}],\"nodes_outputs\":[{\"desc\":\"The upscaled image with the specified total number of pixels, maintaining the original aspect ratio.\",\"name\":\"image\",\"Comfy_dtype\":\"IMAGE\",\"python_dtype\":\"torch.Tensor\"}],\"extension_github_url\":\"https://github.com/comfyanonymous/ComfyUI\"},\"error\":null}","annotations":null,"meta":null} -2025-12-30 06:46:11 | INFO | mcp_client.py:process_stream_events:390 | -- Associating output with tool 'explain_node' -2025-12-30 06:46:11 | INFO | mcp_client.py:process_stream_events:428 | -- Stored result for tool 'explain_node': data=8, ext=None -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 1 -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:533 | Tool: explain_node -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:534 | - Has data: True -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:535 | - Data length: 8 -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:536 | - Has ext: False -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:539 | - Answer preview: None... -2025-12-30 06:46:15 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2025-12-30 06:46:15 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-30 06:46:15 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 1365 chars, ext: False, finished: True -2025-12-30 06:46:30 | ERROR | mcp_client.py:comfyui_agent_invoke:684 | Error in comfyui_agent_invoke: unhandled errors in a TaskGroup (1 sub-exception) -2025-12-30 06:46:30 | ERROR | mcp_client.py:comfyui_agent_invoke:685 | Traceback: + Exception Group Traceback (most recent call last): - | File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 143, in comfyui_agent_invoke - | async with mcp_server, bing_server: - | File "/data/envs/Cheny/lib/python3.11/site-packages/agents/mcp/server.py", line 242, in __aenter__ - | await self.connect() - | File "/data/envs/Cheny/lib/python3.11/site-packages/agents/mcp/server.py", line 267, in connect - | transport = await self.exit_stack.enter_async_context(self.create_streams()) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 650, in enter_async_context - | result = await _enter(cm) - | ^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 210, in __aenter__ - | return await anext(self.gen) - | ^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/mcp/client/sse.py", line 55, in sse_client - | async with anyio.create_task_group() as tg: - | File "/data/envs/Cheny/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__ - | raise BaseExceptionGroup( - | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) - +-+---------------- 1 ---------------- - | Traceback (most recent call last): - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions - | yield - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 394, in handle_async_request - | resp = await self._pool.handle_async_request(req) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 256, in handle_async_request - | raise exc from None - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 236, in handle_async_request - | response = await connection.handle_async_request( - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http_proxy.py", line 343, in handle_async_request - | return await self._connection.handle_async_request(request) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http11.py", line 136, in handle_async_request - | raise exc - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http11.py", line 106, in handle_async_request - | ) = await self._receive_response_headers(**kwargs) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http11.py", line 177, in _receive_response_headers - | event = await self._receive_event(timeout=timeout) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpcore/_async/http11.py", line 231, in _receive_event - | raise RemoteProtocolError(msg) - | httpcore.RemoteProtocolError: Server disconnected without sending a response. - | - | The above exception was the direct cause of the following exception: - | - | Traceback (most recent call last): - | File "/data/envs/Cheny/lib/python3.11/site-packages/mcp/client/sse.py", line 61, in sse_client - | async with aconnect_sse( - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 210, in __aenter__ - | return await anext(self.gen) - | ^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx_sse/_api.py", line 74, in aconnect_sse - | async with client.stream(method, url, headers=headers, **kwargs) as response: - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 210, in __aenter__ - | return await anext(self.gen) - | ^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1583, in stream - | response = await self.send( - | ^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1629, in send - | response = await self._send_handling_auth( - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1657, in _send_handling_auth - | response = await self._send_handling_redirects( - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1694, in _send_handling_redirects - | response = await self._send_single_request(request) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_client.py", line 1730, in _send_single_request - | response = await transport.handle_async_request(request) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 393, in handle_async_request - | with map_httpcore_exceptions(): - | File "/data/envs/Cheny/lib/python3.11/contextlib.py", line 158, in __exit__ - | self.gen.throw(typ, value, traceback) - | File "/data/envs/Cheny/lib/python3.11/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions - | raise mapped_exc(message) from exc - | httpx.RemoteProtocolError: Server disconnected without sending a response. - +------------------------------------ - -2025-12-30 06:46:30 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2025-12-30 06:46:30 | ERROR | conversation_api.py:invoke_chat:351 | Error in invoke_chat: Cannot write to closing transport -2026-01-07 09:42:23 | INFO | llm_api.py:list_models:36 | Received list_models request -2026-01-07 09:42:26 | INFO | conversation_api.py:save_workflow_checkpoint:371 | Received save-workflow-checkpoint request -2026-01-07 09:42:26 | INFO | conversation_api.py:save_workflow_checkpoint:410 | Workflow checkpoint saved with version ID: 32 -2026-01-07 09:42:26 | INFO | conversation_api.py:invoke_chat:222 | Received invoke_chat request -2026-01-07 09:42:26 | INFO | auth_utils.py:extract_and_store_api_key:27 | ComfyUI Copilot API key extracted and stored: null... -2026-01-07 09:42:26 | INFO | auth_utils.py:extract_and_store_api_key:32 | API key verification: Successfully stored in globals -2026-01-07 09:42:26 | INFO | conversation_api.py:invoke_chat:228 | Request JSON: -2026-01-07 09:42:26 | INFO | conversation_api.py:invoke_chat:272 | -- Received 1 historical messages -2026-01-07 09:42:26 | INFO | conversation_api.py:invoke_chat:276 | Using workflow checkpoint ID: 32 for session 94e20361-661f-4600-bda8-e2b6bdc7aeee -2026-01-07 09:42:26 | INFO | conversation_api.py:invoke_chat:295 | config: {'session_id': '94e20361-661f-4600-bda8-e2b6bdc7aeee', 'workflow_checkpoint_id': 32, 'openai_api_key': '', 'openai_base_url': None, 'workflow_llm_api_key': None, 'workflow_llm_base_url': None, 'workflow_llm_model': 'us.anthropic.claude-sonnet-4-20250514-v1:0', 'model_select': 'gemini-2.5-flash'} -2026-01-07 09:42:26 | INFO | mcp_client.py:comfyui_agent_invoke:116 | [MCP] Original messages count: 1 -2026-01-07 09:42:26 | INFO | message_memory.py:message_memory_optimize:49 | [Memory] Starting memory optimization for session: 94e20361-661f-4600-bda8-e2b6bdc7aeee -2026-01-07 09:42:26 | INFO | message_memory.py:message_memory_optimize:50 | [Memory] Input messages count: 1 -2026-01-07 09:42:26 | INFO | message_memory.py:message_memory_optimize:57 | [Memory] New session detected, creating record -2026-01-07 09:42:26 | INFO | mcp_client.py:comfyui_agent_invoke:118 | [MCP] Optimized messages count: 1, messages: [{'role': 'user', 'content': 'Reply in zh-CN language: How does the FaceAlign node work? I need its official usage guide.'}] -2026-01-07 09:43:01 | INFO | mcp_client.py:comfyui_agent_invoke:301 | -- Processing 1 messages -2026-01-07 09:43:01 | INFO | mcp_client.py:comfyui_agent_invoke:314 | === MCP Agent Run starting === -2026-01-07 09:43:01 | INFO | mcp_client.py:process_stream_events:354 | Handoff to: ComfyUI-Copilot -2026-01-07 09:43:03 | ERROR | mcp_client.py:process_stream_events:453 | Unexpected streaming error: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2026-01-07 09:43:03 | ERROR | mcp_client.py:process_stream_events:454 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2026-01-07 09:43:03 | ERROR | mcp_client.py:comfyui_agent_invoke:503 | Non-retryable streaming error or max retries reached: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. -2026-01-07 09:43:03 | ERROR | mcp_client.py:comfyui_agent_invoke:504 | Traceback: Traceback (most recent call last): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 460, in comfyui_agent_invoke - async for stream_data in process_stream_events(result): - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 455, in process_stream_events - raise e - File "/data/chenyan/ComfyUI/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py", line 338, in process_stream_events - async for event in stream_result.stream_events(): - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/result.py", line 347, in stream_events - raise self._stored_exception - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1145, in _start_streaming - turn_result = await cls._run_single_turn_streamed( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/run.py", line 1370, in _run_single_turn_streamed - async for event in model.stream_response( - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 159, in stream_response - response, stream = await self._fetch_response( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/agents/models/openai_chatcompletions.py", line 293, in _fetch_response - ret = await self._get_client().chat.completions.create( - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 2672, in create - return await self._post( - ^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1794, in post - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/data/envs/Cheny/lib/python3.11/site-packages/openai/_base_client.py", line 1594, in request - raise self._make_status_error_from_response(err.response) from None -openai.AuthenticationError: Error code: 401 - You are not authorized, please click ⚙️ to send email, and get the Key from the email to configure your Key. - -2026-01-07 09:43:03 | INFO | mcp_client.py:comfyui_agent_invoke:530 | Total tool results: 0 -2026-01-07 09:43:03 | INFO | mcp_client.py:comfyui_agent_invoke:540 | === End Tool Results Summary === - -2026-01-07 09:43:03 | INFO | conversation_api.py:invoke_chat:310 | -- Received ext data: None, finished: True -2026-01-07 09:43:03 | INFO | conversation_api.py:invoke_chat:335 | -- Sending final response: 0 chars, ext: False, finished: True -2026-01-13 02:24:35 | INFO | llm_api.py:list_models:36 | Received list_models request diff --git a/custom_nodes/ComfyUI-Copilot/backend/service/__init__.py b/custom_nodes/ComfyUI-Copilot/backend/service/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/custom_nodes/ComfyUI-Copilot/backend/service/link_agent_tools.py b/custom_nodes/ComfyUI-Copilot/backend/service/link_agent_tools.py deleted file mode 100644 index aa734b08a19f93fd607ec51eac6471d47ac1b416..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/service/link_agent_tools.py +++ /dev/null @@ -1,467 +0,0 @@ -# Link Agent Tools for ComfyUI Workflow Connection Analysis - -import json -import time -from typing import Dict, Optional, List - -from agents.tool import function_tool -from ..utils.request_context import get_session_id -from ..dao.workflow_table import get_workflow_data, save_workflow_data -from ..utils.comfy_gateway import get_object_info -from ..utils.logger import log - -@function_tool -async def analyze_missing_connections() -> str: - """ - 分析工作流中缺失的连接,枚举所有可能的连接选项和所需的新节点。 - - 返回格式说明: - - missing_connections: 缺失连接的详细列表,包含节点ID、输入名称、需要的数据类型等(仅包含required输入) - - possible_connections: 现有节点可以提供的连接选项 - - universal_inputs: 可以接受任意输出类型的通用输入端口 - - optional_unconnected_inputs: 未连接的可选输入列表,包含节点ID、输入名称、配置信息等 - - required_new_nodes: 需要创建的新节点类型列表 - - connection_summary: 连接分析的统计摘要(包含optional输入的统计) - """ - try: - session_id = get_session_id() - if not session_id: - log.error("analyze_missing_connections: No session_id found in context") - return json.dumps({"error": "No session_id found in context"}) - - workflow_data = get_workflow_data(session_id) - if not workflow_data: - return json.dumps({"error": "No workflow data found for this session"}) - - object_info = await get_object_info() - - analysis_result = { - "missing_connections": [], - "possible_connections": [], - "universal_inputs": [], # 可以连接任意输出的输入端口 - "optional_unconnected_inputs": [], # 未连接的optional输入 - "required_new_nodes": [], - "connection_summary": { - "total_missing": 0, - "auto_fixable": 0, - "requires_new_nodes": 0, - "universal_inputs_count": 0, - "optional_unconnected_count": 0 - } - } - - # 构建现有节点的输出映射 - available_outputs = {} - for node_id, node_data in workflow_data.items(): - node_class = node_data.get("class_type") - if node_class in object_info and "output" in object_info[node_class]: - outputs = object_info[node_class]["output"] - available_outputs[node_id] = { - "class_type": node_class, - "outputs": [(i, output_type) for i, output_type in enumerate(outputs)] - } - - # 分析每个节点的缺失连接 - for node_id, node_data in workflow_data.items(): - node_class = node_data.get("class_type") - if node_class not in object_info: - continue - - node_info = object_info[node_class] - if "input" not in node_info: - continue - - required_inputs = node_info["input"].get("required", {}) - current_inputs = node_data.get("inputs", {}) - - # 检查每个required input - for input_name, input_config in required_inputs.items(): - if input_name not in current_inputs: - # 发现缺失的连接 - missing_connection = { - "node_id": node_id, - "node_class": node_class, - "input_name": input_name, - "input_config": input_config, - "required": True - } - - # 分析输入类型 - if isinstance(input_config, (list, tuple)) and len(input_config) > 0: - expected_types = input_config[0] if isinstance(input_config[0], list) else [input_config[0]] - - missing_connection["expected_types"] = expected_types - - # 检查是否是通用输入(可以接受任意类型) - is_universal_input = "*" in expected_types - - if is_universal_input: - # 这是一个通用输入端口,可以连接任意输出 - universal_input = { - "node_id": node_id, - "node_class": node_class, - "input_name": input_name, - "input_config": input_config, - "can_connect_any_output": True - } - analysis_result["universal_inputs"].append(universal_input) - analysis_result["connection_summary"]["universal_inputs_count"] += 1 - analysis_result["connection_summary"]["auto_fixable"] += 1 - - # 对于通用输入,我们不列出所有可能的连接,而是标记为通用 - missing_connection["possible_matches"] = "universal" - missing_connection["is_universal"] = True - else: - # 查找具体类型匹配的连接 - possible_matches = [] - for source_node_id, source_info in available_outputs.items(): - if source_node_id == node_id: # 不能连接自己 - continue - - for output_index, output_type in source_info["outputs"]: - # 检查类型匹配(排除通用类型的情况) - type_match = (output_type in expected_types or output_type == "*") - - if type_match: - possible_matches.append({ - "source_node_id": source_node_id, - "source_class": source_info["class_type"], - "output_index": output_index, - "output_type": output_type, - "match_confidence": "high" if output_type in expected_types else "medium" - }) - - missing_connection["possible_matches"] = possible_matches - missing_connection["is_universal"] = False - - # 如果有可能的匹配,添加到possible_connections - if possible_matches: - analysis_result["possible_connections"].extend([ - { - "target_node_id": node_id, - "target_input": input_name, - "source_node_id": match["source_node_id"], - "source_output_index": match["output_index"], - "connection": [match["source_node_id"], match["output_index"]], - "confidence": match["match_confidence"], - "types": { - "expected": expected_types, - "provided": match["output_type"] - } - } for match in possible_matches - ]) - analysis_result["connection_summary"]["auto_fixable"] += 1 - else: - # 没有匹配的输出,需要新节点 - analysis_result["connection_summary"]["requires_new_nodes"] += 1 - - # 分析需要什么类型的节点 - required_node_types = analyze_required_node_types(expected_types, object_info) - analysis_result["required_new_nodes"].extend([{ - "for_node": node_id, - "for_input": input_name, - "expected_types": expected_types, - "suggested_node_types": required_node_types - }]) - - analysis_result["missing_connections"].append(missing_connection) - - # 检查optional inputs (未连接的可选输入) - optional_inputs = node_info["input"].get("optional", {}) - for input_name, input_config in optional_inputs.items(): - if input_name not in current_inputs: - # 发现未连接的optional输入 - optional_unconnected = { - "node_id": node_id, - "node_class": node_class, - "input_name": input_name, - "input_config": input_config, - "required": False - } - - # 分析输入类型 - if isinstance(input_config, (list, tuple)) and len(input_config) > 0: - expected_types = input_config[0] if isinstance(input_config[0], list) else [input_config[0]] - else: - expected_types = ["*"] # 默认为通用类型 - - optional_unconnected["expected_types"] = expected_types - - # 检查是否是通用输入(可以接受任意类型) - is_universal_input = "*" in expected_types - optional_unconnected["is_universal"] = is_universal_input - - analysis_result["optional_unconnected_inputs"].append(optional_unconnected) - - # 更新统计信息 - analysis_result["connection_summary"]["total_missing"] = len(analysis_result["missing_connections"]) - analysis_result["connection_summary"]["optional_unconnected_count"] = len(analysis_result["optional_unconnected_inputs"]) - - log.info(f"analysis_result: {json.dumps(analysis_result, ensure_ascii=False)}") - return json.dumps(analysis_result) - - except Exception as e: - return json.dumps({"error": f"Failed to analyze missing connections: {str(e)}"}) - -def analyze_required_node_types(expected_types: List[str], object_info: Dict) -> List[Dict]: - """分析需要什么类型的节点来提供指定的输出类型""" - suggested_nodes = [] - - # 常见类型到节点的映射 - type_to_nodes = { - "MODEL": ["CheckpointLoaderSimple", "CheckpointLoader", "UNETLoader"], - "CLIP": ["CheckpointLoaderSimple", "CheckpointLoader", "CLIPLoader"], - "VAE": ["CheckpointLoaderSimple", "CheckpointLoader", "VAELoader"], - "CONDITIONING": ["CLIPTextEncode"], - "LATENT": ["EmptyLatentImage", "VAEEncode"], - "IMAGE": ["LoadImage", "VAEDecode"], - "MASK": ["LoadImageMask"], - "CONTROL_NET": ["ControlNetLoader"], - "LORA": ["LoraLoader"], - "IPADAPTER": ["IPAdapterModelLoader"] - } - - for expected_type in expected_types: - if expected_type in type_to_nodes: - for node_class in type_to_nodes[expected_type]: - if node_class in object_info: - suggested_nodes.append({ - "node_class": node_class, - "output_type": expected_type, - "confidence": "high", - "description": f"{node_class} can provide {expected_type}" - }) - else: - # 搜索所有能提供该类型输出的节点 - for node_class, node_info in object_info.items(): - if "output" in node_info: - outputs = node_info["output"] - if expected_type in outputs: - suggested_nodes.append({ - "node_class": node_class, - "output_type": expected_type, - "confidence": "medium", - "description": f"{node_class} can provide {expected_type}" - }) - - # 去重并排序 - unique_nodes = {} - for node in suggested_nodes: - key = node["node_class"] - if key not in unique_nodes or unique_nodes[key]["confidence"] == "medium": - unique_nodes[key] = node - - return list(unique_nodes.values()) - -def save_checkpoint_before_link_modification(session_id: str, action_description: str) -> Optional[int]: - """在连接修改前保存checkpoint""" - try: - current_workflow = get_workflow_data(session_id) - if not current_workflow: - return None - - checkpoint_id = save_workflow_data( - session_id, - current_workflow, - workflow_data_ui=None, - attributes={ - "checkpoint_type": "link_agent_start", - "description": f"Checkpoint before {action_description}", - "action": "link_agent_checkpoint", - "timestamp": time.time() - } - ) - log.info(f"Saved link agent checkpoint with ID: {checkpoint_id}") - return checkpoint_id - except Exception as e: - log.error(f"Failed to save checkpoint before link modification: {str(e)}") - return None - -@function_tool -def apply_connection_fixes(fixes_json: str) -> str: - """批量应用连接修复,fixes_json应为包含修复指令的JSON字符串""" - try: - session_id = get_session_id() - if not session_id: - log.error("apply_connection_fixes: No session_id found in context") - return json.dumps({"error": "No session_id found in context"}) - - # 在修改前保存checkpoint - checkpoint_id = save_checkpoint_before_link_modification(session_id, "batch connection fixes") - - # 解析修复指令 - fixes = json.loads(fixes_json) if isinstance(fixes_json, str) else fixes_json - - workflow_data = get_workflow_data(session_id) - if not workflow_data: - return json.dumps({"error": "No workflow data found for this session"}) - - applied_fixes = [] - failed_fixes = [] - - # 处理连接修复 - connections_to_add = fixes.get("connections", []) - for conn_fix in connections_to_add: - try: - target_node_id = conn_fix["target_node_id"] - target_input = conn_fix["target_input"] - source_node_id = conn_fix["source_node_id"] - source_output_index = conn_fix["source_output_index"] - - if target_node_id not in workflow_data: - failed_fixes.append({ - "type": "connection", - "target": f"{target_node_id}.{target_input}", - "error": f"Target node {target_node_id} not found" - }) - continue - - if source_node_id not in workflow_data: - failed_fixes.append({ - "type": "connection", - "target": f"{target_node_id}.{target_input}", - "error": f"Source node {source_node_id} not found" - }) - continue - - # 应用连接 - if "inputs" not in workflow_data[target_node_id]: - workflow_data[target_node_id]["inputs"] = {} - - old_value = workflow_data[target_node_id]["inputs"].get(target_input, "not connected") - new_connection = [source_node_id, source_output_index] - workflow_data[target_node_id]["inputs"][target_input] = new_connection - - applied_fixes.append({ - "type": "connection", - "target": f"{target_node_id}.{target_input}", - "source": f"{source_node_id}[{source_output_index}]", - "old_value": old_value, - "new_value": new_connection - }) - - except Exception as e: - failed_fixes.append({ - "type": "connection", - "target": f"{conn_fix.get('target_node_id', 'unknown')}.{conn_fix.get('target_input', 'unknown')}", - "error": str(e) - }) - - # 处理新节点添加 - nodes_to_add = fixes.get("new_nodes", []) - for node_spec in nodes_to_add: - try: - node_class = node_spec["node_class"] - node_id = node_spec.get("node_id", "") - inputs = node_spec.get("inputs", {}) - - # 生成节点ID - if not node_id: - existing_ids = set(workflow_data.keys()) - node_id = "1" - while node_id in existing_ids: - node_id = str(int(node_id) + 1) - - # 创建新节点 - new_node = { - "class_type": node_class, - "inputs": inputs, - "_meta": {"title": node_class} - } - - workflow_data[node_id] = new_node - - applied_fixes.append({ - "type": "add_node", - "node_id": node_id, - "node_class": node_class, - "inputs": inputs - }) - - # 如果指定了要自动连接的目标,进行连接 - auto_connect = node_spec.get("auto_connect", []) - for auto_conn in auto_connect: - target_node_id = auto_conn["target_node_id"] - target_input = auto_conn["target_input"] - output_index = auto_conn.get("output_index", 0) - - if target_node_id in workflow_data: - if "inputs" not in workflow_data[target_node_id]: - workflow_data[target_node_id]["inputs"] = {} - - workflow_data[target_node_id]["inputs"][target_input] = [node_id, output_index] - - applied_fixes.append({ - "type": "auto_connection", - "from_new_node": node_id, - "to": f"{target_node_id}.{target_input}", - "output_index": output_index - }) - - except Exception as e: - failed_fixes.append({ - "type": "add_node", - "node_class": node_spec.get("node_class", "unknown"), - "error": str(e) - }) - - # 保存更新的工作流 - version_id = save_workflow_data( - session_id, - workflow_data, - attributes={ - "action": "link_agent_batch_fix", - "description": f"Applied {len(applied_fixes)} connection fixes", - "fixes_applied": applied_fixes, - "fixes_failed": failed_fixes - } - ) - - # 构建返回数据,包含checkpoint信息 - ext_data = [{ - "type": "workflow_update", - "data": { - "workflow_data": workflow_data, - "changes": { - "applied_fixes": applied_fixes, - "failed_fixes": failed_fixes - } - } - }] - - # 如果成功保存了checkpoint,添加修改前的checkpoint信息 - if checkpoint_id: - ext_data.append({ - "type": "link_agent_checkpoint", - "data": { - "checkpoint_id": checkpoint_id, - "checkpoint_type": "link_agent_start" - } - }) - - # 添加修改后的版本信息 - ext_data.append({ - "type": "link_agent_complete", - "data": { - "version_id": version_id, - "checkpoint_type": "link_agent_complete" - } - }) - - return json.dumps({ - "success": True, - "version_id": version_id, - "applied_fixes": applied_fixes, - "failed_fixes": failed_fixes, - "summary": { - "total_fixes": len(applied_fixes), - "failed_fixes": len(failed_fixes), - "connections_added": len([f for f in applied_fixes if f["type"] == "connection"]), - "nodes_added": len([f for f in applied_fixes if f["type"] == "add_node"]) - }, - "message": f"Applied {len(applied_fixes)} fixes successfully, {len(failed_fixes)} failed", - "ext": ext_data - }) - - except Exception as e: - return json.dumps({"error": f"Failed to apply connection fixes: {str(e)}"}) \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py b/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py deleted file mode 100644 index 080fdeddeb3147c3f11f9a2ca9cbc1041cfbba54..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/service/mcp_client.py +++ /dev/null @@ -1,714 +0,0 @@ -''' -Author: ai-business-hql qingli.hql@alibaba-inc.com -Date: 2025-06-16 16:50:17 -LastEditors: ai-business-hql ai.bussiness.hql@gmail.com -LastEditTime: 2025-11-25 15:37:50 -FilePath: /comfyui_copilot/backend/service/mcp-client.py -Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE -''' -from ..service.workflow_rewrite_tools import get_current_workflow -from ..utils.globals import BACKEND_BASE_URL, get_comfyui_copilot_api_key, DISABLE_WORKFLOW_GEN -from .. import core -import asyncio -import os -import traceback -from typing import List, Dict, Any, Optional - -try: - from agents._config import set_default_openai_api - from agents.agent import Agent - from agents.items import ItemHelpers - from agents.mcp import MCPServerSse - from agents.run import Runner - from agents.tracing import set_tracing_disabled - from agents import handoff, RunContextWrapper, HandoffInputData - from agents.extensions import handoff_filters - if not hasattr(__import__('agents'), 'Agent'): - raise ImportError -except Exception: - raise ImportError( - "Detected incorrect or missing 'agents' package while loading MCP components. " - "Please install 'openai-agents' and ensure this plugin prefers it. Commands:\n" - " python -m pip uninstall -y agents gym tensorflow\n" - " python -m pip install -U openai-agents" - ) - -from ..agent_factory import create_agent -from ..service.workflow_rewrite_agent import create_workflow_rewrite_agent -from ..service.message_memory import message_memory_optimize -from ..utils.request_context import get_rewrite_context, get_session_id, get_config -from ..utils.logger import log -from openai.types.responses import ResponseTextDeltaEvent -from openai import APIError, RateLimitError -from pydantic import BaseModel - - -class ImageData: - """Image data structure to match reference implementation""" - def __init__(self, filename: str, data: str, url: str = None): - self.filename = filename - self.data = data # base64 data - self.url = url # uploaded URL - -async def comfyui_agent_invoke(messages: List[Dict[str, Any]], images: List[ImageData] = None): - """ - Invoke the ComfyUI agent with MCP tools and image support. - - This function mimics the behavior of the reference facade.py chat function, - yielding (text, ext) tuples similar to the reference implementation. - - Args: - messages: List of messages in OpenAI format [{"role": "user", "content": "..."}, ...] - images: List of image data objects (optional) - - Yields: - tuple: (text, ext) where text is accumulated text and ext is structured data - """ - try: - def _strip_trailing_whitespace_from_messages( - msgs: List[Dict[str, Any]] - ) -> List[Dict[str, Any]]: - cleaned: List[Dict[str, Any]] = [] - for msg in msgs: - role = msg.get("role") - # Only touch assistant messages to minimize impact - if role != "assistant": - cleaned.append(msg) - continue - - msg_copy = dict(msg) - content = msg_copy.get("content") - - # Simple string content - if isinstance(content, str): - msg_copy["content"] = content.rstrip() - # OpenAI / Agents style list content blocks - elif isinstance(content, list): - new_content = [] - for part in content: - if isinstance(part, dict): - part_copy = dict(part) - # Common text block key is "text" - text_val = part_copy.get("text") - if isinstance(text_val, str): - part_copy["text"] = text_val.rstrip() - new_content.append(part_copy) - else: - new_content.append(part) - msg_copy["content"] = new_content - - cleaned.append(msg_copy) - - return cleaned - - messages = _strip_trailing_whitespace_from_messages(messages) - - # Get session_id and config from request context - session_id = get_session_id() - config = get_config() - - if not session_id: - raise ValueError("No session_id found in request context") - if not config: - raise ValueError("No config found in request context") - - # Optimize messages with memory compression - log.info(f"[MCP] Original messages count: {len(messages)}") - messages = message_memory_optimize(session_id, messages) - log.info(f"[MCP] Optimized messages count: {len(messages)}, messages: {messages}") - - # Create MCP server instances - mcp_server = MCPServerSse( - params= { - "url": BACKEND_BASE_URL + "/mcp-server/mcp", - "timeout": 300.0, - "headers": {"X-Session-Id": session_id, "Authorization": f"Bearer {get_comfyui_copilot_api_key()}"} - }, - cache_tools_list=True, - client_session_timeout_seconds=300.0 - ) - - bing_server = MCPServerSse( - params= { - "url": "https://mcp.api-inference.modelscope.net/8c9fe550938e4f/sse", - "timeout": 300.0, - "headers": {"X-Session-Id": session_id, "Authorization": f"Bearer {get_comfyui_copilot_api_key()}"} - }, - cache_tools_list=True, - client_session_timeout_seconds=300.0 - ) - - server_list = [mcp_server, bing_server] - - async with mcp_server, bing_server: - - # 创建workflow_rewrite_agent实例 (session_id通过context获取) - workflow_rewrite_agent_instance = create_workflow_rewrite_agent() - - class HandoffRewriteData(BaseModel): - latest_rewrite_intent: str - - async def on_handoff(ctx: RunContextWrapper[None], input_data: HandoffRewriteData): - get_rewrite_context().rewrite_intent = input_data.latest_rewrite_intent - log.info(f"Rewrite agent called with intent: {input_data.latest_rewrite_intent}") - - def rewrite_handoff_input_filter(data: HandoffInputData) -> HandoffInputData: - """Filter to replace message history with just the rewrite intent""" - intent = get_rewrite_context().rewrite_intent - log.info(f"Rewrite handoff filter called. Intent: {intent}") - - # Construct a new HandoffInputData with cleared history - # We keep new_items (which contains the handoff tool call) so the agent sees the immediate trigger - # But we clear input_history to remove the conversation context - - new_history = () - try: - # Attempt to find a user message in history to clone/modify - # This is a best-effort attempt to make the agent see the intent as a user message - for item in data.input_history: - # Check if item looks like a user message (has role='user') - if hasattr(item, 'role') and getattr(item, 'role') == 'user': - # Try to create a copy with new content if it's a Pydantic model - if hasattr(item, 'model_copy'): - # Pydantic V2 - new_item = item.model_copy(update={"content": intent}) - new_history = (new_item,) - log.info("Successfully constructed new user message item for handoff (Pydantic V2)") - break - elif hasattr(item, 'copy'): - # Pydantic V1 - new_item = item.copy(update={"content": intent}) - new_history = (new_item,) - log.info("Successfully constructed new user message item for handoff (Pydantic V1)") - break - except Exception as e: - log.warning(f"Failed to construct user message item: {e}") - - # If we couldn't construct a user message, we return empty history. - # The agent will still see the handoff tool call in new_items, which contains the intent. - - return HandoffInputData( - input_history=new_history, - pre_handoff_items=(), # Clear pre-handoff items - new_items=tuple(data.new_items), # Keep the handoff tool call - ) - - handoff_rewrite = handoff( - agent=workflow_rewrite_agent_instance, - input_type=HandoffRewriteData, - input_filter=rewrite_handoff_input_filter, - on_handoff=on_handoff, - ) - - # Construct instructions based on DISABLE_WORKFLOW_GEN - if DISABLE_WORKFLOW_GEN: - workflow_creation_instruction = """ -**CASE 3: SEARCH WORKFLOW** -IF the user wants to find or generate a NEW workflow. -- Keywords: "create", "generate", "search", "find", "recommend", "生成", "查找", "推荐". -- Action: Use `recall_workflow`. -""" - workflow_constraint = """ -- [Critical!] When the user's intent is to get workflows or generate images with specific requirements, you MUST call `recall_workflow` tool to find existing similar workflows. -""" - else: - workflow_creation_instruction = """ -**CASE 3: CREATE NEW / SEARCH WORKFLOW** -IF the user wants to find or generate a NEW workflow from scratch. -- Keywords: "create", "generate", "search", "find", "recommend", "生成", "查找", "推荐". -- Action: Use `recall_workflow` AND `gen_workflow`. -""" - workflow_constraint = """ -- [Critical!] When the user's intent is to get workflows or generate images with specific requirements, you MUST ALWAYS call BOTH recall_workflow tool AND gen_workflow tool to provide comprehensive workflow options. Never call just one of these tools - both are required for complete workflow assistance. First call recall_workflow to find existing similar workflows, then call gen_workflow to generate new workflow options. -""" - - agent = create_agent( - name="ComfyUI-Copilot", - instructions=f"""You are a powerful AI assistant for designing image processing workflows, capable of automating problem-solving using tools and commands. - -When handing off to workflow rewrite agent or other agents, this session ID should be used for workflow data management. - -### PRIMARY DIRECTIVE: INTENT CLASSIFICATION & HANDOFF -You act as a router. Your FIRST step is to classify the user's intent. - -**CASE 1: MODIFY/UPDATE/FIX CURRENT WORKFLOW (HIGHEST PRIORITY)** -IF the user wants to: -- Modify, enhance, update, or fix the CURRENT workflow/canvas. -- Add nodes/features to the CURRENT workflow (e.g., "add LoRA", "add controlnet", "fix the error"). -- Change parameters in the CURRENT workflow. -- Keywords: "modify", "update", "add", "change", "fix", "current", "canvas", "修改", "更新", "添加", "画布", "加一个", "换一个", "调一下". - -**ACTION:** -- You MUST IMMEDIATELY handoff to the `Workflow Rewrite Agent`. -- DO NOT call any other tools (like search_node, gen_workflow). -- DO NOT ask for more details. Just handoff. - -**CASE 2: ANALYZE CURRENT WORKFLOW** -IF the user wants to: -- Analyze, explain, or understand the current workflow structure/logic. -- Ask questions about the current workflow (e.g., "how does this work?", "explain the workflow"). -- Keywords: "analyze", "explain", "understand", "how it works", "workflow structure", "分析", "解释", "怎么工作的", "解读". - -**ACTION:** -- You MUST call `get_current_workflow` to retrieve the workflow details. -- Then, based on the returned workflow data, provide a detailed analysis or explanation to the user. - -{workflow_creation_instruction} - -### CONSTRAINT CHECKLIST -You must adhere to the following constraints to complete the task: - -- [Important!] Respond must in the language used by the user in their question. Regardless of the language returned by the tools being called, please return the results based on the language used in the user's query. For example, if user ask by English, you must return -- Ensure that the commands or tools you invoke are within the provided tool list. -- If the execution of a command or tool fails, try changing the parameters or their format before attempting again. -- Your generated responses must follow the factual information given above. Do not make up information. -- If the result obtained is incorrect, try rephrasing your approach. -- Do not query for already obtained information repeatedly. If you successfully invoked a tool and obtained relevant information, carefully confirm whether you need to invoke it again. -- Ensure that the actions you generate can be executed accurately. Actions may include specific methods and target outputs. -- When you encounter a concept, try to obtain its precise definition and analyze what inputs can yield specific values for it. -- When generating a natural language query, include all known information in the query. -- Before performing any analysis or calculation, ensure that all sub-concepts involved have been defined. -- Printing the entire content of a file is strictly prohibited, as such actions have high costs and can lead to unforeseen consequences. -- Ensure that when you call a tool, you have obtained all the input variables for that tool, and do not fabricate any input values for it. -- Respond with markdown, using a minimum of 3 heading levels (H3, H4, H5...), and when including images use the format ![alt text](url), -{workflow_constraint} -- When the user's intent is to query, return the query result directly without attempting to assist the user in performing operations. -- When the user's intent is to get prompts for image generation (like Stable Diffusion). Use specific descriptive language with proper weight modifiers (e.g., (word:1.2)), prefer English terms, and separate elements with commas. Include quality terms (high quality, detailed), style specifications (realistic, anime), lighting (cinematic, golden hour), and composition (wide shot, close up) as needed. When appropriate, include negative prompts to exclude unwanted elements. Return words divided by commas directly without any additional text. -- If you cannot find the information needed to answer a query, consider using bing_search to obtain relevant information. For example, if search_node tool cannot find the node, you can use bing_search to obtain relevant information about those nodes or components. -- If search_node tool cannot find the node, you MUST use bing_search to obtain relevant information about those nodes or components. - -- **ERROR MESSAGE ANALYSIS** - When a user pastes specific error text/logs (containing terms like "Failed", "Error", "Traceback", or stack traces), prioritize providing troubleshooting help rather than invoking search tools. Follow these steps: - 1. Analyze the error to identify the root cause (error type, affected component, missing dependencies, etc.) - 2. Explain the issue in simple terms - 3. Provide concrete, executable solutions including: - - Specific shell commands to fix the issue (e.g., `git pull`, `pip install`, file path corrections) - - Code snippets if applicable - - Configuration file changes with exact paths and values - 4. If the error relates to a specific ComfyUI extension or node, include instructions for: - - Updating the extension (`cd path/to/extension && git pull`) - - Reinstalling dependencies - - Alternative approaches if the extension is problematic - """, - mcp_servers=server_list, - handoffs=[handoff_rewrite], - tools=[get_current_workflow], - config=config - ) - - # Use messages directly as agent input since they're already in OpenAI format - # The caller has already handled image formatting within messages - agent_input = messages - log.info(f"-- Processing {len(messages)} messages") - - # from agents import Agent, Runner, set_trace_processors, set_tracing_disabled, set_default_openai_api - # from langsmith.wrappers import OpenAIAgentsTracingProcessor - # set_tracing_disabled(False) - # set_default_openai_api("chat_completions") - # set_trace_processors([OpenAIAgentsTracingProcessor()]) - - result = Runner.run_streamed( - agent, - input=agent_input, - max_turns=30, - ) - log.info("=== MCP Agent Run starting ===") - - # Variables to track response state similar to reference facade.py - current_text = '' - ext = None - tool_results = {} # Store results from different tools - workflow_tools_called = set() # Track called workflow tools - last_yield_length = 0 - tool_call_queue = [] # Queue to track tool calls in order - current_tool_call = None # Track current tool being called - # Collect workflow update ext data from tools and message outputs - workflow_update_ext = None - # Track if we've seen any handoffs to avoid showing initial handoff - handoff_occurred = False - - # Enhanced retry mechanism for OpenAI streaming errors - max_retries = 3 - retry_count = 0 - - async def process_stream_events(stream_result): - """Process stream events with enhanced error handling""" - nonlocal current_text, last_yield_length, tool_call_queue, workflow_update_ext, tool_results, workflow_tools_called, handoff_occurred - - try: - async for event in stream_result.stream_events(): - # Handle different event types similar to reference implementation - if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): - # Stream text deltas for real-time response - delta_text = event.data.delta - if delta_text: - current_text += delta_text - # Yield tuple (accumulated_text, None) for streaming - similar to facade.py - # Only yield if we have new content to avoid duplicate yields - if len(current_text) > last_yield_length: - last_yield_length = len(current_text) - yield (current_text, None) - continue - - elif event.type == "agent_updated_stream_event": - new_agent_name = event.new_agent.name - log.info(f"Handoff to: {new_agent_name}") - - # Only show handoff message if we've already seen handoffs - # This prevents showing the initial handoff to ComfyUI-Copilot - if handoff_occurred: - # Add handoff information to the stream - handoff_text = f"\n▸ **Switching to {new_agent_name}**\n\n" - current_text += handoff_text - last_yield_length = len(current_text) - - # Yield text update only - yield (current_text, None) - - # Mark that we've seen a handoff - handoff_occurred = True - continue - - elif event.type == "run_item_stream_event": - if event.item.type == "tool_call_item": - # Get tool name correctly using raw_item.name - tool_name = getattr(event.item.raw_item, 'name', 'unknown_tool') - # Add to queue instead of overwriting current_tool_call - tool_call_queue.append(tool_name) - log.info(f"-- Tool '{tool_name}' was called") - - # Track workflow tools being called - if tool_name in ["recall_workflow", "gen_workflow"]: - workflow_tools_called.add(tool_name) - elif event.item.type == "tool_call_output_item": - log.info(f"-- Tool output: {event.item.output}") - # Store tool output for potential ext data processing - tool_output_data_str = str(event.item.output) - - # Get the next tool from the queue (FIFO) - if tool_call_queue: - tool_name = tool_call_queue.pop(0) - log.info(f"-- Associating output with tool '{tool_name}'") - else: - tool_name = 'unknown_tool' - log.info(f"-- Warning: No tool call in queue for output") - - try: - import json - tool_output_data = json.loads(tool_output_data_str) - if "ext" in tool_output_data and tool_output_data["ext"]: - # Store all ext items from tool output, not just workflow_update - tool_ext_items = tool_output_data["ext"] - for ext_item in tool_ext_items: - if ext_item.get("type") == "workflow_update" or ext_item.get("type") == "param_update": - workflow_update_ext = tool_ext_items # Store all ext items, not just one - log.info(f"-- Captured workflow tool ext from tool output: {len(tool_ext_items)} items") - break - - if "text" in tool_output_data and tool_output_data.get('text'): - parsed_output = json.loads(tool_output_data['text']) - - # Handle case where parsed_output might be a list instead of dict - if isinstance(parsed_output, dict): - answer = parsed_output.get("answer") - data = parsed_output.get("data") - tool_ext = parsed_output.get("ext") - else: - # If it's a list or other type, handle gracefully - answer = None - data = parsed_output if isinstance(parsed_output, list) else None - tool_ext = None - - # Store tool results similar to reference facade.py - tool_results[tool_name] = { - "answer": answer, - "data": data, - "ext": tool_ext, - "content_dict": parsed_output - } - log.info(f"-- Stored result for tool '{tool_name}': data={len(data) if data else 0}, ext={tool_ext}") - - # Track workflow tools that produced results - if tool_name in ["recall_workflow", "gen_workflow"]: - log.info(f"-- Workflow tool '{tool_name}' produced result with data: {len(data) if data else 0}") - - - - except (json.JSONDecodeError, TypeError) as e: - # If not JSON or parsing fails, treat as regular text - log.error(f"-- Failed to parse tool output as JSON: {e}") - log.error(f"-- Traceback: {traceback.format_exc()}") - tool_results[tool_name] = { - "answer": tool_output_data_str, - "data": None, - "ext": None, - "content_dict": None - } - - elif event.item.type == "message_output_item": - pass - else: - pass # Ignore other event types - - except Exception as e: - log.error(f"Unexpected streaming error: {e}") - log.error(f"Traceback: {traceback.format_exc()}") - raise e - - # Implement retry mechanism with exponential backoff - while retry_count <= max_retries: - try: - async for stream_data in process_stream_events(result): - if stream_data: - yield stream_data - # If we get here, streaming completed successfully - break - - except (AttributeError, TypeError, ConnectionError, OSError, APIError) as stream_error: - retry_count += 1 - error_msg = str(stream_error) - - # Check for specific streaming errors that are worth retrying - should_retry = ( - "'NoneType' object has no attribute 'strip'" in error_msg or - "Connection broken" in error_msg or - "InvalidChunkLength" in error_msg or - "socket hang up" in error_msg or - "Connection reset" in error_msg - ) - - if should_retry and retry_count <= max_retries: - wait_time = min(2 ** (retry_count - 1), 10) # Exponential backoff, max 10 seconds - log.error(f"Stream error (attempt {retry_count}/{max_retries}): {error_msg}") - log.info(f"Retrying in {wait_time} seconds...") - - # Yield current progress before retry - if current_text: - yield (current_text, None) - - await asyncio.sleep(wait_time) - - try: - # Create a new result object for retry - result = Runner.run_streamed( - agent, - input=agent_input, - ) - log.info(f"=== Retry attempt {retry_count} starting ===") - except Exception as retry_setup_error: - log.error(f"Failed to setup retry: {retry_setup_error}") - if retry_count >= max_retries: - raise stream_error # Re-raise original error if max retries reached - continue - else: - log.error(f"Non-retryable streaming error or max retries reached: {error_msg}") - log.error(f"Traceback: {traceback.format_exc()}") - if isinstance(stream_error, RateLimitError): - default_error_msg = 'Rate limit exceeded, please try again later.' - error_body = stream_error.body - error_msg = error_body['message'] if error_body and 'message' in error_body else None - final_error_msg = error_msg or default_error_msg - yield (final_error_msg, None) - return - else: - # Continue to normal processing, error will be handled by outer try-catch - break - - except Exception as unexpected_error: - retry_count += 1 - log.error(f"Unexpected error during streaming (attempt {retry_count}/{max_retries}): {unexpected_error}") - log.error(f"Traceback: {traceback.format_exc()}") - - if retry_count > max_retries: - log.error("Max retries exceeded for unexpected error") - break - else: - # Brief wait before retry for unexpected errors - await asyncio.sleep(1) - continue - - # Add detailed debugging info about tool results - log.info(f"Total tool results: {len(tool_results)}") - for tool_name, result in tool_results.items(): - result_type = "Message Output" if tool_name == '_message_output_ext' else "Tool" - log.info(f"{result_type}: {tool_name}") - log.info(f" - Has data: {result['data'] is not None}") - log.info(f" - Data length: {len(result['data']) if result['data'] else 0}") - log.info(f" - Has ext: {result['ext'] is not None}") - if result['ext']: - log.info(f" - Ext types: {[item.get('type') for item in (result['ext'] if isinstance(result['ext'], list) else [result['ext']])]}") - log.info(f" - Answer preview: {result['answer'][:100] if result['answer'] else 'None'}...") - log.info(f"=== End Tool Results Summary ===\n") - - # Process workflow tools results integration similar to reference facade.py - workflow_tools_found = [tool for tool in ["recall_workflow", "gen_workflow"] if tool in tool_results] - finished = False # Default finished state - - if workflow_tools_found: - log.info(f"Workflow tools called: {workflow_tools_found}") - - # Check if both workflow tools were called - if "recall_workflow" in tool_results and "gen_workflow" in tool_results: - log.info("Both recall_workflow and gen_workflow were called, merging results") - - # Check each tool's success and merge results - successful_workflows = [] - - recall_result = tool_results["recall_workflow"] - if recall_result["data"] and len(recall_result["data"]) > 0: - log.info(f"recall_workflow succeeded with {len(recall_result['data'])} workflows") - log.info(f" - Workflow IDs: {[w.get('id') for w in recall_result['data']]}") - successful_workflows.extend(recall_result["data"]) - else: - log.error("recall_workflow failed or returned no data") - - gen_result = tool_results["gen_workflow"] - if gen_result["data"] and len(gen_result["data"]) > 0: - log.info(f"gen_workflow succeeded with {len(gen_result['data'])} workflows") - log.info(f" - Workflow IDs: {[w.get('id') for w in gen_result['data']]}") - successful_workflows.insert(0, *gen_result["data"]) - else: - log.error("gen_workflow failed or returned no data") - - # Remove duplicates based on workflow ID - seen_ids = set() - unique_workflows = [] - for workflow in successful_workflows: - workflow_id = workflow.get('id') - if workflow_id and workflow_id not in seen_ids: - seen_ids.add(workflow_id) - unique_workflows.append(workflow) - log.info(f" - Added unique workflow: {workflow_id} - {workflow.get('name', 'Unknown')}") - elif workflow_id: - log.info(f" - Skipped duplicate workflow: {workflow_id} - {workflow.get('name', 'Unknown')}") - else: - # If no ID, add anyway (shouldn't happen but just in case) - unique_workflows.append(workflow) - log.info(f" - Added workflow without ID: {workflow.get('name', 'Unknown')}") - - log.info(f"Total workflows before deduplication: {len(successful_workflows)}") - log.info(f"Total workflows after deduplication: {len(unique_workflows)}") - - # Create final ext structure - if unique_workflows: - ext = [{ - "type": "workflow", - "data": unique_workflows - }] - log.info(f"Returning {len(unique_workflows)} workflows from successful tools") - else: - ext = None - log.error("No successful workflow data to return") - - # Both tools called, finished = True - finished = True - - elif "recall_workflow" in tool_results and "gen_workflow" not in tool_results: - if DISABLE_WORKFLOW_GEN: - # If generation is disabled, we don't wait for gen_workflow - log.info("Only recall_workflow was called and generation is disabled, returning its result") - recall_result = tool_results["recall_workflow"] - if recall_result["data"] and len(recall_result["data"]) > 0: - ext = [{ - "type": "workflow", - "data": recall_result["data"] - }] - log.info(f"Returning {len(recall_result['data'])} workflows from recall_workflow") - else: - ext = None - log.error("recall_workflow failed or returned no data") - finished = True - else: - # Only recall_workflow was called, don't return ext, keep finished=false - log.info("Only recall_workflow was called, waiting for gen_workflow, not returning ext") - ext = None - finished = False # This is the key: keep finished=false to wait for gen_workflow - - elif "gen_workflow" in tool_results and "recall_workflow" not in tool_results: - # Only gen_workflow was called, return its result normally - log.info("Only gen_workflow was called, returning its result") - gen_result = tool_results["gen_workflow"] - if gen_result["data"] and len(gen_result["data"]) > 0: - ext = [{ - "type": "workflow", - "data": gen_result["data"] - }] - log.info(f"Returning {len(gen_result['data'])} workflows from gen_workflow") - else: - ext = None - log.error("gen_workflow failed or returned no data") - - # Only gen_workflow called, finished = True - finished = True - else: - # No workflow tools called, check if other tools or message output returned ext - for tool_name, result in tool_results.items(): - if result["ext"]: - ext = result["ext"] - log.info(f"Using ext from {tool_name}") - break - - # When no workflow tools are called (e.g., handoff to workflow_rewrite_agent) - # The agent stream has completed at this point, so finished should be True - # The workflow_update_ext will be included in final_ext regardless - finished = True - - - # Prepare final ext (debug_ext would be empty here since no debug events) - final_ext = ext - if workflow_update_ext: - # workflow_update_ext is now a list of ext items, so extend rather than wrap - if isinstance(workflow_update_ext, list): - final_ext = workflow_update_ext + (ext if ext else []) - else: - # Backward compatibility: if it's a single item, wrap it - final_ext = [workflow_update_ext] + (ext if ext else []) - log.info(f"-- Including workflow_update ext in final response: {len(workflow_update_ext) if isinstance(workflow_update_ext, list) else 1} items") - - # Final yield with complete text, ext data, and finished status - # Return as tuple (text, ext_with_finished) where ext_with_finished includes finished info - if final_ext: - # Add finished status to ext structure - ext_with_finished = { - "data": final_ext, - "finished": finished - } - else: - ext_with_finished = { - "data": None, - "finished": finished - } - - yield (current_text, ext_with_finished) - - except Exception as e: - log.error(f"Error in comfyui_agent_invoke: {str(e)}") - log.error(f"Traceback: {traceback.format_exc()}") - error_message = f"I apologize, but an error occurred while processing your request: {str(e)}" - - # Check if this is a retryable streaming error that should not finish the conversation - error_msg = str(e) - is_retryable_streaming_error = ( - "'NoneType' object has no attribute 'strip'" in error_msg or - "Connection broken" in error_msg or - "InvalidChunkLength" in error_msg or - "socket hang up" in error_msg or - "Connection reset" in error_msg or - isinstance(e, APIError) - ) - - if is_retryable_streaming_error: - # For retryable streaming errors, don't finish - allow user to retry - log.info(f"Detected retryable streaming error, setting finished=False to allow retry") - error_ext = { - "data": None, - "finished": False - } - error_message = f"A temporary streaming error occurred: {str(e)}. Please try your request again." - else: - # For other errors, finish the conversation - error_ext = { - "data": None, - "finished": True - } - - yield (error_message, error_ext) diff --git a/custom_nodes/ComfyUI-Copilot/backend/service/message_memory.py b/custom_nodes/ComfyUI-Copilot/backend/service/message_memory.py deleted file mode 100644 index 008c0f1c91f5bc90fd9be0de59f7003375acfe38..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/service/message_memory.py +++ /dev/null @@ -1,261 +0,0 @@ -''' -Author: ai-business-hql ai.bussiness.hql@gmail.com -Date: 2025-11-19 -LastEditors: ai-business-hql ai.bussiness.hql@gmail.com -LastEditTime: 2025-11-19 -FilePath: /ComfyUI-Copilot/backend/service/message_memory.py -Description: Message memory manager with automatic compression -''' - -from typing import List, Dict, Any, Tuple -from ..dao.session_message_table import ( - get_session_message, - save_session_message, - update_summary -) -from .summary_agent import generate_summary -from ..utils.logger import log - - -# 配置参数 -COMPRESSION_THRESHOLD = 8 # 超过10条未压缩消息时触发压缩(4个来回) -KEEP_RECENT_MESSAGES = 4 # 保留最新4条消息不压缩(2个来回) - - -def message_memory_optimize( - session_id: str, - messages: List[Dict[str, Any]] -) -> List[Dict[str, Any]]: - """ - 优化消息内存,自动压缩历史消息 - - 工作流程: - 1. 加载 session 数据(如果不存在则创建) - 2. 更新完整的消息列表 - 3. 检查是否需要压缩(未压缩消息 > 10条) - 4. 如果需要,执行压缩并更新数据库 - 5. 返回优化后的消息列表(summary + 未压缩的消息) - - Args: - session_id: 会话ID - messages: 当前的完整消息列表 - - Returns: - 优化后的消息列表,可以直接传给 LLM - - 如果有压缩:[{"role": "system", "content": "Previous summary: ..."}, ...recent_messages] - - 如果无压缩:原始 messages - """ - try: - log.info(f"[Memory] Starting memory optimization for session: {session_id}") - log.info(f"[Memory] Input messages count: {len(messages)}") - - # 1. 加载或创建 session 记录 - record = get_session_message(session_id) - - if record is None: - # 首次访问,创建新记录 - log.info(f"[Memory] New session detected, creating record") - save_session_message( - session_id=session_id, - messages=messages, - index=0, - summary=None - ) - # 新会话不需要压缩,直接返回原始消息 - return messages - - # 2. 更新消息列表(保存当前完整的消息) - current_index = record['index'] - current_summary = record['summary'] - - log.info(f"[Memory] Existing session - index: {current_index}, summary length: {len(current_summary) if current_summary else 0}") - - # 保存更新后的完整消息列表 - save_session_message( - session_id=session_id, - messages=messages, - index=current_index, # 暂时保持不变 - summary=current_summary - ) - - # 3. 检查是否需要压缩 - uncompressed_count = len(messages) - current_index - log.info(f"[Memory] Uncompressed messages: {uncompressed_count} (threshold: {COMPRESSION_THRESHOLD})") - - if uncompressed_count <= COMPRESSION_THRESHOLD: - # 不需要压缩,构建返回消息 - log.info(f"[Memory] No compression needed") - return _build_optimized_messages(current_summary, messages, current_index) - - # 4. 执行压缩 - log.info(f"[Memory] Compression triggered!") - - # 计算压缩范围 - # 待压缩:messages[current_index : len(messages) - KEEP_RECENT_MESSAGES] - compress_end_index = len(messages) - KEEP_RECENT_MESSAGES - messages_to_compress = messages[current_index:compress_end_index] - - log.info(f"[Memory] Compressing messages from index {current_index} to {compress_end_index}") - log.info(f"[Memory] Messages to compress: {len(messages_to_compress)}") - - # 生成新的摘要(整合历史摘要) - new_summary = generate_summary( - messages=messages_to_compress, - previous_summary=current_summary - ) - - log.info(f"[Memory] Generated new summary: {new_summary[:100]}...") - - # 更新数据库 - update_summary( - session_id=session_id, - summary=new_summary, - index=compress_end_index - ) - - log.info(f"[Memory] Updated index to: {compress_end_index}") - - # 5. 返回优化后的消息 - optimized_messages = _build_optimized_messages(new_summary, messages, compress_end_index) - log.info(f"[Memory] Returning {len(optimized_messages)} optimized messages") - - return optimized_messages - - except Exception as e: - log.error(f"[Memory] Error in message_memory_optimize: {str(e)}") - log.error(f"[Memory] Falling back to original messages") - # 发生错误时返回原始消息,确保系统继续运行 - return messages - - -def _build_optimized_messages( - summary: str, - full_messages: List[Dict[str, Any]], - index: int -) -> List[Dict[str, Any]]: - """ - 构建优化后的消息列表 - - Args: - summary: 压缩后的历史摘要 - full_messages: 完整的消息列表 - index: 已压缩的消息索引 - - Returns: - 优化后的消息列表 - """ - # 获取未压缩的消息 - recent_messages = full_messages[index:] - - if summary and len(summary.strip()) > 0 and len(recent_messages) > 0: - # 有摘要且有未压缩消息,将 summary 作为上下文信息添加到第一条消息前 - # 注意:不能使用 role="system",因为 create_agent 已经有 instructions 了 - # 将 summary 作为一条独立的 user 消息添加在开头 - summary_context = { - "role": "user", - "content": f"[Context from previous conversation]: {summary}" - } - return [summary_context] + recent_messages - else: - # 无摘要或无消息,直接返回未压缩的消息 - return recent_messages - - -def get_optimized_messages(session_id: str) -> List[Dict[str, Any]]: - """ - 获取优化后的消息列表(不触发新的压缩) - - Args: - session_id: 会话ID - - Returns: - 优化后的消息列表 - """ - try: - record = get_session_message(session_id) - - if record is None: - return [] - - return _build_optimized_messages( - summary=record['summary'], - full_messages=record['messages'], - index=record['index'] - ) - - except Exception as e: - log.error(f"[Memory] Error in get_optimized_messages: {str(e)}") - return [] - - -def get_compression_stats(session_id: str) -> Dict[str, Any]: - """ - 获取压缩统计信息 - - Args: - session_id: 会话ID - - Returns: - 压缩统计信息字典 - """ - try: - record = get_session_message(session_id) - - if record is None: - return { - "exists": False, - "total_messages": 0, - "compressed_messages": 0, - "uncompressed_messages": 0, - "has_summary": False, - "summary_length": 0 - } - - total = len(record['messages']) - compressed = record['index'] - uncompressed = total - compressed - - return { - "exists": True, - "total_messages": total, - "compressed_messages": compressed, - "uncompressed_messages": uncompressed, - "has_summary": bool(record['summary']), - "summary_length": len(record['summary']) if record['summary'] else 0, - "compression_ratio": f"{compressed}/{total}" if total > 0 else "0/0" - } - - except Exception as e: - log.error(f"[Memory] Error in get_compression_stats: {str(e)}") - return {"error": str(e)} - - -# 测试函数 -def test_message_memory(): - """测试消息内存管理""" - test_session_id = "test_session_123" - - # 模拟对话 - messages = [] - for i in range(15): - messages.append({"role": "user", "content": f"User message {i+1}"}) - messages.append({"role": "assistant", "content": f"Assistant response {i+1}"}) - - print(f"Total messages: {len(messages)}") - - # 第一次优化 - optimized = message_memory_optimize(test_session_id, messages) - print(f"Optimized messages count: {len(optimized)}") - - # 获取统计信息 - stats = get_compression_stats(test_session_id) - print(f"Compression stats: {stats}") - - # 打印优化后的消息 - for msg in optimized[:3]: - print(f"{msg['role']}: {msg['content'][:50]}...") - - -if __name__ == "__main__": - test_message_memory() - diff --git a/custom_nodes/ComfyUI-Copilot/backend/utils/comfy_gateway.py b/custom_nodes/ComfyUI-Copilot/backend/utils/comfy_gateway.py deleted file mode 100644 index a366dbe1b5dae4491286bcf38a92e423a489c8d2..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/utils/comfy_gateway.py +++ /dev/null @@ -1,384 +0,0 @@ -""" -ComfyUI Gateway Utilities - -This module provides Python implementations of ComfyUI API functions, -using HTTP requests to the ComfyUI server for consistency. -""" - -import json -import os -import uuid -import logging -import asyncio -from typing import Dict, Any, Optional, List - -# Import ComfyUI internal modules -import nodes -import execution -import folder_paths -import server -import aiohttp - - -class ComfyGateway: - """ComfyUI API Gateway for Python backend - uses internal functions instead of HTTP requests""" - - def __init__(self, base_url: Optional[str] = None): - """ - Initialize ComfyUI Gateway - - Args: - base_url: Optional base URL for ComfyUI server. If not provided, will auto-detect. - """ - # Get server instance for operations that need it - self.server_instance = server.PromptServer.instance - - # Auto-detect server URL if not provided - if base_url: - self.base_url = base_url.rstrip('/') - else: - # Auto-detect from server instance - if hasattr(self.server_instance, 'address') and hasattr(self.server_instance, 'port'): - address = self.server_instance.address or '127.0.0.1' - port = self.server_instance.port or 8188 - # Use 127.0.0.1 for localhost to avoid potential connection issues - if address in ['0.0.0.0', '::']: - address = '127.0.0.1' - self.base_url = f"http://{address}:{port}" - else: - # Fallback to default - self.base_url = "http://127.0.0.1:8188" - - logging.info(f"ComfyGateway initialized with base_url: {self.base_url}") - - async def run_prompt(self, json_data: Dict[str, Any]) -> Dict[str, Any]: - """ - Run a prompt - HTTP call to ComfyUI /api/prompt endpoint - - This method sends an HTTP POST request to the ComfyUI server's /api/prompt endpoint - to ensure consistent behavior and avoid code duplication. - - Args: - json_data: The prompt/workflow data in the same format as HTTP API - - Returns: - Dict containing the validation result, similar to HTTP API response - """ - try: - # Create a timeout configuration - timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout - - # Make HTTP request to /api/prompt endpoint - url = f"{self.base_url}/api/prompt" - headers = { - 'Content-Type': 'application/json' - } - - - # Create temporary session - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.post(url, json=json_data, headers=headers) as response: - response_data = await response.json() - status_code = response.status - - # Handle the response based on status code - if status_code == 200: - # Success response - add success flag for consistency - return { - "success": True, - **response_data - } - else: - # Error response (400, etc.) - add success flag for consistency - return { - "success": False, - **response_data - } - - except aiohttp.ClientConnectionError as e: - logging.error(f"Connection error in run_prompt: {e}") - return { - "success": False, - "error": { - "type": "connection_error", - "message": f"Failed to connect to ComfyUI server at {self.base_url}", - "details": str(e) - }, - "node_errors": {} - } - except aiohttp.ClientTimeout as e: - logging.error(f"Timeout error in run_prompt: {e}") - return { - "success": False, - "error": { - "type": "timeout_error", - "message": "Request to ComfyUI server timed out", - "details": str(e) - }, - "node_errors": {} - } - except Exception as e: - logging.error(f"Error in run_prompt: {e}") - return { - "success": False, - "error": { - "type": "internal_error", - "message": f"Internal error: {str(e)}", - "details": str(e) - }, - "node_errors": {} - } - - async def get_object_info(self, node_class: Optional[str] = None) -> Dict[str, Any]: - """ - Get ComfyUI node definitions - HTTP call to ComfyUI /api/object_info endpoint - - Args: - node_class: Optional specific node class to get info for - - Returns: - Dict containing node definitions and their parameters - """ - try: - # Create a timeout configuration - timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout - - # Build URL - either specific node or all nodes - if node_class: - url = f"{self.base_url}/api/object_info/{node_class}" - else: - url = f"{self.base_url}/api/object_info" - - # Make HTTP request to /api/object_info endpoint - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(url) as response: - if response.status == 200: - return await response.json() - else: - logging.error(f"Failed to get object info: HTTP {response.status}") - return {} - - except aiohttp.ClientConnectionError as e: - logging.error(f"Connection error in get_object_info: {e}") - return {} - except aiohttp.ClientTimeout as e: - logging.error(f"Timeout error in get_object_info: {e}") - return {} - except Exception as e: - logging.error(f"Error getting object info: {e}") - return {} - - async def get_installed_nodes(self) -> List[str]: - """ - Get list of installed node types - HTTP call to ComfyUI /api/object_info endpoint - - Returns: - List of installed node type names - """ - try: - object_info = await self.get_object_info() - return list(object_info.keys()) - except Exception as e: - logging.error(f"Error getting installed nodes: {e}") - return [] - - async def manage_queue(self, clear: bool = False, delete: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Clear the prompt queue or delete specific queue items - HTTP call to ComfyUI /api/queue endpoint - - Args: - clear: If True, clears the entire queue - delete: List of prompt IDs to delete from the queue - - Returns: - Dict with the response from the queue management operation - """ - try: - # Create a timeout configuration - timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout - - # Prepare request data - json_data = {} - if clear: - json_data["clear"] = True - if delete: - json_data["delete"] = delete - - # Make HTTP request to /api/queue endpoint - url = f"{self.base_url}/api/queue" - headers = { - 'Content-Type': 'application/json' - } - - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.post(url, json=json_data, headers=headers) as response: - if response.status == 200: - return {"success": True} - else: - logging.error(f"Failed to manage queue: HTTP {response.status}") - return {"error": f"HTTP {response.status}"} - - except aiohttp.ClientConnectionError as e: - logging.error(f"Connection error in manage_queue: {e}") - return {"error": f"Connection error: {str(e)}"} - except aiohttp.ClientTimeout as e: - logging.error(f"Timeout error in manage_queue: {e}") - return {"error": f"Timeout error: {str(e)}"} - except Exception as e: - logging.error(f"Error managing queue: {e}") - return {"error": f"Failed to manage queue: {str(e)}"} - - async def interrupt_processing(self) -> Dict[str, Any]: - """ - Interrupt the current processing/generation - HTTP call to ComfyUI /api/interrupt endpoint - - Returns: - Dict with the response from the interrupt operation - """ - try: - # Create a timeout configuration - timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout - - # Make HTTP request to /api/interrupt endpoint - url = f"{self.base_url}/api/interrupt" - headers = { - 'Content-Type': 'application/json' - } - - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.post(url, headers=headers) as response: - if response.status == 200: - return {"success": True} - else: - logging.error(f"Failed to interrupt processing: HTTP {response.status}") - return {"error": f"HTTP {response.status}"} - - except aiohttp.ClientConnectionError as e: - logging.error(f"Connection error in interrupt_processing: {e}") - return {"error": f"Connection error: {str(e)}"} - except aiohttp.ClientTimeout as e: - logging.error(f"Timeout error in interrupt_processing: {e}") - return {"error": f"Timeout error: {str(e)}"} - except Exception as e: - logging.error(f"Error interrupting processing: {e}") - return {"error": f"Failed to interrupt processing: {str(e)}"} - - async def get_history(self, prompt_id: str) -> Dict[str, Any]: - """ - Get execution history for a specific prompt - HTTP call to ComfyUI /api/history/{prompt_id} endpoint - - Args: - prompt_id: The ID of the prompt to get history for - - Returns: - Dict containing the execution history and results - """ - try: - # Create a timeout configuration - timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout - - # Make HTTP request to /api/history/{prompt_id} endpoint - url = f"{self.base_url}/api/history/{prompt_id}" - - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(url) as response: - if response.status == 200: - return await response.json() - else: - logging.error(f"Failed to get history for prompt {prompt_id}: HTTP {response.status}") - return {"error": f"HTTP {response.status}"} - - except aiohttp.ClientConnectionError as e: - logging.error(f"Connection error in get_history: {e}") - return {"error": f"Connection error: {str(e)}"} - except aiohttp.ClientTimeout as e: - logging.error(f"Timeout error in get_history: {e}") - return {"error": f"Timeout error: {str(e)}"} - except Exception as e: - logging.error(f"Error fetching history for prompt {prompt_id}: {e}") - return {"error": f"Failed to get history: {str(e)}"} - - async def get_queue_status(self) -> Dict[str, Any]: - """ - Get current queue status - HTTP call to ComfyUI /api/queue endpoint - - Returns: - Dict containing current queue information - """ - try: - # Create a timeout configuration - timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout - - # Make HTTP request to /api/queue endpoint - url = f"{self.base_url}/api/queue" - - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(url) as response: - if response.status == 200: - return await response.json() - else: - logging.error(f"Failed to get queue status: HTTP {response.status}") - return {"error": f"HTTP {response.status}"} - - except aiohttp.ClientConnectionError as e: - logging.error(f"Connection error in get_queue_status: {e}") - return {"error": f"Connection error: {str(e)}"} - except aiohttp.ClientTimeout as e: - logging.error(f"Timeout error in get_queue_status: {e}") - return {"error": f"Timeout error: {str(e)}"} - except Exception as e: - logging.error(f"Error getting queue status: {e}") - return {"error": f"Failed to get queue status: {str(e)}"} - - -# Convenience functions for backward compatibility and easy importing -async def run_prompt(json_data: Dict[str, Any], base_url: Optional[str] = None) -> Dict[str, Any]: - """ - Standalone function to run a prompt - HTTP call to ComfyUI /api/prompt endpoint - - Args: - json_data: The prompt/workflow data to execute - base_url: Optional base URL for ComfyUI server - - Returns: - Dict containing the API response - """ - gateway = ComfyGateway(base_url) - return await gateway.run_prompt(json_data) - - -async def get_object_info(base_url: Optional[str] = None) -> Dict[str, Any]: - """Standalone function to get object info - HTTP call to ComfyUI /api/object_info endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.get_object_info() - -async def get_object_info_by_class(node_class: str, base_url: Optional[str] = None) -> Dict[str, Any]: - """Standalone function to get object info for specific node class - HTTP call to ComfyUI /api/object_info/{node_class} endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.get_object_info(node_class) - - -async def get_installed_nodes(base_url: Optional[str] = None) -> List[str]: - """Standalone function to get installed nodes - HTTP call to ComfyUI /api/object_info endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.get_installed_nodes() - -async def manage_queue(clear: bool = False, delete: Optional[List[str]] = None, base_url: Optional[str] = None) -> Dict[str, Any]: - """Standalone function to manage queue - HTTP call to ComfyUI /api/queue endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.manage_queue(clear, delete) - -async def interrupt_processing(base_url: Optional[str] = None) -> Dict[str, Any]: - """Standalone function to interrupt processing - HTTP call to ComfyUI /api/interrupt endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.interrupt_processing() - -async def get_history(prompt_id: str, base_url: Optional[str] = None) -> Dict[str, Any]: - """Standalone function to get history - HTTP call to ComfyUI /api/history/{prompt_id} endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.get_history(prompt_id) - -async def get_queue_status(base_url: Optional[str] = None) -> Dict[str, Any]: - """Standalone function to get queue status - HTTP call to ComfyUI /api/queue endpoint""" - gateway = ComfyGateway(base_url) - return await gateway.get_queue_status() diff --git a/custom_nodes/ComfyUI-Copilot/backend/utils/globals.py b/custom_nodes/ComfyUI-Copilot/backend/utils/globals.py deleted file mode 100644 index d27732ea3dcd2c34d18aa7a8cc235d35a1975646..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/utils/globals.py +++ /dev/null @@ -1,164 +0,0 @@ -''' -Author: ai-business-hql qingli.hql@alibaba-inc.com -Date: 2025-08-08 17:14:52 -LastEditors: ai-business-hql ai.bussiness.hql@gmail.com -LastEditTime: 2025-11-27 11:12:18 -FilePath: /comfyui_copilot/backend/utils/globals.py -Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE -''' - -""" -Global utilities for managing application-wide state and configuration. -""" - -import os -import threading -from typing import Optional, Dict, Any -from pathlib import Path -from dotenv import load_dotenv - -# Load .env file if it exists -env_path = Path(__file__).parent.parent.parent / '.env' -if env_path.exists(): - load_dotenv(env_path) - -class GlobalState: - """Thread-safe global state manager for application-wide configuration.""" - - def __init__(self): - self._lock = threading.RLock() - self._state: Dict[str, Any] = { - 'LANGUAGE': 'en', # Default language - } - - def get(self, key: str, default: Any = None) -> Any: - """Get a global state value.""" - with self._lock: - return self._state.get(key, default) - - def set(self, key: str, value: Any) -> None: - """Set a global state value.""" - with self._lock: - self._state[key] = value - - def get_language(self) -> str: - """Get the current language setting.""" - return self.get('LANGUAGE', 'en') - - def set_language(self, language: str) -> None: - """Set the current language setting.""" - self.set('LANGUAGE', language) - - def update(self, **kwargs) -> None: - """Update multiple state values at once.""" - with self._lock: - self._state.update(kwargs) - - def get_all(self) -> Dict[str, Any]: - """Get a copy of all global state.""" - with self._lock: - return self._state.copy() - -# Global instance -_global_state = GlobalState() - -# Convenience functions for external access -def get_global(key: str, default: Any = None) -> Any: - """Get a global state value.""" - return _global_state.get(key, default) - -def set_global(key: str, value: Any) -> None: - """Set a global state value.""" - _global_state.set(key, value) - -def get_language() -> str: - """Get the current language setting.""" - language = _global_state.get_language() - if not language: - language = 'en' - return language - -def set_language(language: str) -> None: - """Set the current language setting.""" - _global_state.set_language(language) - -def update_globals(**kwargs) -> None: - """Update multiple global values at once.""" - _global_state.update(**kwargs) - -def get_all_globals() -> Dict[str, Any]: - """Get a copy of all global state.""" - return _global_state.get_all() - -def get_comfyui_copilot_api_key() -> Optional[str]: - """Get the ComfyUI Copilot API key.""" - return _global_state.get('comfyui_copilot_api_key') - -def set_comfyui_copilot_api_key(api_key: str) -> None: - """Set the ComfyUI Copilot API key.""" - _global_state.set('comfyui_copilot_api_key', api_key) - - -BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "https://comfyui-copilot-server.onrender.com") -LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1" -WORKFLOW_MODEL_NAME = os.getenv("WORKFLOW_MODEL_NAME", "us.anthropic.claude-sonnet-4-20250514-v1:0") -# WORKFLOW_MODEL_NAME = "gpt-5-2025-08-07-GlobalStandard" -LLM_DEFAULT_BASE_URL = "https://comfyui-copilot-server.onrender.com/v1" - -# LLM-related env defaults (used as fallback when request config does not provide values) -OPENAI_API_KEY = os.getenv("CC_OPENAI_API_KEY") or None -OPENAI_BASE_URL = os.getenv("CC_OPENAI_BASE_URL") or None -WORKFLOW_LLM_API_KEY = os.getenv("WORKFLOW_LLM_API_KEY") or None -WORKFLOW_LLM_BASE_URL = os.getenv("WORKFLOW_LLM_BASE_URL") or None -# If WORKFLOW_LLM_MODEL is not set, fall back to WORKFLOW_MODEL_NAME -WORKFLOW_LLM_MODEL = os.getenv("WORKFLOW_LLM_MODEL") or WORKFLOW_MODEL_NAME -DISABLE_WORKFLOW_GEN = os.getenv("DISABLE_WORKFLOW_GEN") or False - - -def apply_llm_env_defaults(config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """ - Apply LLM-related defaults with precedence: - request config > .env > hard-coded defaults. - - This function does NOT mutate the incoming config. - """ - cfg: Dict[str, Any] = dict(config or {}) - - # Chat LLM (OpenAI-compatible) settings - if not cfg.get("openai_api_key") and OPENAI_API_KEY: - cfg["openai_api_key"] = OPENAI_API_KEY - if not cfg.get("openai_base_url") and OPENAI_BASE_URL: - cfg["openai_base_url"] = OPENAI_BASE_URL - - # Workflow LLM settings (tools/agents that might use a different LLM) - if not cfg.get("workflow_llm_api_key") and WORKFLOW_LLM_API_KEY: - cfg["workflow_llm_api_key"] = WORKFLOW_LLM_API_KEY - if not cfg.get("workflow_llm_base_url") and WORKFLOW_LLM_BASE_URL: - cfg["workflow_llm_base_url"] = WORKFLOW_LLM_BASE_URL - if not cfg.get("workflow_llm_model") and WORKFLOW_LLM_MODEL: - cfg["workflow_llm_model"] = WORKFLOW_LLM_MODEL - - return cfg - - -def is_lmstudio_url(base_url: str) -> bool: - """Check if the base URL is likely LMStudio based on common patterns.""" - if not base_url: - return False - - base_url_lower = base_url.lower() - # Common LMStudio patterns (supporting various ports and configurations) - lmstudio_patterns = [ - "localhost:1234", # Standard LMStudio port - "127.0.0.1:1234", - "0.0.0.0:1234", - ":1234/v1", - "localhost:1235", # Alternative port some users might use - "127.0.0.1:1235", - "0.0.0.0:1235", - ":1235/v1", - "localhost/v1", # Generic localhost patterns - "127.0.0.1/v1" - ] - - return any(pattern in base_url_lower for pattern in lmstudio_patterns) diff --git a/custom_nodes/ComfyUI-Copilot/backend/utils/key_utils.py b/custom_nodes/ComfyUI-Copilot/backend/utils/key_utils.py deleted file mode 100644 index 9c46921875f3d89efd47407ea0ac281f46470e47..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/utils/key_utils.py +++ /dev/null @@ -1,34 +0,0 @@ -''' -Author: ai-business-hql ai.bussiness.hql@gmail.com -Date: 2025-10-11 16:46:10 -LastEditors: ai-business-hql ai.bussiness.hql@gmail.com -LastEditTime: 2025-10-15 14:35:41 -FilePath: /ComfyUI-Copilot/backend/utils/key_utils.py -Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE -''' -from copy import deepcopy - -def workflow_config_adapt(config: dict) -> dict: - """Return a deep-copied and adapted workflow config without mutating input. - - - Map workflow_llm_api_key -> openai_api_key (and null out original key) - - Map workflow_llm_base_url -> openai_base_url (and null out original key) - """ - if not config: - return {} - - new_config = deepcopy(config) - - if new_config.get("workflow_llm_api_key"): - new_config["openai_api_key"] = new_config.get("workflow_llm_api_key") - new_config["workflow_llm_api_key"] = None - if new_config.get("workflow_llm_base_url"): - new_config["openai_base_url"] = new_config.get("workflow_llm_base_url") - new_config["workflow_llm_base_url"] = None - if new_config.get("workflow_llm_model"): - new_config["model_select"] = new_config.get("workflow_llm_model") - new_config["workflow_llm_model"] = None - else: - new_config["model_select"] = None - - return new_config diff --git a/custom_nodes/ComfyUI-Copilot/backend/utils/modelscope_gateway.py b/custom_nodes/ComfyUI-Copilot/backend/utils/modelscope_gateway.py deleted file mode 100644 index 3c0e38d7e783facbd85d9a4ebb94af21eed04385..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/utils/modelscope_gateway.py +++ /dev/null @@ -1,330 +0,0 @@ -''' -Author: ai-business-hql ai.bussiness.hql@gmail.com -Date: 2025-08-26 15:06:47 -LastEditors: ai-business-hql ai.bussiness.hql@gmail.com -LastEditTime: 2025-09-01 19:31:03 -FilePath: /ComfyUI-Copilot/backend/utils/modelscope_gateway.py -Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE -''' -import os -from typing import Any, Dict, List, Optional - -import requests -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry -from .logger import log -import folder_paths - - -class ModelScopeGateway: - BASE_URL = "https://www.modelscope.cn" - SUGGEST_ENDPOINT = f"{BASE_URL}/api/v1/dolphin/model/suggestv2" - SEARCH_ENDPOINT = f"{BASE_URL}/api/v1/dolphin/models" - SEARCH_SINGLE_ENDPOINT = f"{BASE_URL}/api/v1/models" - - def __init__(self, timeout: float = 10.0, retries: int = 3, backoff: float = 0.5): - self.timeout = timeout - - # 单实例 Session,会自动保存 Cookie;如需按用户隔离可按 session_key 做多 Session 管理 - self.session = requests.Session() - self.session.headers.update({ - "Accept": "application/json, text/plain, */*", - "Content-Type": "application/json", - "User-Agent": "ComfyUI-Copilot/1.0 (requests)", - "x-modelscope-accept-language": "zh_CN", - }) - - retry_cfg = Retry( - total=retries, - backoff_factor=backoff, - status_forcelist=[429, 500, 502, 503, 504], - allowed_methods=["GET", "POST", "PUT"], - raise_on_status=False, - ) - adapter = HTTPAdapter(max_retries=retry_cfg) - self.session.mount("https://", adapter) - self.session.mount("http://", adapter) - - def formatData(self, data: Any) -> Dict[str, Any]: - inner = data.get('Model', {}) if isinstance(data, dict) else {} - path = data.get('Path') or inner.get('Path') - name = data.get('Name') or inner.get('Name') - revision = data.get('Revision') or inner.get('Revision') - # size = self.get_model_size(path, name, revision) - return { - "Libraries": data.get("Libraries") or inner.get("Libraries"), - "ChineseName": data.get("ChineseName") or inner.get("ChineseName"), - "Id": data.get("Id") or inner.get("Id") or data.get("ModelId") or inner.get("ModelId"), - "Name": data.get("Name") or inner.get("Name"), - "Path": data.get("Path") or inner.get("Path"), - "LastUpdatedTime": data.get("LastUpdatedTime") or inner.get("LastUpdatedTime") or data.get("LastUpdatedAt") or inner.get("LastUpdatedAt"), - "Downloads": data.get("Downloads") or inner.get("Downloads") or data.get("DownloadCount") or inner.get("DownloadCount"), - # "Size": size or 0 - } - - def get_single_model(self, path: str, name: str) -> Optional[Dict[str, Any]]: - """ - 调用单模型详情接口。 - 返回原始 JSON(尽量保持结构,便于 formatData 处理),失败返回 None。 - """ - if path is None or name is None: - return None - try: - url = f"{self.SEARCH_SINGLE_ENDPOINT}/{path}/{name}" - resp = self.session.get(url, timeout=self.timeout) - resp.raise_for_status() - body = resp.json() - # 兼容不同返回包裹层级 - if isinstance(body, dict): - data = body.get("Data") or body.get("data") or body - return data - return body - except Exception as e: - log.error(f"ModelScope single fetch failed for path={path}: name={name}: {e}") - return None - - def get_model_size(self, path: str, name: str, revision: str, root: str = '') -> int: - """ - 调用单模型详情接口。 - 返回原始 JSON(尽量保持结构,便于 formatData 处理),失败返回 None。 - """ - if path is None or name is None or revision is None: - return 0 - try: - url = f"{self.SEARCH_SINGLE_ENDPOINT}/{path}/{name}/repo/files?Revision={revision}&Root={root}" - resp = self.session.get(url, timeout=self.timeout) - resp.raise_for_status() - body = resp.json() - # 兼容不同返回包裹层级 - if isinstance(body, dict): - data = body["Data"]["Files"] - size = 0 - for item in data: - size += item.get("Size") or 0 - return size - return 0 - except Exception as e: - log.error(f"ModelScope model size fetch failed for path={path}: name={name}: rversion={rversion}: {e}") - return 0 - - def suggest( - self, - name: str, - page: int = 1, - page_size: int = 30, - sort_by: str = "Default", - target: str = "", - single_criterion: Optional[List[Dict[str, Any]]] = None, - ) -> Dict[str, Any]: - """ - 调用 suggestv2 模糊搜索接口;返回 body 与当前 Cookie。 - - Returns: - Dict[str, Any]: 返回的模型列表,格式为: - { - "data": [ - { - "ChineseName": "中文名称", - "Id": 294066, - "Name": "SD3-Controlnet-Pose", - "Path": "InstantX", - "Libraries": ["pytorch","lora","safetensors"], - "LastUpdatedTime": "1733042611", - "Downloads": 100, - "Size": 100000 - } - ] - } - """ - payload = { - "PageSize": page_size, - "PageNumber": page, - "SortBy": sort_by, - "Target": target, - "SingleCriterion": single_criterion or [], - "Name": name, - } - - resp = self.session.post( - self.SUGGEST_ENDPOINT, - json=payload, - timeout=self.timeout, - ) - resp.raise_for_status() - body = resp.json() - if body['Data'] is None or body['Data']['Model'] is None \ - or body['Data']['Model']['Suggests'] is None or (len(body['Data']['Model']['Suggests']) == 0): - log.error(f"ModelScope suggest failed: {body}, request: {payload}") - return {"data": None} - models = body['Data']['Model']['Suggests'] - picked: List[Dict[str, Any]] = [] - for item in models: - base = item or {} - inner = base.get('Model', {}) if isinstance(base, dict) else {} - path = base.get('Path') or inner.get('Path') - name = base.get('Name') or inner.get('Name') - detail= self.get_single_model(path, name) - data = self.formatData(detail or base) - picked.append(data) - total = body['Data']['Model'].get('TotalCount') or body['Data']['Model'].get('Total') or 0 - return {"data": picked, "total": total} - - def search( - self, - name: str, - page: int = 1, - page_size: int = 30, - sort_by: str = "Default", - target: str = "", - single_criterion: Optional[List[Dict[str, Any]]] = None, - criterion: Optional[List[Dict[str, Any]]] = None, - ) -> Dict[str, Any]: - """ - 调用 models 模糊搜索接口;返回 body 与当前 Cookie。 - - Returns: - Dict[str, Any]: 返回的模型列表,格式为: - { - "data": [ - { - "ChineseName": "中文名称", - "Id": 294066, - "Name": "SD3-Controlnet-Pose", - "Path": "InstantX", - "Libraries": ["pytorch","lora","safetensors"], - "LastUpdatedTime": "1733042611", - "Downloads": 100, - "Size": 100000 - } - ] - } - """ - payload = { - "PageSize": page_size, - "PageNumber": page, - "SortBy": sort_by, - "Target": target, - "SingleCriterion": single_criterion or [], - "Name": name, - "Criterion" : criterion - } - - resp = self.session.put( - self.SEARCH_ENDPOINT, - json=payload, - timeout=self.timeout, - ) - resp.raise_for_status() - body = resp.json() - if body['Data'] is None or body['Data']['Model'] is None \ - or body['Data']['Model']['Models'] is None or (len(body['Data']['Model']['Models']) == 0): - log.error(f"ModelScope search failed: {body}, request: {payload}") - return {"data": None} - models = body['Data']['Model']['Models'] or [] - picked: List[Dict[str, Any]] = [] - for item in models: - data = self.formatData(item or {}) - picked.append(data) - total = body['Data']['Model'].get('TotalCount') or body['Data']['Model'].get('Total') or 0 - return {"data": picked, "total": total} - - def download_with_sdk( - self, - model_id: str, - model_type: str, - dest_dir: Optional[str] = None, - ) -> str: - """ - 推荐通过 ModelScope 官方 SDK 下载(更稳妥,支持断点与多文件)。 - pip install modelscope - """ - try: - from modelscope.hub.snapshot_download import snapshot_download - except ImportError as e: - raise RuntimeError("缺少依赖 modelscope,请先安装:pip install modelscope") from e - - # Determine destination directory in ComfyUI models folder hierarchy - try: - if dest_dir: - cache_dir = os.path.abspath(os.path.expanduser(dest_dir)) - else: - # Prefer ComfyUI's configured folder for this model_type - try: - model_type_paths = folder_paths.get_folder_paths(model_type) - cache_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir, model_type) - except Exception: - # Fallback to models_dir/model_type if the key is unknown - cache_dir = os.path.join(folder_paths.models_dir, model_type) - - os.makedirs(cache_dir, exist_ok=True) - except Exception as e: - raise RuntimeError(f"Failed to prepare destination directory for download: {e}") from e - - local_dir = snapshot_download(model_id, cache_dir=cache_dir) - return local_dir - - def test_modelscope_gateway(self): - """ - 测试 ModelScope Gateway 的基本功能 - """ - try: - # 测试初始化 - print("Testing ModelScope Gateway initialization...") - gateway = ModelScopeGateway() - - # 测试模糊搜索 - print("Testing suggest method...") - result = gateway.suggest( - name="stable-diffusion", - page_size=5, - page=1 - ) - if result.get("data"): - print(f"Suggest test passed: found {len(result['data'])} models") - for model in result["data"][:2]: # 显示前2个结果 - print(f" - {model.get('Model', {}).get('ModelId', 'Unknown')}") - else: - print("Suggest test failed: no data returned") - - # 测试特定模型搜索 - print("Testing specific model search...") - specific_result = gateway.suggest( - name="qwen", - page_size=3, - page=1, - sort_by="DownloadCount", - target="model" - ) - if specific_result.get("data"): - print(f"Specific search test passed: found {len(specific_result['data'])} models") - else: - print("Specific search test failed") - - print("All tests completed successfully!") - return True - - except Exception as e: - print(f"Test failed with error: {e}") - import traceback - traceback.print_exc() - return False - - -# 测试脚本入口 -if __name__ == "__main__": - # 创建测试实例 - gateway = ModelScopeGateway() - - # 运行测试 - print("=" * 50) - print("ModelScope Gateway Test Script") - print("=" * 50) - - success = gateway.test_modelscope_gateway() - - print("=" * 50) - if success: - print("✅ All tests passed!") - else: - print("❌ Some tests failed!") - print("=" * 50) diff --git a/custom_nodes/ComfyUI-Copilot/backend/utils/request_context.py b/custom_nodes/ComfyUI-Copilot/backend/utils/request_context.py deleted file mode 100644 index 7cd53254b6a932c3fc81d77eb243812885cdeea3..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/backend/utils/request_context.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Request context management for ComfyUI Copilot -Uses contextvars to provide request-scoped context variables with async safety -""" - -import contextvars -from typing import Optional, Dict, Any -from pydantic import BaseModel - - -class RewriteContext(BaseModel): - rewrite_intent: str = "" - current_workflow: str = "" - node_infos: Optional[Dict[str, Any]] = None - rewrite_expert: Optional[str] = "" - -# Define context variables -_session_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar('session_id', default=None) -_workflow_checkpoint_id: contextvars.ContextVar[Optional[int]] = contextvars.ContextVar('workflow_checkpoint_id', default=None) -_config: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar('config', default=None) -_rewrite_context: contextvars.ContextVar[Optional[RewriteContext]] = contextvars.ContextVar('rewrite_context', default=None) - - -def set_session_id(session_id: str) -> None: - """Set the session ID for the current request context""" - _session_id.set(session_id) - -def get_session_id() -> Optional[str]: - """Get the session ID from the current request context""" - return _session_id.get() - -def set_workflow_checkpoint_id(checkpoint_id: Optional[int]) -> None: - """Set the workflow checkpoint ID for the current request context""" - _workflow_checkpoint_id.set(checkpoint_id) - -def get_workflow_checkpoint_id() -> Optional[int]: - """Get the workflow checkpoint ID from the current request context""" - return _workflow_checkpoint_id.get() - -def set_config(config: Dict[str, Any]) -> None: - """Set the request config for the current request context""" - _config.set(config) - -def get_config() -> Optional[Dict[str, Any]]: - """Get the request config from the current request context""" - return _config.get() - -def set_request_context(session_id: str, workflow_checkpoint_id: Optional[int] = None, config: Optional[Dict[str, Any]] = None) -> None: - """Set all request context variables at once""" - set_session_id(session_id) - if workflow_checkpoint_id is not None: - set_workflow_checkpoint_id(workflow_checkpoint_id) - if config is not None: - set_config(config) - -def clear_request_context() -> None: - """Clear all request context variables""" - _session_id.set(None) - _workflow_checkpoint_id.set(None) - _config.set(None) - -def set_rewrite_context(rewrite_context: RewriteContext) -> None: - """Set the rewrite context for the current request context""" - _rewrite_context.set(rewrite_context) - -def get_rewrite_context() -> RewriteContext: - """Get the rewrite context from the current request context""" - context = _rewrite_context.get() - if context is None: - # 初始化一个新的 RewriteContext - context = RewriteContext() - _rewrite_context.set(context) - return context \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/entry/comfyui-bridge.js b/custom_nodes/ComfyUI-Copilot/entry/comfyui-bridge.js deleted file mode 100644 index 7a10398147380894c2fb9dd8421cc424349feaa8..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/entry/comfyui-bridge.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2025 AIDC-AI -// Licensed under the MIT License. - -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; - -// 定义事件常量 -const COPILOT_EVENTS = { - EXPLAIN_NODE: 'copilot:explain-node', - TOOLBOX_USAGE: 'copilot:toolbox-usage', - TOOLBOX_PARAMETERS: 'copilot:toolbox-parameters', - TOOLBOX_DOWNSTREAMNODES: 'copilot:toolbox-downstreamnodes', -}; - -const COPILOT_TOOLBOX_IDS = { - USAGE: 'Copilot.Toolbox.Usage', - PARAMETERS: 'Copilot.Toolbox.Parameters', - DOWNSTREAMNODES: 'Copilot.Toolbox.DownstreamNodes' -} - -function addExtraMenuOptions(nodeType, nodeData, app) { - const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.getExtraMenuOptions = function (_, options) { - original_getExtraMenuOptions?.apply(this, arguments); - options.push({ - content: "Explain with Copilot", - callback: async () => { - const nodeTypeUniqueId = nodeType?.comfyClass; - // 触发自定义事件 - window.dispatchEvent(new CustomEvent(COPILOT_EVENTS.EXPLAIN_NODE, { - detail: { nodeType: nodeTypeUniqueId } - })); - } - }) - } -} - -app.registerExtension({ - name: "ComfyUI-Copilot-Bridge", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - addExtraMenuOptions(nodeType, nodeData, app); - }, - commands: [ - { - id: COPILOT_TOOLBOX_IDS.USAGE, - label: "Usage", - icon: 'pi pi-info-circle', - function: () => { - window.dispatchEvent(new CustomEvent(COPILOT_EVENTS.TOOLBOX_USAGE, { - - })); - } - }, - { - id: COPILOT_TOOLBOX_IDS.PARAMETERS, - label: 'Parameters', - icon: 'pi pi-objects-column', - function: () => { - window.dispatchEvent(new CustomEvent(COPILOT_EVENTS.TOOLBOX_PARAMETERS, { - - })); - } - }, - { - id: COPILOT_TOOLBOX_IDS.DOWNSTREAMNODES, - label: 'Downstream Nodes', - icon: 'pi pi-arrow-circle-right', - function: () => { - window.dispatchEvent(new CustomEvent(COPILOT_EVENTS.TOOLBOX_DOWNSTREAMNODES, { - - })); - } - } - ], - // Return an array of command IDs to show in the selection toolbox - // when an item is selected - getSelectionToolboxCommands: (selectedItem) => [COPILOT_TOOLBOX_IDS.USAGE, COPILOT_TOOLBOX_IDS.PARAMETERS, COPILOT_TOOLBOX_IDS.DOWNSTREAMNODES] -}) \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/entry/entry.js b/custom_nodes/ComfyUI-Copilot/entry/entry.js deleted file mode 100644 index 4cba5ed980ba9aa0b5297791001b2eba2c77f342..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/entry/entry.js +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2025 AIDC-AI -// Licensed under the MIT License. - -//@ts-ignore -import { api } from "../../scripts/api.js"; - -setTimeout(() => { - import(api.api_base + "/copilot_web/input.js"); - const fontsLink = document.createElement("link"); - fontsLink.rel = "stylesheet"; - fontsLink.href = api.api_base + "/copilot_web/fonts.css"; - document.head.appendChild(fontsLink); -}, 500); diff --git a/custom_nodes/ComfyUI-Copilot/locales/en/main.json b/custom_nodes/ComfyUI-Copilot/locales/en/main.json deleted file mode 100644 index 2a94fc6ccfdc61dac14a48407c8385a483c3493d..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/locales/en/main.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "menuLabels": { - "Usage": "Usage", - "Parameters": "Parameters", - "Downstream Nodes": "Downstream Nodes" - } -} \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/public/showcase/showcase.json b/custom_nodes/ComfyUI-Copilot/public/showcase/showcase.json deleted file mode 100644 index f5ba83140b4e51ed6cd34f5c6d7749251041a9d2..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/public/showcase/showcase.json +++ /dev/null @@ -1,210 +0,0 @@ - -[ - { - "type": "chat", - "tool": "workflow_generate", - "name": "给我一个工作流,基于ltx创建一个短视频", - "messages": [ - { - "id": "showcase1-3", - "role": "user", - "content": "给我一个工作流,基于ltx创建一个短视频", - "trace_id": "showcase1" - }, - { - "id": "showcase1-4", - "role": "ai", - "content": "{\"session_id\":\"611f8fda-e441-4e87-9fe3-59f51c29e910\",\"text\":\"\\n\\n🔄 **Switching to ComfyUI-Copilot**\\n\\n### Workflow Recommendations for Creating Short Video Clips from LTX\\n\\nHere are some workflow options to help you create short video clips based on \\\"ltx\\\":\\n\\n#### 1. Recalled Workflows\\n\\nBased on your request, I found these existing workflows that might be helpful:\\n\\n* **api-t2i2v-txt\\\\_to\\\\_image\\\\_to\\\\_video**\\n * **Description:** Creates a short animated video clip from a text description of a nature scene.\\n * **Use Case:** Ideal if your \\\"ltx\\\" content can be translated into a text description of a scene you want to animate.\\n * **Image:** ![api-t2i2v-txt\\\\_to\\\\_image\\\\_to\\\\_video](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2F648f8d5e2e924162852e98f2a249e3b0.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066264036&Signature=ZcP5qLm%2B4W23KXaNhJlQnPJljDw%3D)\\n\\n* **sdxl-t2i2v-api**\\n * **Description:** Creates a short animated video clip based on your text description.\\n * **Use Case:** Similar to the above, this workflow is good for generating smooth, high-quality animations from text prompts.\\n * **Image:** ![sdxl-t2i2v-api](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Fbb9685f63da3498f8e743172a8083f9d.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066264381&Signature=BiWhDeYVswBNpH23UtP9WqDNUEk%3D)\\n\\n* **api-t2v\\\\_ltxv\\\\_text\\\\_to\\\\_video**\\n * **Description:** Creates a short animated video clip (about 4 seconds) from text, with a focus on natural and fluid movement. This workflow specifically mentions \\\"ltxv\\\".\\n * **Use Case:** This workflow seems highly relevant as it explicitly includes \\\"ltxv\\\" in its description, suggesting it might be directly applicable to your \\\"ltx\\\" content.\\n * **Image:** ![api-t2v\\\\_ltxv\\\\_text\\\\_to\\\\_video](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2F2f5343c2b19c4afba48b8f9079ed736d.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066264124&Signature=w1X1XSG3Pxay02KBI3twT%2B3xEpQ%3D)\\n\\n#### 2. AI Generated Workflow\\n\\nI have also generated a new personalized workflow solution for you:\\n\\n* **AI Generated Workflow**\\n * **Description:** Build a workflow that creates a short video clip based on 'ltx' content.\\n * **Use Case:** This is a custom-generated workflow tailored to your specific request.\\n * **Image:** ![AI Generated Workflow](https://ppc-img.oss-ap-southeast-1.aliyuncs.com/AI_workflow_default.jpg)\\n\\nPlease let me know if you have any questions about these workflows or would like further adjustments!\",\"finished\":true,\"type\":\"message\",\"format\":\"markdown\",\"ext\":[{\"type\":\"workflow\",\"data\":[{\"id\":31809,\"name\":\"AI Generated Workflow\",\"description\":\"Build a workflow that creates a short video clip based on 'ltx' content.\",\"image\":\"https://ppc-img.oss-ap-southeast-1.aliyuncs.com/AI_workflow_default.jpg\",\"unique_id\":\"4d7e074f326a544bfcba0daf615ae220\",\"score\":null,\"source\":\"AI Generated\"},{\"id\":26740,\"name\":\"api-t2i2v-txt_to_image_to_video\",\"description\":\"Create a short animated video clip from a text description of a nature scene. I want to describe a beautiful landscape and have it turned into a flowing video with natural movement, like clouds drifting or water flowing, without needing to provide any reference video or images.\",\"image\":\"https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2F648f8d5e2e924162852e98f2a249e3b0.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066264036&Signature=ZcP5qLm%2B4W23KXaNhJlQnPJljDw%3D\",\"unique_id\":\"554fd2c996083c5b9525051d49a6b2f5\",\"score\":[0.5690833843847055,0.5]},{\"id\":26745,\"name\":\"sdxl-t2i2v-api\",\"description\":\"Create a short animated video clip based on my text description. I want to be able to type in what I want to see (like \\\"a cute dog running on the beach\\\"), and get back a smooth, high-quality animation that brings my description to life. The video should be a few seconds long and loop seamlessly.\",\"image\":\"https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Fbb9685f63da3498f8e743172a8083f9d.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066264381&Signature=BiWhDeYVswBNpH23UtP9WqDNUEk%3D\",\"unique_id\":\"ef568bd54974e83122c8ac59ba02c8c2\",\"score\":[0.616972625255589,0.5]},{\"id\":26831,\"name\":\"api-t2v_ltxv_text_to_video\",\"description\":\"Create a short animated video clip (about 4 seconds) showing two women having a conversation, where one has brown hair wearing a black jacket and the other has blonde hair. Make sure the video looks natural and fluid, focusing on the brown-haired woman's face with warm sunset lighting.\",\"image\":\"https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2F2f5343c2b19c4afba48b8f9079ed736d.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066264124&Signature=w1X1XSG3Pxay02KBI3twT%2B3xEpQ%3D\",\"unique_id\":\"6fc80d5671a79696ddff0db0bd39ee02\",\"score\":[0.589295913064967,1]}]}],\"message_id\":\"3cd1418e-e57c-4c01-9005-93365b0acc70\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "chat", - "tool": "workflow_generate", - "name": "给我一个工作流,把图片变成动漫风格", - "messages": [ - { - "id": "showcase1-5", - "role": "user", - "content": "给我一个工作流,把图片变成动漫风格", - "trace_id": "showcase1" - }, - { - "id": "showcase1-6", - "role": "ai", - "content": "{\"session_id\":\"fd931296-4337-4f1a-bd23-27b3a17caf07\",\"text\":\"\\n\\n🔄 **Switching to ComfyUI-Copilot**\\n\\n### Image to Anime Style Workflows\\n\\nHere are some workflow options to transform your images into an anime style:\\n\\n#### Recalled Workflows\\n\\n1. **api-i2i_image2image**\\n * **Description:** This workflow is designed to modify portrait photos to add anime-style features like furry ears while preserving most of the original facial features. It aims for a clean final image without watermarks or text overlays.\\n * **Image:** ![api-i2i_image2image](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Ff3a37b904b6d4ad4ae7db5039c8fffe9.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066263152&Signature=DsEDthKG%2BEHXHWO3S0dSD7rRURY%3D)\\n\\n2. **api-i2i_flux_canny_model_example**\\n * **Description:** This workflow focuses on transforming portrait photos into anime-style characters while maintaining the basic pose and composition of the original photo. It can incorporate specific anime features such as fennec ears, a fluffy tail, blonde hair, and blue eyes, with characters wearing casual clothes. The goal is a high-quality anime illustration.\\n * **Image:** ![api-i2i_flux_canny_model_example](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Fa36288a992704596b74556539fefbd25.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066263038&Signature=oTHt155s64%2FHAzETosaTYesTUc%3D)\\n\\n3. **api-i2v_image_to_video_wan**\\n * **Description:** While primarily for animation, this workflow involves an initial image of an anime character and can be used to create movement from a still anime image. This might be relevant if you later want to animate the anime-styled output.\\n * **Image:** ![api-i2v_image_to_video_wan](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Fbb12a2decb0c4f55a66caefa98eaf1d8.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066263536&Signature=c9MxE0hqAMEb4%2FNGG7UnwRylz6Y%3D)\\n\\n#### AI Generated Workflow\\n\\n* **Name:** AI Generated Workflow\\n* **Description:** This custom workflow is designed to transform an input image into an anime art style, effectively preserving key features from the original image while applying characteristic anime aesthetics.\\n* **Image:** ![AI Generated Workflow](https://ppc-img.oss-ap-southeast-1.aliyuncs.com/AI_workflow_default.jpg)\\n\\nYou can choose one of the recommended workflows above, or explore the AI-generated solution. If you have any further questions about these workflows, feel free to ask!\",\"finished\":true,\"type\":\"message\",\"format\":\"markdown\",\"ext\":[{\"type\":\"workflow\",\"data\":[{\"id\":31813,\"name\":\"AI Generated Workflow\",\"description\":\"Build a workflow that transforms an input image into an anime art style, preserving key features while applying anime aesthetics.\",\"image\":\"https://ppc-img.oss-ap-southeast-1.aliyuncs.com/AI_workflow_default.jpg\",\"unique_id\":\"578e2c1ebc888b09319bfc7310dd3b02\",\"score\":null,\"source\":\"AI Generated\"},{\"id\":26711,\"name\":\"api-i2i_image2image\",\"description\":\"I want to modify my portrait photo to add anime-style features like furry ears while keeping most of my original facial features intact. The final image should look clean without any watermarks or text overlays.\",\"image\":\"https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Ff3a37b904b6d4ad4ae7db5039c8fffe9.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066263152&Signature=DsEDthKG%2BEHXHWO3S0dSD7rRURY%3D\",\"unique_id\":\"7326dc2eb7014aea9bb8853293f1261b\",\"score\":[0.5843760618939373,0.5]},{\"id\":26819,\"name\":\"api-i2i_flux_canny_model_example\",\"description\":\"I want to transform my portrait photos into anime-style characters while keeping the basic pose and composition of the original photo. The anime character should have specific features like fennec ears and a fluffy tail, with blonde hair and blue eyes, wearing casual clothes. The final result should look like a high-quality anime illustration.\",\"image\":\"https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Fa36288a992704596b74556539fefbd25.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066263038&Signature=oTHt155s64%2FLHAzETosaTYesTUc%3D\",\"unique_id\":\"8e456b0c483b5eb64b4aac78bd5cd4be\",\"score\":[0.6201659297745311,0.5]},{\"id\":26822,\"name\":\"api-i2v_image_to_video_wan\",\"description\":\"I have a still image of an anime character and I want to create a short animation where the character smoothly turns around. The animation should maintain the character's original style and details while adding natural movement. Can you create a workflow that can do this automatically?\",\"image\":\"https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-06-26%2Fbb12a2decb0c4f55a66caefa98eaf1d8.png?x-oss-process=style%2Fresize&OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2066263536&Signature=c9MxE0hqAMEb4%2FNGG7UnwRylz6Y%3D\",\"unique_id\":\"0ed3d66aea19873488df2b6821dcf7f0\",\"score\":[0.632876618757651,0.5]}]}],\"message_id\":\"8895ed9c-f188-4bdb-9646-13b0ec909aa2\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "chat", - "tool": "workflow_generate", - "name": "给我一个编辑图片的工作流", - "messages": [ - { - "id": "showcase1-7", - "role": "user", - "content": "给我一个编辑图片的工作流", - "trace_id": "showcase1" - }, - { - "id": "showcase1-8", - "role": "ai", - "content": "{\"session_id\":\"bb570efe-14c4-4bde-b595-593026375d5f\",\"text\":\"\\n\\n🔄 **Switching to ComfyUI-Copilot**\\n\\n### Image Editing Workflows\\n\\nHere are some workflow options for image editing, including both recalled and newly generated solutions:\\n\\n#### **Recalled Workflows**\\n\\nBased on your request, I've found a few relevant workflows that handle various image editing tasks:\\n\\n1. **image-edit-api (1)**\\n * **Description:** This workflow allows you to modify photos using text descriptions. For example, you can change hair color or add specific features while preserving the natural look and original composition of the image. It takes your photo and a text description of the desired changes as input.\\n\\n2. **image-edit-api**\\n * **Description:** This workflow is designed for modifying photos of people. You can add specific objects or change expressions while maintaining the person's identity and overall appearance. For instance, you could make someone smile more or add a coffee cup they're holding, with the changes blending in seamlessly.\\n\\n3. **i2i-editing**\\n * **Description:** If you have a landscape image and want to extend its canvas size in any direction, this workflow can help. It maintains the original artistic style and atmosphere, ensuring the new areas blend seamlessly with the existing image.\\n\\n#### **AI Generated Workflow**\\n\\nI've also generated a new, personalized workflow solution for general image editing:\\n\\n1. **AI Generated Workflow**\\n * **Description:** This workflow is designed to perform general image editing operations, including resizing, cropping, and color correction. It provides a versatile solution for common image manipulation needs.\\n\\nYou can choose one of the recommended workflows above, or let AI generate a personalized solution based on your input. If you have any specific questions about any of these workflows, feel free to ask!\",\"finished\":true,\"type\":\"message\",\"format\":\"markdown\",\"ext\":[{\"type\":\"workflow\",\"data\":[{\"id\":31815,\"name\":\"AI Generated Workflow\",\"description\":\"Build a workflow that allows for general image editing, including operations like resizing, cropping, and color correction.\",\"image\":\"https://ppc-img.oss-ap-southeast-1.aliyuncs.com/AI_workflow_default.jpg\",\"unique_id\":\"6f7faac37e32285a935b8699ecc70c15\",\"score\":null,\"source\":\"AI Generated\"},{\"id\":26728,\"name\":\"image-edit-api (1)\",\"description\":\"I want to modify my photos by describing the changes I want in plain text. For example, I want to be able to change hair color or add specific features while keeping the rest of the image looking natural and maintaining the original composition. The workflow should take my photo and a text description of the desired changes as input, and produce a modified version of my photo that incorporates these changes.\",\"image\":\"\",\"unique_id\":\"7c9029b78952b52475f5a77d3b5dd781\",\"score\":[0.6382076740264893,0]},{\"id\":26733,\"name\":\"image-edit-api\",\"description\":\"I have a photo of a person, and I'd like to modify it to add specific objects or change their expression while keeping their identity and overall appearance intact. For example, I want to make them smile more and add objects they're holding, like a coffee cup. The changes should look natural and seamless.\",\"image\":\"\",\"unique_id\":\"4080a7a9b33783892cf3cf4fed1755de\",\"score\":[0.5695966821668564,0]},{\"id\":26776,\"name\":\"i2i-editing\",\"description\":\"I have a beautiful landscape image and I want to extend its canvas size in all directions while maintaining the same artistic style and atmosphere. The new areas should blend seamlessly with the original image and follow the same artistic pattern. Can you create a workflow that achieves this?\",\"image\":\"\",\"unique_id\":\"2e7970867f24319161de9a09504350d3\",\"score\":[0.5364670477191844,0.25]}]}],\"message_id\":\"225cc1d8-10d9-44ae-bccd-007d4f3e88b2\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "chat", - "tool": "workflow_rewrite", - "name": "修改当前工作流,检测并裁剪出图片中的猫", - "messages": [ - { - "id": "showcase1-9", - "role": "user", - "content": "修改当前工作流,检测并裁剪出图片中的猫", - "trace_id": "showcase1" - }, - { - "id": "showcase1-10", - "role": "ai", - "content": "{\"session_id\":\"62d2eed7-a9c2-4b34-b596-4a73b9ce6dad\",\"text\":\"\\n▸ **Switching to Workflow Rewrite Agent**\\n\\nI'll help you rewrite the current workflow to add cat detection and cutout functionality. Let me first get the current workflow and then add the appropriate nodes for intelligent background removal.Now let me get the intelligent background removal experience to add cat detection and cutout functionality:Perfect! I'll rewrite your workflow to add intelligent cat detection and cutout functionality. I'll use the SAM (Segment Anything Model) with GroundingDINO for precise cat detection and segmentation.Perfect! I've successfully rewritten your workflow to add intelligent cat detection and cutout functionality. Here's what I've added:\\n\\n## New Features Added:\\n\\n### **Cat Detection & Segmentation System:**\\n1. **SAM Model Loader (Node 29)** - Loads the Segment Anything Model for precise segmentation\\n2. **GroundingDINO Model Loader (Node 30)** - Loads the object detection model for finding cats\\n3. **Cat Detection & Segmentation (Node 31)** - The main node that:\\n - Detects cats in your generated image using the prompt \\\"cat\\\"\\n - Uses a threshold of 0.3 for detection sensitivity\\n - Automatically segments and cuts out the detected cat with precise boundaries\\n\\n### **Enhanced Output Options:**\\n1. **Save Original Image (Node 20)** - Saves your original generated image with prefix \\\"ComfyUI_original\\\"\\n2. **Save Cat Cutout (Node 32)** - Saves the extracted cat as a separate image with prefix \\\"ComfyUI_cat_cutout\\\"\\n3. **Preview Cat Cutout (Node 33)** - Shows a preview of the extracted cat in the interface\\n\\n## How It Works:\\n1. Your original workflow generates the cat image as before\\n2. The generated image is fed into the GroundingDINO + SAM pipeline\\n3. GroundingDINO detects the cat using natural language (\\\"cat\\\" prompt)\\n4. SAM performs precise segmentation to cut out the cat with clean edges\\n5. You get both the original image and the extracted cat as separate outputs\\n\\n## Model Requirements:\\n- **SAM Model**: \\\"sam_vit_h (2.56GB)\\\" - High-quality segmentation model\\n- **GroundingDINO**: \\\"GroundingDINO_SwinT_OGC (694MB)\\\" - Object detection model\\n\\nThe workflow will now automatically detect and cut out any cats in your generated images with high precision, giving you clean cutouts suitable for further use or editing!\",\"finished\":true,\"type\":\"message\",\"format\":\"markdown\",\"ext\":[{\"type\":\"workflow_update\",\"data\":{\"workflow_data\":{\"2\":{\"inputs\":{\"noise\":[\"5\",0],\"guider\":[\"7\",0],\"sampler\":[\"4\",0],\"sigmas\":[\"3\",0],\"latent_image\":[\"10\",0]},\"class_type\":\"SamplerCustomAdvanced\",\"_meta\":{\"title\":\"SamplerCustomAdvanced\"}},\"3\":{\"inputs\":{\"scheduler\":\"simple\",\"steps\":20,\"denoise\":1,\"model\":[\"8\",0]},\"class_type\":\"BasicScheduler\",\"_meta\":{\"title\":\"BasicScheduler\"}},\"4\":{\"inputs\":{\"sampler_name\":\"euler\"},\"class_type\":\"KSamplerSelect\",\"_meta\":{\"title\":\"KSamplerSelect\"}},\"5\":{\"inputs\":{\"noise_seed\":571457442342604},\"class_type\":\"RandomNoise\",\"_meta\":{\"title\":\"RandomNoise\"}},\"6\":{\"inputs\":{\"clip_name1\":\"t5xxl_fp8_e4m3fn.safetensors\",\"clip_name2\":\"clip_l.safetensors\",\"type\":\"flux\",\"device\":\"default\"},\"class_type\":\"DualCLIPLoader\",\"_meta\":{\"title\":\"DualCLIPLoader\"}},\"7\":{\"inputs\":{\"model\":[\"8\",0],\"conditioning\":[\"11\",0]},\"class_type\":\"BasicGuider\",\"_meta\":{\"title\":\"BasicGuider\"}},\"8\":{\"inputs\":{\"unet_name\":\"flux1-krea-dev.safetensors\",\"weight_dtype\":\"fp8_e4m3fn\"},\"class_type\":\"UNETLoader\",\"_meta\":{\"title\":\"Load Diffusion Model\"}},\"9\":{\"inputs\":{\"vae_name\":\"ae.safetensors\"},\"class_type\":\"VAELoader\",\"_meta\":{\"title\":\"Load VAE\"}},\"10\":{\"inputs\":{\"width\":[\"27\",0],\"height\":[\"28\",0],\"batch_size\":1},\"class_type\":\"EmptyLatentImage\",\"_meta\":{\"title\":\"Empty Latent Image\"}},\"11\":{\"inputs\":{\"text\":[\"25\",0],\"clip\":[\"6\",0]},\"class_type\":\"CLIPTextEncode\",\"_meta\":{\"title\":\"CLIP Text Encode (Prompt)\"}},\"14\":{\"inputs\":{\"samples\":[\"2\",0],\"vae\":[\"9\",0]},\"class_type\":\"VAEDecode\",\"_meta\":{\"title\":\"VAE Decode\"}},\"20\":{\"inputs\":{\"filename_prefix\":\"ComfyUI_original\",\"images\":[\"14\",0]},\"class_type\":\"SaveImage\",\"_meta\":{\"title\":\"Save Original Image\"}},\"25\":{\"inputs\":{\"text\":\"a cat,simple background\"},\"class_type\":\"Text _O\",\"_meta\":{\"title\":\"Text _O\"}},\"27\":{\"inputs\":{\"value\":1024},\"class_type\":\"easy int\",\"_meta\":{\"title\":\"Int\"}},\"28\":{\"inputs\":{\"value\":1400},\"class_type\":\"easy int\",\"_meta\":{\"title\":\"Int\"}},\"29\":{\"inputs\":{\"model_name\":\"sam_vit_h (2.56GB)\"},\"class_type\":\"SAMModelLoader (segment anything)\",\"_meta\":{\"title\":\"SAM Model Loader\"}},\"30\":{\"inputs\":{\"model_name\":\"GroundingDINO_SwinT_OGC (694MB)\"},\"class_type\":\"GroundingDinoModelLoader (segment anything)\",\"_meta\":{\"title\":\"GroundingDINO Model Loader\"}},\"31\":{\"inputs\":{\"prompt\":\"cat\",\"threshold\":0.3,\"sam_model\":[\"29\",0],\"grounding_dino_model\":[\"30\",0],\"image\":[\"14\",0]},\"class_type\":\"GroundingDinoSAMSegment (segment anything)\",\"_meta\":{\"title\":\"Cat Detection & Segmentation\"}},\"32\":{\"inputs\":{\"filename_prefix\":\"ComfyUI_cat_cutout\",\"images\":[\"31\",0]},\"class_type\":\"SaveImage\",\"_meta\":{\"title\":\"Save Cat Cutout\"}},\"33\":{\"inputs\":{\"images\":[\"31\",0]},\"class_type\":\"PreviewImage\",\"_meta\":{\"title\":\"Preview Cat Cutout\"}}}}},{\"type\":\"workflow_rewrite_checkpoint\",\"data\":{\"checkpoint_id\":21,\"checkpoint_type\":\"workflow_rewrite_start\"}}],\"message_id\":\"b0ddb085-c47b-496a-9f63-ecb5f7afafe1\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "chat", - "tool": "workflow_debug", - "name": "调试当前工作流", - "messages": [ - { - - "id": "showcase1-11", - "role": "ai", - "content": "{\"text\":\"Would you like me to help you debug the current workflow on the canvas?\",\"ext\":[{\"type\":\"debug_start_checkpoint\",\"data\":{\"checkpoint_id\":2168,\"checkpoint_type\":\"debug_start\"}}]}", - "format": "debug_guide", - "name": "Assistant" - }, - { - - "id": "showcase1-12", - "role": "ai", - "content": "{\"text\":\"▸ **Switching to ComfyUI-Debug-Coordinator**\\n\\nI'll validate and debug your ComfyUI workflow. Let me start by checking the current state of the workflow to identify any errors.\\n\\n⚙ *ComfyUI-Debug-Coordinator is using run_workflow...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"success\\\": false, \\\"error\\\": {\\\"type\\\": \\\"prompt_outputs_failed_validation\\\", \\\"message\\\": \\\"Prompt outputs failed validation\\\", \\\"details\\\": \\\"\\\", \\\"extra_info\\\": {}}, \\\"node_errors\\\": {\\\"21\\\": {\\\"errors\\\": [{\\\"type\\\": \\\"re...\\n```\\n\\nI found an error in the workflow! There's a connection issue where node 21 (VAEDecode) is missing its required \\\"samples\\\" input. Let me analyze this error type to determine the best approach for fixing it.\\n\\n⚙ *ComfyUI-Debug-Coordinator is using analyze_error_type...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"error_type\\\": \\\"connection_error\\\", \\\"recommended_agent\\\": \\\"link_agent\\\", \\\"error_details\\\": [{\\\"error_type\\\": \\\"connection_error\\\", \\\"message\\\": \\\"Detected 4 connection-related issues\\\", \\\"details\\\": \\\"Connection or ...\\n```\\n\\nThis is a connection error where the VAEDecode node is missing its required \\\"samples\\\" input connection. I'll hand this off to the Link Agent to fix the missing connection.\\n\\n▸ **Switching to Link Agent**\\n\\nPerfect! Now I'm the Link Agent and will analyze and fix the connection issues in your workflow. Let me start by performing a comprehensive connection analysis.\\n\\n⚙ *Link Agent is using analyze_missing_connections...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"missing_connections\\\": [{\\\"node_id\\\": \\\"7\\\", \\\"node_class\\\": \\\"KSampler\\\", \\\"input_name\\\": \\\"latent_image\\\", \\\"input_config\\\": [\\\"LATENT\\\", {\\\"tooltip\\\": \\\"The latent image to denoise.\\\"}], \\\"required\\\": true, \\\"expected_t...\\n```\\n\\n**Connection analysis:** Found 5 missing required connections across multiple nodes:\\n- Node 7 (KSampler): missing latent_image input \\n- Node 21 (VAEDecode): missing samples input\\n- Node 28 (ReduxAdvanced): missing conditioning, style_model, and image inputs\\n\\nAll missing connections can be resolved using existing nodes with high confidence matches available.\\n\\n**Chosen strategy:** Connect existing nodes - all required inputs have perfect type-compatible outputs available from existing nodes. This is a pure connection fix scenario with no new nodes needed.\\n\\nLet me apply the connection fixes using the optimal connection strategy:\\n\\n⚙ *Link Agent is using apply_connection_fixes...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"success\\\": true, \\\"version_id\\\": 2171, \\\"applied_fixes\\\": [{\\\"type\\\": \\\"connection\\\", \\\"target\\\": \\\"7.latent_image\\\", \\\"source\\\": \\\"75[0]\\\", \\\"old_value\\\": \\\"not connected\\\", \\\"new_value\\\": [\\\"75\\\", 0]}, {\\\"type\\\": \\\"connectio...\\n```\\n\\n**Fixes applied:** Successfully connected all 5 missing required inputs:\\n- ✅ Connected EmptyLatentImage → KSampler (latent_image)\\n- ✅ Connected KSampler → VAEDecode (samples) \\n- ✅ Connected CLIPTextEncode → ReduxAdvanced (conditioning)\\n- ✅ Connected StyleModelLoader → ReduxAdvanced (style_model)\\n- ✅ Connected LoadImage → ReduxAdvanced (image)\\n\\nAll connections were established with high confidence type matches. The workflow now has a complete data flow from input nodes through processing to final output.\\n\\n▸ **Switching to ComfyUI-Debug-Coordinator**\\n\\nExcellent! The Link Agent has successfully fixed all the missing connections. Now let me validate the workflow again to confirm all issues have been resolved.\\n\\n⚙ *ComfyUI-Debug-Coordinator is using run_workflow...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"success\\\": false, \\\"error\\\": {\\\"type\\\": \\\"prompt_outputs_failed_validation\\\", \\\"message\\\": \\\"Prompt outputs failed validation\\\", \\\"details\\\": \\\"\\\", \\\"extra_info\\\": {}}, \\\"node_errors\\\": {\\\"3\\\": {\\\"errors\\\": [{\\\"type\\\": \\\"val...\\n```\\n\\nGreat progress! The connection issues have been resolved, but now there's a parameter error. Node 3 (LoraLoader) has an invalid LoRA name that's not in the available list. Let me analyze this error type.\\n\\n⚙ *ComfyUI-Debug-Coordinator is using analyze_error_type...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"error_type\\\": \\\"mixed_connection_error\\\", \\\"recommended_agent\\\": \\\"link_agent\\\", \\\"error_details\\\": [{\\\"error_type\\\": \\\"connection_error\\\", \\\"message\\\": \\\"Detected 3 connection-related issues\\\", \\\"details\\\": \\\"Connecti...\\n```\\n\\nThis is actually a parameter error (value_not_in_list), not a connection error. The analysis might be mixed up. Let me hand this off to the Parameter Agent to fix the invalid LoRA name.\\n\\n▸ **Switching to Parameter Agent**\\n\\nIssue identified: enum_value_mismatch - LoRA file 'flux/Flux_Ghibli_v2_25.safetensors' not found in available LoRA list.\\n\\nLet me analyze this parameter error to find a suitable replacement:\\n\\n⚙ *Parameter Agent is using find_matching_parameter_value...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"found_match\\\": false, \\\"error_type\\\": \\\"model_missing\\\", \\\"message\\\": \\\"Missing model file: flux/Flux_Ghibli_v2_25.safetensors\\\", \\\"recommendation\\\": \\\"Check for available model replacements, otherwise download...\\n```\\n\\nGreat! I can see the available LoRA list and found several Ghibli-style LoRAs. The requested \\\"flux/Flux_Ghibli_v2_25.safetensors\\\" is not available, but there are several similar Ghibli LoRAs in the FLUX folder. \\n\\nLooking at the available options, the closest matches are:\\n- \\\"FLUX/Flux_Ghibli_people_v3_23.safetensors\\\"\\n- \\\"FLUX/Flux_Ghibli_v1_10.safetensors\\\", \\\"FLUX/Flux_Ghibli_v1_15.safetensors\\\", \\\"FLUX/Flux_Ghibli_v1_20.safetensors\\\"\\n- \\\"FLUX/Flux_Ghibli_v2_30.safetensors\\\"\\n\\nThe \\\"FLUX/Flux_Ghibli_v2_30.safetensors\\\" seems like the best match since it's from the same v2 series and has a higher version number (30 vs 25).\\n\\n**Solution:** auto-fixed - Replacing with the most similar available Ghibli v2 version LoRA.\\n\\n⚙ *Parameter Agent is using update_workflow_parameter...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"success\\\": true, \\\"answer\\\": \\\"Successfully updated lora_name from 'flux/Flux_Ghibli_v2_25.safetensors' to 'FLUX/Flux_Ghibli_v2_30.safetensors' in node 3\\\", \\\"node_id\\\": \\\"3\\\", \\\"parameter\\\": \\\"lora_name\\\", \\\"old...\\n```\\n\\n**Status:** Fixed - Successfully updated the LoRA from unavailable 'flux/Flux_Ghibli_v2_25.safetensors' to the available 'FLUX/Flux_Ghibli_v2_30.safetensors' which is a very similar Ghibli v2 style LoRA.\\n\\n▸ **Switching to ComfyUI-Debug-Coordinator**\\n\\nExcellent! The Parameter Agent has fixed the invalid LoRA name. Now let me validate the workflow again to check if all issues have been resolved.\\n\\n⚙ *ComfyUI-Debug-Coordinator is using run_workflow...*\\n\\n\\n\\n● *Tool execution completed*\\n\\n```\\n{\\\"success\\\": true, \\\"prompt_id\\\": \\\"a028058b-4aab-4f9b-9d0d-48c12ace616b\\\", \\\"number\\\": 3, \\\"node_errors\\\": {}}\\n```\\n\\n🎉 **Workflow Validation Complete - SUCCESS!**\\n\\nYour ComfyUI workflow has been successfully debugged and is now fully functional! Here's a summary of the issues that were fixed:\\n\\n## Issues Fixed:\\n\\n1. **Connection Issues (5 fixes via Link Agent):**\\n - ✅ Connected EmptyLatentImage (75) → KSampler (7) latent_image input\\n - ✅ Connected KSampler (7) → VAEDecode (21) samples input \\n - ✅ Connected CLIPTextEncode (8) → ReduxAdvanced (28) conditioning input\\n - ✅ Connected StyleModelLoader (29) → ReduxAdvanced (28) style_model input\\n - ✅ Connected LoadImage (6) → ReduxAdvanced (28) image input\\n\\n2. **Parameter Issue (1 fix via Parameter Agent):**\\n - ✅ Updated invalid LoRA: `flux/Flux_Ghibli_v2_25.safetensors` → `FLUX/Flux_Ghibli_v2_30.safetensors`\\n\\n## Current Workflow Status:\\n- **Validation:** ✅ PASSED\\n- **Prompt ID:** a028058b-4aab-4f9b-9d0d-48c12ace616b\\n- **Node Errors:** None\\n- **Ready to Execute:** Yes\\n\\nYour workflow now has a complete data flow from input nodes through processing to final output, with all required connections established and valid parameters configured. The workflow is ready for execution!\",\"ext\":[{\"type\":\"param_update\",\"data\":{\"workflow_data\":{\"1\":{\"inputs\":{\"unet_name\":\"flux1-dev.safetensors\",\"weight_dtype\":\"default\"},\"class_type\":\"UNETLoader\",\"_meta\":{\"title\":\"Load Diffusion Model\"}},\"2\":{\"inputs\":{\"clip_name1\":\"clip_l.safetensors\",\"clip_name2\":\"t5xxl_fp16.safetensors\",\"type\":\"flux\",\"device\":\"default\"},\"class_type\":\"DualCLIPLoader\",\"_meta\":{\"title\":\"DualCLIPLoader\"}},\"3\":{\"inputs\":{\"lora_name\":\"FLUX/Flux_Ghibli_v2_30.safetensors\",\"strength_model\":0.8,\"strength_clip\":1,\"model\":[\"1\",0],\"clip\":[\"2\",0]},\"class_type\":\"LoraLoader\",\"_meta\":{\"title\":\"Load LoRA\"}},\"6\":{\"inputs\":{\"image\":\"狗-德国短毛指示犬-4.jpeg\"},\"class_type\":\"LoadImage\",\"_meta\":{\"title\":\"Load Image\"}},\"7\":{\"inputs\":{\"seed\":720601145747867,\"steps\":20,\"cfg\":1,\"sampler_name\":\"euler\",\"scheduler\":\"normal\",\"denoise\":1,\"model\":[\"3\",0],\"positive\":[\"28\",0],\"negative\":[\"9\",0],\"latent_image\":[\"75\",0]},\"class_type\":\"KSampler\",\"_meta\":{\"title\":\"KSampler\"}},\"8\":{\"inputs\":{\"text\":\"Ghibli style,illustration,graphic,2d,Rich colors\",\"clip\":[\"3\",1]},\"class_type\":\"CLIPTextEncode\",\"_meta\":{\"title\":\"CLIP Text Encode (Prompt)\"}},\"9\":{\"inputs\":{\"text\":\"\",\"clip\":[\"3\",1]},\"class_type\":\"CLIPTextEncode\",\"_meta\":{\"title\":\"CLIP Text Encode (Prompt)\"}},\"20\":{\"inputs\":{\"vae_name\":\"ae.safetensors\"},\"class_type\":\"VAELoader\",\"_meta\":{\"title\":\"Load VAE\"}},\"21\":{\"inputs\":{\"vae\":[\"20\",0],\"samples\":[\"7\",0]},\"class_type\":\"VAEDecode\",\"_meta\":{\"title\":\"VAE Decode\"}},\"28\":{\"inputs\":{\"downsampling_factor\":1,\"downsampling_function\":\"area\",\"mode\":\"keep aspect ratio\",\"weight\":0.6,\"autocrop_margin\":0.1,\"clip_vision\":[\"30\",0],\"conditioning\":[\"8\",0],\"style_model\":[\"29\",0],\"image\":[\"6\",0]},\"class_type\":\"ReduxAdvanced\",\"_meta\":{\"title\":\"ReduxAdvanced\"}},\"29\":{\"inputs\":{\"style_model_name\":\"flux1-redux-dev.safetensors\"},\"class_type\":\"StyleModelLoader\",\"_meta\":{\"title\":\"Load Style Model\"}},\"30\":{\"inputs\":{\"clip_name\":\"sigclip_vision_patch14_384.safetensors\"},\"class_type\":\"CLIPVisionLoader\",\"_meta\":{\"title\":\"Load CLIP Vision\"}},\"75\":{\"inputs\":{\"width\":768,\"height\":1344,\"batch_size\":1},\"class_type\":\"EmptyLatentImage\",\"_meta\":{\"title\":\"Empty Latent Image\"}},\"78\":{\"inputs\":{\"images\":[\"21\",0]},\"class_type\":\"PreviewImage\",\"_meta\":{\"title\":\"Preview Image\"}}},\"changes\":[{\"node_id\":\"3\",\"parameter\":\"lora_name\",\"old_value\":\"flux/Flux_Ghibli_v2_25.safetensors\",\"new_value\":\"FLUX/Flux_Ghibli_v2_30.safetensors\"}]}},{\"type\":\"debug_complete\",\"data\":{\"status\":\"completed\",\"final_agent\":\"ComfyUI-Debug-Coordinator\",\"events\":[{\"type\":\"agent_handoff\",\"current_agent\":\"ComfyUI-Debug-Coordinator\",\"to_agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":46},{\"type\":\"tool_call\",\"tool\":\"run_workflow\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":233},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"success\\\": false, \\\"error\\\": {\\\"type\\\": \\\"prompt_outputs_failed_validation\\\", \\\"message\\\": \\\"Prompt outputs failed validation\\\", \\\"details\\\": \\\"\\\", \\\"extra_info\\\": {}}, \\\"node_errors\\\": {\\\"21\\\": {\\\"errors\\\": [{\\\"type\\\": \\\"re...\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":478},{\"type\":\"tool_call\",\"tool\":\"analyze_error_type\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":746},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"error_type\\\": \\\"connection_error\\\", \\\"recommended_agent\\\": \\\"link_agent\\\", \\\"error_details\\\": [{\\\"error_type\\\": \\\"connection_error\\\", \\\"message\\\": \\\"Detected 4 connection-related issues\\\", \\\"details\\\": \\\"Connection or ...\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":991},{\"type\":\"agent_handoff\",\"current_agent\":\"Link Agent\",\"to_agent\":\"Link Agent\",\"timestamp\":1195},{\"type\":\"tool_call\",\"tool\":\"analyze_missing_connections\",\"agent\":\"Link Agent\",\"timestamp\":1413},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"missing_connections\\\": [{\\\"node_id\\\": \\\"7\\\", \\\"node_class\\\": \\\"KSampler\\\", \\\"input_name\\\": \\\"latent_image\\\", \\\"input_config\\\": [\\\"LATENT\\\", {\\\"tooltip\\\": \\\"The latent image to denoise.\\\"}], \\\"required\\\": true, \\\"expected_t...\",\"agent\":\"Link Agent\",\"timestamp\":1658},{\"type\":\"tool_call\",\"tool\":\"apply_connection_fixes\",\"agent\":\"Link Agent\",\"timestamp\":2344},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"success\\\": true, \\\"version_id\\\": 2171, \\\"applied_fixes\\\": [{\\\"type\\\": \\\"connection\\\", \\\"target\\\": \\\"7.latent_image\\\", \\\"source\\\": \\\"75[0]\\\", \\\"old_value\\\": \\\"not connected\\\", \\\"new_value\\\": [\\\"75\\\", 0]}, {\\\"type\\\": \\\"connectio...\",\"agent\":\"Link Agent\",\"timestamp\":2589},{\"type\":\"agent_handoff\",\"current_agent\":\"ComfyUI-Debug-Coordinator\",\"to_agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":3145},{\"type\":\"tool_call\",\"tool\":\"run_workflow\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":3361},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"success\\\": false, \\\"error\\\": {\\\"type\\\": \\\"prompt_outputs_failed_validation\\\", \\\"message\\\": \\\"Prompt outputs failed validation\\\", \\\"details\\\": \\\"\\\", \\\"extra_info\\\": {}}, \\\"node_errors\\\": {\\\"3\\\": {\\\"errors\\\": [{\\\"type\\\": \\\"val...\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":3606},{\"type\":\"tool_call\",\"tool\":\"analyze_error_type\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":3873},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"error_type\\\": \\\"mixed_connection_error\\\", \\\"recommended_agent\\\": \\\"link_agent\\\", \\\"error_details\\\": [{\\\"error_type\\\": \\\"connection_error\\\", \\\"message\\\": \\\"Detected 3 connection-related issues\\\", \\\"details\\\": \\\"Connecti...\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":4118},{\"type\":\"agent_handoff\",\"current_agent\":\"Parameter Agent\",\"to_agent\":\"Parameter Agent\",\"timestamp\":4341},{\"type\":\"tool_call\",\"tool\":\"find_matching_parameter_value\",\"agent\":\"Parameter Agent\",\"timestamp\":4595},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"found_match\\\": false, \\\"error_type\\\": \\\"model_missing\\\", \\\"message\\\": \\\"Missing model file: flux/Flux_Ghibli_v2_25.safetensors\\\", \\\"recommendation\\\": \\\"Check for available model replacements, otherwise download...\",\"agent\":\"Parameter Agent\",\"timestamp\":4840},{\"type\":\"tool_call\",\"tool\":\"update_workflow_parameter\",\"agent\":\"Parameter Agent\",\"timestamp\":5613},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"success\\\": true, \\\"answer\\\": \\\"Successfully updated lora_name from 'flux/Flux_Ghibli_v2_25.safetensors' to 'FLUX/Flux_Ghibli_v2_30.safetensors' in node 3\\\", \\\"node_id\\\": \\\"3\\\", \\\"parameter\\\": \\\"lora_name\\\", \\\"old...\",\"agent\":\"Parameter Agent\",\"timestamp\":5858},{\"type\":\"agent_handoff\",\"current_agent\":\"ComfyUI-Debug-Coordinator\",\"to_agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":6109},{\"type\":\"tool_call\",\"tool\":\"run_workflow\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":6312},{\"type\":\"tool_result\",\"output_preview\":\"{\\\"success\\\": true, \\\"prompt_id\\\": \\\"a028058b-4aab-4f9b-9d0d-48c12ace616b\\\", \\\"number\\\": 3, \\\"node_errors\\\": {}}\",\"agent\":\"ComfyUI-Debug-Coordinator\",\"timestamp\":6456}],\"total_events\":23}},{\"type\":\"debug_checkpoint\",\"data\":{\"checkpoint_id\":2173,\"checkpoint_type\":\"debug_complete\"}},{\"type\":\"debug_checkpoint\",\"data\":{\"checkpoint_id\":2174,\"checkpoint_type\":\"debug_complete\"}}]}", - "format": "markdown", - "name": "Assistant", - "finished": true, - "debugGuide": true - } - ] - }, - { - "type": "chat", - "tool": "node_recommend", - "name": "给我一个把多张图合并的节点", - "messages": [ - { - "id": "showcase2-1", - "role": "user", - "content": "给我一个把多张图合并的节点", - "trace_id": "showcase2" - }, - { - "id": "showcase2-2", - "role": "ai", - "content": "{\"session_id\":\"bbe71936-8402-4fd0-8bb4-69eecac5ee01\",\"text\":\"以下是可以合并多张图片的节点:\\n\\n1. **MonoMerge** \\n [GitHub链接](https://github.com/bmad4ever/comfyui_bmad_nodes): 将两张图片合并为单色图片,可选择在白色或黑色方向合并。\\n\\n2. **Concat Images (mtb)** \\n [GitHub链接](https://github.com/melMass/comfy_mtb): 可将多张图片合并成一个批次,适用于处理和操控图片集。\\n\\n3. **JoinImageBatch** \\n [GitHub链接](https://github.com/yolain/ComfyUI-Easy-Use): 合并批量图片成一张大图,支持水平或垂直拼接。\\n\\n4. **MergeLayers** \\n [GitHub链接](https://github.com/shadowcz007/comfyui-mixlab-nodes): 将多层图像合并为单张图片,支持指定的遮罩和混合选项。\\n\\n5. **Composite Images 🧅** \\n [GitHub链接](https://github.com/natto-maki/ComfyUI-NegiTools): 结合两张图片,支持多种复合方法,如乘法、划分、叠加等。\\n\\n6. **ImageContainerInheritanceSum** \\n [GitHub链接](https://github.com/Nourepide/ComfyUI-Allor): 通过总结图像维度以创建复合图像,适用于需要考虑输入图像总宽和高的情况。\\n\\n7. **Join Image Lists** \\n [GitHub链接](https://github.com/M1kep/Comfy_KepListStuff): 合并多张图片列表成为一个列表,同时提供原列表大小。\\n\\n8. **Merge Image List** \\n [GitHub链接](https://github.com/AInseven/ComfyUI-fastblend): 合并图像张量列表为一个单一张量,便于图像相关任务的批处理。\\n\\n根据你的具体需求选择合适的节点使用。\",\"finished\":true,\"format\":\"markdown\",\"ext\":[{\"type\":\"node\",\"data\":[{\"id\":28311,\"unique_id\":\"7cca1314298b9a9d4b785e009099ea28\",\"name\":\"MonoMerge\",\"name_oral\":\"MonoMerge\",\"description\":\"The MonoMerge node is designed to merge two images into a monochromatic image based on a target color scheme (either towards white or black). This process involves comparing the luminance (L component) of corresponding pixels from both images and selecting the higher or lower value depending on the target, resulting in an image that emphasizes either the lighter or darker aspects of the combined images.\",\"github_url\":\"https://github.com/bmad4ever/comfyui_bmad_nodes\",\"github_stars\":50,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":29658,\"unique_id\":\"e3a2782874d3bf8f99bb2b70c7090f74\",\"name\":\"Concat Images (mtb)\",\"name_oral\":\"Concat Images (mtb)\",\"description\":\"The Concat Images node is designed to merge multiple images into a single batch, allowing for efficient processing and manipulation of image collections. It provides flexibility in handling size mismatches through customizable strategies.\",\"github_url\":\"https://github.com/melMass/comfy_mtb\",\"github_stars\":350,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":29719,\"unique_id\":\"458e445b2f21e81111dc9a7d3fb8cb7b\",\"name\":\"easy joinImageBatch\",\"name_oral\":\"JoinImageBatch\",\"description\":\"This node is designed to merge a batch of images into a single large image, either by stacking them horizontally or vertically based on the specified mode. It abstracts the complexity of image manipulation and resizing, providing a straightforward way to create composite images from multiple inputs.\",\"github_url\":\"https://github.com/yolain/ComfyUI-Easy-Use\",\"github_stars\":510,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":32325,\"unique_id\":\"0ea3707eaaf8052e37630f95f4e5b72b\",\"name\":\"MergeLayers\",\"name_oral\":\"Merge Layers ♾️Mixlab\",\"description\":\"The MergeLayers node is designed to combine multiple image layers into a single image while applying specified masks and blending options. It facilitates the creation of complex images by layering and compositing various elements based on their defined properties.\",\"github_url\":\"https://github.com/shadowcz007/comfyui-mixlab-nodes\",\"github_stars\":null,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":32571,\"unique_id\":\"0bd39afd25f012f877c9676b3c9837a1\",\"name\":\"NegiTools_CompositeImages\",\"name_oral\":\"Composite Images 🧅\",\"description\":\"NegiTools_CompositeImages is a versatile node designed to combine two images using various compositing methods. This node allows you to blend images in multiple ways, such as multiplying, dividing, overlaying, and more, to achieve different visual effects.\",\"github_url\":\"https://github.com/natto-maki/ComfyUI-NegiTools\",\"github_stars\":30,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":32865,\"unique_id\":\"4e6b0fa27366545e19695c4e8931e270\",\"name\":\"ImageContainerInheritanceSum\",\"name_oral\":\"ImageContainerInheritanceSum\",\"description\":\"Efficiently combine and sum image dimensions for creating composite images with integrity and visual quality. This node is designed to facilitate the combination of multiple images by summing their dimensions. This node is particularly useful when you need to create a composite image that encompasses the total width and height of the input images.\",\"github_url\":\"https://github.com/Nourepide/ComfyUI-Allor\",\"github_stars\":200,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":72827,\"unique_id\":\"1c76ce5bcc2c5097bdf515bddd32fb0b\",\"name\":\"Join Image Lists\",\"name_oral\":\"Join Image Lists\",\"description\":\"This node is designed to combine multiple lists of images into a single list while also providing the sizes of the original lists. It facilitates the management of image data by allowing users to easily merge and track the number of images from various sources.\",\"github_url\":\"https://github.com/M1kep/Comfy_KepListStuff\",\"github_stars\":null,\"from_index\":0,\"to_index\":0,\"is_existed\":true},{\"id\":81206,\"unique_id\":\"1cc97ed3bc709de3f23c378eef1ce74e\",\"name\":\"Merge_Image_List\",\"name_oral\":\"Merge Image List\",\"description\":\"This node is designed to merge a list of image tensors into a single tensor, facilitating batch processing in image-related tasks. It efficiently concatenates the input images along a specified dimension, enabling seamless integration of multiple images for further processing.\",\"github_url\":\"https://github.com/AInseven/ComfyUI-fastblend\",\"github_stars\":null,\"from_index\":0,\"to_index\":0,\"is_existed\":true}]}],\"message_id\":\"f60c3cf2-0b54-47a3-97e3-f536fc992c81\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "chat", - "tool": "model_recommend", - "name": "给我一个宫崎骏风格的lora", - "messages": [ - { - "id": "showcase3-1", - "role": "user", - "content": "给我一个宫崎骏风格的lora", - "trace_id": "showcase3" - }, - { - "id": "showcase3-2", - "role": "ai", - "content": "{\"session_id\":\"0b26efaf-e417-4344-9942-658fedbbc12c\",\"text\":\"请问您需要基于哪种基座模型的宫崎骏风格LORA?可选择以下基座模型:\\n\\n1. Stable Diffusion v1.5\\n2. Illustrious\\n3. Pony\\n4. Flux.1\\n5. Flux.1 D\\n6. Stable Diffusion XL 1.0\",\"finished\":true,\"format\":\"markdown\",\"ext\":[{\"type\":\"guides\",\"data\":[\"Stable Diffusion v1.5\",\"Illustrious\",\"Pony\",\"Flux.1\",\"Flux.1 D\",\"Stable Diffusion XL 1.0\"]}],\"message_id\":\"cfecdc29-5131-4afa-9807-9fe911e6b670\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - }, - { - "id": "showcase3-3", - "role": "user", - "content": "Flux.1", - "trace_id": "showcase3" - }, - { - "id": "showcase3-4", - "role": "ai", - "content": "{\"session_id\":\"0b26efaf-e417-4344-9942-658fedbbc12c\",\"text\":\"以下是几个基于Flux.1的宫崎骏风格LORA模型:\\n\\n1. **宫崎骏风格插画** \\n - 描述:此模型擅长创作灵感来自宫崎骏风格的插图,展示奇幻、细致的风景和幻想元素。它强调丰富的质感、鲜艳的色彩和复杂的自然环境,擅长营造充满生机和暖意的场景,以讲述具有特色的宫崎骏风格的故事。 \\n - [获取链接](https://www.liblib.art/modelinfo/51b6a44f87b543a6b21dfbadc2952313) \\n\\n2. **宫崎骏的现实主义** \\n - 描述:模型擅长生成吉卜力风格的梦幻景观,展现葱郁的绿地、鲜亮的蓝色以及自然与建筑之间的和谐融合。它捕捉梦幻般的场景,具有柔和的幻想氛围,体现出在宁静森林和空中城堡间的魔幻现实主义。 \\n - ![示例图片1](https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||20d56a93ad9fd5daeaf043453766b60d.jpg) \\n - ![示例图片2](https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||4fca763050d3e8941193b4230e8e7410.jpg) \\n - [获取链接](https://www.liblib.art/modelinfo/c2ea3eebba764e388a3f08c2ca6eed3a) \\n\\n3. **F.1-宫崎骏风格** \\n - 描述:模型擅长生成宫崎骏影响下的动漫风格图像,呈现鲜艳的色彩、富有想象力的构图和生动的表现。专注于生成年轻女孩在各种迷人环境中的场景,擅长渲染云的细节纹理和复杂的光影。 \\n - [获取链接](https://www.liblib.art/modelinfo/5638cc813c264f899b43536e02abfc82) \\n\\n您可以选择一个适合您需求的模型进行使用。\",\"finished\":true,\"format\":\"markdown\",\"ext\":[{\"type\":\"model\",\"data\":[{\"id\":375989,\"name\":\"宫崎骏风格插画\",\"description\":\"This model excels in creating illustrations inspired by Miyazaki's style, featuring whimsical, detailed landscapes and fantastical elements. It consistently captures scenes brimming with life and warmth, emphasizing lush textures, vibrant colors, and intricate natural settings. This model is adept at generating compositions that convey a sense of wonder and storytelling in a distinctively Miyazaki fashion.\",\"model_type\":\"LORA\",\"model_algorithm\":\"Flux.1\",\"model_pics\":[],\"source_url\":\"https://www.liblib.art/modelinfo/51b6a44f87b543a6b21dfbadc2952313\",\"unique_id\":\"c3123b5ab427eebeaae884a3f1fe10cc\"},{\"id\":378349,\"name\":\"宫崎骏的现实主义\",\"description\":\"The model excels in generating whimsical, serene landscapes with a Ghibli-inspired aesthetic, featuring lush greenery, vibrant blues, and harmonious nature-architecture integration. It captures dreamy sceneries with a soft, fantasy-like ambiance, demonstrating strengths in depicting tranquil forests, ethereal sky castles, and interactions between natural and man-made elements with a magical realism touch.\",\"model_type\":\"LORA\",\"model_algorithm\":\"Flux.1\",\"model_pics\":[\"https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||20d56a93ad9fd5daeaf043453766b60d.jpg\",\"https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||4fca763050d3e8941193b4230e8e7410.jpg\",\"https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||b782cc39b8d159cb2c52fbb31b0c51b8.jpg\",\"https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||cddb9c4648639e78805dd2c088f3f039.jpg\",\"https://aib-data-sg.oss-ap-southeast-1.aliyuncs.com/ext-img/comuify/liblib||505b51e94736455c9d78ebe38afc3148||ef8b418d2ad9c6092c14c17e4aa1b051.jpg\"],\"source_url\":\"https://www.liblib.art/modelinfo/c2ea3eebba764e388a3f08c2ca6eed3a\",\"unique_id\":\"256ec6894dd15b42eb60c91fde22b61b\"},{\"id\":385880,\"name\":\"F.1-宫崎骏风格\",\"description\":\"The model excels in generating whimsical anime-style images influenced by Miyazaki's distinct aesthetic, characterized by vibrant colors, playful compositions, and lively expressions. It specializes in scenes featuring young girls in varied enchanting settings, demonstrating strong skills in rendering detailed textures like clouds and intricate lighting. Notably adept at creating charming and imaginative animated environments.\",\"model_type\":\"LORA\",\"model_algorithm\":\"Flux.1\",\"model_pics\":[],\"source_url\":\"https://www.liblib.art/modelinfo/5638cc813c264f899b43536e02abfc82\",\"unique_id\":\"cbcbc993133822bccd46f16d49f5a4db\"}]}],\"message_id\":\"6903403f-cd32-447e-a9a6-a9247ab77c15\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "chat", - "tool": "prompt", - "name": "基于我的描述生成prompt", - "messages": [ - { - "id": "showcase4-1", - "role": "user", - "content": "请为我生成英文prompt: \"一个红衣女子在海边跳舞,笑容很灿烂\"", - "trace_id": "showcase4" - }, - { - "id": "showcase4-2", - "role": "ai", - "content": "{\"session_id\":\"6272c460-04da-4b30-9090-1c05406f46e3\",\"text\":\"woman in a red dress dancing by the seaside, bright smile, high quality, cinematic lighting, detailed, realistic, wide shot\",\"finished\":true,\"format\":\"markdown\",\"ext\":null,\"message_id\":\"13f0a58e-8670-4457-97da-cbad7a7d7530\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - }, - { - "type": "genlab", - "tool": "genlab_prompt", - "name": "根据我的诉求生成3个prompt,各自生图查看效果", - "messages": [ - { - "id": "showcase5-1", - "role": "user", - "content": "根据我的诉求生成3个prompt,各自生图查看效果", - "trace_id": "showcase5" - }, - { - "id": "showcase5-2", - "role": "ai", - "content": "{\"text\":\"请根据下面的演示,使用Genlab来实现您的调试prompt的诉求。\\n ![](https://aib-vision-hz.oss-accelerate.aliyuncs.com/mood_board%2Finput%2F2025-07-16%2Fab89513606fd4966b0362f95f3f1ab86.gif?OSSAccessKeyId=LTAI5tQyhnsJhtiinV3B1gfV&Expires=2068018398&Signature=3XUgadQjjsGRu46S1fRxA7iDWJU%3D)\"}", - "format": "markdown", - "finished": true, - "name": "Assistant" - } - ] - } -] \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/public/workflows/face_restore.json b/custom_nodes/ComfyUI-Copilot/public/workflows/face_restore.json deleted file mode 100644 index 67565c4f8b38d9fdfaea6bc5119f39c6f4fb8e7a..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/public/workflows/face_restore.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "last_node_id": 5, - "last_link_id": 4, - "nodes": [ - { - "id": 1, - "type": "LoadImage", - "pos": [200, 200] - }, - { - "id": 2, - "type": "FaceRestore", - "pos": [500, 200] - } - ] - } diff --git a/custom_nodes/ComfyUI-Copilot/ui/.prettierrc b/custom_nodes/ComfyUI-Copilot/ui/.prettierrc deleted file mode 100644 index b93d496c06aa2cab91b3e20b6211d6bd09376f68..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/ui/.prettierrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "bracketSpacing": true, - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxBracketSameLine": false, - "printWidth": 80, - "proseWrap": "always", - "quoteProps": "as-needed", - "requirePragma": false, - "semi": true, - "tabWidth": 2, - "trailingComma": "all", - "useTabs": false, - "endOfLine": "auto" -} diff --git a/custom_nodes/ComfyUI-Copilot/ui/scripts/post-build.js b/custom_nodes/ComfyUI-Copilot/ui/scripts/post-build.js deleted file mode 100644 index 58fab1560b9b295863be9bbc4bda61e60b566e7d..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/ui/scripts/post-build.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * @Author: ai-business-hql qingli.hql@alibaba-inc.com - * @Date: 2025-02-17 20:53:45 - * @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com - * @LastEditTime: 2025-06-05 11:18:48 - * @FilePath: /comfyui_copilot/ui/scripts/post-build.js - * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - */ -import fs from 'fs'; -import path from 'path'; -import glob from 'glob'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -// 获取 __dirname 的等效值 -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// 在 dist/copilot_web 目录下查找所有 JS 文件 -const distDir = path.resolve(__dirname, '../../dist/copilot_web'); -const files = glob.sync(`${distDir}/**/*.js`); - -files.forEach(file => { - console.log('start modify', file) - let content = fs.readFileSync(file, 'utf-8'); - - // 检查是否已经有 getImportPath 函数(function 或 const 形式) -// if (!content.includes('getImportPath')) { -// content = `function getImportPath(filename) { -// const apiBase = window.comfyAPI?.api?.api?.api_base; -// if (apiBase) { -// // 有 API base 时,使用完整路径 -// return \`\${apiBase.substring(1)}/copilot_web/\${filename}\`; -// } else { -// // 没有 API base 时,使用相对路径(因为所有文件都在同一目录) -// return \`./\${filename}\`; -// } -// } -// ` + content; -// } - if (!content.includes('getImportPath')) { - content = `function getImportPath(filename) { - return \`./\${filename}\`; - } - ` + content; - } - - if (content.includes('__vite__mapDeps')) { - // 尝试两种不同的格式 - let depsMatch = content.match(/const __vite__mapDeps=\(.*?m\.f=\[(.*?)\]/); - - // 如果第一种格式不匹配,尝试第二种格式(用于 input.js) - if (!depsMatch) { - depsMatch = content.match(/const __vite__mapDeps=\(.*?m\.f\|\|\(m\.f=\[(.*?)\]\)\)\)/); - } - - if (depsMatch && depsMatch[1]) { - const originalDeps = depsMatch[1]; - - // 如果文件中还没有路径转换逻辑,则添加 - if (!content.includes('.map(path =>')) { - // 替换整个 __vite__mapDeps 函数,处理两种可能的格式 - content = content.replace( - /const __vite__mapDeps=\([^;]+\);/, - `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[${originalDeps}].map(path => { - const apiBase = window.comfyAPI?.api?.api?.api_base; - if (apiBase) { - // 有 API base 时,使用完整路径 - return \`\${apiBase.substring(1)}/\${path}\`; - } else { - // 没有 API base 时,使用相对路径 - return \`./\${path}\`; - } - }))))=>i.map(i=>d[i]);` - ); - } - console.log(`Modified ${path.basename(file)} - __vite__mapDeps`); - } else { - console.log(`No deps pattern found in ${path.basename(file)}`); - } - } else { - console.log(`No __vite__mapDeps found in ${path.basename(file)}`); - } - - // 处理 import("filename") 形式的动态导入 - const dynamicImportPattern = /import\("([^"]+\.js)"\)/g; - if (dynamicImportPattern.test(content)) { - content = content.replace(dynamicImportPattern, (match, filename) => { - // 只处理相对路径的导入 - if (filename.startsWith('./') || !filename.includes('/')) { - const cleanFilename = filename.startsWith('./') ? filename.substring(2) : filename; - return `import(getImportPath("${cleanFilename}"))`; - } - return match; - }); - } - - fs.writeFileSync(file, content, 'utf-8'); - console.log(`Modified ${path.basename(file)}`); -}); - -// 不需要单独处理 input.js,因为它已经在上面的循环中被处理了 \ No newline at end of file diff --git a/custom_nodes/ComfyUI-Copilot/ui/src/components/chat/ChatInput.tsx b/custom_nodes/ComfyUI-Copilot/ui/src/components/chat/ChatInput.tsx deleted file mode 100644 index 2c689b69ddec1473af0165af7ebf626c49d65fbd..0000000000000000000000000000000000000000 --- a/custom_nodes/ComfyUI-Copilot/ui/src/components/chat/ChatInput.tsx +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright (C) 2025 AIDC-AI -// Licensed under the MIT License. - -import { ChangeEvent, KeyboardEvent, useState, useRef, useEffect, useLayoutEffect, ReactNode, forwardRef, useImperativeHandle } from 'react'; -import { SendIcon, ImageIcon, XIcon, StopIcon } from './Icons'; -import { WorkflowChatAPI } from '../../apis/workflowChatApi'; -import { generateUUID } from '../../utils/uuid'; -import ImageLoading from '../ui/Image-Loading'; -import { useChatContext } from '../../context/ChatContext'; -import RewriteExpertModal from './RewriteExpertModal'; -import ButtonWithModal from '../ui/ButtonWithModal'; -import { UploadedImage } from '../../types/types'; -import ImageUploadModal from './ImageUploadModal'; -import PackageDownloadModal from './ModelDownloadModal'; -import { getLocalStorage, LocalStorageKeys, setLocalStorage } from '../../utils/localStorageManager'; - -// Debug icon component -const DebugIcon = ({ className }: { className: string }) => ( - - - - - - -); - -const ExpertIcon = ({ className }: { className: string }) => ( - - - - - - -); - -const PackageIcon = ({ className }: { className: string }) => ( - - - - -); - -interface ChatInputProps { - input: string; - loading: boolean; - onChange: (event: ChangeEvent) => void; - onSend: () => void; - onKeyPress: (event: KeyboardEvent) => void; - onUploadImages: (files: FileList) => void; - uploadedImages: UploadedImage[]; - onRemoveImage: (imageId: string) => void; - selectedModel: string; - onModelChange: (model: string) => void; - onStop?: () => void; - onAddDebugMessage?: (message: any) => void; -} - -export interface ChatInputRef { - refreshModels: () => void; -} - -export const ChatInput = forwardRef(({ - input, - loading, - onChange, - onSend, - onKeyPress, - onUploadImages, - uploadedImages, - onRemoveImage, - selectedModel, - onModelChange, - onStop, - onAddDebugMessage, -}, ref) => { - const { state, dispatch } = useChatContext(); - const { messages } = state; - const [models, setModels] = useState<{label: ReactNode; name: string; image_enable: boolean }[]>([]); - - const textareaRef = useRef(null); - - // 精确监听 textarea 的 scrollHeight 变化(处理 flex 布局) - useEffect(() => { - const textarea = textareaRef.current; - if (!textarea) return; - - const updateScrollHeight = () => { - // 使用 requestAnimationFrame 确保布局完成 - requestAnimationFrame(() => { - // 临时保存原始样式 - const originalStyle = textarea.style.cssText; - - // 强制重新计算 - textarea.style.height = 'auto'; - textarea.style.minHeight = 'auto'; - - // 恢复原始样式 - textarea.style.cssText = originalStyle; - }); - }; - - const resizeObserver = new ResizeObserver(updateScrollHeight); - resizeObserver.observe(textarea); - - // 监听内容变化 - const mutationObserver = new MutationObserver(updateScrollHeight); - mutationObserver.observe(textarea, { - childList: true, - subtree: true, - characterData: true - }); - - // 初始计算 - updateScrollHeight(); - - return () => { - resizeObserver.disconnect(); - mutationObserver.disconnect(); - }; - }, []); - - // Auto-resize textarea based on content - 使用 useLayoutEffect 防止闪烁 - useLayoutEffect(() => { - if (textareaRef.current) { - // Reset height to auto to get the correct scrollHeight - textareaRef.current.style.height = 'auto'; - // Set the height to scrollHeight to fit all content - textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 400)}px`; - } - }, [input]); - - const updateModels = (list: {label: string; name: string; image_enable: boolean }[]) => { - const selectedModel = getLocalStorage(LocalStorageKeys.MODELS_POP_VIEW_SELECTED); - // 之前记录的能在列表中找到,使用之前的记录。否则使用列表第一项重置 - if (selectedModel && list.findIndex(model => model.name === selectedModel) !== -1) { - onModelChange(selectedModel) - } else { - onModelChange(list[0].name) - } - setLocalStorage(LocalStorageKeys.MODELS_POP_VIEW_LIST, JSON.stringify(list)); - setModels([...list, - { - "label": "reload", - "name": "reload", - "image_enable": true - } - ]); - } - - // Function to load models from API - const loadModels = async () => { - try { - const result = await WorkflowChatAPI.listModels(); - updateModels(result.models); - } catch (error) { - console.error('Failed to load models:', error); - // Fallback to default models if API fails - const list = [ - { - "label": "gemini-2.5-flash", - "name": "gemini-2.5-flash", - "image_enable": true - }, - { - "label": "gpt-4.1-mini", - "name": "gpt-4.1-mini-2025-04-14-GlobalStandard", - "image_enable": true, - }, - { - "label": "gpt-4.1", - "name": "gpt-4.1-2025-04-14-GlobalStandard", - "image_enable": true, - }, - { - "label": "reload", - "name": "reload", - "image_enable": true - } - ] - updateModels(list); - } - }; - - // Expose refreshModels method to parent component - useImperativeHandle(ref, () => ({ - refreshModels: loadModels - }), []); - - // Load models on component mount - useEffect(() => { - // 1天之内不再重新获取models - const currentTime = new Date().getTime() - const time = getLocalStorage(LocalStorageKeys.MODELS_POP_VIEW_TIME); - // 一天之内使用当前缓存 - if (!!Number(time) && currentTime - Number(time) < 1000 * 60 * 60 * 24) { - const list = getLocalStorage(LocalStorageKeys.MODELS_POP_VIEW_LIST); - if (!!list) { - updateModels(JSON.parse(list)); - } - return; - } - setLocalStorage(LocalStorageKeys.MODELS_POP_VIEW_TIME, new Date().getTime().toString()); - loadModels(); - }, []); - - const handleDebugClick = () => { - WorkflowChatAPI.trackEvent({ - event_type: 'debug_icon_click', - message_type: 'debug', - data: {} - }) - if (onAddDebugMessage) { - if (messages?.[0]?.role === 'showcase') { - dispatch({ type: 'CLEAR_MESSAGES' }); - } - const debugMessage = { - id: generateUUID(), - role: 'ai' as const, - content: JSON.stringify({ - text: "Would you like me to help you debug the current workflow on the canvas?", - ext: [] - }), - format: 'debug_guide' as const, - name: 'Assistant' - }; - onAddDebugMessage(debugMessage); - } - }; - - const handleModelSelected = (value: string) => { - if (value === 'reload') { - loadModels(); - } else { - onModelChange(value); - } - } - - return ( -
0 ? 'mt-12' : ''}`}> - {/* 已上传图片预览 */} - {uploadedImages.length > 0 && ( -
- {uploadedImages.map(image => ( -
- uploaded - { - !!image?.url && image?.url !== '' ? : - } -
- ))} -
- )} - -
- } - onOpen={() => { - WorkflowChatAPI.trackEvent({ - event_type: 'model_download_icon_click', - message_type: 'ui', - data: {} - }) - }} - renderModal={(onClose) => } - /> - } - renderModal={(onClose) => } - /> - -
- -