From de4b4aece9e42501c3e767182d803664814fe754 Mon Sep 17 00:00:00 2001 From: Satadal Dhara Date: Thu, 27 Nov 2025 20:13:59 +0530 Subject: [PATCH] added_new_node --- __init__.py | 3 + js/prompt_machine.js | 41 +++++++++- js/resolution_machine.js | 53 ++++++++++++- nodes/latent_machine.py | 148 ++++++++++++++++++++++++++++++++++++ nodes/prompt_machine.py | 7 +- nodes/resolution_machine.py | 5 +- 6 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 nodes/latent_machine.py diff --git a/__init__.py b/__init__.py index 787cc29..18c3756 100644 --- a/__init__.py +++ b/__init__.py @@ -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 diff --git a/js/prompt_machine.js b/js/prompt_machine.js index e88efed..cefd990 100644 --- a/js/prompt_machine.js +++ b/js/prompt_machine.js @@ -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); + }); } }); \ No newline at end of file diff --git a/js/resolution_machine.js b/js/resolution_machine.js index 30f9987..d99dcb8 100644 --- a/js/resolution_machine.js +++ b/js/resolution_machine.js @@ -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(); } }); \ No newline at end of file diff --git a/nodes/latent_machine.py b/nodes/latent_machine.py new file mode 100644 index 0000000..a137af4 --- /dev/null +++ b/nodes/latent_machine.py @@ -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 diff --git a/nodes/prompt_machine.py b/nodes/prompt_machine.py index b1aadb3..d4c2046 100644 --- a/nodes/prompt_machine.py +++ b/nodes/prompt_machine.py @@ -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)}") diff --git a/nodes/resolution_machine.py b/nodes/resolution_machine.py index 80bdfe2..f07c37a 100644 --- a/nodes/resolution_machine.py +++ b/nodes/resolution_machine.py @@ -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: