added_new_node

This commit is contained in:
Satadal Dhara
2025-11-27 20:13:59 +05:30
parent ede28cbf33
commit de4b4aece9
6 changed files with 250 additions and 7 deletions
+3
View File
@@ -6,6 +6,7 @@ from .nodes.resolution_machine import Resolution_Machine
from .nodes.save_image_metadata import ImageSaveWithMetadata
from .nodes.upscale_machine import Upscale_Machine
from .nodes.prompt_autocomplete import PromptAutocomplete
from .nodes.latent_machine import Latent_Machine
# A dictionary that maps class names to class objects
NODE_CLASS_MAPPINGS = {
@@ -14,6 +15,7 @@ NODE_CLASS_MAPPINGS = {
"ImageSaveWithMetadata": ImageSaveWithMetadata,
"Upscale_Machine": Upscale_Machine,
"PromptAutocomplete": PromptAutocomplete,
"Latent_Machine": Latent_Machine,
}
# A dictionary that maps display names to class names
@@ -23,6 +25,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Save Image w/Metadata": "ImageSaveWithMetadata",
"Upscale Machine": "Upscale_Machine",
"Prompt Autocomplete": "PromptAutocomplete",
"Latent Machine": "Latent_Machine",
}
# A dictionary that contains web directory information
+38 -3
View File
@@ -20,7 +20,7 @@ app.registerExtension({
node.widgets.splice(index, 1);
// Add proper dropdown
const nameWidget = node.addWidget("combo", "name", "None", () => {}, {
const nameWidget = node.addWidget("combo", "name", "None", () => { }, {
values: ["None"]
});
@@ -50,17 +50,52 @@ app.registerExtension({
nameWidget.value = available[0]; // safe fallback
}
// Force update previews for the new name
updatePreviews(nameWidget.value);
app.graph.change();
}
// Hook CSV change
const oldCsvCallback = csvWidget.callback;
csvWidget.callback = function(value) {
csvWidget.callback = function (value) {
if (oldCsvCallback) oldCsvCallback(value);
loadNames(value); // refresh dynamically
};
// --- Preview Logic ---
const posPreview = node.widgets.find(w => w.name === "positive_preview");
const negPreview = node.widgets.find(w => w.name === "negative_preview");
const notePreview = node.widgets.find(w => w.name === "note_preview");
async function updatePreviews(name) {
const csv = csvWidget.value;
if (!csv || !name || name === "None") return;
try {
const resp = await fetch(`/sata/prompt_machine/get?csv=${encodeURIComponent(csv)}&name=${encodeURIComponent(name)}`);
const data = await resp.json();
if (posPreview) posPreview.value = data.positive || "";
if (negPreview) negPreview.value = data.negative || "";
if (notePreview) notePreview.value = data.note || "";
app.graph.change();
} catch (err) {
console.error("[PromptMachineFrontend] Failed to load preview:", err);
}
}
// Hook Name change
const oldNameCallback = nameWidget.callback;
nameWidget.callback = function (value) {
if (oldNameCallback) oldNameCallback(value);
updatePreviews(value);
};
// Initial sync
loadNames(csvWidget.value);
loadNames(csvWidget.value).then(() => {
updatePreviews(nameWidget.value);
});
}
});
+51 -2
View File
@@ -77,21 +77,70 @@ app.registerExtension({
app.graph.change();
}
// --- Preview Logic ---
const previewWidget = node.widgets.find(w => w.name === "dimension_preview");
function updatePreview() {
if (!previewWidget) return;
const w = widthWidget.value;
const h = heightWidget.value;
previewWidget.value = `${w} x ${h}`;
}
// --- Hook model change ---
const oldModelCallback = modelWidget.callback;
modelWidget.callback = function(value) {
modelWidget.callback = function (value) {
if (oldModelCallback) oldModelCallback(value);
refreshResolutions(value);
};
// --- Hook resolution change ---
const oldResolutionCallback = resolutionWidget.callback;
resolutionWidget.callback = function(value) {
resolutionWidget.callback = function (value) {
if (oldResolutionCallback) oldResolutionCallback(value);
applyResolution();
};
// Hook width/height changes for preview
const oldWidthCallback = widthWidget.callback;
widthWidget.callback = function (value) {
if (oldWidthCallback) oldWidthCallback(value);
updatePreview();
};
const oldHeightCallback = heightWidget.callback;
heightWidget.callback = function (value) {
if (oldHeightCallback) oldHeightCallback(value);
updatePreview();
};
// --- Add Swap Button ---
node.addWidget("button", "Swap Dimensions", null, () => {
const w = widthWidget.value;
const h = heightWidget.value;
// Swap values
widthWidget.value = h;
heightWidget.value = w;
// Force "Custom (manual)" to prevent preset overwrite
if (resolutionWidget.value !== "Custom (manual)") {
// Check if "Custom (manual)" exists in options
if (!resolutionWidget.options.values.includes("Custom (manual)")) {
resolutionWidget.options.values.push("Custom (manual)");
}
resolutionWidget.value = "Custom (manual)";
// Unlock widgets since we are now in custom mode
widthWidget.disabled = false;
heightWidget.disabled = false;
}
updatePreview();
app.graph.change();
});
// --- Initial sync ---
refreshResolutions(modelWidget.value);
updatePreview();
}
});
+148
View File
@@ -0,0 +1,148 @@
import torch
import torch.fft
import random
class Latent_Machine:
"""
Creates an empty latent initialized with Power-Law (1/f) noise, Perlin-like noise, or Plasma noise.
Supports 4-channel (SD1.5/SDXL) and 16-channel (Flux/SD3) latents.
"""
def __init__(self):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
"model_type": ([
"SD1.5/SDXL/PicsArt/Kolors/Auraflow (4ch)",
"Flux/Qwen/SD3/Lumina (16ch)"
],),
"noise_type": ([
"Gaussian (White): Sharp Architecture, Text, intricate mechanics",
"Pink (1/f): Photorealism, Portraits, Nature",
"Brown (1/f²): Anime, Digital Art, Bokeh/Backgrounds",
"Perlin : Fantasy landscapes, Fluids, Hair/Fabric",
"Plasma : Sci-Fi, Abstract, Alien terrain"
],),
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "generate_noise"
CATEGORY = "SATA_UtilityNode"
def generate_noise(self, width, height, batch_size, model_type, noise_type, intensity, seed):
# Set seed for reproducibility
torch.manual_seed(seed)
random.seed(seed)
# Determine channels based on model type
if "16ch" in model_type:
c = 16
else:
c = 4
# Latent dimensions (compressed by 8)
h = height // 8
w = width // 8
# Generate base noise
if "Gaussian" in noise_type:
noise = torch.randn((batch_size, c, h, w), device=self.device)
elif "Perlin" in noise_type:
# Multi-Octave Value Noise Approximation
noise = self.generate_perlin_approx(batch_size, c, h, w)
else:
# FFT-based Power Law Noise (Pink, Brown, Plasma)
if "Pink" in noise_type:
alpha = 1.0
elif "Brown" in noise_type:
alpha = 2.0
elif "Plasma" in noise_type:
alpha = 3.0 # Very smooth
else:
alpha = 0.0 # Fallback to white
noise = self.generate_power_law_noise(batch_size, c, h, w, alpha)
# Normalize standard deviation to match expected latent variance (approx 1.0 for standard Gaussian)
# This ensures intensity works consistently across different noise types
current_std = noise.std()
if current_std > 1e-6:
noise = noise / current_std
# Apply intensity
noise = noise * intensity
return ({"samples": noise},)
def generate_power_law_noise(self, batch_size, c, h, w, alpha):
# Generate White Noise (Standard Gaussian)
white_noise = torch.randn((batch_size, c, h, w), device=self.device)
# Convert to Frequency Domain (FFT)
fft_noise = torch.fft.fft2(white_noise)
# Create Frequency Grid
y = torch.fft.fftfreq(h, device=self.device)
x = torch.fft.fftfreq(w, device=self.device)
dy, dx = torch.meshgrid(y, x, indexing='ij')
# Calculate distance from center (frequency magnitude)
# Add epsilon to avoid division by zero at DC component
frequency_magnitude = torch.sqrt(dy**2 + dx**2) + 1e-8
# Apply Power Law Scaling: Amplitude ~ 1 / f^(alpha/2)
scale = 1.0 / (frequency_magnitude ** (alpha / 2.0))
# Zero out the DC component (mean brightness) to keep it neutral
scale[0, 0] = 0
fft_structured = fft_noise * scale
# Convert back to Spatial Domain (Inverse FFT)
structured_noise = torch.fft.ifft2(fft_structured).real
return structured_noise
def generate_perlin_approx(self, batch_size, c, h, w):
"""
Generates a Perlin-like noise by blending upsampled noise from multiple octaves.
This is faster than calculating actual Perlin noise per pixel on the CPU/GPU.
"""
noise = torch.zeros((batch_size, c, h, w), device=self.device)
# Octaves: scale down, generate noise, scale up
octaves = [2, 4, 8, 16]
weights = [0.5, 0.25, 0.125, 0.0625] # 1/f amplitude falloff
total_weight = 0
for scale, weight in zip(octaves, weights):
# Calculate small dimensions
sh, sw = max(1, h // scale), max(1, w // scale)
# Generate random noise at this scale
small_noise = torch.randn((batch_size, c, sh, sw), device=self.device)
# Upsample to full size using bicubic interpolation for smoothness
upsampled = torch.nn.functional.interpolate(
small_noise, size=(h, w), mode='bicubic', align_corners=False
)
noise += upsampled * weight
total_weight += weight
# Add a bit of fine-grained white noise for texture
noise += torch.randn((batch_size, c, h, w), device=self.device) * 0.05
return noise
+6 -1
View File
@@ -107,6 +107,11 @@ class Prompt_Machine:
}),
# make 'name' a free text field (STRING) — avoids list validation problems
"name": ("STRING", {"default": default_name}),
},
"optional": {
"positive_preview": ("STRING", {"multiline": True, "forceInput": False, "default": ""}),
"negative_preview": ("STRING", {"multiline": True, "forceInput": False, "default": ""}),
"note_preview": ("STRING", {"multiline": True, "forceInput": False, "default": ""}),
}
}
@@ -115,7 +120,7 @@ class Prompt_Machine:
FUNCTION = "get_prompts"
CATEGORY = "SATA_UtilityNode"
def get_prompts(self, csv_file, name):
def get_prompts(self, csv_file, name, positive_preview=None, negative_preview=None, note_preview=None):
print(f"[PromptMachine] get_prompts - csv_file={csv_file}, name={name}")
pos, neg, note = read_prompt_row(csv_file, name)
print(f"[PromptMachine] get_prompts -> pos_len={len(pos)}, neg_len={len(neg)}, note_len={len(note)}")
+4 -1
View File
@@ -40,6 +40,9 @@ class Resolution_Machine:
"resolution": (resolutions, {"default": default_resolution}),
"custom_width": ("INT", {"default": 512, "min": 1, "max": 8192}),
"custom_height": ("INT", {"default": 512, "min": 1, "max": 8192}),
},
"optional": {
"dimension_preview": ("STRING", {"multiline": False, "forceInput": False, "default": ""}),
}
}
@@ -48,7 +51,7 @@ class Resolution_Machine:
FUNCTION = "get_resolution"
CATEGORY = "SATA_UtilityNode"
def get_resolution(self, model, resolution, custom_width, custom_height):
def get_resolution(self, model, resolution, custom_width, custom_height, dimension_preview=None):
config = load_config()
if model not in config: