ComfyUI图生压缩加密输出节点以及对ai的一些感叹

ComfyUI图生压缩加密输出节点以及对ai的一些感叹

:拒绝AI拉取数据炼丹白嫖,关联请标注版权。

前言

这是一套ComfyUI图生视频输出加密方案,你懂得。

标注版权原因

让ai解决 "图生输出的一连串帧直接打包压缩加密",绕来绕去折腾了半天,蠢蛋ai给一堆不能用的方案,甚至还产生了很严重的ai幻觉,把加密工作流节点当成加密输出整理给我,最后我是找到了下面这个仓库,ai理解了代码后,窃取了架构思路生成了基本加密代码,还存在一堆bug,后经过我的指挥调整,生成了真正可用节点。

基本思路框架源自这个仓库
https://github.com/Twist-ttt/comfyui-encrypted-image-node/tree/main

还没被我用的ai模型 skill走,虽然仅仅200来行代码,没有它,ai做这个事就特别蠢,让我吸收生成后,如果我不说,根本没人知道来源。代码能力越强的模型窃取的越厉害,指不定github的私人仓库都拿去训练了。

这涉及AI发展引发的两大问题:

  1. 版权困境

    AI不偷就没法创作,一旦偷了炼丹成功就不说是偷了。原始版权往往被刻意淡化,侵权时刻存在。长期来看,原创动力将受挫,闭源趋势加剧,最终导致技术社区发展停滞。

  2. 认知偏差

    侵权很多人还不知道,古法编程中手动cv是很普遍的事,就算偷了不标注版本,改造也是二创大部分劳动结晶在里面,二次开发者也通常保持对原作者的尊重。但AI时代催生了"数典忘祖"的现象:使用者既不了解内容来源,又贬低原创价值。窃取了别人的东西,吃水要忘挖井人的现象普遍存在。当AI将原创内容"技能化"后,抄袭痕迹被完美掩盖,只剩下若有若无的既视感。

我们必须认识到:应当尊重每一位原创,持续侵权将扼杀创新源头。当原创者集体沉默,技术社区将陷入发展瓶颈,你还指望ai左脚踩右脚上天吗。

使用案例

① 加密流程

图生对显卡要求高,在不愿暴露内容场合使用。

VAE解码后,原流程是输出接插帧 或 Video Combine

这里选择直接接入压缩

自定义的LoadImageWithFilename 的 文件名输出给压缩包前缀。

② 解密流程

对显卡要求不高,在个人电脑上解密。

目前是简化节点,压缩包扔进input文件夹,配好密码,可输出images给原先的插帧或视频生成节点。

需要定制的小修小改ai干起来还是比较得心应手。

文件名可接给生成视频当前缀。

③ 批量任务提交

  1. 做好解密工作流,左上角导出api。
  2. tasks_submit.py
    让ai根据工作流api定制批量执行脚本
    然后运行脚本即可

源码

SaveFramesAsZIP.py

作用:将帧压缩保存为zip的节点,支持无损png和压缩jpg格式。

毕竟保存了每帧,无损生成的压缩包比较大,考虑用jpg压缩,减少压缩包体积,

又或者定制修改节点,压缩生成的mp4文件,可以丢给ai改,问题不大。

python 复制代码
import os
import torch
import folder_paths
import numpy as np
from PIL import Image
import io
import zipfile
import random
import datetime
import re

try:
    import pyzipper
    HAS_PYZIPPER = True
except ImportError:
    HAS_PYZIPPER = False

