added_new_node
This commit is contained in:
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user