Add GIF, Krea, and Set/Get workflow tools

Add timed GIF load, stitch, and save nodes; modular Krea 2 editors and prompt encoders; a downscale node; and compatible Set/Get frontend nodes. Also improve video-save progress and chooser positioning, update schema and release checks, and bump the package version to 3.3.1.
This commit is contained in:
Maxed-Out-99
2026-11-12 17:50:23 -08:00
parent a5db511a70
commit fc3aba2e38
16 changed files with 3138 additions and 30 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Release checks using synthetic images only; no model weights or personal media."""
import ast
from pathlib import Path
import torch
import torch.nn.functional as F
def functions(path,names):
tree=ast.parse(Path(path).read_text(encoding='utf-8'))
return compile(ast.Module(body=[n for n in tree.body if isinstance(n,ast.FunctionDef) and n.name in names],type_ignores=[]),path,'exec')
ns={'F':F,'torch':torch}
exec(functions('nodes/krea2_edit_core.py',{'fit_encode_image','fit_boost_mask'}),ns)
class VAE:
def encode(self,image):
self.image=image
return image.movedim(-1,1)
for h,w,th,tw in [(100,100,96,88),(120,240,96,96),(240,120,96,96),(96,96,96,96)]:
image=torch.zeros(1,h,w,3);image[:,h//4:3*h//4,w//4:3*w//4]=1
mask=image[...,0].clone();vae=VAE()
ns['fit_encode_image'](image,vae,th//8,tw//8,{},(0,),'fit')
cropped=ns['fit_boost_mask'](mask,image,th,tw)
aligned=F.interpolate(cropped[:,None],size=vae.image.shape[1:3],mode='bicubic',antialias=True).clamp(0,1)[:,0]
torch.testing.assert_close(aligned,vae.image[...,0])
assert ns['fit_boost_mask'](None,image,th,tw) is None
print('PASS: boost-mask crop matches encoded image for square, near-square, portrait and landscape references')
# The original published model class remains unchanged.
import subprocess
old=ast.parse(subprocess.check_output(['git','show','HEAD:nodes/krea.py']).decode());new=ast.parse(Path('nodes/krea.py').read_text())
get=lambda tree,name:next(n for n in tree.body if isinstance(n,ast.ClassDef) and n.name==name)
assert ast.dump(get(old,'Krea2EditModelPatchMXD'))==ast.dump(get(new,'Krea2EditModelPatchMXD'))
for tree in [old,new]:
c=get(tree,'Krea2EditGroundedEncodeMXD');tree.schema=ast.dump(next(n for n in c.body if isinstance(n,ast.FunctionDef) and n.name=='INPUT_TYPES'))
assert old.schema==new.schema
print('PASS: published legacy model and legacy prompt input schema preserved')
+87
View File
@@ -0,0 +1,87 @@
// Run with node scripts/check_set_get.mjs. No server or personal files needed.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const source = fs.readFileSync(new URL('../web/nodes/mxd_set_get.js', import.meta.url), 'utf8')
.replace(/^import .*;\r?\n/gm, '');
const run = context => vm.runInContext(`(async () => {${source}\n})()`, context);
function environment(extensions = [], existing = {}) {
const registered = [];
class Node {
constructor() { this.properties = {}; this.inputs = []; this.outputs = []; this.widgets = []; }
addInput(name, type) { this.inputs.push({ name, type, link: null }); }
addOutput(name, type) { this.outputs.push({ name, type, links: [] }); }
addWidget(type, name, value, callback, options) {
const widget = { type, name, value, callback, options };
this.widgets.push(widget); return widget;
}
serialize() { return { type: this.type, widgets_values: this.widgets.map(w => w.value) }; }
}
const LiteGraph = {
LGraphNode: Node, registered_node_types: { ...existing },
registerNodeType(key, cls) {
assert.ok(!this.registered_node_types[key], `Duplicate ${key}`);
cls.prototype.type = key; this.registered_node_types[key] = cls;
}
};
const context = vm.createContext({
LiteGraph, window: {}, console, setTimeout, clearTimeout,
api: { getExtensions: async () => extensions },
app: {
registerExtension: ext => registered.push(ext),
canvas: { setDirty() {} },
ui: { settings: { getSettingValue() { return false; } } }
}
});
return { context, registered, LiteGraph };
}
// Detection is based on the served file list, even before KJ runs its code.
for (const url of [
'/extensions/comfyui-kjnodes/js/setgetnodes.js',
'/extensions/renamed%20KJ%20pack/js/setgetnodes.js?version=2'
]) {
const env = environment([url]);
await run(env.context);
assert.equal(env.registered.length, 0);
assert.equal(env.context.window.kjNodes, undefined);
}
const existing = environment([], { SetNode: class {}, GetNode: class {} });
await run(existing.context);
assert.equal(existing.registered.length, 0);
const failed = environment();
failed.context.api.getExtensions = async () => { throw new Error('simulated offline'); };
failed.context.console = { warn() {} };
await run(failed.context);
assert.equal(failed.registered.length, 0);
// Two MXD installations racing must install only one provider.
const env = environment(['/extensions/ComfyUI-MaxedOut/nodes/mxd_set_get.js']);
await Promise.all([run(env.context), run(env.context)]);
assert.equal(new Set(env.registered.map(e => e.name)).size, env.registered.length);
assert.equal(env.registered.filter(e => e.name === 'KJNodes.SetNode').length, 1);
for (const ext of env.registered) ext.registerCustomNodes?.();
assert.deepEqual(Object.keys(env.LiteGraph.registered_node_types).sort(), ['GetNode', 'SetNode']);
const { SetNode, GetNode } = env.LiteGraph.registered_node_types;
const setter = new SetNode();
const getter = new GetNode();
setter.widgets[0].value = getter.widgets[0].value = 'VAE';
setter.inputs[0] = { name: 'VAE', type: 'VAE', link: 7 };
const upstream = { id: 1, type: 'VAELoader' };
const link = { id: 7, origin_id: 1, origin_slot: 0, target_id: 2, target_slot: 0, type: 'VAE' };
const graph = { _nodes: [upstream, setter, getter], _links: { 7: link }, getNodeById: id => id === 1 ? upstream : undefined };
setter.graph = getter.graph = graph;
getter.onRename();
assert.equal(getter.findSetter(graph), setter);
assert.equal(getter.outputs[0].type, 'VAE');
assert.equal(getter.getInputLink(0), link);
assert.equal(getter.serialize().widgets_values[0], 'VAE');
// Renaming a setter must update the paired getter's saved name.
setter.properties.previousName = 'VAE';
setter.widgets[0].value = 'Shared VAE';
setter.update();
assert.equal(getter.widgets[0].value, 'Shared VAE');
assert.equal(getter.getInputLink(0), link);
console.log('OK: Set/Get provider coexistence, singleton, routing, serialization and rename checks');
+2 -2
View File
@@ -49,7 +49,7 @@ def _norm_combo(values, force_dynamic=False):
and not any(_PATHY.search(v) for v in vals)
):
return {"combo": vals}
return {"combo": "nonempty" if vals else "empty", "dynamic": True}
return {"combo": "dynamic", "dynamic": True}
def _norm(obj, in_type_position=False):
@@ -197,7 +197,7 @@ def dump(comfy_root, out_path):
errors = [k for k, v in result["nodes"].items() if "__error__" in v.get("input_types", {})]
if errors:
print(f"[dump_node_schema] WARNING: INPUT_TYPES failed for: {errors}")
return 0
return 1 if errors or not class_map else 0
def compare(a_path, b_path):
+537 -15
View File
@@ -1,5 +1,5 @@
{
"node_count": 70,
"node_count": 80,
"nodes": {
"BboxDetectorCombinedBatchMXD": {
"category": "MXD/Detector",
@@ -175,7 +175,8 @@
"auto",
"mp4",
"mkv",
"webm"
"webm",
"mov"
],
"tooltip": "The format to save the video as."
},
@@ -231,6 +232,58 @@
"IMAGE"
]
},
"DownscaleImageToTotalPixelsMXD": {
"category": "MXD/Upscaling",
"class": "DownscaleImageToTotalPixelsMXD",
"display_name": "Downscale Image MXD",
"function": "downscale",
"input_types": {
"required": {
"image": {
"type": "IMAGE"
},
"megapixels": {
"config": {
"default": 1.0,
"max": 16.0,
"min": 0.01,
"step": 0.01,
"tooltip": "Downscale toward this target; smaller images are only aligned to resolution_steps. Uses 1024 × 1024 pixels per MP, like core."
},
"type": "FLOAT"
},
"resolution_steps": {
"config": {
"advanced": true,
"default": 1,
"max": 256,
"min": 1,
"tooltip": "Align both dimensions to this multiple without enlarging. Rounding may put the result slightly above the megapixel target."
},
"type": "INT"
},
"upscale_method": {
"config": {
"default": "lanczos"
},
"type": {
"combo": [
"nearest-exact",
"bilinear",
"area",
"bicubic",
"lanczos"
]
}
}
}
},
"output_node": false,
"return_names": null,
"return_types": [
"IMAGE"
]
},
"Extract Workflow From Image MXD": {
"category": "MXD/Image",
"class": "ExtractWorkflowFromImageMXD",
@@ -620,6 +673,75 @@
"IMAGE"
]
},
"GIF Stitch MXD": {
"category": "Maxed Out/GIF",
"class": "GifStitchMXD",
"display_name": "GIF Stitch MXD",
"function": "stitch",
"input_types": {
"optional": {
"gif2": {
"type": "MXD_GIF"
}
},
"required": {
"direction": {
"config": {
"default": "right"
},
"type": {
"combo": [
"right",
"down",
"left",
"up"
]
}
},
"gif1": {
"type": "MXD_GIF"
},
"match_image_size": {
"config": {
"default": true
},
"type": "BOOLEAN"
},
"spacing_color": {
"config": {
"default": "white"
},
"type": {
"combo": [
"white",
"black",
"red",
"green",
"blue"
]
}
},
"spacing_width": {
"config": {
"default": 0,
"max": 1024,
"min": 0,
"step": 2
},
"type": "INT"
}
}
},
"output_node": false,
"return_names": [
"gif",
"images"
],
"return_types": [
"MXD_GIF",
"IMAGE"
]
},
"Get Latent Size": {
"category": "MXD/Latent",
"class": "GetLatentSizeMXD",
@@ -820,10 +942,110 @@
"IMAGE"
]
},
"Krea2EditAdvancedMXD": {
"category": "MXD/Krea",
"class": "Krea2EditAdvancedMXD",
"display_name": "Krea2 2 Img Edit Adv MXD",
"function": "patch",
"input_types": {
"optional": {
"image_1_boost": {
"config": {
"default": 1.0,
"max": 1000.0,
"min": 0.0,
"round": 0.001,
"step": 0.01,
"tooltip": "Attention strength for Image 1. 1.0 = unchanged."
},
"type": "FLOAT"
},
"image_1_boost_mask": {
"config": {
"tooltip": "Limit the boost for Image 1 to white areas. This is not an edit or inpainting mask."
},
"type": "MASK"
},
"image_1_position": {
"config": {
"default": "Base",
"tooltip": "Base: follow this image's layout and framing. Ref: use its appearance with freer positioning. This does not change which image supplies source_latent."
},
"type": {
"combo": [
"Base",
"Ref"
]
}
},
"image_2_boost": {
"config": {
"default": 2.0,
"max": 1000.0,
"min": 0.0,
"round": 0.001,
"step": 0.01,
"tooltip": "Attention strength for Image 2. 1.0 = unchanged."
},
"type": "FLOAT"
},
"image_2_boost_mask": {
"config": {
"tooltip": "Limit the boost for Image 2 to white areas. This is not an edit or inpainting mask."
},
"type": "MASK"
},
"image_2_position": {
"config": {
"default": "Ref",
"tooltip": "Base: follow this image's layout and framing. Ref: use its appearance with freer positioning. This does not change which image supplies source_latent."
},
"type": {
"combo": [
"Base",
"Ref"
]
}
}
},
"required": {
"image_1": {
"config": {
"tooltip": "Primary reference image, usually the scene or image to edit."
},
"type": "IMAGE"
},
"image_2": {
"config": {
"tooltip": "Optional second reference, usually a subject to place into Image 1."
},
"type": "IMAGE"
},
"model": {
"type": "MODEL"
},
"vae": {
"config": {
"tooltip": "Required. VAE used to encode the reference images."
},
"type": "VAE"
}
}
},
"output_node": false,
"return_names": [
"model",
"source_latent"
],
"return_types": [
"MODEL",
"LATENT"
]
},
"Krea2EditGroundedEncodeMXD": {
"category": "MXD/Krea",
"class": "Krea2EditGroundedEncodeMXD",
"display_name": "Krea2 Edit MXD",
"display_name": "Krea2 Edit Prompt MXD",
"function": "encode",
"input_types": {
"optional": {
@@ -942,6 +1164,250 @@
"LATENT"
]
},
"Krea2ImgEditAdvMXD": {
"category": "MXD/Krea",
"class": "Krea2ImgEditAdvMXD",
"display_name": "Krea2 Img Edit Adv MXD",
"function": "patch",
"input_types": {
"optional": {
"boost": {
"config": {
"default": 1.0,
"max": 1000.0,
"min": 0.0,
"round": 0.001,
"step": 0.01,
"tooltip": "Attention strength for the image. 1.0 = unchanged."
},
"type": "FLOAT"
},
"boost_mask": {
"config": {
"tooltip": "Limit the boost for the image to white areas. This is not an edit or inpainting mask."
},
"type": "MASK"
},
"position": {
"config": {
"default": "Base",
"tooltip": "Base: follow this image's layout and framing. Ref: use its appearance with freer positioning. This does not change which image supplies source_latent."
},
"type": {
"combo": [
"Base",
"Ref"
]
}
}
},
"required": {
"image": {
"config": {
"tooltip": "The image to edit; supplies source_latent."
},
"type": "IMAGE"
},
"model": {
"type": "MODEL"
},
"vae": {
"config": {
"tooltip": "Required. VAE used to encode the reference images."
},
"type": "VAE"
}
}
},
"output_node": false,
"return_names": [
"model",
"source_latent"
],
"return_types": [
"MODEL",
"LATENT"
]
},
"Krea2ImgEditMXD": {
"category": "MXD/Krea",
"class": "Krea2ImgEditMXD",
"display_name": "Krea2 Img Edit MXD",
"function": "patch",
"input_types": {
"optional": {
"boost": {
"config": {
"default": 1.0,
"max": 1000.0,
"min": 0.0,
"round": 0.001,
"step": 0.01,
"tooltip": "Attention strength for the image. 1.0 = unchanged."
},
"type": "FLOAT"
}
},
"required": {
"image": {
"config": {
"tooltip": "The image to edit; supplies source_latent."
},
"type": "IMAGE"
},
"model": {
"type": "MODEL"
},
"vae": {
"config": {
"tooltip": "Required. VAE used to encode the reference images."
},
"type": "VAE"
}
}
},
"output_node": false,
"return_names": [
"model",
"source_latent"
],
"return_types": [
"MODEL",
"LATENT"
]
},
"Krea2ImgPromptMXD": {
"category": "MXD/Krea",
"class": "Krea2ImgPromptMXD",
"display_name": "Krea2 Img Edit Prompt MXD",
"function": "encode",
"input_types": {
"required": {
"clip": {
"type": "CLIP"
},
"image": {
"config": {
"tooltip": "Connect the same image as the single-image edit node."
},
"type": "IMAGE"
},
"prompt": {
"config": {
"default": "",
"multiline": true
},
"type": "STRING"
}
}
},
"output_node": false,
"return_names": null,
"return_types": [
"CONDITIONING"
]
},
"Krea2TwoImgEditMXD": {
"category": "MXD/Krea",
"class": "Krea2TwoImgEditMXD",
"display_name": "Krea2 2 Img Edit MXD",
"function": "patch",
"input_types": {
"optional": {
"img_base_boost": {
"config": {
"default": 1.0,
"max": 1000.0,
"min": 0.0,
"round": 0.001,
"step": 0.01,
"tooltip": "Attention strength for the base image. 1.0 = unchanged."
},
"type": "FLOAT"
},
"img_ref_boost": {
"config": {
"default": 2.0,
"max": 1000.0,
"min": 0.0,
"round": 0.001,
"step": 0.01,
"tooltip": "Attention strength for the reference image. 1.0 = unchanged."
},
"type": "FLOAT"
}
},
"required": {
"img_base": {
"config": {
"tooltip": "Image to edit. Guides layout and framing; supplies source_latent."
},
"type": "IMAGE"
},
"img_ref": {
"config": {
"tooltip": "Appearance reference with freer placement than the base image."
},
"type": "IMAGE"
},
"model": {
"type": "MODEL"
},
"vae": {
"config": {
"tooltip": "Required. VAE used to encode the reference images."
},
"type": "VAE"
}
}
},
"output_node": false,
"return_names": [
"model",
"source_latent"
],
"return_types": [
"MODEL",
"LATENT"
]
},
"Krea2TwoImgPromptMXD": {
"category": "MXD/Krea",
"class": "Krea2TwoImgPromptMXD",
"display_name": "Krea2 2 Img Edit Prompt MXD",
"function": "encode",
"input_types": {
"required": {
"clip": {
"type": "CLIP"
},
"img_base": {
"config": {
"tooltip": "The base image, or Image 1 when using the advanced editor."
},
"type": "IMAGE"
},
"img_ref": {
"config": {
"tooltip": "The reference image, or Image 2 when using the advanced editor."
},
"type": "IMAGE"
},
"prompt": {
"config": {
"default": "",
"multiline": true
},
"type": "STRING"
}
}
},
"output_node": false,
"return_names": null,
"return_types": [
"CONDITIONING"
]
},
"KreaLayerVarianceMXD": {
"category": "MXD/conditioning",
"class": "KreaLayerVarianceMXD",
@@ -1346,6 +1812,36 @@
"STRING"
]
},
"Load GIF MXD": {
"category": "Maxed Out/GIF",
"class": "LoadGifMXD",
"display_name": "Load GIF MXD",
"function": "load",
"input_types": {
"required": {
"gif": {
"config": {
"animated_image_upload": true,
"default": "<dynamic-combo-default>",
"image_upload": true
},
"type": {
"combo": "dynamic",
"dynamic": true
}
}
}
},
"output_node": false,
"return_names": [
"gif",
"images"
],
"return_types": [
"MXD_GIF",
"IMAGE"
]
},
"Load Image Batch MXD": {
"category": "MXD/Image",
"class": "LoadImageBatchMXD",
@@ -1358,7 +1854,7 @@
"default": "<dynamic-combo-default>"
},
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
},
@@ -1401,7 +1897,7 @@
"default": "<dynamic-combo-default>"
},
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
},
@@ -1435,7 +1931,7 @@
"required": {
"ckpt_name": {
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
}
@@ -1458,7 +1954,7 @@
"required": {
"image": {
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
},
@@ -1511,7 +2007,7 @@
"image_upload": true
},
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
}
@@ -1540,7 +2036,7 @@
"required": {
"latent": {
"type": {
"combo": "empty",
"combo": "dynamic",
"dynamic": true
}
},
@@ -1599,7 +2095,7 @@
"required": {
"latent": {
"type": {
"combo": "empty",
"combo": "dynamic",
"dynamic": true
}
},
@@ -1636,7 +2132,7 @@
"required": {
"latent": {
"type": {
"combo": "empty",
"combo": "dynamic",
"dynamic": true
}
},
@@ -1695,7 +2191,7 @@
"required": {
"subfolder": {
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
}
@@ -1738,7 +2234,7 @@
"required": {
"subfolder": {
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
}
@@ -1761,7 +2257,7 @@
"required": {
"subfolder": {
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
}
@@ -1853,7 +2349,7 @@
},
"video": {
"type": {
"combo": "nonempty",
"combo": "dynamic",
"dynamic": true
}
}
@@ -2351,6 +2847,32 @@
"BOOLEAN"
]
},
"Save GIF MXD": {
"category": "Maxed Out/GIF",
"class": "SaveGifMXD",
"display_name": "Save GIF MXD",
"function": "save",
"input_types": {
"required": {
"filename_prefix": {
"config": {
"default": "MaxedOut/GIF"
},
"type": "STRING"
},
"gif": {
"type": "MXD_GIF"
}
}
},
"output_node": true,
"return_names": [
"gif"
],
"return_types": [
"MXD_GIF"
]
},
"Save Image MXD": {
"category": "MXD/Image",
"class": "SaveImage_MXD",