class SaveFramesAsZIP:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "images": ("IMAGE",),
                "filename_prefix": ("STRING", {"default": "frames"}),
                "password": ("STRING", {"default": "", "multiline": False}),
                "use_aes": ("BOOLEAN", {"default": True}),
                "image_format": (["PNG", "JPEG"], {"default": "JPEG"}),
                "jpeg_quality": ("INT", {"default": 85, "min": 1, "max": 100}),
                "zip_compression": (["DEFLATE", "LZMA"], {"default": "DEFLATE"}),
            }
        }

    RETURN_TYPES = ()
    RETURN_NAMES = ()
    FUNCTION = "save_as_zip"
    CATEGORY = "bbq_nodes"
    OUTPUT_NODE = True

    @classmethod
    def IS_CHANGED(cls, images, filename_prefix, password, use_aes, image_format, jpeg_quality, zip_compression):
        return True

    def replace_time_placeholders(self, s):
        def replacer(match):
            fmt = match.group(1)
            return datetime.datetime.now().strftime(fmt)
        pattern = r"%date:(.*?)%"
        return re.sub(pattern, replacer, s)

    def save_as_zip(self, images, filename_prefix, password, use_aes, image_format, jpeg_quality, zip_compression):
        print(f"=== SaveFramesAsZIP called ===")
        print(f"  images shape: {images.shape}")
        print(f"  raw prefix: {filename_prefix}")
        print(f"  password: {'*' * len(password) if password else 'empty'}")
        print(f"  use_aes: {use_aes}")
        print(f"  image_format: {image_format}, jpeg_quality: {jpeg_quality}")
        print(f"  zip_compression: {zip_compression}")

        # 替换时间占位符
        processed_prefix = self.replace_time_placeholders(filename_prefix)
        print(f"  processed prefix: {processed_prefix}")

        # 处理子目录
        norm_prefix = processed_prefix.replace('\\', '/').rstrip('/')
        parts = norm_prefix.split('/')
        if len(parts) > 1:
            subdir = os.path.join(*parts[:-1])
            base_name = parts[-1]
        else:
            subdir = ""
            base_name = norm_prefix

        output_dir = folder_paths.get_output_directory()
        if subdir:
            full_dir = os.path.join(output_dir, subdir)
            os.makedirs(full_dir, exist_ok=True)
            save_dir = full_dir
        else:
            save_dir = output_dir

        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        if not base_name:
            base_name = "frames"
        zip_filename = f"{base_name}_{timestamp}.zip"
        zip_path = os.path.join(save_dir, zip_filename)

        frames = self.tensor_to_pil(images)

        # 选择 ZIP 压缩方式
        compression = zipfile.ZIP_DEFLATED if zip_compression == "DEFLATE" else zipfile.ZIP_LZMA

        # 写入 ZIP
        if password and use_aes and HAS_PYZIPPER:
            # pyzipper 支持 AES 加密,但压缩方式只能使用 ZIP_DEFLATED
            with pyzipper.AESZipFile(zip_path, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as zf:
                zf.setpassword(password.encode())
                for idx, img in enumerate(frames):
                    img_bytes = io.BytesIO()
                    if image_format == "PNG":
                        img.save(img_bytes, format='PNG')
                    else:
                        img.save(img_bytes, format='JPEG', quality=jpeg_quality)
                    img_bytes.seek(0)
                    ext = "png" if image_format == "PNG" else "jpg"
                    zf.writestr(f"frame_{idx:04d}.{ext}", img_bytes.getvalue())
        else:
            # 使用标准 zipfile
            with zipfile.ZipFile(zip_path, 'w', compression=compression) as zf:
                if password:
                    zf.setpassword(password.encode())
                for idx, img in enumerate(frames):
                    img_bytes = io.BytesIO()
                    if image_format == "PNG":
                        img.save(img_bytes, format='PNG')
                    else:
                        img.save(img_bytes, format='JPEG', quality=jpeg_quality)
                    img_bytes.seek(0)
                    ext = "png" if image_format == "PNG" else "jpg"
                    zf.writestr(f"frame_{idx:04d}.{ext}", img_bytes.getvalue())

        print(f"  ZIP saved to: {zip_path}")
        return ()

    def tensor_to_pil(self, tensor):
        if tensor is None:
            return []
        if len(tensor.shape) == 3:
            tensor = tensor.unsqueeze(0)
        frames = []
        for i in range(tensor.shape[0]):
            img_np = tensor[i].cpu().numpy()
            img_np = (img_np * 255).astype(np.uint8)
            img_pil = Image.fromarray(img_np, mode='RGB')
            frames.append(img_pil)
        return frames

NODE_CLASS_MAPPINGS = {
    "SaveFramesAsZIP": SaveFramesAsZIP,
}
NODE_DISPLAY_NAME_MAPPINGS = {
    "SaveFramesAsZIP": "Save Frames as ZIP",
}

依赖
requirements.txt

bash 复制代码
pyzipper

LoadFramesFromZIP.py

作用:将zip读取解压为帧的节点。

因为我的插帧需求低配电脑和4090耗时表现差不多,所以迁移插帧生成视频在本地解密节点做。

如果修改加密节点直接生成视频的话,可省略这一步。

python 复制代码
import os
import torch
import numpy as np
import folder_paths
import hashlib
from PIL import Image
import zipfile
import re

# 尝试导入 pyzipper 以支持 AES-256 解密
try:
    import pyzipper
    HAS_PYZIPPER = True
except ImportError:
    HAS_PYZIPPER = False

def natural_sort_key(s):
    """自然排序:将数字部分转为整数,便于排序 frame_0001.png 等"""
    return [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', s)]

class LoadFramesFromZIP:
    @classmethod
    def INPUT_TYPES(cls):
        # 列出 input 目录下的 zip 文件
        input_dir = folder_paths.get_input_directory()
        files = [f for f in os.listdir(input_dir) if f.lower().endswith('.zip')]
        return {
            "required": {
                "zip_file": (sorted(files), {"zip_upload": True}),
                "password": ("STRING", {"default": "", "multiline": False}),
            }
        }

    CATEGORY = "bbq_nodes"
    RETURN_TYPES = ("IMAGE", "INT", "STRING")
    RETURN_NAMES = ("images", "frame_count", "source_filename")
    FUNCTION = "load_zip"

    def load_zip(self, zip_file, password):
        zip_path = folder_paths.get_annotated_filepath(zip_file)
        print(f"Loading ZIP: {zip_path}")
        print(f"Password: {'*' * len(password) if password else 'empty'}")

        # 根据加密方式选择打开 ZIP 的方法
        if password and HAS_PYZIPPER:
            # 使用 pyzipper 支持 AES-256
            try:
                zf = pyzipper.AESZipFile(zip_path, 'r')
                zf.setpassword(password.encode())
            except Exception as e:
                raise ValueError(f"Failed to open encrypted ZIP with pyzipper: {e}")
        else:
            # 使用标准 zipfile(传统加密或无密码)
            try:
                zf = zipfile.ZipFile(zip_path, 'r')
                if password:
                    zf.setpassword(password.encode())
            except Exception as e:
                raise ValueError(f"Failed to open ZIP: {e}")

        # 获取所有 PNG 文件并排序(自然排序)
        png_files = [f for f in zf.namelist() if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
        if not png_files:
            raise ValueError("No PNG files found in ZIP archive.")
        png_files.sort(key=natural_sort_key)
        print(f"Found {len(png_files)} frames: {png_files[0]} ... {png_files[-1]}")

        frames = []
        for fname in png_files:
            with zf.open(fname) as img_file:
                img = Image.open(img_file)
                # 转换为 RGB(如果是 RGBA 或其他模式)
                img = img.convert('RGB')
                img_np = np.array(img).astype(np.float32) / 255.0
                frames.append(torch.from_numpy(img_np))

        zf.close()

        if not frames:
            raise ValueError("No frames could be loaded.")

        # 堆叠为 [N, H, W, C] 张量
        images = torch.stack(frames, dim=0)
        frame_count = images.shape[0]
        # 提取源文件名(不含扩展名)作为辅助信息
        source_filename = os.path.splitext(os.path.basename(zip_path))[0]

        print(f"Loaded {frame_count} frames, shape: {images.shape}")
        print(torch.cuda.is_available())
        return (images, frame_count, source_filename)

    @classmethod
    def IS_CHANGED(cls, zip_file, password):
        # 文件内容变化时重新加载
        zip_path = folder_paths.get_annotated_filepath(zip_file)
        m = hashlib.sha256()
        with open(zip_path, 'rb') as f:
            m.update(f.read())
        return m.digest().hex()

NODE_CLASS_MAPPINGS = {
    "LoadFramesFromZIP": LoadFramesFromZIP,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "LoadFramesFromZIP": "Load Frames from ZIP",
}

LoadImageWithFilename.py

作用:定制的加载图片节点,支持文件名输出连接给压缩包名称前缀

python 复制代码
import os
import torch
import numpy as np
import folder_paths
from PIL import Image, ImageOps, ImageSequence
import node_helpers
import hashlib

class LoadImageWithFilename:
    @classmethod
    def INPUT_TYPES(cls):
        input_dir = folder_paths.get_input_directory()
        files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
        return {
            "required": {
                "image": (sorted(files), {"image_upload": True}),
            }
        }

    CATEGORY = "bbq_nodes"
    RETURN_TYPES = ("IMAGE", "MASK", "STRING")
    RETURN_NAMES = ("image", "mask", "filename")
    FUNCTION = "load_image"

    def load_image(self, image):
        # 完全复制原生 LoadImage 的加载逻辑
        image_path = folder_paths.get_annotated_filepath(image)
        img = node_helpers.pillow(Image.open, image_path)
        
        output_images = []
        output_masks = []
        w, h = None, None

        for i in ImageSequence.Iterator(img):
            i = node_helpers.pillow(ImageOps.exif_transpose, i)

            if i.mode == 'I':
                i = i.point(lambda i: i * (1 / 255))
            image = i.convert("RGB")

            if len(output_images) == 0:
                w = image.size[0]
                h = image.size[1]

            if image.size[0] != w or image.size[1] != h:
                continue

            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)

        if len(output_images) == 1:
            output_images = output_images[0]
            output_masks = output_masks[0]
        elif len(output_images) > 1:
            output_images = torch.cat(output_images, dim=0)
            output_masks = torch.cat(output_masks, dim=0)

        # 提取文件名(不含扩展名)作为输出
        base_name = os.path.splitext(os.path.basename(image_path))[0]

        return (output_images, output_masks, base_name)

    @classmethod
    def IS_CHANGED(cls, image):
        # 与原生 LoadImage 保持一致:基于文件修改时间和大小计算哈希
        image_path = folder_paths.get_annotated_filepath(image)
        try:
            stat = os.stat(image_path)
            # 组合修改时间和大小,确保文件变化时节点重新执行
            return (stat.st_mtime, stat.st_size)
        except OSError:
            return None

NODE_CLASS_MAPPINGS = {
    "LoadImageWithFilename": LoadImageWithFilename,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "LoadImageWithFilename": "Load Image (with filename)",
}

tasks_submit.py

让ai根据 api.json 生成批量提交脚本。

python 复制代码
import os
import json
import requests
import time

# 配置
COMFYUI_API_URL = "http://127.0.0.1:8188/prompt"
INPUT_DIR = r"E:\AI\。。。\ComfyUI\input"
TEMPLATE_JSON = r"E:\AI\。。。\ComfyUI\custom_nodes\bbq_nodes\tasks_submit\LoadFramesWorkflow.json"  # 保存您的 API 格式 JSON 文件路径
PASSWORD = "bbq"

# 读取模板
with open(TEMPLATE_JSON, 'r', encoding='utf-8') as f:
    template = json.load(f)

# 找到 LoadFramesFromZIP 节点 ID
load_node_id = None
for nid, node in template.items():
    if node.get("class_type") == "LoadFramesFromZIP":
        load_node_id = nid
        break
if load_node_id is None:
    print("Error: No LoadFramesFromZIP node found.")
    exit()

# 获取所有 ZIP 文件
zip_files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith('.zip')]
if not zip_files:
    print("No ZIP files found.")
    exit()

# 批量提交
for zip_file in zip_files:
    prompt = json.loads(json.dumps(template))  # 深拷贝
    prompt[load_node_id]["inputs"]["zip_file"] = zip_file
    prompt[load_node_id]["inputs"]["password"] = PASSWORD

    response = requests.post(COMFYUI_API_URL, json={"prompt": prompt})
    if response.status_code == 200:
        print(f"Submitted: {zip_file} -> {response.json()}")
    else:
        print(f"Failed: {zip_file} - {response.text}")

    time.sleep(0.5)

print("All tasks submitted.")

init.py

作用:包整理,映射3节点的文件夹位置

python 复制代码
from .LoadFramesFromZIP import NODE_CLASS_MAPPINGS as load_mappings
from .SaveFramesAsZIP import NODE_CLASS_MAPPINGS as save_mappings
from .LoadImageWithFilename import NODE_CLASS_MAPPINGS as loadimg_mappings

NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(load_mappings)
NODE_CLASS_MAPPINGS.update(save_mappings)
NODE_CLASS_MAPPINGS.update(loadimg_mappings)

NODE_DISPLAY_NAME_MAPPINGS = {
    # 如果每个文件都有各自的显示映射,可以合并;也可以留空使用默认
}

⑥ LoadFramesWorkflow.json 仅供参考

案例工作流:使用加载压缩包输出帧节点,生成视频的案例,仅供参考,得定制

python 复制代码
{
  "1": {
    "inputs": {
      "ckpt_name": "rife47.pth",
      "clear_cache_after_n_frames": 100,
      "multiplier": 2,
      "fast_mode": true,
      "ensemble": true,
      "scale_factor": 1,
      "frames": [
        "3",
        0
      ]
    },
    "class_type": "RIFE VFI",
    "_meta": {
      "title": "RIFE VFI (recommend rife47 and rife49)"
    }
  },
  "2": {
    "inputs": {
      "frame_rate": 32,
      "loop_count": 0,
      "filename_prefix": [
        "3",
        2
      ],
      "format": "video/h264-mp4",
      "pix_fmt": "yuv420p",
      "crf": 15,
      "save_metadata": true,
      "trim_to_audio": false,
      "pingpong": false,
      "save_output": true,
      "images": [
        "1",
        0
      ]
    },
    "class_type": "VHS_VideoCombine",
    "_meta": {
      "title": "Video Combine"
    }
  },
  "3": {
    "inputs": {
      "zip_file": "56.zip",
      "password": "bbq"
    },
    "class_type": "LoadFramesFromZIP",
    "_meta": {
      "title": "Load Frames from ZIP"
    }
  }
}

⑦ tasks_submit.py 仅供参考

案例批量提交任务脚本:针对工作流进行批量提交,仅供参考,得定制。

javascript 复制代码
import os
import json
import requests
import time

# 配置
COMFYUI_API_URL = "http://127.0.0.1:8188/prompt"
INPUT_DIR = r"E:\AI\。。。\ComfyUI\input"
TEMPLATE_JSON = r"E:\AI\。。。\ComfyUI\custom_nodes\bbq_nodes\tasks_submit\LoadFramesWorkflow.json"  # 保存您的 API 格式 JSON 文件路径
PASSWORD = "bbq"

# 读取模板
with open(TEMPLATE_JSON, 'r', encoding='utf-8') as f:
    template = json.load(f)

# 找到 LoadFramesFromZIP 节点 ID
load_node_id = None
for nid, node in template.items():
    if node.get("class_type") == "LoadFramesFromZIP":
        load_node_id = nid
        break
if load_node_id is None:
    print("Error: No LoadFramesFromZIP node found.")
    exit()

# 获取所有 ZIP 文件
zip_files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith('.zip')]
if not zip_files:
    print("No ZIP files found.")
    exit()

