Initial clean commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# ComfyUI-MaxedOut
|
||||
Custom ComfyUI nodes used in Maxed Out workflows (SDXL, Flux, etc.)
|
||||
@@ -0,0 +1,9 @@
|
||||
from .maxedoutnodes import (
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
+3
File diff suppressed because one or more lines are too long
@@ -0,0 +1,338 @@
|
||||
import torch, math, comfy
|
||||
import comfy.utils
|
||||
import comfy.model_management
|
||||
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict
|
||||
import node_helpers
|
||||
########################################################################################################################
|
||||
# Flux Empty Latent Image (SD3-compatible)
|
||||
class FluxEmptyLatentImage:
|
||||
DESCRIPTION = """
|
||||
- Provides a wide selection of resolutions for easy selection.
|
||||
|
||||
- Meant to save time from manually entering
|
||||
the resolution in the "Empty Latent Image" node.
|
||||
"""
|
||||
TITLE = "Flux Empty Latent Image"
|
||||
CATEGORY = "KJNodes/Latent"
|
||||
|
||||
RESOLUTIONS = {
|
||||
"— High Resolutions —": None,
|
||||
"Square (1:1) 1408x1408": (1408, 1408),
|
||||
"Standard (4:3) 1664x1216": (1664, 1216),
|
||||
"Landscape (3:2) 1728x1152": (1728, 1152),
|
||||
"Widescreen (16:9) 1920x1088": (1920, 1088),
|
||||
"Ultrawide (21:9) 2176x960": (2176, 960),
|
||||
|
||||
"— Standard Resolutions —": None,
|
||||
"Square (1:1) 1024x1024": (1024, 1024),
|
||||
"Standard (4:3) 1152x896": (1152, 896),
|
||||
"Landscape (3:2) 1216x832": (1216, 832),
|
||||
"Widescreen (16:9) 1344x768": (1344, 768),
|
||||
"Ultrawide (21:9) 1536x640": (1536, 640),
|
||||
|
||||
"— Low Resolutions —": None,
|
||||
"Square (1:1) 320x320": (320, 320),
|
||||
"Standard (4:3) 448x320": (448, 320),
|
||||
"Landscape (3:2) 384x256": (384, 256),
|
||||
"Widescreen (16:9) 448x256": (448, 256),
|
||||
"Ultrawide (21:9) 576x256": (576, 256),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.device = comfy.model_management.intermediate_device()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
return {
|
||||
"required": {
|
||||
"resolution": (
|
||||
list(cls.RESOLUTIONS.keys()),
|
||||
{"default": "Square (1:1) 1024x1024"}
|
||||
),
|
||||
"vertical": ("BOOLEAN",),
|
||||
"batch_size": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 4096,
|
||||
"tooltip": "The number of latent images in the batch."
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
OUTPUT_TOOLTIPS = ("The empty latent image batch.",)
|
||||
FUNCTION = "generate"
|
||||
|
||||
def generate(self, resolution, vertical, batch_size=1) -> tuple:
|
||||
size = self.RESOLUTIONS.get(resolution)
|
||||
if size is None:
|
||||
raise ValueError(f"'{resolution}' is a header or invalid option.")
|
||||
|
||||
width, height = size
|
||||
if vertical:
|
||||
width, height = height, width
|
||||
|
||||
latent = torch.zeros([batch_size, 16, height // 8, width // 8], device=self.device)
|
||||
return ({"samples": latent},)
|
||||
|
||||
########################################################################################################################
|
||||
# Sdxl Empty Latent Image
|
||||
class SdxlEmptyLatentImage:
|
||||
DESCRIPTION = """
|
||||
- Generates empty latent images.
|
||||
|
||||
- All supported SDXL resolutions
|
||||
are predefined for ease of use.
|
||||
|
||||
- Meant to save time from manually entering
|
||||
the resolution in the "Empty Latent Image" node.
|
||||
"""
|
||||
TITLE = "Sdxl Empty Latent Image (With Resolutions)"
|
||||
CATEGORY = "KJNodes/Latent"
|
||||
|
||||
# SDXL predefined resolutions (width, height)
|
||||
RESOLUTIONS = {
|
||||
"Square (1:1) 1024x1024": (1024, 1024),
|
||||
"Standard (4:3) 1152x896": (1152, 896),
|
||||
"Landscape (3:2) 1216x832": (1216, 832),
|
||||
"Widescreen (16:9) 1344x768": (1344, 768),
|
||||
"Ultra-Wide (21:9) 1536x640": (1536, 640),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
# Retrieve the intermediate device (usually the GPU) from ComfyUI's model management.
|
||||
self.device = comfy.model_management.intermediate_device()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
return {
|
||||
"required": {
|
||||
# Dropdown selection for one of the predefined SDXL resolutions.
|
||||
"resolution": (list(cls.RESOLUTIONS.keys()),),
|
||||
# Toggle for vertical mode (swaps width and height).
|
||||
"vertical": ("BOOLEAN",),
|
||||
# Number of latent images to create in the batch.
|
||||
"batch_size": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 4096,
|
||||
"tooltip": "The number of latent images in the batch."
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
OUTPUT_TOOLTIPS = ("The empty latent image batch.",)
|
||||
FUNCTION = "generate"
|
||||
|
||||
def generate(self, resolution, vertical, batch_size=1) -> tuple:
|
||||
# Get the selected resolution tuple (width, height)
|
||||
width, height = self.RESOLUTIONS[resolution]
|
||||
# If vertical mode is enabled, swap width and height.
|
||||
if vertical:
|
||||
width, height = height, width
|
||||
|
||||
# Create an empty latent tensor.
|
||||
# Typically, the latent space has 4 channels and each spatial dimension is 1/8th of the image.
|
||||
latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
|
||||
return ({"samples": latent},)
|
||||
|
||||
########################################################################################################################
|
||||
# Image Scale To Total Pixels (SDXL Safe)
|
||||
class ImageScaleToTotalPixelsSafe:
|
||||
DESCRIPTION = """
|
||||
- Scales to target megapixel count, preserving aspect ratio.
|
||||
|
||||
- If image matches SDXL resolutions (e.g. those used in
|
||||
"SDXL Empty Latent Image" node), scaling is skipped.
|
||||
|
||||
- Meant for SDXL workflows (e.g. image-to-image, inpainting)
|
||||
to auto-scale random images but not images already made with SDXL.
|
||||
"""
|
||||
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
||||
|
||||
# SDXL-safe resolutions (width, height) – store one orientation only,
|
||||
# the code will check both (w, h) and (h, w)
|
||||
SDXL_SAFE_RESOLUTIONS = [
|
||||
(1024, 1024),
|
||||
(1152, 896),
|
||||
(1216, 832),
|
||||
(1344, 768),
|
||||
(1536, 640),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"upscale_method": (cls.upscale_methods,),
|
||||
"total_megapixels": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.01,
|
||||
"max": 128.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "Set the total megapixels (e.g., 1.0 = 1 MP)",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "upscale"
|
||||
CATEGORY = "KJNodes/Upscaling"
|
||||
|
||||
def upscale(self, image, upscale_method, total_megapixels):
|
||||
b, h, w, c = image.shape
|
||||
|
||||
# Skip scaling if the image already matches an SDXL-safe resolution
|
||||
if (w, h) in self.SDXL_SAFE_RESOLUTIONS or (h, w) in self.SDXL_SAFE_RESOLUTIONS:
|
||||
return (image,)
|
||||
|
||||
# ComfyUI-native megapixel math
|
||||
samples = image.movedim(-1, 1)
|
||||
orig_h, orig_w = samples.shape[2], samples.shape[3]
|
||||
|
||||
target_pixels = int(round(total_megapixels * 1024 * 1024))
|
||||
scale_by = math.sqrt(target_pixels / (orig_w * orig_h))
|
||||
|
||||
new_w = max(1, round(orig_w * scale_by))
|
||||
new_h = max(1, round(orig_h * scale_by))
|
||||
|
||||
scaled = comfy.utils.common_upscale(samples, new_w, new_h, upscale_method, "disabled")
|
||||
scaled = scaled.movedim(1, -1)
|
||||
return (scaled,)
|
||||
|
||||
########################################################################################################################
|
||||
# Flux Image Scale To Total Pixels (Flux Safe)
|
||||
class FluxImageScaleToTotalPixelsSafe:
|
||||
DESCRIPTION = """
|
||||
- Scales to target megapixel count, preserving aspect ratio.
|
||||
|
||||
- If image matches Flux-safe resolutions (e.g. those used in
|
||||
the Flux Empty Latent Image node), scaling is skipped.
|
||||
|
||||
- Meant for image-to-image or inpainting workflows to auto-scale
|
||||
arbitrary images, but skip images already matching Flux resolutions.
|
||||
"""
|
||||
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
||||
|
||||
# Flux-safe resolutions (width, height) – stored in one orientation only
|
||||
FLUX_SAFE_RESOLUTIONS = [
|
||||
(1408, 1408),
|
||||
(1728, 1152),
|
||||
(1664, 1216),
|
||||
(1920, 1088),
|
||||
(2176, 960),
|
||||
(1024, 1024),
|
||||
(1216, 832),
|
||||
(1152, 896),
|
||||
(1344, 768),
|
||||
(1536, 640),
|
||||
(320, 320),
|
||||
(384, 256),
|
||||
(448, 320),
|
||||
(448, 256),
|
||||
(576, 256),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"upscale_method": (cls.upscale_methods, ),
|
||||
"total_megapixels": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.01,
|
||||
"max": 128.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "Set the total megapixels (e.g., 1.0 = 1 MP)",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "upscale"
|
||||
CATEGORY = "KJNodes/Upscaling"
|
||||
|
||||
def upscale(self, image, upscale_method, total_megapixels):
|
||||
b, h, w, c = image.shape
|
||||
|
||||
# Skip scaling if image matches any Flux-safe resolution
|
||||
if (w, h) in self.FLUX_SAFE_RESOLUTIONS or (h, w) in self.FLUX_SAFE_RESOLUTIONS:
|
||||
return (image,)
|
||||
|
||||
samples = image.movedim(-1, 1)
|
||||
orig_h, orig_w = samples.shape[2], samples.shape[3]
|
||||
|
||||
target_pixels = int(round(total_megapixels * 1024 * 1024))
|
||||
scale_by = math.sqrt(target_pixels / (orig_w * orig_h))
|
||||
|
||||
new_w = max(1, round(orig_w * scale_by))
|
||||
new_h = max(1, round(orig_h * scale_by))
|
||||
|
||||
scaled = comfy.utils.common_upscale(samples, new_w, new_h, upscale_method, "disabled")
|
||||
scaled = scaled.movedim(1, -1)
|
||||
return (scaled,)
|
||||
|
||||
########################################################################################################################
|
||||
# Prompt with Guidance (Flux)
|
||||
class PromptWithGuidance(ComfyNodeABC):
|
||||
DESCRIPTION = """
|
||||
- Combines clip text encode with flux guidance to lower node count.
|
||||
|
||||
- Also removes the need to convert them into node group within ComfyUI.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> InputTypeDict:
|
||||
return {
|
||||
"required": {
|
||||
"text": (IO.STRING, {"multiline": True, "dynamicPrompts": True}),
|
||||
"clip": (IO.CLIP, {"tooltip": "The CLIP model used for encoding the text."}),
|
||||
"guidance": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0, "step": 0.1})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.CONDITIONING,)
|
||||
FUNCTION = "encode_and_guide"
|
||||
CATEGORY = "KJNodes/conditioning"
|
||||
|
||||
def encode_and_guide(self, text, clip, guidance):
|
||||
if clip is None:
|
||||
raise RuntimeError("CLIP model is None. Your checkpoint may not contain a text encoder.")
|
||||
|
||||
tokens = clip.tokenize(text)
|
||||
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
||||
conditioning = node_helpers.conditioning_set_values(conditioning, {"guidance": guidance})
|
||||
return (conditioning,)
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
# NODE MAPPING
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Flux Empty Latent Image": FluxEmptyLatentImage,
|
||||
"Sdxl Empty Latent Image": SdxlEmptyLatentImage,
|
||||
"Image Scale To Total Pixels (SDXL Safe)": ImageScaleToTotalPixelsSafe,
|
||||
"Flux Image Scale To Total Pixels (Flux Safe)": FluxImageScaleToTotalPixelsSafe,
|
||||
"Prompt With Guidance (Flux)": PromptWithGuidance,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Flux Empty Latent Image": "Flux Empty Latent Image MXD",
|
||||
"Sdxl Empty Latent Image": "SDXL Empty Latent Image MXD",
|
||||
"Image Scale To Total Pixels (SDXL Safe)": "Scale Image (SDXL Safe) MXD",
|
||||
"Flux Image Scale To Total Pixels (Flux Safe)": "Scale Image (Flux Safe) MXD",
|
||||
"Prompt With Guidance (Flux)": "Prompt with Flux Guidance MXD",
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,326 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
// code based on mtb nodes by Mel Massadian https://github.com/melMass/comfy_mtb/
|
||||
export const loadScript = (
|
||||
FILE_URL,
|
||||
async = true,
|
||||
type = 'text/javascript',
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Check if the script already exists
|
||||
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||||
if (existingScript) {
|
||||
resolve({ status: true, message: 'Script already loaded' })
|
||||
return
|
||||
}
|
||||
|
||||
const scriptEle = document.createElement('script')
|
||||
scriptEle.type = type
|
||||
scriptEle.async = async
|
||||
scriptEle.src = FILE_URL
|
||||
|
||||
scriptEle.addEventListener('load', (ev) => {
|
||||
resolve({ status: true })
|
||||
})
|
||||
|
||||
scriptEle.addEventListener('error', (ev) => {
|
||||
reject({
|
||||
status: false,
|
||||
message: `Failed to load the script ${FILE_URL}`,
|
||||
})
|
||||
})
|
||||
|
||||
document.body.appendChild(scriptEle)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadScript('/kjweb_async/marked.min.js').catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
loadScript('/kjweb_async/purify.min.js').catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold", "IC-Light", "WanVideoWrapper"];
|
||||
app.registerExtension({
|
||||
name: "KJNodes.HelpPopup",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
|
||||
if (app.ui.settings.getSettingValue("KJNodes.helpPopup") === false) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
categories.forEach(category => {
|
||||
if (nodeData?.category?.startsWith(category)) {
|
||||
addDocumentation(nodeData, nodeType);
|
||||
}
|
||||
else return
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in registering KJNodes.HelpPopup", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const create_documentation_stylesheet = () => {
|
||||
const tag = 'kj-documentation-stylesheet'
|
||||
|
||||
let styleTag = document.head.querySelector(tag)
|
||||
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.type = 'text/css'
|
||||
styleTag.id = tag
|
||||
styleTag.innerHTML = `
|
||||
.kj-documentation-popup {
|
||||
background: var(--comfy-menu-bg);
|
||||
position: absolute;
|
||||
color: var(--fg-color);
|
||||
font: 12px monospace;
|
||||
line-height: 1.5em;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border-style: solid;
|
||||
border-width: medium;
|
||||
border-color: var(--border-color);
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content-wrapper {
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
/* Scrollbar styling for Chrome */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--fg-color);
|
||||
border-radius: 6px;
|
||||
border: 3px solid var(--bg-color);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for Firefox */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--fg-color) var(--bg-color);
|
||||
a {
|
||||
color: yellow;
|
||||
}
|
||||
a:visited {
|
||||
color: orange;
|
||||
}
|
||||
a:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
/** Add documentation widget to the selected node */
|
||||
export const addDocumentation = (
|
||||
nodeData,
|
||||
nodeType,
|
||||
opts = { icon_size: 14, icon_margin: 4 },) => {
|
||||
|
||||
opts = opts || {}
|
||||
const iconSize = opts.icon_size ? opts.icon_size : 14
|
||||
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
|
||||
let docElement = null
|
||||
let contentWrapper = null
|
||||
//if no description in the node python code, don't do anything
|
||||
if (!nodeData.description) {
|
||||
return
|
||||
}
|
||||
|
||||
const drawFg = nodeType.prototype.onDrawForeground
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const r = drawFg ? drawFg.apply(this, arguments) : undefined
|
||||
if (this.flags.collapsed) return r
|
||||
|
||||
// icon position
|
||||
const x = this.size[0] - iconSize - iconMargin
|
||||
|
||||
// create the popup
|
||||
if (this.show_doc && docElement === null) {
|
||||
docElement = document.createElement('div')
|
||||
contentWrapper = document.createElement('div');
|
||||
docElement.appendChild(contentWrapper);
|
||||
|
||||
create_documentation_stylesheet()
|
||||
contentWrapper.classList.add('content-wrapper');
|
||||
docElement.classList.add('kj-documentation-popup')
|
||||
|
||||
//parse the string from the python node code to html with marked, and sanitize the html with DOMPurify
|
||||
contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(nodeData.description,))
|
||||
|
||||
// resize handle
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.style.width = '0';
|
||||
resizeHandle.style.height = '0';
|
||||
resizeHandle.style.position = 'absolute';
|
||||
resizeHandle.style.bottom = '0';
|
||||
resizeHandle.style.right = '0';
|
||||
resizeHandle.style.cursor = 'se-resize';
|
||||
|
||||
// Add pseudo-elements to create a triangle shape
|
||||
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim();
|
||||
resizeHandle.style.borderTop = '10px solid transparent';
|
||||
resizeHandle.style.borderLeft = '10px solid transparent';
|
||||
resizeHandle.style.borderBottom = `10px solid ${borderColor}`;
|
||||
resizeHandle.style.borderRight = `10px solid ${borderColor}`;
|
||||
|
||||
docElement.appendChild(resizeHandle)
|
||||
let isResizing = false
|
||||
let startX, startY, startWidth, startHeight
|
||||
|
||||
resizeHandle.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isResizing = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10);
|
||||
startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10);
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
// close button
|
||||
const closeButton = document.createElement('div');
|
||||
closeButton.textContent = '❌';
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.top = '0';
|
||||
closeButton.style.right = '0';
|
||||
closeButton.style.cursor = 'pointer';
|
||||
closeButton.style.padding = '5px';
|
||||
closeButton.style.color = 'red';
|
||||
closeButton.style.fontSize = '12px';
|
||||
|
||||
docElement.appendChild(closeButton)
|
||||
|
||||
closeButton.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
this.show_doc = !this.show_doc
|
||||
docElement.parentNode.removeChild(docElement)
|
||||
docElement = null
|
||||
if (contentWrapper) {
|
||||
contentWrapper.remove()
|
||||
contentWrapper = null
|
||||
}
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!isResizing) return;
|
||||
const scale = app.canvas.ds.scale;
|
||||
const newWidth = startWidth + (e.clientX - startX) / scale;
|
||||
const newHeight = startHeight + (e.clientY - startY) / scale;;
|
||||
docElement.style.width = `${newWidth}px`;
|
||||
docElement.style.height = `${newHeight}px`;
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
document.addEventListener('mouseup', function () {
|
||||
isResizing = false
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
)
|
||||
|
||||
document.body.appendChild(docElement)
|
||||
}
|
||||
// close the popup
|
||||
else if (!this.show_doc && docElement !== null) {
|
||||
docElement.parentNode.removeChild(docElement)
|
||||
docElement = null
|
||||
}
|
||||
// update position of the popup
|
||||
if (this.show_doc && docElement !== null) {
|
||||
const rect = ctx.canvas.getBoundingClientRect()
|
||||
const scaleX = rect.width / ctx.canvas.width
|
||||
const scaleY = rect.height / ctx.canvas.height
|
||||
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(scaleX, scaleY)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(this.size[0] * scaleX * Math.max(1.0,window.devicePixelRatio) , 0)
|
||||
.translateSelf(10, -32)
|
||||
|
||||
const scale = new DOMMatrix()
|
||||
.scaleSelf(transform.a, transform.d);
|
||||
const bcr = app.canvas.canvas.getBoundingClientRect()
|
||||
|
||||
const styleObject = {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.a + bcr.x + transform.e}px`,
|
||||
top: `${transform.d + bcr.y + transform.f}px`,
|
||||
};
|
||||
Object.assign(docElement.style, styleObject);
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(x - 2, iconSize - 34)
|
||||
ctx.scale(iconSize / 32, iconSize / 32)
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.lineWidth = 2.4
|
||||
ctx.font = 'bold 36px monospace'
|
||||
ctx.fillStyle = 'orange';
|
||||
ctx.fillText('?', 0, 24)
|
||||
ctx.restore()
|
||||
return r
|
||||
}
|
||||
// handle clicking of the icon
|
||||
const mouseDown = nodeType.prototype.onMouseDown
|
||||
nodeType.prototype.onMouseDown = function (e, localPos, canvas) {
|
||||
const r = mouseDown ? mouseDown.apply(this, arguments) : undefined
|
||||
const iconX = this.size[0] - iconSize - iconMargin
|
||||
const iconY = iconSize - 34
|
||||
if (
|
||||
localPos[0] > iconX &&
|
||||
localPos[0] < iconX + iconSize &&
|
||||
localPos[1] > iconY &&
|
||||
localPos[1] < iconY + iconSize
|
||||
) {
|
||||
if (this.show_doc === undefined) {
|
||||
this.show_doc = true
|
||||
} else {
|
||||
this.show_doc = !this.show_doc
|
||||
}
|
||||
if (this.show_doc) {
|
||||
this.docCtrl = new AbortController()
|
||||
} else {
|
||||
this.docCtrl.abort()
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
const onRem = nodeType.prototype.onRemoved
|
||||
|
||||
nodeType.prototype.onRemoved = function () {
|
||||
const r = onRem ? onRem.apply(this, []) : undefined
|
||||
|
||||
if (docElement) {
|
||||
docElement.remove()
|
||||
docElement = null
|
||||
}
|
||||
|
||||
if (contentWrapper) {
|
||||
contentWrapper.remove()
|
||||
contentWrapper = null
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { applyTextReplacements } from "../../../scripts/utils.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "KJNodes.jsnodes",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if(!nodeData?.category?.startsWith("KJNodes")) {
|
||||
return;
|
||||
}
|
||||
switch (nodeData.name) {
|
||||
case "ConditioningMultiCombine":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this._type = "CONDITIONING"
|
||||
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`conditioning_${i}`, this._type)
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "ImageBatchMulti":
|
||||
case "ImageAddMulti":
|
||||
case "ImageConcatMulti":
|
||||
case "CrossFadeImagesMulti":
|
||||
case "TransitionImagesMulti":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this._type = "IMAGE"
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`image_${i}`, this._type)
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "MaskBatchMulti":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this._type = "MASK"
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`mask_${i}`, this._type)
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "FluxBlockLoraSelect":
|
||||
case "HunyuanVideoBlockLoraSelect":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this.addWidget("button", "Set all", null, () => {
|
||||
const userInput = prompt("Enter the values to set for widgets (e.g., s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0):", "");
|
||||
if (userInput) {
|
||||
const regex = /([sd])?(\d+(?:,\d+|-?\d+)*?)?=(\d+(\.\d+)?)/;
|
||||
const match = userInput.match(regex);
|
||||
if (match) {
|
||||
const type = match[1];
|
||||
const indicesPart = match[2];
|
||||
const value = parseFloat(match[3]);
|
||||
|
||||
let targetWidgets = [];
|
||||
if (type === 's') {
|
||||
targetWidgets = this.widgets.filter(widget => widget.name.includes("single"));
|
||||
} else if (type === 'd') {
|
||||
targetWidgets = this.widgets.filter(widget => widget.name.includes("double"));
|
||||
} else {
|
||||
targetWidgets = this.widgets; // No type specified, all widgets
|
||||
}
|
||||
|
||||
if (indicesPart) {
|
||||
const indices = indicesPart.split(',').flatMap(part => {
|
||||
if (part.includes('-')) {
|
||||
const [start, end] = part.split('-').map(Number);
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
}
|
||||
return Number(part);
|
||||
});
|
||||
|
||||
for (const index of indices) {
|
||||
if (index < targetWidgets.length) {
|
||||
targetWidgets[index].value = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No indices provided, set value for all target widgets
|
||||
for (const widget of targetWidgets) {
|
||||
widget.value = value;
|
||||
}
|
||||
}
|
||||
} else if (!isNaN(parseFloat(userInput))) {
|
||||
// Single value provided, set it for all widgets
|
||||
const value = parseFloat(userInput);
|
||||
for (const widget of this.widgets) {
|
||||
widget.value = value;
|
||||
}
|
||||
} else {
|
||||
alert("Invalid input format. Please use the format s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0");
|
||||
}
|
||||
} else {
|
||||
alert("Invalid input. Please enter a value.");
|
||||
}
|
||||
});
|
||||
};
|
||||
break;
|
||||
|
||||
case "GetMaskSizeAndCount":
|
||||
const onGetMaskSizeConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const v = onGetMaskSizeConnectInput? onGetMaskSizeConnectInput.apply(this, arguments): undefined
|
||||
this.outputs[1]["label"] = "width"
|
||||
this.outputs[2]["label"] = "height"
|
||||
this.outputs[3]["label"] = "count"
|
||||
return v;
|
||||
}
|
||||
const onGetMaskSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = onGetMaskSizeExecuted? onGetMaskSizeExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x').map(Number);
|
||||
this.outputs[1]["label"] = values[1] + " width"
|
||||
this.outputs[2]["label"] = values[2] + " height"
|
||||
this.outputs[3]["label"] = values[0] + " count"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "GetImageSizeAndCount":
|
||||
const onGetImageSizeConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
console.log(this)
|
||||
const v = onGetImageSizeConnectInput? onGetImageSizeConnectInput.apply(this, arguments): undefined
|
||||
//console.log(this)
|
||||
this.outputs[1]["label"] = "width"
|
||||
this.outputs[2]["label"] = "height"
|
||||
this.outputs[3]["label"] = "count"
|
||||
return v;
|
||||
}
|
||||
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
|
||||
const onGetImageSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
console.log(this)
|
||||
const r = onGetImageSizeExecuted? onGetImageSizeExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x').map(Number);
|
||||
console.log(values)
|
||||
this.outputs[1]["label"] = values[1] + " width"
|
||||
this.outputs[2]["label"] = values[2] + " height"
|
||||
this.outputs[3]["label"] = values[0] + " count"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "PreviewAnimation":
|
||||
const onPreviewAnimationConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const v = onPreviewAnimationConnectInput? onPreviewAnimationConnectInput.apply(this, arguments): undefined
|
||||
this.title = "Preview Animation"
|
||||
return v;
|
||||
}
|
||||
const onPreviewAnimationExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = onPreviewAnimationExecuted? onPreviewAnimationExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString();
|
||||
this.title = "Preview Animation " + values
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "VRAM_Debug":
|
||||
const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const v = onVRAM_DebugConnectInput? onVRAM_DebugConnectInput.apply(this, arguments): undefined
|
||||
this.outputs[3]["label"] = "freemem_before"
|
||||
this.outputs[4]["label"] = "freemem_after"
|
||||
return v;
|
||||
}
|
||||
const onVRAM_DebugExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = onVRAM_DebugExecuted? onVRAM_DebugExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x');
|
||||
this.outputs[3]["label"] = values[0] + " freemem_before"
|
||||
this.outputs[4]["label"] = values[1] + " freemem_after"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "JoinStringMulti":
|
||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated || function() {};
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
originalOnNodeCreated.apply(this, arguments);
|
||||
|
||||
this._type = "STRING";
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.name && input.name.toLowerCase().includes("string_")).length
|
||||
if (target_number_of_inputs === num_inputs) return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`string_${i}`, this._type)
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "SoundReactive":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
let audioContext;
|
||||
let microphoneStream;
|
||||
let animationFrameId;
|
||||
let analyser;
|
||||
let dataArray;
|
||||
let startRangeHz;
|
||||
let endRangeHz;
|
||||
let smoothingFactor = 0.5;
|
||||
let smoothedSoundLevel = 0;
|
||||
|
||||
// Function to update the widget value in real-time
|
||||
const updateWidgetValueInRealTime = () => {
|
||||
// Ensure analyser and dataArray are defined before using them
|
||||
if (analyser && dataArray) {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
const startRangeHzWidget = this.widgets.find(w => w.name === "start_range_hz");
|
||||
if (startRangeHzWidget) startRangeHz = startRangeHzWidget.value;
|
||||
const endRangeHzWidget = this.widgets.find(w => w.name === "end_range_hz");
|
||||
if (endRangeHzWidget) endRangeHz = endRangeHzWidget.value;
|
||||
const smoothingFactorWidget = this.widgets.find(w => w.name === "smoothing_factor");
|
||||
if (smoothingFactorWidget) smoothingFactor = smoothingFactorWidget.value;
|
||||
|
||||
// Calculate frequency bin width (frequency resolution)
|
||||
const frequencyBinWidth = audioContext.sampleRate / analyser.fftSize;
|
||||
// Convert the widget values from Hz to indices
|
||||
const startRangeIndex = Math.floor(startRangeHz / frequencyBinWidth);
|
||||
const endRangeIndex = Math.floor(endRangeHz / frequencyBinWidth);
|
||||
|
||||
// Function to calculate the average value for a frequency range
|
||||
const calculateAverage = (start, end) => {
|
||||
const sum = dataArray.slice(start, end).reduce((acc, val) => acc + val, 0);
|
||||
const average = sum / (end - start);
|
||||
|
||||
// Apply exponential moving average smoothing
|
||||
smoothedSoundLevel = (average * (1 - smoothingFactor)) + (smoothedSoundLevel * smoothingFactor);
|
||||
return smoothedSoundLevel;
|
||||
};
|
||||
// Calculate the average levels for each frequency range
|
||||
const soundLevel = calculateAverage(startRangeIndex, endRangeIndex);
|
||||
|
||||
// Update the widget values
|
||||
|
||||
const lowLevelWidget = this.widgets.find(w => w.name === "sound_level");
|
||||
if (lowLevelWidget) lowLevelWidget.value = soundLevel;
|
||||
|
||||
animationFrameId = requestAnimationFrame(updateWidgetValueInRealTime);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to start capturing audio from the microphone
|
||||
const startMicrophoneCapture = () => {
|
||||
// Only create the audio context and analyser once
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
// Access the sample rate of the audio context
|
||||
console.log(`Sample rate: ${audioContext.sampleRate}Hz`);
|
||||
analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 2048;
|
||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
// Get the range values from widgets (assumed to be in Hz)
|
||||
const lowRangeWidget = this.widgets.find(w => w.name === "low_range_hz");
|
||||
if (lowRangeWidget) startRangeHz = lowRangeWidget.value;
|
||||
|
||||
const midRangeWidget = this.widgets.find(w => w.name === "mid_range_hz");
|
||||
if (midRangeWidget) endRangeHz = midRangeWidget.value;
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
||||
microphoneStream = stream;
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
microphone.connect(analyser);
|
||||
updateWidgetValueInRealTime();
|
||||
}).catch(error => {
|
||||
console.error('Access to microphone was denied or an error occurred:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to stop capturing audio from the microphone
|
||||
const stopMicrophoneCapture = () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
if (microphoneStream) {
|
||||
microphoneStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
// Reset audioContext to ensure it can be created again when starting
|
||||
audioContext = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Add start button
|
||||
this.addWidget("button", "Start mic capture", null, startMicrophoneCapture);
|
||||
|
||||
// Add stop button
|
||||
this.addWidget("button", "Stop mic capture", null, stopMicrophoneCapture);
|
||||
};
|
||||
break;
|
||||
case "SaveImageKJ":
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0;
|
||||
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
||||
widget.serializeValue = () => {
|
||||
return applyTextReplacements(app, widget.value);
|
||||
};
|
||||
return r;
|
||||
};
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
async setup() {
|
||||
// to keep Set/Get node virtual connections visible when offscreen
|
||||
const originalComputeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
||||
LGraphCanvas.prototype.computeVisibleNodes = function () {
|
||||
const visibleNodesSet = new Set(originalComputeVisibleNodes.apply(this, arguments));
|
||||
for (const node of this.graph._nodes) {
|
||||
if ((node.type === "SetNode" || node.type === "GetNode") && node.drawConnection) {
|
||||
visibleNodesSet.add(node);
|
||||
}
|
||||
}
|
||||
return Array.from(visibleNodesSet);
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Reference in New Issue
Block a user