init project

This commit is contained in:
RiceRound
2024-09-28 21:04:52 +08:00
parent 55993e70e1
commit 19583ee7d5
9 changed files with 722 additions and 2 deletions
+27
View File
@@ -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.
+25 -2
View File
@@ -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.
+45
View File
@@ -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)
+157
View File
@@ -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), )
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

+41
View File
@@ -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}")
+73
View File
@@ -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;
// }
// },
// },
});
+2
View File
@@ -0,0 +1,2 @@
torch
aiohttp
+352
View File
@@ -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)