# 批量提交
for zip_file in zip_files:
    prompt = json.loads(json.dumps(template))  # 深拷贝
    prompt[load_node_id]["inputs"]["zip_file"] = zip_file
    prompt[load_node_id]["inputs"]["password"] = PASSWORD

    response = requests.post(COMFYUI_API_URL, json={"prompt": prompt})
    if response.status_code == 200:
        print(f"Submitted: {zip_file} -> {response.json()}")
    else:
        print(f"Failed: {zip_file} - {response.text}")

    time.sleep(0.5)

print("All tasks submitted.")

下载链接

https://pan.baidu.com/s/1KohS3ZotX_GDnNIVtfY9uA?pwd=km4u

总结

这样就形成了一套加密输出工作流,代码不多,比较简单,但非专职py开发者在ai的辅助下也还是挺麻烦的。

相关推荐
Alvin千里无风7 小时前
在 Ubuntu 上从源码安装 Nanobot:轻量级 AI 助手完整指南
linux·人工智能·ubuntu
环黄金线HHJX.7 小时前
龙虾钳足启发的AI集群语言交互新范式
开发语言·人工智能·算法·编辑器·交互
Omics Pro7 小时前
虚拟细胞:开启HIV/AIDS治疗新纪元的关键?
大数据·数据库·人工智能·深度学习·算法·机器学习·计算机视觉
书到用时方恨少!7 小时前
Python Pandas 使用指南:数据分析的瑞士军刀
python·数据分析·pandas
悦来客栈的老板7 小时前
AI逆向|猿人学逆向反混淆练习平台第七题加密分析
人工智能
KOYUELEC光与电子努力加油7 小时前
JAE日本航空端子推出支持自走式机器人的自主充电功能浮动式连接器“DW15系列“方案与应用
服务器·人工智能·机器人·无人机
萤火阳光7 小时前
13|自定义 Skill 创作:打造专属自动化利器
人工智能
我哪会这个啊7 小时前
SpringAlibaba Ai基础入门
人工智能
智算菩萨7 小时前
【Pygame】第8章 文字渲染与字体系统(支持中文字体)
开发语言·python·pygame
:mnong7 小时前
全图纸语义理解升级分析
python·openvino·paddleocr·qt6.3·paddleocr-vl