Merge with upstream

This commit is contained in:
doctorpangloss 2024-02-07 14:27:50 -08:00
commit 8e9052c843
17 changed files with 249 additions and 43 deletions

View File

@ -159,7 +159,7 @@ On macOS, install exactly Python 3.11 using `brew`, which you can download from
curl -L https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -o ./models/checkpoints/v1-5-pruned-emaonly.ckpt curl -L https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -o ./models/checkpoints/v1-5-pruned-emaonly.ckpt
``` ```
3. Put your VAE into the `models/vae` folder. 3. (Optional) Put your VAE into the `models/vae` folder.
4. (Optional) Create a virtual environment: 4. (Optional) Create a virtual environment:
1. Create an environment: 1. Create an environment:

View File

@ -497,7 +497,7 @@ def unet_dtype(device=None, model_params=0):
return torch.float8_e4m3fn return torch.float8_e4m3fn
if args.fp8_e5m2_unet: if args.fp8_e5m2_unet:
return torch.float8_e5m2 return torch.float8_e5m2
if should_use_fp16(device=device, model_params=model_params): if should_use_fp16(device=device, model_params=model_params, manual_cast=True):
return torch.float16 return torch.float16
return torch.float32 return torch.float32
@ -547,10 +547,8 @@ def text_encoder_dtype(device=None):
if is_device_cpu(device): if is_device_cpu(device):
return torch.float16 return torch.float16
if should_use_fp16(device, prioritize_performance=False): return torch.float16
return torch.float16
else:
return torch.float32
def intermediate_device(): def intermediate_device():
if args.gpu_only: if args.gpu_only:
@ -699,7 +697,7 @@ def is_device_mps(device):
return True return True
return False return False
def should_use_fp16(device=None, model_params=0, prioritize_performance=True): def should_use_fp16(device=None, model_params=0, prioritize_performance=True, manual_cast=False):
global directml_enabled global directml_enabled
if device is not None: if device is not None:
@ -725,10 +723,13 @@ def should_use_fp16(device=None, model_params=0, prioritize_performance=True):
if is_intel_xpu(): if is_intel_xpu():
return True return True
if torch.cuda.is_bf16_supported(): if torch.version.hip:
return True return True
props = torch.cuda.get_device_properties("cuda") props = torch.cuda.get_device_properties("cuda")
if props.major >= 8:
return True
if props.major < 6: if props.major < 6:
return False return False
@ -741,7 +742,7 @@ def should_use_fp16(device=None, model_params=0, prioritize_performance=True):
if x in props.name.lower(): if x in props.name.lower():
fp16_works = True fp16_works = True
if fp16_works: if fp16_works or manual_cast:
free_model_memory = (get_free_memory() * 0.9 - minimum_inference_memory()) free_model_memory = (get_free_memory() * 0.9 - minimum_inference_memory())
if (not prioritize_performance) or model_params * 4 > free_model_memory: if (not prioritize_performance) or model_params * 4 > free_model_memory:
return True return True

View File

@ -462,7 +462,7 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o
model.load_model_weights(sd, "model.diffusion_model.") model.load_model_weights(sd, "model.diffusion_model.")
if output_vae: if output_vae:
vae_sd = utils.state_dict_prefix_replace(sd, {"first_stage_model.": ""}, filter_keys=True) vae_sd = utils.state_dict_prefix_replace(sd, {k: "" for k in model_config.vae_key_prefix}, filter_keys=True)
vae_sd = model_config.process_vae_state_dict(vae_sd) vae_sd = model_config.process_vae_state_dict(vae_sd)
vae = VAE(sd=vae_sd) vae = VAE(sd=vae_sd)

View File

@ -21,6 +21,7 @@ class BASE:
noise_aug_config = None noise_aug_config = None
sampling_settings = {} sampling_settings = {}
latent_format = latent_formats.LatentFormat latent_format = latent_formats.LatentFormat
vae_key_prefix = ["first_stage_model."]
manual_cast_dtype = None manual_cast_dtype = None

View File

