Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c130c72d7a | ||
|
|
c5c2c6693c | ||
|
|
680e1413af | ||
|
|
62a857abd8 | ||
|
|
de4b4aece9 | ||
|
|
67f5446069 | ||
|
|
97ff05a208 |
@@ -0,0 +1,28 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'SatadalAI' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -1,7 +1,7 @@
|
||||
# SATA Utility Node Suite for ComfyUI
|
||||
|
||||
**Author:** Satadal
|
||||
**Version:** 1.2.0
|
||||
**Version:** 1.5.0
|
||||
**License:** MIT
|
||||
**Repository:** https://github.com/SatadalAI/SATA_UtilityNode
|
||||
|
||||
@@ -18,6 +18,7 @@ It includes:
|
||||
- **Resolution Machine:** Quick selection of model-specific image resolutions for popular models.
|
||||
- **Save Image w/Metadata:** saving images with meta data and with different file formats.
|
||||
- **Prompt Autocomplete:** Text widget with autocompletion, random selection, and preview capabilities.
|
||||
- **Latent Machine:** Advanced noise generation for creating empty latents initialized with specific noise patterns instead of standard Gaussian noise.
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +31,6 @@ It includes:
|
||||
- **CPU-based Rescaling:** Efficient resizing using OpenCV.
|
||||
- **Supersampling:** Optional detail preservation for downscaling/fractional resizing.
|
||||
- **Rounding Modulus:** Ensures output dimensions align with model/UI constraints.
|
||||
- **Dual Output:** Returns both the processed image and a help/documentation link.
|
||||
|
||||
### 💬 Prompt Machine
|
||||
|
||||
@@ -70,6 +70,18 @@ It includes:
|
||||
- **Global Mode:** Optional setting to enable autocompletion on ALL text widgets in ComfyUI.
|
||||
- **Custom Data:** Add your own `.csv` or `.json` files to the `prompt` folder to extend the library.
|
||||
|
||||
### 🌌 Latent Machine
|
||||
|
||||
- **Advanced Noise Generation:** Create empty latents initialized with specific noise patterns instead of standard Gaussian noise.
|
||||
- **Noise Types:**
|
||||
- **Gaussian (White):** Standard noise, good for sharp details.
|
||||
- **Pink (1/f):** Balanced noise, great for photorealism and nature.
|
||||
- **Brown (1/f²):** Smoother noise, ideal for backgrounds and anime.
|
||||
- **Perlin:** Cloud-like structure, perfect for landscapes and fluids.
|
||||
- **Plasma:** Very smooth, abstract blobs for sci-fi and alien terrains.
|
||||
- **Model Support:** Select between **4-channel** (SD1.5, SDXL, etc.) and **16-channel** (Flux, SD3) latents.
|
||||
- **Reproducibility:** Full seed support for consistent noise generation.
|
||||
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
|
||||
@@ -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
-38
@@ -3,7 +3,7 @@ import { app } from "../../../scripts/app.js";
|
||||
// Register extension
|
||||
app.registerExtension({
|
||||
name: "SATA_UtilityNode.PromptAutocomplete",
|
||||
|
||||
|
||||
async setup() {
|
||||
// Add settings
|
||||
app.ui.settings.addSetting({
|
||||
@@ -59,7 +59,7 @@ app.registerExtension({
|
||||
const contentData = await contentResp.json();
|
||||
this.snippetsCache[file] = contentData.items || [];
|
||||
}
|
||||
console.log("[PromptAutocomplete] Loaded snippets:", Object.keys(this.snippetsCache));
|
||||
|
||||
} catch (err) {
|
||||
console.error("[PromptAutocomplete] Failed to load snippets:", err);
|
||||
}
|
||||
@@ -77,12 +77,12 @@ app.registerExtension({
|
||||
// Hook into the input element creation/usage
|
||||
// NOTE: ComfyUI's customtext widget often creates a textarea.
|
||||
// We can try to find the textarea in the DOM if it's active, or hook the input callback.
|
||||
|
||||
|
||||
// A more robust way for ComfyUI widgets:
|
||||
// The widget has an `inputEl` property if it's a DOM widget.
|
||||
|
||||
|
||||
// Let's try to hook the `inputEl` if it exists, or wait for it.
|
||||
|
||||
|
||||
// Helper to setup the listener
|
||||
function setupListener(input) {
|
||||
if (input.dataset.hasAutocomplete) return;
|
||||
@@ -99,7 +99,7 @@ app.registerExtension({
|
||||
// If not, it might be created later. We can hook `onInput` or similar,
|
||||
// but `inputEl` is usually created when the node is drawn or added.
|
||||
// Let's poll or hook draw? Hooking draw is safer to catch it.
|
||||
widget.draw = function(ctx, node, widgetWidth, y, widgetHeight) {
|
||||
widget.draw = function (ctx, node, widgetWidth, y, widgetHeight) {
|
||||
const result = originalDraw?.apply(this, arguments);
|
||||
if (this.inputEl) {
|
||||
setupListener(this.inputEl);
|
||||
@@ -110,7 +110,7 @@ app.registerExtension({
|
||||
},
|
||||
|
||||
// --- Autocomplete Logic ---
|
||||
|
||||
|
||||
popup: null,
|
||||
active: false,
|
||||
currentCategory: null, // null = showing categories, string = showing items
|
||||
@@ -127,7 +127,7 @@ app.registerExtension({
|
||||
|
||||
// Simple check: is the character before cursor the trigger?
|
||||
// Or are we in a "trigger session"?
|
||||
|
||||
|
||||
// If we just typed the trigger
|
||||
if (e.data === trigger) {
|
||||
this.showPopup(input, cursor);
|
||||
@@ -144,11 +144,11 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
const query = val.substring(lastTriggerIndex + 1, cursor);
|
||||
|
||||
|
||||
// Check if we have a category selected (format: #category:query)
|
||||
// But wait, the plan said: Select Category -> Then Items.
|
||||
// So maybe we just filter categories first.
|
||||
|
||||
|
||||
if (this.currentCategory) {
|
||||
// We are inside a category
|
||||
// Check if the query contains the category prefix?
|
||||
@@ -161,17 +161,17 @@ app.registerExtension({
|
||||
// But how do we represent that in text?
|
||||
// Maybe we don't change text, just internal state?
|
||||
// Standard way: #category:item
|
||||
|
||||
|
||||
// Let's parse the text to see where we are.
|
||||
// If text is "#cat", we filter categories.
|
||||
// If text is "#cat:ite", we filter items in "cat".
|
||||
|
||||
|
||||
const parts = query.split(":");
|
||||
if (parts.length > 1) {
|
||||
// We have a category selected
|
||||
const catName = parts[0];
|
||||
const itemQuery = parts.slice(1).join(":"); // rest is item query
|
||||
|
||||
|
||||
// Check if catName is valid
|
||||
if (this.snippetsCache[catName + ".csv"] || this.snippetsCache[catName + ".json"] || this.snippetsCache[catName]) {
|
||||
// It's a valid category (roughly).
|
||||
@@ -184,7 +184,7 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no colon, or invalid category, we are filtering categories
|
||||
this.currentCategory = null;
|
||||
this.updateCategories(query);
|
||||
@@ -192,17 +192,17 @@ app.registerExtension({
|
||||
} else {
|
||||
// No category selected yet
|
||||
// Check for colon
|
||||
const parts = query.split(":");
|
||||
if (parts.length > 1) {
|
||||
// Maybe user typed "style:" manually?
|
||||
const catName = parts[0];
|
||||
let key = Object.keys(this.snippetsCache).find(k => k.replace(/\.(csv|json)$/, "") === catName);
|
||||
if (key) {
|
||||
this.currentCategory = key;
|
||||
this.updateItems(parts.slice(1).join(":"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const parts = query.split(":");
|
||||
if (parts.length > 1) {
|
||||
// Maybe user typed "style:" manually?
|
||||
const catName = parts[0];
|
||||
let key = Object.keys(this.snippetsCache).find(k => k.replace(/\.(csv|json)$/, "") === catName);
|
||||
if (key) {
|
||||
this.currentCategory = key;
|
||||
this.updateItems(parts.slice(1).join(":"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCategories(query);
|
||||
}
|
||||
@@ -289,10 +289,10 @@ app.registerExtension({
|
||||
updateItems(query) {
|
||||
if (!this.currentCategory) return;
|
||||
const items = this.snippetsCache[this.currentCategory] || [];
|
||||
|
||||
|
||||
// Filter
|
||||
let matches = items.filter(i => i.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
|
||||
// Add "Random" option at top
|
||||
this.filteredItems = [
|
||||
{ type: "random", value: "RANDOM", display: "🎲 Random" },
|
||||
@@ -328,7 +328,7 @@ app.registerExtension({
|
||||
if (idx === this.selectedIndex) {
|
||||
div.style.backgroundColor = "#444";
|
||||
div.style.color = "#fff";
|
||||
|
||||
|
||||
// Show preview if it's a long item
|
||||
if (item.type === "item" && item.value.length > 30) {
|
||||
this.showPreview(item.value);
|
||||
@@ -341,10 +341,10 @@ app.registerExtension({
|
||||
this.selectedIndex = idx;
|
||||
this.selectItem(this.activeInput); // We need reference to input
|
||||
};
|
||||
|
||||
|
||||
this.popup.appendChild(div);
|
||||
});
|
||||
|
||||
|
||||
// Keep active input ref
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && activeElement.tagName === "TEXTAREA") {
|
||||
@@ -370,7 +370,7 @@ app.registerExtension({
|
||||
});
|
||||
document.body.appendChild(this.previewBox);
|
||||
}
|
||||
|
||||
|
||||
this.previewBox.textContent = text;
|
||||
const rect = this.popup.getBoundingClientRect();
|
||||
this.previewBox.style.left = `${rect.right + 5}px`;
|
||||
@@ -402,16 +402,16 @@ app.registerExtension({
|
||||
this.currentCategory = item.value;
|
||||
// Don't close popup, just update to items
|
||||
// We need to update the input value first
|
||||
|
||||
// Replace everything from trigger to cursor with trigger + textToInsert
|
||||
|
||||
// Replace everything from trigger to cursor with trigger + textToInsert
|
||||
const before = val.substring(0, lastTriggerIndex + 1); // includes trigger
|
||||
const after = val.substring(cursor);
|
||||
input.value = before + textToInsert + after;
|
||||
|
||||
|
||||
// Move cursor
|
||||
const newCursor = lastTriggerIndex + 1 + textToInsert.length;
|
||||
input.setSelectionRange(newCursor, newCursor);
|
||||
|
||||
|
||||
// Trigger input event manually to update state
|
||||
this.handleInput({ data: null }, input, null);
|
||||
return;
|
||||
@@ -430,13 +430,13 @@ app.registerExtension({
|
||||
// Replace
|
||||
const before = val.substring(0, lastTriggerIndex); // remove trigger
|
||||
const after = val.substring(cursor);
|
||||
|
||||
|
||||
// If we are replacing a category query like #style:something, we need to find the start of #
|
||||
// Actually, if we are in item mode, the text is "#style:query".
|
||||
// We want to replace the whole "#style:query" with "textToInsert".
|
||||
|
||||
|
||||
input.value = before + textToInsert + after;
|
||||
|
||||
|
||||
// Move cursor
|
||||
const newCursor = before.length + textToInsert.length;
|
||||
input.setSelectionRange(newCursor, newCursor);
|
||||
|
||||
+39
-4
@@ -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"]
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ app.registerExtension({
|
||||
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
|
||||
const data = await resp.json();
|
||||
namesConfig = data.names || ["None"];
|
||||
console.log("[PromptMachineFrontend] Loaded names:", namesConfig);
|
||||
|
||||
refreshNames();
|
||||
} catch (err) {
|
||||
console.error("[PromptMachineFrontend] Failed to load names:", err);
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -25,7 +25,7 @@ app.registerExtension({
|
||||
try {
|
||||
const resp = await fetch("/SATA_UtilityNode/resolutions_config");
|
||||
resolutionsConfig = await resp.json();
|
||||
console.log("[Resolution_Machine] Loaded config:", resolutionsConfig);
|
||||
|
||||
} catch (err) {
|
||||
console.error("[Resolution_Machine] Failed to load config:", err);
|
||||
}
|
||||
@@ -46,7 +46,7 @@ app.registerExtension({
|
||||
resolutionWidget.value = available[0];
|
||||
}
|
||||
|
||||
console.log(`[Resolution_Machine] Resolutions for ${selectedModel}:`, available);
|
||||
|
||||
app.graph.change();
|
||||
applyResolution(); // auto-apply width/height on refresh
|
||||
}
|
||||
@@ -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,10 +120,10 @@ class Prompt_Machine:
|
||||
FUNCTION = "get_prompts"
|
||||
CATEGORY = "SATA_UtilityNode"
|
||||
|
||||
def get_prompts(self, csv_file, name):
|
||||
print(f"[PromptMachine] get_prompts - csv_file={csv_file}, name={name}")
|
||||
def get_prompts(self, csv_file, name, positive_preview=None, negative_preview=None, note_preview=None):
|
||||
|
||||
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)}")
|
||||
|
||||
return (pos, neg, 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:
|
||||
@@ -67,7 +70,7 @@ class Resolution_Machine:
|
||||
entry = resolutions[resolution]
|
||||
width, height = entry["width"], entry["height"]
|
||||
|
||||
print(f"[{NODE_NAME}] Model={model}, Resolution={resolution}, Output=({width}, {height})")
|
||||
|
||||
return (width, height)
|
||||
|
||||
|
||||
|
||||
+38
-2
@@ -1,7 +1,43 @@
|
||||
[project]
|
||||
name = "sata_utilitynode"
|
||||
description = "A collection of utility nodes including image upscale,touchpad support and CSV-driven prompt machine for ComfyUI."
|
||||
version = "1.0.0"
|
||||
license = {file = "LICENSE"}
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
# "Operating System :: OS Independent",
|
||||
#
|
||||
# # OR for OS-specific nodes, specify the supported systems:
|
||||
# "Operating System :: Microsoft :: Windows", # Windows specific
|
||||
# "Operating System :: POSIX :: Linux", # Linux specific
|
||||
# "Operating System :: MacOS", # macOS specific
|
||||
#
|
||||
# # GPU Accelerator support. Pick the ones that are supported by your extension.
|
||||
# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support
|
||||
# "Environment :: GPU :: AMD ROCm", # AMD ROCm support
|
||||
# "Environment :: GPU :: Intel Arc", # Intel Arc support
|
||||
# "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support
|
||||
# "Environment :: GPU :: Apple Metal", # Apple Metal support
|
||||
# ]
|
||||
|
||||
dependencies = ["opencv-python>=4.5.0", "spandrel", "nvidia-tensorrt>=8.0", "onnx>=1.12.0", "onnxruntime-gpu>=1.12.0"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/SatadalAI/SATA_UtilityNode"
|
||||
# Used by Comfy Registry https://registry.comfy.org
|
||||
Documentation = "https://github.com/SatadalAI/SATA_UtilityNode/wiki"
|
||||
"Bug Tracker" = "https://github.com/SatadalAI/SATA_UtilityNode/issues"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = ""
|
||||
DisplayName = "SATA_UtilityNode"
|
||||
Icon = ""
|
||||
includes = []
|
||||
# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility
|
||||
|
||||
name = "comfyui_sata_utilitynode"
|
||||
description = "ComfyUI utility nodes collection providing features like prompt machine, resolution machine, and upscale machine to enhance workflow efficiency."
|
||||
version = "1.2.0"
|
||||
version = "1.5.0"
|
||||
license = "MIT"
|
||||
|
||||
[project.urls]
|
||||
@@ -11,4 +47,4 @@ Repository = "https://github.com/SatadalAI/SATA_UtilityNode"
|
||||
[tool.comfy]
|
||||
PublisherId = "SatadalAI"
|
||||
DisplayName = "SATA_UtilityNode"
|
||||
Icon = ""
|
||||
Icon = ""
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
opencv-python>=4.5.0
|
||||
spandrel
|
||||
nvidia-tensorrt>=8.0
|
||||
onnx>=1.12.0
|
||||
onnxruntime-gpu>=1.12.0
|
||||
piexif
|
||||
Reference in New Issue
Block a user