diff --git a/CHINESE_README.md b/CHINESE_README.md new file mode 100644 index 0000000..00ec518 --- /dev/null +++ b/CHINESE_README.md @@ -0,0 +1,27 @@ + +# CryptoCat: ComfyUI压缩加密节点 + +## 简介 +CryptoCat是一个小巧的ComfyUI开源节点,它的作用在于简化工作流,同时给工作流提供加密保护。 +![image](docs/image1.png) + +## 应用场景 +- 流程简化:可以大幅度简化工作流。 +- 加密授权:可以保护工作流里的一些核心思路。 + +## 快速开始 +1. 打开ComfyUI\custom_ Nodes\目录,克隆仓库到本地。 +2. 启动ComfyUI,菜单“高级”(advance)中找到CryptoCat目录。 +3. InputCrypto节点用于压缩工作流,连线运行之后会在目录下得到压缩后的工作流 +4. OutputCrypto节点不用管,它只是用于自动解压工作流 +5. RandomSeedNode会在服务端生成随机数,用于修补工作流封装后随机数不起效的情况 + + +## 贡献指南 +欢迎对CryptoCat项目做出贡献!你可以通过提交Pull Request或开设Issue来提出新功能建议或报告问题。 + +## 许可证 +本项目遵循MIT许可证。有关详细信息,请参阅LICENSE文件。 + +--- +CryptoCat © 2024. All Rights Reserved. diff --git a/README.md b/README.md index 51e4a70..ac34566 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# ComfyUI_CryptoCat -designed to simplify workflows while providing encryption protection for them. +# CryptoCat: ComfyUI Compression and Encryption Node + +## Introduction +CryptoCat is a lightweight open-source node for ComfyUI, designed to simplify workflows while providing encryption protection for them. +![image](docs/image1.png) + +## Use Cases +- Workflow Simplification: It can greatly simplify workflows. +- Encryption Authorization: It can protect core ideas within the workflow. + +## Quick Start +1. Open the ComfyUI\custom_Nodes\ directory and clone the repository locally. +2. Start ComfyUI, and find the CryptoCat directory in the "Advanced" menu. +3. The InputCrypto node is used to compress the workflow, and after connecting and running it, you will get the compressed workflow in the directory. +4. You don't need to worry about the OutputCrypto node, it just automatically decompresses the workflow. +5. The RandomSeedNode generates random numbers on the server side to fix issues where the random numbers don't work after the workflow is packaged. + +## Contribution Guide +Contributions to the CryptoCat project are welcome! You can submit Pull Requests or open Issues to propose new features or report bugs. + +## License +This project is licensed under the MIT License. For more details, please refer to the LICENSE file. + +--- +CryptoCat © 2024. All Rights Reserved. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d154cab --- /dev/null +++ b/__init__.py @@ -0,0 +1,45 @@ +from functools import wraps +import json +from .trim_workflow import PromptTrim +from .crypto_node import SaveCryptoNode, ExcuteCryptoNode, RandomSeedNode + +NODE_CLASS_MAPPINGS = { + "SaveCryptoNode": SaveCryptoNode, + "ExcuteCryptoNode": ExcuteCryptoNode, + "RandomSeedNode": RandomSeedNode +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "SaveCryptoNode": "InputCrypto", + "ExcuteCryptoNode": "OutputCrypto", + "RandomSeedNode": "RandomSeedNode" +} + +WEB_DIRECTORY = "./js" + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAMES_MAPPINGS', 'WEB_DIRECTORY'] + + +from server import PromptServer + + +method_name = "trigger_on_prompt" +original_method = getattr(PromptServer, method_name) + +# 创建 hook +@wraps(original_method) +def new_trigger_on_prompt(self, json_data): + prompt = json_data["prompt"] + pr = PromptTrim(prompt) + if pr.has_crypto_node(): + print("has crypto node") + json_data["prompt"] = pr.replace_prompt() + # 删除 extra_data 字段 + if "extra_data" in json_data: + json_data["extra_data"] = {} + print("hook new_trigger_on_prompt delete extra_data") + result = original_method(self, json_data) + return result + +setattr(PromptServer, method_name, new_trigger_on_prompt) \ No newline at end of file diff --git a/crypto_node.py b/crypto_node.py new file mode 100644 index 0000000..f527294 --- /dev/null +++ b/crypto_node.py @@ -0,0 +1,157 @@ +import json +import os +import random +import torch +from .trim_workflow import PromptTrim, WorkflowTrim +from.file_compressor import FileCompressor +import folder_paths + + +class SaveCryptoNode(): + def __init__(self): + pass + + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "crypto_folder": ("STRING", {"default": folder_paths.output_directory}), + "crypto_name": ("STRING", {"default": "my_cat.json"}), + "output_images" : ("IMAGE",), + }, + "optional": { + "input_anything" : ("*",), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", + } + } + + # @classmethod + # def VALIDATE_INPUTS(s, input_types): + # return True + + + RETURN_TYPES = () #"STRING", + OUTPUT_NODE = True + FUNCTION = "crypto" #这个节点中的主函数 + CATEGORY = "advanced/CryptoCat" + + def crypto(self, crypto_folder, crypto_name, output_images, **kwargs): + if not crypto_name or len(crypto_folder) < 2 or len(crypto_name) == 0: + raise Exception("CryptoCat folder and filename must be at least two characters long") + + # 从 kwargs 中提取特定参数 + unique_id = kwargs.pop('unique_id', None) + prompt = kwargs.pop('prompt', None) + extra_pnginfo = kwargs.pop('extra_pnginfo', None) + + # 检查必需参数是否存在 + if unique_id is None: + raise Exception("Warning: 'unique_id' is missing.") + if prompt is None: + raise Exception("Warning: 'prompt' is missing.") + + # 将剩余的 kwargs 存储在 inputs 列表中 + inputs = list(kwargs.values()) + + temp_dir = os.environ.get('TEMP') or os.environ.get('TMP') or '/tmp' + project_temp_folder = os.path.join(temp_dir, crypto_name) + if not os.path.exists(project_temp_folder): + os.makedirs(project_temp_folder) + + + # 保存源文件,用于调试 + with open(os.path.join(project_temp_folder, "prompt.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(prompt, indent=4, ensure_ascii=False)) + with open(os.path.join(project_temp_folder, "workflow.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(extra_pnginfo, indent=4, ensure_ascii=False)) + + # 保存需要的输出文件 + project_folder = os.path.join(crypto_folder, crypto_name) + if not os.path.exists(project_folder): + os.makedirs(project_folder) + + hide_prompt_path = os.path.join(project_folder, "prompt.dat") + # 解析workflow + wt = WorkflowTrim(extra_pnginfo) + wt.trim_workflow() + show_workflow = wt.replace_workflow(hide_prompt_path) + with open(os.path.join(project_folder, "workflow.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(show_workflow, indent=4, ensure_ascii=False)) + + # 拆分prompt + pr = PromptTrim(prompt) + show_part_prompt, hide_part_prompt = pr.split_prompt(wt.get_remaining_node_ids()) + with open(os.path.join(project_temp_folder, "prompt_show.json"), "w", encoding="utf-8") as f: + f.write(json.dumps(show_part_prompt, indent=4, ensure_ascii=False)) + + FileCompressor.compress_to_json(hide_part_prompt, hide_prompt_path, "19040822") + return (hide_part_prompt,) + + + +class ExcuteCryptoNode(): + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "crypto_file_path": ("STRING", {"default": folder_paths.output_directory}), + }, + "optional": { + "input_anything" : ("*",), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "excute" #这个节点中的主函数 + CATEGORY = "advanced/CryptoCat" + + def excute(self, **kwargs): + batch_size = 1 + height = 1024 + width = 1024 + color = 0xFF0000 + r = torch.full([batch_size, height, width, 1], ((color >> 16) & 0xFF) / 0xFF) + g = torch.full([batch_size, height, width, 1], ((color >> 8) & 0xFF) / 0xFF) + b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF) + return (torch.cat((r, g, b), dim=-1), ) + + +# 产生随机数 +class RandomSeedNode(): + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + }, + "optional": { + }, + "hidden": { + } + } + + RETURN_TYPES = ("INT",) + FUNCTION = "random" #这个节点中的主函数 + CATEGORY = "advanced/CryptoCat" + + def IS_CHANGED(): + return float("NaN") + + def random(self): + # 产生随机数 + return (random.randint(0, 999999), ) \ No newline at end of file diff --git a/docs/image1.png b/docs/image1.png new file mode 100644 index 0000000..c5e4694 Binary files /dev/null and b/docs/image1.png differ diff --git a/file_compressor.py b/file_compressor.py new file mode 100644 index 0000000..c6d15fe --- /dev/null +++ b/file_compressor.py @@ -0,0 +1,41 @@ +import json +import zlib +import os + +class FileCompressor: + + @staticmethod + def obfuscate(data, password): + return bytes([b ^ ord(password[i % len(password)]) for i, b in enumerate(data)]) + + @staticmethod + def compress_string(string_data, zip_path, password): + compressed_data = zlib.compress(string_data.encode('utf-8')) + obfuscated_data = FileCompressor.obfuscate(compressed_data, password) + with open(zip_path, 'wb') as zipf: + zipf.write(obfuscated_data) + + @staticmethod + def decompress_to_string(zip_path, password): + with open(zip_path, 'rb') as zipf: + obfuscated_data = zipf.read() + compressed_data = FileCompressor.obfuscate(obfuscated_data, password) # 反混淆 + return zlib.decompress(compressed_data).decode('utf-8') + + @staticmethod + def compress_to_json(data, zip_path, password): + try: + json_string = json.dumps(data, indent=4, ensure_ascii=False) # Convert data to JSON string + FileCompressor.compress_string(json_string, zip_path, password) # Call compress_string + except Exception as e: + print(f"Compression failed: {e}") + + @staticmethod + def decompress_from_json(zip_path, password): + try: + if not os.path.exists(zip_path): + raise FileNotFoundError(f"{zip_path} does not exist") + json_string = FileCompressor.decompress_to_string(zip_path, password) # Call decompress_to_string + return json.loads(json_string) # Convert back to dictionary + except Exception as e: + print(f"Decompression failed: {e}") diff --git a/js/mycat.js b/js/mycat.js new file mode 100644 index 0000000..d4eabeb --- /dev/null +++ b/js/mycat.js @@ -0,0 +1,73 @@ +import { ComfyApp, app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "cryptocat.mycat", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + + if (nodeData.name === 'SaveCryptoNode') { + const input_name = "input_anything"; + + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { + if (!link_info) return; + // 更新输入名称 + let slot_i = 1; + for (let i = 1; i < this.inputs.length; i++) { + let input_i = this.inputs[i]; + if (input_i.name !== 'select' && input_i.name !== 'sel_mode') { + input_i.name = `${input_name}${slot_i}`; + slot_i++; + } + } + + // 处理断开连接后的输入移除逻辑 + if (!connected && (this.inputs.length > 2)) { + const stackTrace = new Error().stack; + + if ( + !stackTrace.includes('LGraphNode.prototype.connect') && // for touch device + !stackTrace.includes('LGraphNode.connect') && // for mouse device + !stackTrace.includes('loadGraphData') && + this.inputs[index].name != 'select') { + this.removeInput(index); + } + + let slot_i = 1; + for (let i = 1; i < this.inputs.length; i++) { + let input_i = this.inputs[i]; + if (input_i.name != 'select' && input_i.name != 'sel_mode') { + input_i.name = `${input_name}${slot_i}` + slot_i++; + } + } + } + + // 动态添加输入 + let last_slot = this.inputs[this.inputs.length - 1]; + if ( + (last_slot.name == 'select' && last_slot.name != 'sel_mode' && this.inputs[this.inputs.length - 2].link != undefined) + || (last_slot.name != 'select' && last_slot.name != 'sel_mode' && last_slot.link != undefined)) { + this.addInput(`${input_name}${slot_i}`, "*"); + } + + }; + } + }, + + nodeCreated(node, app) { + if (node.comfyClass == "SaveCryptoNode") { + if (node.widgets && node.widgets.length > 1 && app.workflowManager) { + node.widgets[1].value = app.workflowManager.activeWorkflow.name; + } + } + }, + + // async setup(app) { + // console.log(app.workflowManager.activeWorkflow.name) + // if (this.widgets) { + // this.widgets[1].value = app.workflowManager.activeWorkflow.name; + // } + // }, + + // }, +}); \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ba0793 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +torch +aiohttp diff --git a/trim_workflow.py b/trim_workflow.py new file mode 100644 index 0000000..00cea41 --- /dev/null +++ b/trim_workflow.py @@ -0,0 +1,352 @@ +import copy +import os +import random +import sys +import folder_paths +import json +from.file_compressor import FileCompressor + +class WorkflowTrim(): + def __init__(self, workflow): + if 'workflow' in workflow : + self.workflow = copy.deepcopy(workflow['workflow']) + else: + self.workflow = copy.deepcopy(workflow) + + self.output_images_link = 0 + + nodes_dict = {node['id']: node for node in self.workflow['nodes']} + self.max_id_index = max(nodes_dict.keys()) + 1 + + + @staticmethod + def find_workflow_related_nodes(nodes, input_ids): + found_ids = set() + stack = list(input_ids) + while stack: + link_id = stack.pop() + for node_id, node in nodes.items(): + outputs = node.get('outputs', None) + if not outputs: + continue + for output in outputs: + links = output.get('links', None) + if not links: + continue + if link_id in links: + if node_id not in found_ids: + found_ids.add(node_id) + inputs = node.get('inputs', []) + for input_node in inputs: + link_id = input_node.get('link') + if link_id is not None: + stack.append(link_id) + break + return found_ids + + def trim_workflow(self): + if not self.workflow: + raise ValueError("Invalid JSON format.") + + nodes_dict = {node['id']: node for node in self.workflow['nodes']} + save_crypto_node_ids = [node_id for node_id, details in nodes_dict.items() if details.get('type') == 'SaveCryptoNode'] + if len(save_crypto_node_ids) == 1: + save_crypto_node_id = save_crypto_node_ids[0] + + # 获取所有以 'input_anything' 开头的输入链接 + input_ids = set() + for input_node in nodes_dict[save_crypto_node_id].get('inputs', []): + if input_node['name'] and input_node['name'].startswith('input_anything'): + input_ids.add(input_node['link']) + if input_node['name'] and input_node['name'] == "output_images": + self.output_images_link = input_node['link'] + + # 只处理有效的链接 ID + input_ids = {link_id for link_id in input_ids if link_id is not None} + + # 找到与输入链接相关的节点 + related_node_ids = self.find_workflow_related_nodes(nodes_dict, input_ids) + + # 添加 SaveCryptoNode 本身 + related_node_ids.add(save_crypto_node_id) + + # # 打印和删除不相关的节点 + # for node_id in nodes_dict.keys(): + # if node_id not in related_node_ids: + # print(f"Removing node ID: {node_id} - {nodes_dict[node_id]}") + + # 更新节点和链接 + self.workflow['nodes'] = [details for node_id, details in nodes_dict.items() if node_id in related_node_ids] + remaining_node_ids = {node['id'] for node in self.workflow['nodes']} + self.workflow.pop('groups', None) + self.workflow['output_images_id'] = self.output_images_link + + + # 保留包含任一节点 ID 的链接 + self.workflow['links'] = [ + link for link in self.workflow['links'] + if link[1] in remaining_node_ids and link[3] in remaining_node_ids + ] + + return self.workflow + + elif len(save_crypto_node_ids) > 1: + raise ValueError("Error: Multiple 'SaveCryptoNode' instances found.") + else: + raise ValueError("Error: No 'SaveCryptoNode' instances found.") + + + + + def replace_workflow(self,hide_prompt_path): + if not self.workflow: + raise ValueError("Invalid JSON format.") + + nodes_dict = {node['id']: node for node in self.workflow['nodes']} + output_images_link = None + + # 替换 SaveCryptoNode 为 ExcuteCryptoNode + for node in self.workflow['nodes']: + if node.get('type') == 'SaveCryptoNode': + node['type'] = 'ExcuteCryptoNode' + node['properties']['Node name for S&R'] = 'ExcuteCryptoNode' + + # 移除 output_images 输入并获取 link + for inp in node['inputs']: + if inp['name'] == 'output_images': + output_images_link = inp.get('link') + node['inputs'] = [inp for inp in node['inputs'] if inp['name'] != 'output_images'] + node['widgets_values'] = [hide_prompt_path] + + # 找到 ExcuteCryptoNode 的位置 + execute_node = next((node for node in self.workflow['nodes'] if node.get('type') == 'ExcuteCryptoNode'), None) + + if not execute_node: + raise ValueError("No 'ExcuteCryptoNode' instance found.") + + # 创建新的 SaveImage 节点 + execute_node_pos = execute_node['pos'] + self.max_id_index = max(max(nodes_dict.keys()) + 1, self.max_id_index) + new_save_image_node = { + "id": self.max_id_index, # 新节点 ID,确保唯一 + "type": "SaveImage", + "pos": { + "0": execute_node_pos['0'] + 200, # 设置位置在 ExcuteCryptoNode 右边 20 像素 + "1": execute_node_pos['1'] + }, + "size": { + "0": 210, + "1": 162 + }, + "flags": {}, + "order": 50, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": output_images_link, # 使用获取的 link + "label": "images" + } + ], + "outputs": [], # 新节点没有输出 + "properties": { + "Node name for S&R": "SaveImage" + } + } + # 将新节点添加到节点列表 + self.workflow['nodes'].append(new_save_image_node) + + # 删除原有的链接以避免冲突 + self.workflow['links'] = [ + link for link in self.workflow['links'] if link[0] != output_images_link + ] + + # 用新的 outputs 替换 ExcuteCryptoNode 的 outputs + execute_node['outputs'] = [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [output_images_link], # 连接到新节点 + "shape": 3, + "label": "IMAGE", + "slot_index": 0 + } + ] + + # 添加链接信息 + self.workflow['links'].append([ + output_images_link, # ExcuteCryptoNode 的输出 ID + execute_node['id'], # 新节点 ID + 0, # 新节点的输入索引 + new_save_image_node['id'], # ExcuteCryptoNode ID(旧节点 ID) + 0, # 旧节点的输出索引 + "IMAGE" # 数据类型 + ]) + + return self.workflow + + + def set_excute_crypto_node_path(self, path): + if not self.workflow: + raise ValueError("Invalid JSON format.") + for node in self.workflow['nodes']: + if node.get('type') == 'ExcuteCryptoNode': + node['properties']['Path to executable file'] = path + + + def get_remaining_node_ids(self): + if not self.workflow: + raise ValueError("Invalid JSON format.") + remaining_node_ids = {node['id'] for node in self.workflow['nodes']} + return remaining_node_ids + + + +class PromptTrim(): + def __init__(self, prompt): + self.prompt = prompt + self.debug = True + self.hide_part_prompt = {} + self.show_part_prompt = {} + + def split_prompt(self, related_node_ids): + if not self.prompt: + raise ValueError("Invalid JSON format.") + + # 查找第一个 'SaveCryptoNode' 实例并转换为 int + # save_crypto_node_id = next((int(node_id) for node_id, details in self.prompt.items() if details['class_type'] == 'SaveCryptoNode'), None) + save_crypto_node_id = None + output_images_ids = [] + for node_id, details in self.prompt.items(): + if details['class_type'] == 'SaveCryptoNode': + output_images_ids = details['inputs'].get('output_images') + save_crypto_node_id = int(node_id) + break + + if save_crypto_node_id is None: + raise ValueError("Error: No 'SaveCryptoNode' instances found.") + + if save_crypto_node_id not in related_node_ids: + raise AssertionError("SaveCryptoNode not found in related node list.") + + self.hide_part_prompt = {} + self.show_part_prompt = {} + + # 输出不在链中的节点 + for node_id in self.prompt.keys(): + node_id_int = int(node_id) # 转换为 int 类型 + if node_id_int not in related_node_ids: + self.hide_part_prompt[node_id_int] = self.prompt[node_id] + else: + self.show_part_prompt[node_id_int] = self.prompt[node_id] + + self.hide_part_prompt['output_images_ids'] = output_images_ids + return self.show_part_prompt, self.hide_part_prompt + + + def replace_prompt(self): + if not self.prompt: + raise ValueError("Invalid JSON format.") + + crypto_file_path = '' + excute_crypto_id = None + + # 查找 ExcuteCryptoNode 并获取路径,同时构建需要删除的节点 + for node_id, node in list(self.prompt.items()): + if node.get('class_type') == 'ExcuteCryptoNode': + crypto_file_path = node['inputs'].get('crypto_file_path') + excute_crypto_id = node_id + # 删除当前节点 + del self.prompt[node_id] + break + + if not crypto_file_path: + print("No 'ExcuteCryptoNode' found in prompt.") + return self.prompt + + # print(f"{crypto_file_path=}") + + inject_json = FileCompressor.decompress_from_json(crypto_file_path, "19040822") + output_images_ids = inject_json['output_images_ids'] + inject_json.pop('output_images_ids', None) + for node_id, node in list(self.prompt.items()): + if node.get('class_type') == 'SaveImage': + ids = node['inputs']['images'] #input_images + if excute_crypto_id in ids: + node['inputs']['images'] = output_images_ids + + # 随机生成一个种子,避免缓存问题 + random_seed_node = next((node for node in inject_json.values() if node.get('class_type') == 'RandomSeedNode'), None) + if random_seed_node: + random_seed_node["inputs"]["is_changed"] = random.randint(0, 999999) + + + # 将 inject_json 的节点合并到当前 prompt + # for node in inject_json.get('nodes', []): + # self.prompt[node['id']] = node # 使用 ID 添加或更新节点 + self.prompt.update(inject_json) + + if self.debug == True: + temp_dir = os.environ.get('TEMP') or os.environ.get('TMP') or '/tmp' + filename = os.path.basename(crypto_file_path) + "_prompt.json" + with open(os.path.join(temp_dir, filename), "w", encoding="utf-8") as f: + json.dump(self.prompt, f, indent=4) + print(f"prompt len = {len(self.prompt)}") + + return self.prompt + + def has_crypto_node(self): + if not self.prompt: + return False + return any(node.get('class_type') == 'ExcuteCryptoNode' for node in self.prompt.values()) + + + + + + + + + + + + +if __name__ == "__main__": + + + current_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(os.path.dirname(current_dir)) + sys.path.append(parent_dir) + import folder_paths + + with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_original_workflow.json", "r", encoding="utf-8") as f: + workflow = json.load(f) + + wt = WorkflowTrim(workflow) + wt.trim_workflow() + replace_json = wt.replace_workflow('') + + with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_trim.json", "w", encoding="utf-8") as f: + json.dump(replace_json, f, indent=4) + + + ###################################################### + with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版.txt", "r", encoding="utf-8") as f: + prompt = json.load(f) + + pt = PromptTrim(prompt) + show_part_prompt, hide_part_prompt = pt.split_prompt(wt.get_remaining_node_ids()) + + with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_show_part_prompt.json", "w", encoding="utf-8") as f: + json.dump(show_part_prompt, f, indent=4) + + with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_hide_part_prompt.json", "w", encoding="utf-8") as f: + json.dump(hide_part_prompt, f, indent=4) + + + + + + +