@ -415,6 +415,8 @@ def tiled_scale(samples, function, tile_x=64, tile_y=64, overlap = 8, upscale_am
out_div = torch.zeros((s.shape[0], out_channels, round(s.shape[2] * upscale_amount), round(s.shape[3] * upscale_amount)), device=output_device) out_div = torch.zeros((s.shape[0], out_channels, round(s.shape[2] * upscale_amount), round(s.shape[3] * upscale_amount)), device=output_device)
for y in range(0, s.shape[2], tile_y - overlap): for y in range(0, s.shape[2], tile_y - overlap):
for x in range(0, s.shape[3], tile_x - overlap): for x in range(0, s.shape[3], tile_x - overlap):
x = max(0, min(s.shape[-1] - overlap, x))
y = max(0, min(s.shape[-2] - overlap, y))
s_in = s[:,:,y:y+tile_y,x:x+tile_x] s_in = s[:,:,y:y+tile_y,x:x+tile_x]
ps = function(s_in).to(output_device) ps = function(s_in).to(output_device)

View File

@ -910,6 +910,9 @@ export class GroupNodeHandler {
const self = this; const self = this;
const onNodeCreated = this.node.onNodeCreated; const onNodeCreated = this.node.onNodeCreated;
this.node.onNodeCreated = function () { this.node.onNodeCreated = function () {
if (!this.widgets) {
return;
}
const config = self.groupData.nodeData.config; const config = self.groupData.nodeData.config;
if (config) { if (config) {
for (const n in config) { for (const n in config) {

View File

@ -48,7 +48,7 @@
list-style: none; list-style: none;
} }
.comfy-group-manage-list-items { .comfy-group-manage-list-items {
max-height: 70vh; max-height: calc(100% - 40px);
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
} }

View File

@ -62,7 +62,7 @@ async function uploadMask(filepath, formData) {
ClipspaceDialog.invalidatePreview(); ClipspaceDialog.invalidatePreview();
} }
function prepare_mask(image, maskCanvas, maskCtx) { function prepare_mask(image, maskCanvas, maskCtx, maskColor) {
// paste mask data into alpha channel // paste mask data into alpha channel
maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height); maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);
@ -74,9 +74,9 @@ function prepare_mask(image, maskCanvas, maskCtx) {
else else
maskData.data[i+3] = 255; maskData.data[i+3] = 255;
maskData.data[i] = 0; maskData.data[i] = maskColor.r;
maskData.data[i+1] = 0; maskData.data[i+1] = maskColor.g;
maskData.data[i+2] = 0; maskData.data[i+2] = maskColor.b;
} }
maskCtx.globalCompositeOperation = 'source-over'; maskCtx.globalCompositeOperation = 'source-over';
@ -110,6 +110,7 @@ class MaskEditorDialog extends ComfyDialog {
createButton(name, callback) { createButton(name, callback) {
var button = document.createElement("button"); var button = document.createElement("button");
button.style.pointerEvents = "auto";
button.innerText = name; button.innerText = name;
button.addEventListener("click", callback); button.addEventListener("click", callback);
return button; return button;
@ -146,6 +147,7 @@ class MaskEditorDialog extends ComfyDialog {
divElement.style.display = "flex"; divElement.style.display = "flex";
divElement.style.position = "relative"; divElement.style.position = "relative";
divElement.style.top = "2px"; divElement.style.top = "2px";
divElement.style.pointerEvents = "auto";
self.brush_slider_input = document.createElement('input'); self.brush_slider_input = document.createElement('input');
self.brush_slider_input.setAttribute('type', 'range'); self.brush_slider_input.setAttribute('type', 'range');
self.brush_slider_input.setAttribute('min', '1'); self.brush_slider_input.setAttribute('min', '1');
@ -173,6 +175,7 @@ class MaskEditorDialog extends ComfyDialog {
bottom_panel.style.left = "20px"; bottom_panel.style.left = "20px";
bottom_panel.style.right = "20px"; bottom_panel.style.right = "20px";
bottom_panel.style.height = "50px"; bottom_panel.style.height = "50px";
bottom_panel.style.pointerEvents = "none";
var brush = document.createElement("div"); var brush = document.createElement("div");
brush.id = "brush"; brush.id = "brush";
@ -191,14 +194,29 @@ class MaskEditorDialog extends ComfyDialog {
this.element.appendChild(bottom_panel); this.element.appendChild(bottom_panel);
document.body.appendChild(brush); document.body.appendChild(brush);
var clearButton = this.createLeftButton("Clear", () => {
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
});
this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => { this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => {
self.brush_size = event.target.value; self.brush_size = event.target.value;
self.updateBrushPreview(self, null, null); self.updateBrushPreview(self, null, null);
}); });
var clearButton = this.createLeftButton("Clear",
() => { this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height); if (self.brush_color_mode === "black") {
}); self.brush_color_mode = "white";
}
else if (self.brush_color_mode === "white") {
self.brush_color_mode = "negative";
}
else {
self.brush_color_mode = "black";
}
self.updateWhenBrushColorModeChanged();
});
var cancelButton = this.createRightButton("Cancel", () => { var cancelButton = this.createRightButton("Cancel", () => {
document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp); document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
@ -219,6 +237,7 @@ class MaskEditorDialog extends ComfyDialog {
bottom_panel.appendChild(this.saveButton); bottom_panel.appendChild(this.saveButton);
bottom_panel.appendChild(cancelButton); bottom_panel.appendChild(cancelButton);
bottom_panel.appendChild(this.brush_size_slider); bottom_panel.appendChild(this.brush_size_slider);
bottom_panel.appendChild(this.colorButton);
imgCanvas.style.position = "absolute"; imgCanvas.style.position = "absolute";
maskCanvas.style.position = "absolute"; maskCanvas.style.position = "absolute";
@ -228,6 +247,10 @@ class MaskEditorDialog extends ComfyDialog {
maskCanvas.style.top = imgCanvas.style.top; maskCanvas.style.top = imgCanvas.style.top;
maskCanvas.style.left = imgCanvas.style.left; maskCanvas.style.left = imgCanvas.style.left;
const maskCanvasStyle = this.getMaskCanvasStyle();
maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
maskCanvas.style.opacity = maskCanvasStyle.opacity;
} }
async show() { async show() {
@ -313,7 +336,7 @@ class MaskEditorDialog extends ComfyDialog {
let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true }); let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true });
imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height); imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height);
prepare_mask(mask_image, this.maskCanvas, maskCtx); prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor());
} }
async setImages(imgCanvas) { async setImages(imgCanvas) {
@ -439,7 +462,84 @@ class MaskEditorDialog extends ComfyDialog {
} }
} }
getMaskCanvasStyle() {
if (this.brush_color_mode === "negative") {
return {
mixBlendMode: "difference",
opacity: "1",
};
}
else {
return {
mixBlendMode: "initial",
opacity: "0.7",
};
}
}
getMaskColor() {
if (this.brush_color_mode === "black") {
return { r: 0, g: 0, b: 0 };
}
if (this.brush_color_mode === "white") {
return { r: 255, g: 255, b: 255 };
}
if (this.brush_color_mode === "negative") {
// negative effect only works with white color
return { r: 255, g: 255, b: 255 };
}
return { r: 0, g: 0, b: 0 };
}
getMaskFillStyle() {
const maskColor = this.getMaskColor();
return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")";
}
getColorButtonText() {
let colorCaption = "unknown";
if (this.brush_color_mode === "black") {
colorCaption = "black";
}
else if (this.brush_color_mode === "white") {
colorCaption = "white";
}
else if (this.brush_color_mode === "negative") {
colorCaption = "negative";
}
return "Color: " + colorCaption;
}
updateWhenBrushColorModeChanged() {
this.colorButton.innerText = this.getColorButtonText();
// update mask canvas css styles
const maskCanvasStyle = this.getMaskCanvasStyle();
this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
this.maskCanvas.style.opacity = maskCanvasStyle.opacity;
// update mask canvas rgb colors
const maskColor = this.getMaskColor();
const maskData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
for (let i = 0; i < maskData.data.length; i += 4) {
maskData.data[i] = maskColor.r;
maskData.data[i+1] = maskColor.g;
maskData.data[i+2] = maskColor.b;
}
this.maskCtx.putImageData(maskData, 0, 0);
}
brush_size = 10; brush_size = 10;
brush_color_mode = "black";
drawing_mode = false; drawing_mode = false;
lastx = -1; lastx = -1;
lasty = -1; lasty = -1;
@ -518,6 +618,19 @@ class MaskEditorDialog extends ComfyDialog {
event.preventDefault(); event.preventDefault();
self.pan_move(self, event); self.pan_move(self, event);
} }
let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
if(event.shiftKey && left_button_down) {
self.drawing_mode = false;
const y = event.clientY;
let delta = (self.zoom_lasty - y)*0.005;
self.zoom_ratio = Math.max(Math.min(10.0, self.last_zoom_ratio - delta), 0.2);
this.invalidatePanZoom();
return;
}
} }
pan_move(self, event) { pan_move(self, event) {
@ -535,7 +648,7 @@ class MaskEditorDialog extends ComfyDialog {
} }
draw_move(self, event) { draw_move(self, event) {
if(event.ctrlKey) { if(event.ctrlKey || event.shiftKey) {
return; return;
} }
@ -546,7 +659,10 @@ class MaskEditorDialog extends ComfyDialog {
self.updateBrushPreview(self); self.updateBrushPreview(self);
if (window.TouchEvent && event instanceof TouchEvent || event.buttons == 1) { let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
let right_button_down = [2, 5, 32].includes(event.buttons);
if (!event.altKey && left_button_down) {
var diff = performance.now() - self.lasttime; var diff = performance.now() - self.lasttime;
const maskRect = self.maskCanvas.getBoundingClientRect(); const maskRect = self.maskCanvas.getBoundingClientRect();
@ -581,7 +697,7 @@ class MaskEditorDialog extends ComfyDialog {
if(diff > 20 && !this.drawing_mode) if(diff > 20 && !this.drawing_mode)
requestAnimationFrame(() => { requestAnimationFrame(() => {
self.maskCtx.beginPath(); self.maskCtx.beginPath();
self.maskCtx.fillStyle = "rgb(0,0,0)"; self.maskCtx.fillStyle = this.getMaskFillStyle();
self.maskCtx.globalCompositeOperation = "source-over"; self.maskCtx.globalCompositeOperation = "source-over";
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
self.maskCtx.fill(); self.maskCtx.fill();
@ -591,7 +707,7 @@ class MaskEditorDialog extends ComfyDialog {
else else
requestAnimationFrame(() => { requestAnimationFrame(() => {
self.maskCtx.beginPath(); self.maskCtx.beginPath();
self.maskCtx.fillStyle = "rgb(0,0,0)"; self.maskCtx.fillStyle = this.getMaskFillStyle();
self.maskCtx.globalCompositeOperation = "source-over"; self.maskCtx.globalCompositeOperation = "source-over";
var dx = x - self.lastx; var dx = x - self.lastx;
@ -613,7 +729,7 @@ class MaskEditorDialog extends ComfyDialog {
self.lasttime = performance.now(); self.lasttime = performance.now();
} }
else if(event.buttons == 2 || event.buttons == 5 || event.buttons == 32) { else if((event.altKey && left_button_down) || right_button_down) {
const maskRect = self.maskCanvas.getBoundingClientRect(); const maskRect = self.maskCanvas.getBoundingClientRect();
const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio; const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio; const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
@ -687,13 +803,20 @@ class MaskEditorDialog extends ComfyDialog {
self.drawing_mode = true; self.drawing_mode = true;
event.preventDefault(); event.preventDefault();
if(event.shiftKey) {
self.zoom_lasty = event.clientY;
self.last_zoom_ratio = self.zoom_ratio;
return;
}
const maskRect = self.maskCanvas.getBoundingClientRect(); const maskRect = self.maskCanvas.getBoundingClientRect();
const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio; const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio; const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
self.maskCtx.beginPath(); self.maskCtx.beginPath();
if (event.button == 0) { if (!event.altKey && event.button == 0) {
self.maskCtx.fillStyle = "rgb(0,0,0)"; self.maskCtx.fillStyle = this.getMaskFillStyle();
self.maskCtx.globalCompositeOperation = "source-over"; self.maskCtx.globalCompositeOperation = "source-over";
} else { } else {
self.maskCtx.globalCompositeOperation = "destination-out"; self.maskCtx.globalCompositeOperation = "destination-out";

View File

@ -260,6 +260,12 @@ app.registerExtension({
async beforeRegisterNodeDef(nodeType, nodeData, app) { async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Add menu options to conver to/from widgets // Add menu options to conver to/from widgets
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.convertWidgetToInput = function (widget) {
const config = getConfig.call(this, widget.name) ?? [widget.type, widget.options || {}];
if (!isConvertableWidget(widget, config)) return false;
convertToInput(this, widget, config);
return true;
};
nodeType.prototype.getExtraMenuOptions = function (_, options) { nodeType.prototype.getExtraMenuOptions = function (_, options) {
const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined;

View File

@ -11549,7 +11549,7 @@ LGraphNode.prototype.executeAction = function(action)
dialog.close(); dialog.close();
} else if (e.keyCode == 13) { } else if (e.keyCode == 13) {
if (selected) { if (selected) {
select(selected.innerHTML); select(unescape(selected.dataset["type"]));
} else if (first) { } else if (first) {
select(first); select(first);
} else { } else {
@ -11910,7 +11910,7 @@ LGraphNode.prototype.executeAction = function(action)
var ctor = LiteGraph.registered_node_types[ type ]; var ctor = LiteGraph.registered_node_types[ type ];
if(filter && ctor.filter != filter ) if(filter && ctor.filter != filter )
return false; return false;
if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1) if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1 && (!ctor.title || ctor.title.toLowerCase().indexOf(str) === -1))
return false; return false;
// filter by slot IN, OUT types // filter by slot IN, OUT types
@ -11964,7 +11964,18 @@ LGraphNode.prototype.executeAction = function(action)
if (!first) { if (!first) {
first = type; first = type;
} }
help.innerText = type;
const nodeType = LiteGraph.registered_node_types[type];
if (nodeType?.title) {
help.innerText = nodeType?.title;
const typeEl = document.createElement("span");
typeEl.className = "litegraph lite-search-item-type";
typeEl.textContent = type;
help.append(typeEl);
} else {
help.innerText = type;
}
help.dataset["type"] = escape(type); help.dataset["type"] = escape(type);
help.className = "litegraph lite-search-item"; help.className = "litegraph lite-search-item";
if (className) { if (className) {

View File

@ -184,6 +184,7 @@
color: white; color: white;
padding-left: 10px; padding-left: 10px;
margin-right: 5px; margin-right: 5px;
max-width: 300px;
} }
.litegraph.litesearchbox .name { .litegraph.litesearchbox .name {
@ -227,6 +228,18 @@
color: black; color: black;
} }
.litegraph.lite-search-item-type {
display: inline-block;
background: rgba(0,0,0,0.2);
margin-left: 5px;
font-size: 14px;
padding: 2px 5px;
position: relative;
top: -2px;
opacity: 0.8;
border-radius: 4px;
}
/* DIALOGs ******/ /* DIALOGs ******/
.litegraph .dialog { .litegraph .dialog {

View File

@ -5,6 +5,7 @@ class ComfyApi extends EventTarget {
super(); super();
this.api_host = location.host; this.api_host = location.host;
this.api_base = location.pathname.split('/').slice(0, -1).join('/'); this.api_base = location.pathname.split('/').slice(0, -1).join('/');
this.initialClientId = sessionStorage.getItem("clientId");
} }
apiURL(route) { apiURL(route) {
@ -118,7 +119,8 @@ class ComfyApi extends EventTarget {
case "status": case "status":
if (msg.data.sid) { if (msg.data.sid) {
this.clientId = msg.data.sid; this.clientId = msg.data.sid;
window.name = this.clientId; window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
} }
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status })); this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
break; break;

View File

@ -1499,12 +1499,17 @@ export class ComfyApp {
// Load previous workflow // Load previous workflow
let restored = false; let restored = false;
try { try {
const json = localStorage.getItem("workflow"); const loadWorkflow = async (json) => {
if (json) { if (json) {
const workflow = JSON.parse(json); const workflow = JSON.parse(json);
await this.loadGraphData(workflow); await this.loadGraphData(workflow);
restored = true; return true;
} }
};
const clientId = api.initialClientId ?? api.clientId;
restored =
(clientId && (await loadWorkflow(sessionStorage.getItem(`workflow:${clientId}`)))) ||
(await loadWorkflow(localStorage.getItem("workflow")));
} catch (err) { } catch (err) {
console.error("Error loading previous workflow", err); console.error("Error loading previous workflow", err);
} }
@ -1515,7 +1520,13 @@ export class ComfyApp {
} }
// Save current workflow automatically // Save current workflow automatically
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000); setInterval(() => {
const workflow = JSON.stringify(this.graph.serialize());
localStorage.setItem("workflow", workflow);
if (api.clientId) {
sessionStorage.setItem(`workflow:${api.clientId}`, workflow);
}
}, 1000);
this.#addDrawNodeHandler(); this.#addDrawNodeHandler();
this.#addDrawGroupsHandler(); this.#addDrawGroupsHandler();
@ -2096,6 +2107,8 @@ export class ComfyApp {
this.loadGraphData(JSON.parse(pngInfo.Workflow)); // Support loading workflows from that webp custom node. this.loadGraphData(JSON.parse(pngInfo.Workflow)); // Support loading workflows from that webp custom node.
} else if (pngInfo.prompt) { } else if (pngInfo.prompt) {
this.loadApiJson(JSON.parse(pngInfo.prompt)); this.loadApiJson(JSON.parse(pngInfo.prompt));
} else if (pngInfo.Prompt) {
this.loadApiJson(JSON.parse(pngInfo.Prompt)); // Support loading prompts from that webp custom node.
} }
} }
} else if (file.type === "application/json" || file.name?.endsWith(".json")) { } else if (file.type === "application/json" || file.name?.endsWith(".json")) {
@ -2149,8 +2162,17 @@ export class ComfyApp {
if (value instanceof Array) { if (value instanceof Array) {
const [fromId, fromSlot] = value; const [fromId, fromSlot] = value;
const fromNode = app.graph.getNodeById(fromId); const fromNode = app.graph.getNodeById(fromId);
const toSlot = node.inputs?.findIndex((inp) => inp.name === input); let toSlot = node.inputs?.findIndex((inp) => inp.name === input);
if (toSlot !== -1) { if (toSlot == null || toSlot === -1) {
try {
// Target has no matching input, most likely a converted widget
const widget = node.widgets?.find((w) => w.name === input);
if (widget && node.convertWidgetToInput?.(widget)) {
toSlot = node.inputs?.length - 1;
}
} catch (error) {}
}
if (toSlot != null || toSlot !== -1) {
fromNode.connect(fromSlot, node, toSlot); fromNode.connect(fromSlot, node, toSlot);
} }
} else { } else {

View File

@ -81,6 +81,9 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
const isCombo = targetWidget.type === "combo"; const isCombo = targetWidget.type === "combo";
let comboFilter; let comboFilter;
if (isCombo) {
valueControl.options.values.push("increment-wrap");
}
if (isCombo && options.addFilterList !== false) { if (isCombo && options.addFilterList !== false) {
comboFilter = node.addWidget( comboFilter = node.addWidget(
"string", "string",
@ -128,6 +131,12 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
case "increment": case "increment":
current_index += 1; current_index += 1;
break; break;
case "increment-wrap":
current_index += 1;
if ( current_index >= current_length ) {
current_index = 0;
}
break;
case "decrement": case "decrement":
current_index -= 1; current_index -= 1;
break; break;
@ -295,7 +304,7 @@ export const ComfyWidgets = {
let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding") let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding")
if (precision == 0) precision = undefined; if (precision == 0) precision = undefined;
const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding); const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding);
return { widget: node.addWidget(widgetType, inputName, val, return { widget: node.addWidget(widgetType, inputName, val,
function (v) { function (v) {
if (config.round) { if (config.round) {
this.value = Math.round(v/config.round)*config.round; this.value = Math.round(v/config.round)*config.round;

View File

@ -126,7 +126,7 @@ class LatentBatchSeedBehavior:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
return {"required": { "samples": ("LATENT",), return {"required": { "samples": ("LATENT",),
"seed_behavior": (["random", "fixed"],),}} "seed_behavior": (["random", "fixed"],{"default": "fixed"}),}}
RETURN_TYPES = ("LATENT",) RETURN_TYPES = ("LATENT",)
FUNCTION = "op" FUNCTION = "op"

View File

@ -6,6 +6,8 @@ class Example:
------------- -------------
INPUT_TYPES (dict): INPUT_TYPES (dict):
Tell the main program input parameters of nodes. Tell the main program input parameters of nodes.
IS_CHANGED:
optional method to control when the node is re executed.
Attributes Attributes
---------- ----------
@ -89,6 +91,17 @@ class Example:
image = 1.0 - image image = 1.0 - image
return (image,) return (image,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
# A dictionary that contains all nodes you want to export with their names # A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique # NOTE: names should be globally unique

View File

@ -28,7 +28,7 @@ version = '0.0.1'
""" """
The package index to the torch built with AMD ROCm. The package index to the torch built with AMD ROCm.
""" """
amd_torch_index = ("https://download.pytorch.org/whl/rocm5.6", "https://download.pytorch.org/whl/nightly/rocm5.7") amd_torch_index = ("https://download.pytorch.org/whl/rocm5.7", "https://download.pytorch.org/whl/nightly/rocm6.0")
""" """
The package index to torch built with CUDA. The package index to torch built with CUDA.