11 Commits
18 changed files with 3502 additions and 667 deletions
+39 -30
View File
@@ -1,7 +1,7 @@
# SATA Utility Node Suite for ComfyUI # SATA Utility Node Suite for ComfyUI
**Author:** Satadal **Author:** Satadal
**Version:** 1.5.0 **Version:** 2.0
**License:** MIT **License:** MIT
**Repository:** https://github.com/SatadalAI/SATA_UtilityNode **Repository:** https://github.com/SatadalAI/SATA_UtilityNode
@@ -12,13 +12,15 @@
SATA Utility Node Suite is a collection of custom nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) designed to enhance your image processing and workflow experience. SATA Utility Node Suite is a collection of custom nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) designed to enhance your image processing and workflow experience.
It includes: It includes:
- **Upscale Machine:** High-quality model-based image upscaling with aspect-ratio preserving rescale. - **Upscale Machine:** High-quality model-based image upscaling with Frequency-Split SR and Blue Noise injection.
- **Prompt Machine:** Easy prompt selection from a CSV for positive/negative prompt workflows. - **Prompt Machine (Eight-Category Framework):** Professional structured prompting with JSON output support.
- **Prompt Style Machine:** Easy prompt selection from a CSV for positive/negative prompt workflows.
- **Touchpad Pan & Zoom:** Seamless navigation for large images in the ComfyUI preview panel. - **Touchpad Pan & Zoom:** Seamless navigation for large images in the ComfyUI preview panel.
- **Resolution Machine:** Quick selection of model-specific image resolutions for popular models. - **Resolution Machine:** Quick selection of model-specific image resolutions with intelligent Megapixel scaling.
- **Save Image w/Metadata:** saving images with meta data and with different file formats. - **Save Image w/Metadata:** Saving images with metadata and with different file formats.
- **Prompt Autocomplete:** Text widget with autocompletion, random selection, and preview capabilities. - **Prompt Autocomplete:** Text widget with highly optimized subsequence fuzzy autocompletion.
- **Latent Machine:** Advanced noise generation for creating empty latents initialized with specific noise patterns instead of standard Gaussian noise. - **Latent Machine:** Advanced noise generation (Power-Law, Plasma, Blue, Offset) supporting both 4-channel and 16-channel modern architectures.
- **Preview Machine:** Privacy-focused image preview node with auto-hide and hover-to-reveal functionality.
--- ---
@@ -27,16 +29,23 @@ It includes:
### 🖼️ Upscale Machine ### 🖼️ Upscale Machine
- **Model-based Upscaling:** Uses custom or pre-trained models from the `upscale_models` folder. - **Model-based Upscaling:** Uses custom or pre-trained models from the `upscale_models` folder.
- **Frequency-Split SR:** Separates image into base colors (low frequencies) and sharp edges (high frequencies). The neural network only upscales edges, while a bicubic algorithm scales colors, eliminating "plastic/waxy" looks and edge halos.
- **Blue Noise Injection:** Adds high-frequency micro-textures (`inject_noise_for_realism`) during upscaling. Automatically activates when using a chained model to fix the "smoothness" problem in low-step/distilled models like SDXL Turbo.
- **Aspect Ratio Preserving Rescale:** Output size is always original size × rescale factor. - **Aspect Ratio Preserving Rescale:** Output size is always original size × rescale factor.
- **CPU-based Rescaling:** Efficient resizing using OpenCV. - **CPU-based Rescaling:** Efficient resizing using OpenCV.
- **Supersampling:** Optional detail preservation for downscaling/fractional resizing. - **Rounding Modulus:** Ensures output dimensions seamlessly align with UNet architectural constraints.
- **Rounding Modulus:** Ensures output dimensions align with model/UI constraints.
### 💬 Prompt Machine ### 💬 Prompt Machine (Eight-Category Framework)
- **Structured Prompting:** Dedicated input boxes for `subject`, `pose_and_expression`, `secondary_subject`, `background_and_environment`, `lighting`, `style`, `creative_hook`, and `technical` details following an inside-out spatial sequence.
- **JSON Output Mode:** Toggle `json_output` to compile the slots into a strict JSON string, perfect for piping into LLM rewriting nodes or API pipelines.
- **Autocomplete Ready:** Fully integrated with the autocomplete system—just type `#` in any slot to trigger the smart tag popup!
- **Universal Model Compatibility:** Formats naturally for FLUX, SD3.5, SDXL, SD 1.5, PixArt, and LLM expansion pipelines.
### 💬 Prompt Style Machine (Legacy)
- **CSV-based Prompt Selection:** Select prompt sets from a CSV file (`prompts.csv`). - **CSV-based Prompt Selection:** Select prompt sets from a CSV file (`prompts.csv`).
- **Positive/Negative Output:** Outputs both positive and negative prompts as strings. - **Clean Output:** Outputs the positive prompt string with a live, enlarged text preview inside the node.
- **Easy Integration:** Designed for use in prompt-driven workflows.
### 🖱️ Touchpad Pan & Zoom ### 🖱️ Touchpad Pan & Zoom
@@ -45,43 +54,43 @@ It includes:
### 📏 Resolution Machine ### 📏 Resolution Machine
- **Model-aware Resolution Selection:** Choose from a list of recommended resolutions for each supported model. - **Intelligent Megapixel Scaling:** Uses a target Megapixel slider to automatically calculate the exact width/height required to hit the target while perfectly preserving your chosen aspect ratio. Rounds to the nearest multiple of 8.
- **Auto-populate Width/Height:** Selecting a resolution automatically fills in the correct width and height. - **Model-aware Resolution Selection:** Choose from a list of recommended resolutions for each supported model (SD1.5, SDXL, Flux.1 & Flux.2, SD3 & SD3.5, Ideogram 4, Lumina, Qwen-Image, Z-Image Turbo, Hunyuan-DiT, and Video Models like Wan2.2, HunyuanVideo, Mochi-1, LTX-Video).
- **Manual Override:** You can manually set width/height, which clears the preset selection.
- **Supports Many Models:** Includes presets for SD1.5, SDXL, Illustrious XL, Pony XL, Flux.1 Dev, Qwen-Image, Wan2.2-S2V, SD3.5 Large, Lumina-Image 2.0, and more.
- **UI Integration:** Intuitive dropdowns for model and resolution selection in the ComfyUI interface.
--- ---
### 💾 Save Image w/Metadata ### 💾 Save Image w/Metadata
- **Single path+filename input:** Provide a single required `path_and_filename` string (examples: `comfy`, `Test/comfy`, `SDXL/Test/comfy`). The last path segment is used as the filename; preceding segments become folders under the ComfyUI output directory. - **Single path+filename input:** Provide a single required `path_and_filename` string.
- **Supported formats:** PNG, JPEG (saved as .jpg), WEBP (choose with the `extension` dropdown). - **Supported formats:** PNG, JPEG (saved as .jpg), WEBP (choose with the `extension` dropdown).
- **Automatic folder creation:** Any missing folders in the provided path are created automatically. - **Automatic folder creation:** Any missing folders in the provided path are created automatically.
- **Metadata embedding:** PNG files receive tEXt entries (parameters/prompt). JPEG/WEBP receive EXIF UserComment where possible. - **Metadata embedding:** PNG files receive tEXt entries (parameters/prompt). JPEG/WEBP receive EXIF UserComment where possible.
- **Placeholder resolution:** Filenames and paths accept `%...%` placeholders which are resolved from the node's `prompt` dictionary (best-effort). Example: if `prompt` contains `{"title": "MyTitle"}`, using `%title%/img` will save under `MyTitle/img.png`. - **Placeholder resolution:** Filenames and paths accept `%...%` placeholders which are resolved from the node's `prompt` dictionary.
- **Collision-safe saving:** If a file already exists the node will append `_0001`, `_0002`, ... to the base filename to avoid overwriting. - **Collision-safe saving:** Automatically appends numerical suffixes to avoid overwriting.
### ✍️ Prompt Autocomplete ### ✍️ Prompt Autocomplete
- **Autocompletion:** Type `#` (configurable) to trigger a popup with suggestions from CSV/JSON files in the `prompt` folder. - **Subsequence Fuzzy Matching:** Ultra-fast, highly optimized fuzzy search with rank-based scoring.
- **Autocompletion:** Type `#` (configurable) to trigger a popup with suggestions from CSV/JSON files in the `prompt` folder. Supports strict 100-item render limits to prevent UI lag.
- **Random Selection:** Select `🎲 Random` to insert a random item from a category. - **Random Selection:** Select `🎲 Random` to insert a random item from a category.
- **Preview:** View full text of long snippets before inserting.
- **Global Mode:** Optional setting to enable autocompletion on ALL text widgets in ComfyUI. - **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 ### 🌌 Latent Machine
- **Advanced Noise Generation:** Create empty latents initialized with specific noise patterns instead of standard Gaussian noise. - **Advanced Noise Generation:** Initialize latents with specific noise patterns:
- **Noise Types:**
- **Gaussian (White):** Standard noise, good for sharp details. - **Gaussian (White):** Standard noise, good for sharp details.
- **Pink (1/f):** Balanced noise, great for photorealism and nature. - **Blue (High-Frequency):** Enhances detail and textures.
- **Brown (1/f²):** Smoother noise, ideal for backgrounds and anime. - **Pink (1/f) / Brown (1/f²):** Photorealism, nature, anime backgrounds.
- **Perlin:** Cloud-like structure, perfect for landscapes and fluids. - **Perlin / Plasma:** Fluid landscapes, sci-fi, abstract terrains.
- **Plasma:** Very smooth, abstract blobs for sci-fi and alien terrains. - **High-Contrast Offset Noise:** Toggle `high_contrast` to inject a `0.1` mean shift to the starting noise, allowing the model to generate pure blacks (pitch-black nights) and blown-out whites (snowstorms).
- **Model Support:** Select between **4-channel** (SD1.5, SDXL, etc.) and **16-channel** (Flux, SD3) latents. - **Auto 16-Channel Detection:** Automatically provisions the correct latent depth for modern architectures (Flux, SD3, Lumina, Z-Image).
- **Reproducibility:** Full seed support for consistent noise generation. - **Reproducibility:** Full seed support for consistent noise generation.
### 👁️ Preview Machine
- **Privacy-Focused Preview:** Image preview node that hides content by default.
- **Hover to Reveal / Auto-Hide:** Simply hover over the node to temporarily reveal the image.
- **Global Hide Mode:** Optional setting to hide ALL preview nodes in the workflow (Settings → SATA Utility: Global Hide Previews).
## 📦 Installation ## 📦 Installation
+14 -11
View File
@@ -1,31 +1,34 @@
# __init__.py
# Import all the node classes from the nodes directory # Import all the node classes from the nodes directory
from .nodes.prompt_machine import Prompt_Machine from .nodes.prompt_machine import Prompt_Machine, Prompt_Style_Machine
from .nodes.resolution_machine import Resolution_Machine from .nodes.resolution_machine import Resolution_Machine
from .nodes.save_image_metadata import ImageSaveWithMetadata from .nodes.save_machine import Save_Machine
from .nodes.upscale_machine import Upscale_Machine from .nodes.upscale_machine import Upscale_Machine
from .nodes.prompt_autocomplete import PromptAutocomplete from .nodes.prompt_autocomplete import PromptAutocomplete
from .nodes.latent_machine import Latent_Machine from .nodes.latent_machine import Latent_Machine
from .nodes.preview_machine import Preview_Machine
# A dictionary that maps class names to class objects # A dictionary that maps class names to class objects
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"Prompt_Machine": Prompt_Machine, "Prompt_Machine": Prompt_Machine,
"Prompt_Style_Machine": Prompt_Style_Machine,
"Resolution_Machine": Resolution_Machine, "Resolution_Machine": Resolution_Machine,
"ImageSaveWithMetadata": ImageSaveWithMetadata, "Save_Machine": Save_Machine,
"Upscale_Machine": Upscale_Machine, "Upscale_Machine": Upscale_Machine,
"PromptAutocomplete": PromptAutocomplete, "PromptAutocomplete": PromptAutocomplete,
"Latent_Machine": Latent_Machine, "Latent_Machine": Latent_Machine,
"Preview_Machine": Preview_Machine,
} }
# A dictionary that maps display names to class names # A dictionary that maps display names to class names
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"Prompt Machine": "Prompt_Machine", "Prompt_Machine": "Prompt Machine",
"Resolution Machine": "Resolution_Machine", "Prompt_Style_Machine": "Prompt Style Machine",
"Save Image w/Metadata": "ImageSaveWithMetadata", "Resolution_Machine": "Resolution Machine",
"Upscale Machine": "Upscale_Machine", "Save_Machine": "Save Machine",
"Prompt Autocomplete": "PromptAutocomplete", "Upscale_Machine": "Upscale Machine",
"Latent Machine": "Latent_Machine", "PromptAutocomplete": "Prompt Autocomplete",
"Latent_Machine": "Latent Machine",
"Preview_Machine": "Preview Machine",
} }
# A dictionary that contains web directory information # A dictionary that contains web directory information
+359 -57
View File
@@ -1,61 +1,363 @@
{ {
"SD 1.5": { "resolutions": {
"Square (1:1) 512x512": { "width": 512, "height": 512 }, "512": {
"Square high-res (1:1) 768x768": { "width": 768, "height": 768 }, "Square": {
"Landscape (3:2) 768x512": { "width": 768, "height": 512 }, "Square (1:1) 512x512": {
"Portrait (2:3) 512x768": { "width": 512, "height": 768 }, "width": 512,
"Landscape (4:3) 768x576": { "width": 768, "height": 576 }, "height": 512
"Portrait (3:4) 576x768": { "width": 576, "height": 768 }, }
"Widescreen (16:9) 912x512": { "width": 912, "height": 512 }, },
"Tall (9:16) 512x912": { "width": 512, "height": 912 }, "Landscape": {
"Custom (manual)": { "width": 0, "height": 0 } "Photography (3:2) 768x512": {
"width": 768,
"height": 512
},
"Widescreen (16:9) 912x512": {
"width": 912,
"height": 512
}
},
"Portrait": {
"Photo Portrait (2:3) 512x768": {
"width": 512,
"height": 768
},
"Vertical (9:16) 512x912": {
"width": 512,
"height": 912
}
}
},
"768": {
"Square": {
"Square (1:1) 768x768": {
"width": 768,
"height": 768
}
},
"Landscape": {
"Cinematic (21:9) 1344x576": {
"width": 1344,
"height": 576
},
"Photography (3:2) 768x512": {
"width": 768,
"height": 512
},
"Photo Landscape (4:3) 768x576": {
"width": 768,
"height": 576
}
},
"Portrait": {
"Tall (9:21) 576x1344": {
"width": 576,
"height": 1344
},
"Photo Portrait (2:3) 512x768": {
"width": 512,
"height": 768
},
"Portrait (3:4) 576x768": {
"width": 576,
"height": 768
}
}
},
"1024": {
"Square": {
"Square (1:1) 1024x1024": {
"width": 1024,
"height": 1024
}
},
"Landscape": {
"Cinematic (21:9) 1536x640": {
"width": 1536,
"height": 640
},
"Cinematic (21:9) 1680x720": {
"width": 1680,
"height": 720
},
"Ultra Wide (2:1) 1536x768": {
"width": 1536,
"height": 768
},
"Widescreen (16:9) 1280x720": {
"width": 1280,
"height": 720
},
"Widescreen (16:9) 1344x768": {
"width": 1344,
"height": 768
},
"Widescreen (16:9) 1536x864": {
"width": 1536,
"height": 864
},
"Wide Landscape (10:7) 1280x896": {
"width": 1280,
"height": 896
},
"Photo Landscape (4:3) 1152x864": {
"width": 1152,
"height": 864
},
"Photo Landscape (4:3) 1152x896": {
"width": 1152,
"height": 896
},
"Photography (3:2) 1152x768": {
"width": 1152,
"height": 768
},
"Photography (3:2) 1248x832": {
"width": 1248,
"height": 832
},
"Photography (3:2) 1280x832": {
"width": 1280,
"height": 832
},
"Balanced Landscape (19:13) 1216x832": {
"width": 1216,
"height": 832
}
},
"Portrait": {
"Tall (9:21) 640x1536": {
"width": 640,
"height": 1536
},
"Tall (9:21) 720x1680": {
"width": 720,
"height": 1680
},
"Extra Tall (1:2) 768x1536": {
"width": 768,
"height": 1536
},
"Vertical (9:16) 720x1280": {
"width": 720,
"height": 1280
},
"Vertical (9:16) 768x1344": {
"width": 768,
"height": 1344
},
"Vertical (9:16) 864x1536": {
"width": 864,
"height": 1536
},
"Tall Portrait (7:10) 896x1280": {
"width": 896,
"height": 1280
},
"Portrait (3:4) 864x1152": {
"width": 864,
"height": 1152
},
"Portrait (3:4) 896x1152": {
"width": 896,
"height": 1152
},
"Photo Portrait (2:3) 768x1152": {
"width": 768,
"height": 1152
},
"Photo Portrait (2:3) 832x1248": {
"width": 832,
"height": 1248
},
"Photo Portrait (2:3) 832x1280": {
"width": 832,
"height": 1280
},
"Balanced Portrait (13:19) 832x1216": {
"width": 832,
"height": 1216
}
}
},
"1280": {
"Square": {
"Square (1:1) 1280x1280": {
"width": 1280,
"height": 1280
},
"Square (1:1) 1328x1328": {
"width": 1328,
"height": 1328
}
},
"Landscape": {
"Cinematic (21:9) 2016x864": {
"width": 2016,
"height": 864
},
"Wide Medium (12:7) 1536x896": {
"width": 1536,
"height": 896
},
"Widescreen (16:9) 1664x928": {
"width": 1664,
"height": 928
},
"Balanced Landscape (9:7) 1440x1120": {
"width": 1440,
"height": 1120
},
"Photo Landscape (4:3) 1472x1104": {
"width": 1472,
"height": 1104
},
"Photo Landscape (4:3) 1472x1140": {
"width": 1472,
"height": 1140
},
"Photography (3:2) 1536x1024": {
"width": 1536,
"height": 1024
}
},
"Portrait": {
"Tall (9:21) 864x2016": {
"width": 864,
"height": 2016
},
"Vertical (9:16) 928x1664": {
"width": 928,
"height": 1664
},
"Balanced Portrait (7:9) 1120x1440": {
"width": 1120,
"height": 1440
},
"Portrait (3:4) 1104x1472": {
"width": 1104,
"height": 1472
},
"Portrait (3:4) 1140x1472": {
"width": 1140,
"height": 1472
},
"Photo Portrait (2:3) 1024x1536": {
"width": 1024,
"height": 1536
}
}
},
"1536": {
"Square": {
"Square (1:1) 1536x1536": {
"width": 1536,
"height": 1536
}
},
"Landscape": {
"Widescreen (16:9) 1920x1088": {
"width": 1920,
"height": 1088
},
"Widescreen (16:9) 2048x1152": {
"width": 2048,
"height": 1152
},
"Balanced Landscape (9:7) 1728x1344": {
"width": 1728,
"height": 1344
},
"Photo Landscape (4:3) 1728x1296": {
"width": 1728,
"height": 1296
},
"Photography (3:2) 1872x1248": {
"width": 1872,
"height": 1248
}
},
"Portrait": {
"Vertical (9:16) 1152x2048": {
"width": 1152,
"height": 2048
},
"Balanced Portrait (7:9) 1344x1728": {
"width": 1344,
"height": 1728
},
"Portrait (3:4) 1296x1728": {
"width": 1296,
"height": 1728
},
"Photo Portrait (2:3) 1248x1872": {
"width": 1248,
"height": 1872
}
}
},
"Video": {
"Square": {},
"Landscape": {
"Widescreen (16:9) 1280x720": {
"width": 1280,
"height": 720
}
},
"Portrait": {
"Vertical (9:16) 720x1280": {
"width": 720,
"height": 1280
}
}
}
}, },
"models": {
"SDXL_Illustrious_Pony": { "SD 1.5 Family": [
"Square (1:1) 1024x1024": { "width": 1024, "height": 1024 }, "512",
"Landscape (3:2) 1280x832": { "width": 1280, "height": 832 }, "768"
"Portrait (2:3) 832x1280": { "width": 832, "height": 1280 }, ],
"Photography (4:3) 1152x896": { "width": 1152, "height": 896 }, "SDXL Family (Pony, Juggernaut)": [
"Portrait Insta (3:4) 896x1152": { "width": 896, "height": 1152 }, "768",
"Widescreen (16:9) 1344x768": { "width": 1344, "height": 768 }, "1024"
"Vertical (9:16) 768x1344": { "width": 768, "height": 1344 }, ],
"Cinematic (21:9) 1536x640": { "width": 1536, "height": 640 }, "FLUX Family (FLUX.1, FLUX.2)": [
"Cinematic Portrait (9:21) 640x1536": { "width": 640, "height": 1536 }, "1024",
"Best Landscape (19:13) 1216x832": { "width": 1216, "height": 832 }, "1280",
"Best Portrait (13:19) 832x1216": { "width": 832, "height": 1216 }, "1536"
"Mini Portrait (768x1152)": { "width": 768, "height": 1152 }, ],
"Mini Landscape (1152x768)": { "width": 1152, "height": 768 }, "SD3 Family (SD3, SD3.5)": [
"Custom (manual)": { "width": 0, "height": 0 } "1024",
}, "1280",
"1536"
"Flux": { ],
"High-res (16:9) 1920x1088": { "width": 1920, "height": 1088 }, "Z-Image Family": [
"Square Medium (1:1) 768x768": { "width": 768, "height": 768 }, "1024",
"Portrait Small (3:4) 768x1024": { "width": 768, "height": 1024 }, "1280",
"Landscape Small (4:3) 1024x768": { "width": 1024, "height": 768 }, "1536"
"Wide Small (3:2) 1152x768": { "width": 1152, "height": 768 }, ],
"Portrait Medium (2:3) 768x1152": { "width": 768, "height": 1152 }, "Hunyuan-DiT": [
"Ultra Wide Small (2:1) 1536x768": { "width": 1536, "height": 768 }, "1024",
"Portrait Large (1:2) 768x1536": { "width": 768, "height": 1536 }, "1280"
"Square Large (1:1) 1024x1024": { "width": 1024, "height": 1024 }, ],
"Portrait Small HD (7:10) 896x1280": { "width": 896, "height": 1280 }, "Qwen-Image": [
"Landscape Small HD (10:7) 1280x896": { "width": 1280, "height": 896 }, "1024",
"Wide Medium (12:7) 1536x896": { "width": 1536, "height": 896 }, "1280"
"Portrait HD (7:12) 896x1536": { "width": 896, "height": 1536 }, ],
"Custom (manual)": { "width": 0, "height": 0 } "Lumina": [
}, "1024",
"1280"
"Qwen-Image": { ],
"Square (1:1)": { "width": 1328, "height": 1328 }, "Ideogram 4": [
"Widescreen (16:9)": { "width": 1664, "height": 928 }, "1024",
"Tall (9:16)": { "width": 928, "height": 1664 }, "1280",
"Landscape (4:3)": { "width": 1472, "height": 1140 }, "1536"
"Portrait (3:4)": { "width": 1140, "height": 1472 }, ],
"Custom (manual)": { "width": 0, "height": 0 } "AuraFlow": [
}, "1024"
],
"Wan2.2": { "Video Models (Wan, Hunyuan, Mochi, LTX)": [
"720p HD video": { "width": 1280, "height": 720 }, "Video"
"Custom (manual)": { "width": 0, "height": 0 } ]
} }
} }
+306
View File
@@ -47,3 +47,309 @@ Velvet,"{prompt} in smooth velvet fabric, gorgeous exquisite cloth, highly detai
"Wool, Fine knit","{prompt} in wool cloth, highly detailed, gorgeous, fine knit cashmere wool fabric","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted", "Wool, Fine knit","{prompt} in wool cloth, highly detailed, gorgeous, fine knit cashmere wool fabric","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted",
"Wool, Heavy knit","{prompt} in chunky heavy wool yarn, highly detailed, heavy knit large wool fabric, sailor's wool","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted", "Wool, Heavy knit","{prompt} in chunky heavy wool yarn, highly detailed, heavy knit large wool fabric, sailor's wool","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted",
"Wool, Cable knit","{prompt} in chunky heavy cable knit wool yarn, highly detailed, cable knit large wool fabric","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted", "Wool, Cable knit","{prompt} in chunky heavy cable knit wool yarn, highly detailed, cable knit large wool fabric","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted",
80s Dark Fantasy Photo,"YOUR CONTEXT:
You are a photographer working on the set of a dark fantasy film reminiscent of the 1980s (think *Excalibur*, *Legend*, or *Conan the Barbarian*).
Your photograph showcases {$spicy-content-with} a heavily stylized composition with practical effects, matte paintings, and the dramatic lighting of that era. The image is shot with soft focus. Lighting is theatrical with saturated colors: deep blues, fiery oranges, and sickly greens. Emphasis is placed on creating a sense of scale and grandeur through forced perspective and miniature work. Post-processing involves adding subtle film grain, bloom effects around light sources, and a slight color shift.
YOUR PHOTOGRAPHY:
{prompt}",,
Anime,"YOUR CONTEXT:
You are a Japanese anime artist.
Your Japanese animation includes {$spicy-content-with} an ominous atmosphere, clean lines, and dramatic facial expressions. The artwork uses cool and dark tones along with dramatic lighting and shadows that emphasize the central figure and create a somber, intense mood.
YOUR DRAWING:
{prompt}",,
Casual Photo,"YOUR CONTEXT:
You are an amateur documentary photographer taking low quality photos.
Your photographs exhibit {$spicy-content-with} sharp backgrounds, unpolished realism with natural lighting, and candid friendship‑level moments that feel immediate and authentic.
YOUR PHOTO:
{prompt}",,
Classic Film Photo,"YOUR CONTEXT:
You are a professional cinematographer of iconic classic mid‑century cinema (1950s‑1960s Hollywood golden age).
Your photographs are on 35mm film with grain, displaying {$spicy-content-with} a warm color temperature, and an anamorphic lens that creates imperfections and blurry edges.
YOUR PHOTO:
{prompt}",,
Comic 1,"YOUR CONTEXT:
You are an American artist.
Your illustration has a comic book cover style, with vibrant colors and outlined shapes, featuring {$spicy-content-with} realistic and epic details in the style of modern .
YOUR ILLUSTRATION:
{prompt}",,
Comic 2,"YOUR CONTEXT:
You are an Italian artist.
Your illustration has a European comic book style, featuring {$spicy-content-with} black hand-drawn outlines, solid colors, realistic anatomical proportions, and cross-hatching for shading.
YOUR ILLUSTRATION:
{prompt}",,
Comic 3,"YOUR CONTEXT:
You are a European artist creating fiction worlds.
Your image is a classic ligne claire futurist illustration, featuring {$spicy-content-with} exceptionally clean lines that define each element with the utmost precision. The composition highlights an intense atmosphere with complex technology. The color palette is vibrant, with flat colors and intricate lines and design.
YOUR IMAGE:
{prompt}",,
Comic Book Cover,"YOUR CONTEXT:
You are a celebrated comic book cover artist, renowned for hyper-realistic action and monumental heroics.
Your cover art is a Dynamic Heroic Realism illustration, characterized by {$spicy-content-with} intricate anatomical accuracy, meticulously rendered textures (fabric, metal, skin), and an explosive sense of motion. The composition often features dramatic foreshortening and a heroic, powerful pose for the central figure, surrounded by dynamic environmental destruction or energy effects. Lighting is grand and directional, sculpting forms with sharp highlights and deep shadows, ensuring every detail contributes to an overwhelming sense of epic scale and immediate impact.
YOUR ILLUSTRATION:
{prompt}",,
Cottagecore Pastoral Photo,"YOUR CONTEXT:
You are a lifestyle photographer capturing the ""Cottagecore"" aesthetic, celebrating a nostalgic, rural life.
Your photograph showcases {$spicy-content-with} a soft-focus, romantic composition bathed in warm, hazy afternoon light. The color palette is soft and organic, featuring sage green, dried rose, lavender, and warm ochre. The atmosphere is cozy and rustic, filled with natural textures like linen, dried flowers, and weathered wood in a peaceful countryside setting.
YOUR PHOTOGRAPHY:
{prompt}",,
Cyberpunk Rain Photo,"YOUR CONTEXT:
You are a cinematic photographer specializing in the Neon Noir aesthetic of a futuristic night city.
Your photograph showcases {$spicy-content-with} high-contrast lighting with deep shadows and glowing neon accents. The color palette is dominated by electric cyan, hot magenta, and deep violet. The atmosphere is moody and atmospheric, featuring wet surfaces, rain reflections, and hazy mist.
YOUR PHOTOGRAPH:
{prompt}",,
Digital Cyberpunk,"YOUR CONTEXT:
You are a digital artist working in a dystopian, technologically advanced future.
Your illustration is a Cyberpunk Noir scene, characterized by {$spicy-content-with} harsh, dramatic lighting that casts long, sharp shadows, reflecting the grimy underbelly of a futuristic scene. It employs a limited color palette dominated by deep blues, neon greens, and stark oranges, giving a mysterious aesthetic.
YOUR ILLUSTRATION:
{prompt}",,
Drone PhotoPhoto,"YOUR CONTEXT:
You are an aerial photographer who enjoys using wide-angle drone cams.
Your photographs exhibit {$spicy-content-with} panoramic scenes captured from afar, high up with elevated perspectives and intense colors.
YOUR PHOTO:
{prompt}",,
Electric Blue Outline,"YOUR CONTEXT:
Your image showcases {$spicy-content-with} a digital cyberpunk draw, characterized by vibrant and saturated colors with solid backgrounds and blue outlines.
YOUR IMAGE:
{prompt}",,
Epic Greg,"YOUR CONTEXT:
You are a master digital painter of heroic sagas.
Your illustration is a grand-scale digital oil painting that includes {$spicy-content-with} characterized by masterful, sweeping brushwork and thick impasto textures. It features dramatic, cinematic lighting with intense chiaroscuro, where light sources like embers or magical energy pierce through atmospheric haze, smoke, and swirling clouds. The composition is heroic and classically inspired, using a rich, moody palette of deep earth tones accented by vibrant, glowing highlights for a legendary and immersive feel.
YOUR PAINTING:
{prompt}",,
Flash 90s PhotoPhoto,"YOUR CONTEXT:
You are an amateur photographer, taking photos with point‑and‑shoot cam-quality, in the early 1990s.
Your photographs exhibit {$spicy-content-with} a rough, low-resolution quality, washed-out colors, including a harsh built-in flash, grainy texture, and overexposed highlights, capturing a casual atmosphere that evokes nostalgia.
YOUR PHOTO:
{prompt}",,
Folk-Art Mosaic,"YOUR CONTEXT:
You are a traditional artisan celebrating the vibrant colors and symbolic imagery of folk art.
Your artwork is a Folk Art Mosaic, composed of {$spicy-content-with} small, tessellated pieces of vibrant, solid colors, forming bold patterns and stylized representations of everyday life or mythical creatures. The aesthetic is naive and handcrafted, with a rich texture and a joyful, earthy color palette.
YOUR ARTWORK:
{prompt}",,
Glass Encased Advertising,"YOUR CONTEXT:
An eye-level, realistic photo of a glass-encased advertising poster (mupi) located on a clean city sidewalk. There are subtle reflections of nearby buildings and trees on the glass surface. The lighting is natural overcast daylight.
WHAT'S BEHIND THE REFLECTIVE GLASS:
A faded and sun-faded, low quality printed advertisement, showing: {prompt}",,
Gothic Engraving,"YOUR CONTEXT:
You are an artist inspired by the macabre aesthetics of 19th-century gothic literature and medieval manuscripts.
Your illustration is a Neo-Gothic engraving, executed with intricate linework. It features {$spicy-content-with} dramatic chiaroscuro and a sense of brooding mystery. The color palette is muted, favoring deep indigos, charcoal grays, and touches of crimson, evoking a sense of antique dread.
YOUR ILLUSTRATION:
{prompt}",,
High-Key Fashion PhotoPhoto,"YOUR CONTEXT:
You are a fashion photographer who creates ultra-bright photos for luxury productions.
Your photograph showcases {$spicy-content-with} a high‑key composition shot flooded with soft studio lighting. The exposure is over‑lit with white background, eliminating shadows and delivering a crisp, polished look. Colors are saturated yet controlled (vivid reds, cobalt blues, and crisp whites) while the photo has immaculate details thanks to high‑resolution capture and minimal post‑processing to maintain the airy, glamorous aesthetic.
YOUR PHOTOGRAPH:
{prompt}",,
Ink Draw,"YOUR CONTEXT:
You are an ink drawing artist.
Your illustration is a realistic ink drawing featuring {$spicy-content-with} fine cross-hatching and stippling techniques. These methods create deep shadows and highlights using only black lines. The image includes large areas of white space and Imagine the colors as all shades of gray.
The colors mentioned are actually shades of gray.
YOUR ILLUSTRATION:
{prompt}",,
Ink Frenzy,"YOUR CONTEXT:
You are an underground street-art comic illustrator.
Your image is a high-energy, chaotic ink drawing featuring {$spicy-content-with} aggressive brush strokes, intentional ink splatters, and ""controlled mess"" textures. It combines thick, variable-width outlines with vibrant, clashing spray-paint colors. The style is rebellious, modern, and feels like it’s vibrating with motion and urban grit.
YOUR IMAGE:
{prompt}",,
Kawaii Pop Photo,"YOUR CONTEXT:
You are a fashion photographer specializing in the bright, playful aesthetic of Kawaii Pop.
Your photograph showcases {$spicy-content-with} a high-key composition flooded with vibrant, saturated colors. The exposure is bright and cheerful, with a focus on pastel pinks, electric blues, and sunny yellows. Scene is styled in over-the-top Kawaii fashion with playful poses. The background is simple and colorful, with elements like balloons, confetti, or cartoonish graphics.
YOUR PHOTOGRAPHY:
{prompt}",,
Light Painting Photo,"YOUR CONTEXT:
You are an experimental photographer who enjoys creating light painting images with long exposures and LED sources.
Your photographs exhibit {$spicy-content-with} luminous trails, vibrant colors, and elements that are strikingly illuminated by the long exposure.
YOUR PHOTO:
{prompt}",,
Light and Airy PhotoPhoto,"YOUR CONTEXT:
You are a photographer who loves bright, ethereal, and romantic images.
Your photograph is bathed in soft, diffused light, creating a bright and airy aesthetic. Colors are pastel and delicate, with a focus on whites, creams, and light blues. The image features {$spicy-content-with} a shallow depth of field, blurring the background and drawing attention to the subject. Post-processing emphasizes highlights and reduces shadows, resulting in a clean, luminous look.
YOUR PHOTOGRAPH:
{prompt}",,
LomographyPhoto,"YOUR CONTEXT:
You are an adventurous experimental photographer exploring the world through a lomographic lens.
Your photographs exhibit {$spicy-content-with} film grain, colorful lens flares, soft focus with motion blur, and analog filter effects, trying to capture emotions.
YOUR PHOTO:
{prompt}",,
Low-Poly Render,"YOUR CONTEXT:
Your unity engine render shows {$spicy-content-with} a low-poly 3d platformer level made from low-poly meshes and low-quality textures.
YOUR UNITY ENGINE RENDER:
{prompt}",,
LowRes Pixel Art,"YOUR CONTEXT:
You are a pixel artist.
Your illustration is a low-resolution pixel art featuring {$spicy-content-with} chunky pixels and vibrant intense colors, arcade game style.
YOUR ILLUSTRATION:
{prompt}",,
Manga,"YOUR CONTEXT:
You are a Japanese manga artist.
Your illustration features {$spicy-content-with} black and white manga-style that includes manga screentone patterns with high-contrast black and white drawing, in a violent atmosphere. Always imagine colors as shades of gray.
YOUR ILLUSTRATION:
{prompt}",,
Minimalist Digital Illustration,"YOUR CONTEXT:
You are a cover artist known for powerful, minimalist designs.
Your cover art is a bold graphic iconography piece, featuring {$spicy-content-with} stark, impactful silhouettes and clean, sharp lines that define forms with absolute clarity. The color palette is vibrant, employing large fields of solid color with subtle gradients. Most importantly, the proportions are exaggerated, and the perspective is extremely distorted, creating a sense of vertigo and action.
YOUR ILLUSTRATION:
{prompt}",,
Minimalist PhotoPhoto,"YOUR CONTEXT:
You are a photographer who prefers serene and tranquil images.
Your photographs exhibit {$spicy-content-with} high contrast and clean, minimalist compositions that emphasize empty space to highlight negative space.
YOUR PHOTO:
{prompt}",,
Modern Impressionism,"YOUR CONTEXT:
Your image showcases {$spicy-content-with} a loose and highly expressive brushstroke, characterized by a rich texture created through visible layers of paint and quick, broad strokes reminiscent of modern Impressionism. This technique employing a limited yet impactful color palette with strong contrasts of light and shadow and a focus on warm and cool tones to create a dynamic and energetic feel.
YOUR IMAGE:
{prompt}",,
Modern Ukiyo-e Print,"YOUR CONTEXT:
You are a master of traditional Japanese woodblock printing adapted to a modern narrative.
Your illustration features {$spicy-content-with} bold, flowing outlines and flat areas of color with subtle wood-grain textures. The composition uses dramatic perspective typical of ""Ukiyo-e"" art, with stylized waves, clouds, or smoke, and a color palette of deep indigo, vermillion, and ochre on an aged washi paper background.
YOUR DRAWING:
{prompt}",,
Noir PhotoPhoto,"YOUR CONTEXT:
You are a cinematographer who works in dark movies.
Your photographs exhibit {$spicy-content-with} intense side lighting and gobo‑crafted patterns to sculpt deep, sharply defined shadows, along with a muted Ektachrome palette that evokes film noir.
YOUR PHOTO:
{prompt}",,
Nostalgic 90s Photo,"YOUR CONTEXT:
You are a street photographer capturing the raw, rebellious energy of the 1990s underground scene.
Your photograph showcases {$spicy-content-with} a low-fidelity, grainy analog film aesthetic. The exposure features harsh camera flash, slight motion blur, and light leaks. The colors are slightly de-saturated with warm skin tones and heavy shadows, giving it a candid, ""snapshot"" feel.
YOUR PHOTOGRAPHY:
{prompt}",,
Orthochromatic Spirit PhotoPhoto,"YOUR CONTEXT:
You are a photographer inspired by the look of early photographic processes, specifically orthochromatic and colorized films.
Your photograph showcases {$spicy-content-with} a scene with a high-contrast, blue-sensitive aesthetic. Reds appear very dark, while blues and greens are bright. The image is characterized by a stark, graphic quality. Post-processing focuses on enhancing the contrast and emphasizing the tonal separation.
YOUR PHOTOGRAPH:
{prompt}",,
Paper-Cut Diorama,"YOUR CONTEXT:
You are a conceptual artist creating a tactile, handcrafted world.
Your illustration is a digital ""paper-cut"" style including {$spicy-content-with} distinct layered shapes with subtle drop shadows to create depth. Each element looks like it was cut from different textures of colored construction paper. The style is geometric, clean, and vibrant, making the comic look like a 3D physical diorama.
YOUR IMAGE:
{prompt}",,
Phone Photo,"YOUR CONTEXT:
Your photographs has android phone cam-quality.
Your photographs exhibit {$spicy-content-with} surprising compositions, sharp complex backgrounds, natural lighting, and candid moments that feel immediate and authentic.
Your photographs are actual gritty candid photographic background.
YOUR PHOTO:
{prompt}",,
Pop PhotoPhoto,"YOUR CONTEXT:
You are a contemporary pop artist known for your vibrant, oversaturated palettes and energetic, graphic compositions.
Your photographs exhibit {$spicy-content-with} exaggerated colors and solid backgrounds using Ben-Day dots, achieving an unconventional and clearly pop art effect.
YOUR PHOTO:
{prompt}",,
Pop-Art,"YOUR CONTEXT:
You are a bold artist.
Your illustration features {$spicy-content-with} Pop Art style, characterized by bold vibrant colors. The colors are executed using the pointillist technique, where dots of color come together to create the overall picture. Dazzling patterns and comic style characters. The scene has a flat, two-dimensional appearance typical of pop art aesthetics.
YOUR ILLUSTRATION:
{prompt}",,
Portra Film PhotoPhoto,"YOUR CONTEXT:
You are a photographer who appreciates the classic aesthetic of Kodak Portra film.
Your photograph emulates that look, known for its soft colors, fine grain, and natural skin tones. The image features {$spicy-content-with} a subtle warmth, with a focus on accurate color rendition and a gentle, diffused glow. Highlights are smooth and creamy, while shadows retain detail. Minimal post-processing is applied to preserve the organic, film-like quality.
YOUR PHOTOGRAPH:
{prompt}",,
Post-Processed Artistry,"YOUR CONTEXT:
You are a digital artist.
Your image is a masterpiece featuring {$spicy-content-with} heavy chromatic aberration, and digital intense color. The aesthetic is ultra-modern and fragmented, using a high-contrast palette of neon purples and cyans, creating a sense of a digital reality.
YOUR IMAGE:
{prompt}",,
Production PhotoPhoto,"YOUR CONTEXT:
You are a Hollywood filmmaker making a high-budget film.
Your photographs exhibit {$spicy-content-with} atmospheric composition that emphasize intense emotions using selective focus, and warm and cold colors with high quality studio lighting.
YOUR PHOTO:
{prompt}",,
Quiet Luxury Photo,"YOUR CONTEXT:
You are an editorial fashion photographer specializing in the Old Money aesthetic.
Your photograph showcases {$spicy-content-with} a sophisticated, clean composition using natural, soft morning light. The color palette is muted and neutral, focusing on beige, cream, navy blue, and forest green. The scene feels expensive and timeless, set in a luxury location.
YOUR PHOTOGRAPH:
{prompt}",,
Retro 80s ,"YOUR CONTEXT:
Your illustration is made with brightly colored bold lines and touches of cracked paint, while having {$spicy-content-with} a 1980s graphic design aesthetic.
YOUR ILLUSTRATION:
{prompt}",,
Risograph Print,"YOUR CONTEXT:
You are an underground indie press printer.
Your illustration is a tactile Risograph-style print, featuring {$spicy-content-with} vibrant fluorescent ink and a heavy grainy texture. It uses a punchy palette of neon pink, intense yellow, and deep teal.
YOUR MULTI SUBJECT PRINT:
{prompt}",,
Shadow Fantasy Illustration,"YOUR CONTEXT:
You are a dark fantasy comic book artist.
Your drawing is a high-contrast illustration characterized by {$spicy-content-with} massive areas of solid black ink and minimal white highlights. Inspired by ""Mignola-esque"" aesthetics, it uses chunky, angular silhouettes and geometric shadows to define form rather than internal lines. The atmosphere is ancient, heavy, and supernatural.
YOUR DRAWING:
{prompt}",,
Simple 3D Render,"YOUR CONTEXT:
You are a 3d artist.
Your render is a high-gloss, illustration featuring {$spicy-content-with} ultra-reflective surfaces. The style is dark with dark backgrounds and iridescent highlights.
YOUR RENDER:
{prompt}",,
Spotlight Stage Photo,"YOUR CONTEXT:
You are a professional stage and theater photographer capturing dramatic performances.
Your photographs exhibit {$spicy-content-with} a single, high-intensity overhead spotlight that creates a sharp circular pool of light, dramatic chiaroscuro contrast that isolates the subject against a pitch-black background, and visible volumetric light beams with subtle dust motes for an authentic theatrical atmosphere.
YOUR PHOTO:
{prompt}",,
Street Documentary PhotoPhoto,"YOUR CONTEXT:
You are a documentary photographer chronicling urban life.
Your photograph showcases {$spicy-content-with} candid moments in natural street environments. The image employs a natural color balance, modest contrast, and a shallow depth of field to isolate subjects within bustling scenes. Emphasis is placed on authentic emotion, body language, and the interplay of ambient city light (neon signs, street lamps). Minimal post‑processing preserves the raw, honest feel—only slight exposure adjustments and noise reduction are applied to maintain the immediacy of the moment.
YOUR PHOTOGRAPH:
{prompt}",,
Studio Anime,"YOUR CONTEXT:
You are a Japanese anime artist.
Your Japanese animation includes {$spicy-content-with} warm artwork inspired by Studio Ghibli, with detailed characters and a whimsical aesthetic, full of emotion.
YOUR DRAWING:
{prompt}",,
Synthwave Photo,"YOUR CONTEXT:
You are a photographer inspired by the retro-futuristic visuals of the 1980s.
Your photograph showcases {$spicy-content-with} Synthwave aesthetic, characterized by neon colors and geometric shapes, it includes vibrant pinks, purples, and blues, with a glowing effect. Grids, scanlines, and a retro-futuristic atmosphere. The photograph feels like a scene from a classic 80s sci-fi film.
Real people against a computer-generated background.
YOUR PHOTOGRAPH:
{prompt}",,
Teal and Orange PhotoPhoto,"YOUR CONTEXT:
You are a color specialist photographer.
Your photograph showcases {$spicy-content-with} a satured teal and orange colors. The image features cool teal tones in the shadows and highlights, balanced by warm orange tones in the midtones and skin tones, creating a vibrant photography with high contrast and vivid colors.
YOUR PHOTOGRAPH:
{prompt}",,
Tilt Shift / Toy PhotoPhoto,"YOUR CONTEXT:
You are a photographer who enjoys using tilt‑shift lenses to turn real‑world scenes into miniature toy models.
Your photographs exhibit {$spicy-content-with} crisp, high‑resolution details captured in an exaggerated perspective, with a very narrow focus plane, and a dreamy, toy‑like atmosphere that creates the classic “miniature” illusion.
YOUR PHOTO:
{prompt}",,
Unsettling,"YOUR CONTEXT:
You are an illustrator of dark and disturbing themes.
Your illustration includes {$spicy-content-with} stippling textures, dramatic raking side lighting, and a dread-filled atmosphere that is oppressive and claustrophobic. High contrast emphasizes form and shadow, employing cinematic chiaroscuro for depth.
YOUR DRAWING:
{prompt}",,
Vintage Comic,"YOUR CONTEXT:
You are a 1950s artist.
Your illustration in a worn sheet of paper features {$spicy-content-with} a vintage 1950s comic book style made with hand-drawn lines and painted in solid white, red, blue, and black.
YOUR ILLUSTRATION:
{prompt}",,
Vintage Illustration,"YOUR CONTEXT:
You are a sci-fi pulp magazine illustrator from the 1940s.
Your drawing is a lush, painted-style comic cover including {$spicy-content-with} dramatic, heroic lighting and a vivid ""Technicolor"" palette of saturated oranges, deep teals, and bright yellows. It features soft airbrushed textures, muscular anatomy, and a grainy film-stock finish that evokes a sense of retro-futuristic adventure.
YOUR DRAWING:
{prompt}",,
Vintage PhotoPhoto,"YOUR CONTEXT:
You are an 80s photographer who enjoys informal shots.
Your worn vintage photographs exhibit {$spicy-content-with} a minimalist, amateurish composition, warm desaturated tones and a soft focus that creates a cozy atmosphere.
YOUR PHOTO:
{prompt}",,
Vintage VGA Monitor,"YOUR CONTEXT:
You have an old VGA CRT monitor sitting on a wooden desk, and a window is reflected on its screen. The monitor's casing has a raised black inscription in the upper left corner that reads ""VGA"". The screen has a slight curve and sweep lines typical of CRT displays.
WHAT'S INSIDE YOUR MONITOR'S SCREEN:
A low-resolution, 2D pixel art platform videogame form early 1990s, featuring: {prompt}",,
Vintage Polaroid Photo,"YOUR CONTEXT:
You are a nostalgic photographer who embraces instant film’s tactile charm.
Your Polaroid photograph is using vintage‑style film. The photograph features the iconic square format including {$spicy-content-with} a soft focus, a subtle vignetting, and the characteristic pastel color shift of instant film. Showing candid moments or intimate still‑lifes. The photograph retains the authentic border, emulsion texture, and occasional “film‑like” imperfections (light leaks, scratches).
YOUR PHOTOGRAPH:
{prompt}",,
Whimsical Watercolor,"YOUR CONTEXT:
You are an illustrator specializing in gentle, enchanting narratives for children's books.
Your drawing is a whimsical watercolor image, featuring {$spicy-content-with} loose, fluid watercolor washes, delicate linework, and playful, anthropomorphic elements within a lush natural setting. The style is soft and inviting, with a vibrant yet gentle color palette that captures the magic of innocence.
YOUR DRAWING:
{prompt}",,
Wide Angle / Peephole,"YOUR CONTEXT:
Your peephole shows {$spicy-content-with} a shot from a high angle, inside a fisheye lens distortion glass circle, highly distorted with a wide angle.
YOUR PEEPHOLE:
{prompt}",,
Witchcore Photo,"YOUR CONTEXT:
You are a photographer drawn to the mystical and earthy aesthetic of Witchcore.
Your photograph showcases {$spicy-content-with} a moody, atmospheric scene steeped in natural elements. The image employs a natural color balance, with a focus on deep greens, browns, and blacks. Emphasis is placed on textures: moss, wood, crystals, and flowing fabrics. The photograph has a raw, organic feel, with mystical ambiance.
YOUR PHOTOGRAPHY:
{prompt}",,
1 name positive negative note
47 Wool, Fine knit {prompt} in wool cloth, highly detailed, gorgeous, fine knit cashmere wool fabric anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted
48 Wool, Heavy knit {prompt} in chunky heavy wool yarn, highly detailed, heavy knit large wool fabric, sailor's wool anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted
49 Wool, Cable knit {prompt} in chunky heavy cable knit wool yarn, highly detailed, cable knit large wool fabric anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted
50 80s Dark Fantasy Photo YOUR CONTEXT: You are a photographer working on the set of a dark fantasy film reminiscent of the 1980s (think *Excalibur*, *Legend*, or *Conan the Barbarian*). Your photograph showcases {$spicy-content-with} a heavily stylized composition with practical effects, matte paintings, and the dramatic lighting of that era. The image is shot with soft focus. Lighting is theatrical with saturated colors: deep blues, fiery oranges, and sickly greens. Emphasis is placed on creating a sense of scale and grandeur through forced perspective and miniature work. Post-processing involves adding subtle film grain, bloom effects around light sources, and a slight color shift. YOUR PHOTOGRAPHY: {prompt}
51 Anime YOUR CONTEXT: You are a Japanese anime artist. Your Japanese animation includes {$spicy-content-with} an ominous atmosphere, clean lines, and dramatic facial expressions. The artwork uses cool and dark tones along with dramatic lighting and shadows that emphasize the central figure and create a somber, intense mood. YOUR DRAWING: {prompt}
52 Casual Photo YOUR CONTEXT: You are an amateur documentary photographer taking low quality photos. Your photographs exhibit {$spicy-content-with} sharp backgrounds, unpolished realism with natural lighting, and candid friendship‑level moments that feel immediate and authentic. YOUR PHOTO: {prompt}
53 Classic Film Photo YOUR CONTEXT: You are a professional cinematographer of iconic classic mid‑century cinema (1950s‑1960s Hollywood golden age). Your photographs are on 35mm film with grain, displaying {$spicy-content-with} a warm color temperature, and an anamorphic lens that creates imperfections and blurry edges. YOUR PHOTO: {prompt}
54 Comic 1 YOUR CONTEXT: You are an American artist. Your illustration has a comic book cover style, with vibrant colors and outlined shapes, featuring {$spicy-content-with} realistic and epic details in the style of modern . YOUR ILLUSTRATION: {prompt}
55 Comic 2 YOUR CONTEXT: You are an Italian artist. Your illustration has a European comic book style, featuring {$spicy-content-with} black hand-drawn outlines, solid colors, realistic anatomical proportions, and cross-hatching for shading. YOUR ILLUSTRATION: {prompt}
56 Comic 3 YOUR CONTEXT: You are a European artist creating fiction worlds. Your image is a classic ligne claire futurist illustration, featuring {$spicy-content-with} exceptionally clean lines that define each element with the utmost precision. The composition highlights an intense atmosphere with complex technology. The color palette is vibrant, with flat colors and intricate lines and design. YOUR IMAGE: {prompt}
57 Comic Book Cover YOUR CONTEXT: You are a celebrated comic book cover artist, renowned for hyper-realistic action and monumental heroics. Your cover art is a Dynamic Heroic Realism illustration, characterized by {$spicy-content-with} intricate anatomical accuracy, meticulously rendered textures (fabric, metal, skin), and an explosive sense of motion. The composition often features dramatic foreshortening and a heroic, powerful pose for the central figure, surrounded by dynamic environmental destruction or energy effects. Lighting is grand and directional, sculpting forms with sharp highlights and deep shadows, ensuring every detail contributes to an overwhelming sense of epic scale and immediate impact. YOUR ILLUSTRATION: {prompt}
58 Cottagecore Pastoral Photo YOUR CONTEXT: You are a lifestyle photographer capturing the "Cottagecore" aesthetic, celebrating a nostalgic, rural life. Your photograph showcases {$spicy-content-with} a soft-focus, romantic composition bathed in warm, hazy afternoon light. The color palette is soft and organic, featuring sage green, dried rose, lavender, and warm ochre. The atmosphere is cozy and rustic, filled with natural textures like linen, dried flowers, and weathered wood in a peaceful countryside setting. YOUR PHOTOGRAPHY: {prompt}
59 Cyberpunk Rain Photo YOUR CONTEXT: You are a cinematic photographer specializing in the Neon Noir aesthetic of a futuristic night city. Your photograph showcases {$spicy-content-with} high-contrast lighting with deep shadows and glowing neon accents. The color palette is dominated by electric cyan, hot magenta, and deep violet. The atmosphere is moody and atmospheric, featuring wet surfaces, rain reflections, and hazy mist. YOUR PHOTOGRAPH: {prompt}
60 Digital Cyberpunk YOUR CONTEXT: You are a digital artist working in a dystopian, technologically advanced future. Your illustration is a Cyberpunk Noir scene, characterized by {$spicy-content-with} harsh, dramatic lighting that casts long, sharp shadows, reflecting the grimy underbelly of a futuristic scene. It employs a limited color palette dominated by deep blues, neon greens, and stark oranges, giving a mysterious aesthetic. YOUR ILLUSTRATION: {prompt}
61 Drone PhotoPhoto YOUR CONTEXT: You are an aerial photographer who enjoys using wide-angle drone cams. Your photographs exhibit {$spicy-content-with} panoramic scenes captured from afar, high up with elevated perspectives and intense colors. YOUR PHOTO: {prompt}
62 Electric Blue Outline YOUR CONTEXT: Your image showcases {$spicy-content-with} a digital cyberpunk draw, characterized by vibrant and saturated colors with solid backgrounds and blue outlines. YOUR IMAGE: {prompt}
63 Epic Greg YOUR CONTEXT: You are a master digital painter of heroic sagas. Your illustration is a grand-scale digital oil painting that includes {$spicy-content-with} characterized by masterful, sweeping brushwork and thick impasto textures. It features dramatic, cinematic lighting with intense chiaroscuro, where light sources like embers or magical energy pierce through atmospheric haze, smoke, and swirling clouds. The composition is heroic and classically inspired, using a rich, moody palette of deep earth tones accented by vibrant, glowing highlights for a legendary and immersive feel. YOUR PAINTING: {prompt}
64 Flash 90s PhotoPhoto YOUR CONTEXT: You are an amateur photographer, taking photos with point‑and‑shoot cam-quality, in the early 1990s. Your photographs exhibit {$spicy-content-with} a rough, low-resolution quality, washed-out colors, including a harsh built-in flash, grainy texture, and overexposed highlights, capturing a casual atmosphere that evokes nostalgia. YOUR PHOTO: {prompt}
65 Folk-Art Mosaic YOUR CONTEXT: You are a traditional artisan celebrating the vibrant colors and symbolic imagery of folk art. Your artwork is a Folk Art Mosaic, composed of {$spicy-content-with} small, tessellated pieces of vibrant, solid colors, forming bold patterns and stylized representations of everyday life or mythical creatures. The aesthetic is naive and handcrafted, with a rich texture and a joyful, earthy color palette. YOUR ARTWORK: {prompt}
66 Glass Encased Advertising YOUR CONTEXT: An eye-level, realistic photo of a glass-encased advertising poster (mupi) located on a clean city sidewalk. There are subtle reflections of nearby buildings and trees on the glass surface. The lighting is natural overcast daylight. WHAT'S BEHIND THE REFLECTIVE GLASS: A faded and sun-faded, low quality printed advertisement, showing: {prompt}
67 Gothic Engraving YOUR CONTEXT: You are an artist inspired by the macabre aesthetics of 19th-century gothic literature and medieval manuscripts. Your illustration is a Neo-Gothic engraving, executed with intricate linework. It features {$spicy-content-with} dramatic chiaroscuro and a sense of brooding mystery. The color palette is muted, favoring deep indigos, charcoal grays, and touches of crimson, evoking a sense of antique dread. YOUR ILLUSTRATION: {prompt}
68 High-Key Fashion PhotoPhoto YOUR CONTEXT: You are a fashion photographer who creates ultra-bright photos for luxury productions. Your photograph showcases {$spicy-content-with} a high‑key composition shot flooded with soft studio lighting. The exposure is over‑lit with white background, eliminating shadows and delivering a crisp, polished look. Colors are saturated yet controlled (vivid reds, cobalt blues, and crisp whites) while the photo has immaculate details thanks to high‑resolution capture and minimal post‑processing to maintain the airy, glamorous aesthetic. YOUR PHOTOGRAPH: {prompt}
69 Ink Draw YOUR CONTEXT: You are an ink drawing artist. Your illustration is a realistic ink drawing featuring {$spicy-content-with} fine cross-hatching and stippling techniques. These methods create deep shadows and highlights using only black lines. The image includes large areas of white space and Imagine the colors as all shades of gray. The colors mentioned are actually shades of gray. YOUR ILLUSTRATION: {prompt}
70 Ink Frenzy YOUR CONTEXT: You are an underground street-art comic illustrator. Your image is a high-energy, chaotic ink drawing featuring {$spicy-content-with} aggressive brush strokes, intentional ink splatters, and "controlled mess" textures. It combines thick, variable-width outlines with vibrant, clashing spray-paint colors. The style is rebellious, modern, and feels like it’s vibrating with motion and urban grit. YOUR IMAGE: {prompt}
71 Kawaii Pop Photo YOUR CONTEXT: You are a fashion photographer specializing in the bright, playful aesthetic of Kawaii Pop. Your photograph showcases {$spicy-content-with} a high-key composition flooded with vibrant, saturated colors. The exposure is bright and cheerful, with a focus on pastel pinks, electric blues, and sunny yellows. Scene is styled in over-the-top Kawaii fashion with playful poses. The background is simple and colorful, with elements like balloons, confetti, or cartoonish graphics. YOUR PHOTOGRAPHY: {prompt}
72 Light Painting Photo YOUR CONTEXT: You are an experimental photographer who enjoys creating light painting images with long exposures and LED sources. Your photographs exhibit {$spicy-content-with} luminous trails, vibrant colors, and elements that are strikingly illuminated by the long exposure. YOUR PHOTO: {prompt}
73 Light and Airy PhotoPhoto YOUR CONTEXT: You are a photographer who loves bright, ethereal, and romantic images. Your photograph is bathed in soft, diffused light, creating a bright and airy aesthetic. Colors are pastel and delicate, with a focus on whites, creams, and light blues. The image features {$spicy-content-with} a shallow depth of field, blurring the background and drawing attention to the subject. Post-processing emphasizes highlights and reduces shadows, resulting in a clean, luminous look. YOUR PHOTOGRAPH: {prompt}
74 LomographyPhoto YOUR CONTEXT: You are an adventurous experimental photographer exploring the world through a lomographic lens. Your photographs exhibit {$spicy-content-with} film grain, colorful lens flares, soft focus with motion blur, and analog filter effects, trying to capture emotions. YOUR PHOTO: {prompt}
75 Low-Poly Render YOUR CONTEXT: Your unity engine render shows {$spicy-content-with} a low-poly 3d platformer level made from low-poly meshes and low-quality textures. YOUR UNITY ENGINE RENDER: {prompt}
76 LowRes Pixel Art YOUR CONTEXT: You are a pixel artist. Your illustration is a low-resolution pixel art featuring {$spicy-content-with} chunky pixels and vibrant intense colors, arcade game style. YOUR ILLUSTRATION: {prompt}
77 Manga YOUR CONTEXT: You are a Japanese manga artist. Your illustration features {$spicy-content-with} black and white manga-style that includes manga screentone patterns with high-contrast black and white drawing, in a violent atmosphere. Always imagine colors as shades of gray. YOUR ILLUSTRATION: {prompt}
78 Minimalist Digital Illustration YOUR CONTEXT: You are a cover artist known for powerful, minimalist designs. Your cover art is a bold graphic iconography piece, featuring {$spicy-content-with} stark, impactful silhouettes and clean, sharp lines that define forms with absolute clarity. The color palette is vibrant, employing large fields of solid color with subtle gradients. Most importantly, the proportions are exaggerated, and the perspective is extremely distorted, creating a sense of vertigo and action. YOUR ILLUSTRATION: {prompt}
79 Minimalist PhotoPhoto YOUR CONTEXT: You are a photographer who prefers serene and tranquil images. Your photographs exhibit {$spicy-content-with} high contrast and clean, minimalist compositions that emphasize empty space to highlight negative space. YOUR PHOTO: {prompt}
80 Modern Impressionism YOUR CONTEXT: Your image showcases {$spicy-content-with} a loose and highly expressive brushstroke, characterized by a rich texture created through visible layers of paint and quick, broad strokes reminiscent of modern Impressionism. This technique employing a limited yet impactful color palette with strong contrasts of light and shadow and a focus on warm and cool tones to create a dynamic and energetic feel. YOUR IMAGE: {prompt}
81 Modern Ukiyo-e Print YOUR CONTEXT: You are a master of traditional Japanese woodblock printing adapted to a modern narrative. Your illustration features {$spicy-content-with} bold, flowing outlines and flat areas of color with subtle wood-grain textures. The composition uses dramatic perspective typical of "Ukiyo-e" art, with stylized waves, clouds, or smoke, and a color palette of deep indigo, vermillion, and ochre on an aged washi paper background. YOUR DRAWING: {prompt}
82 Noir PhotoPhoto YOUR CONTEXT: You are a cinematographer who works in dark movies. Your photographs exhibit {$spicy-content-with} intense side lighting and gobo‑crafted patterns to sculpt deep, sharply defined shadows, along with a muted Ektachrome palette that evokes film noir. YOUR PHOTO: {prompt}
83 Nostalgic 90s Photo YOUR CONTEXT: You are a street photographer capturing the raw, rebellious energy of the 1990s underground scene. Your photograph showcases {$spicy-content-with} a low-fidelity, grainy analog film aesthetic. The exposure features harsh camera flash, slight motion blur, and light leaks. The colors are slightly de-saturated with warm skin tones and heavy shadows, giving it a candid, "snapshot" feel. YOUR PHOTOGRAPHY: {prompt}
84 Orthochromatic Spirit PhotoPhoto YOUR CONTEXT: You are a photographer inspired by the look of early photographic processes, specifically orthochromatic and colorized films. Your photograph showcases {$spicy-content-with} a scene with a high-contrast, blue-sensitive aesthetic. Reds appear very dark, while blues and greens are bright. The image is characterized by a stark, graphic quality. Post-processing focuses on enhancing the contrast and emphasizing the tonal separation. YOUR PHOTOGRAPH: {prompt}
85 Paper-Cut Diorama YOUR CONTEXT: You are a conceptual artist creating a tactile, handcrafted world. Your illustration is a digital "paper-cut" style including {$spicy-content-with} distinct layered shapes with subtle drop shadows to create depth. Each element looks like it was cut from different textures of colored construction paper. The style is geometric, clean, and vibrant, making the comic look like a 3D physical diorama. YOUR IMAGE: {prompt}
86 Phone Photo YOUR CONTEXT: Your photographs has android phone cam-quality. Your photographs exhibit {$spicy-content-with} surprising compositions, sharp complex backgrounds, natural lighting, and candid moments that feel immediate and authentic. Your photographs are actual gritty candid photographic background. YOUR PHOTO: {prompt}
87 Pop PhotoPhoto YOUR CONTEXT: You are a contemporary pop artist known for your vibrant, oversaturated palettes and energetic, graphic compositions. Your photographs exhibit {$spicy-content-with} exaggerated colors and solid backgrounds using Ben-Day dots, achieving an unconventional and clearly pop art effect. YOUR PHOTO: {prompt}
88 Pop-Art YOUR CONTEXT: You are a bold artist. Your illustration features {$spicy-content-with} Pop Art style, characterized by bold vibrant colors. The colors are executed using the pointillist technique, where dots of color come together to create the overall picture. Dazzling patterns and comic style characters. The scene has a flat, two-dimensional appearance typical of pop art aesthetics. YOUR ILLUSTRATION: {prompt}
89 Portra Film PhotoPhoto YOUR CONTEXT: You are a photographer who appreciates the classic aesthetic of Kodak Portra film. Your photograph emulates that look, known for its soft colors, fine grain, and natural skin tones. The image features {$spicy-content-with} a subtle warmth, with a focus on accurate color rendition and a gentle, diffused glow. Highlights are smooth and creamy, while shadows retain detail. Minimal post-processing is applied to preserve the organic, film-like quality. YOUR PHOTOGRAPH: {prompt}
90 Post-Processed Artistry YOUR CONTEXT: You are a digital artist. Your image is a masterpiece featuring {$spicy-content-with} heavy chromatic aberration, and digital intense color. The aesthetic is ultra-modern and fragmented, using a high-contrast palette of neon purples and cyans, creating a sense of a digital reality. YOUR IMAGE: {prompt}
91 Production PhotoPhoto YOUR CONTEXT: You are a Hollywood filmmaker making a high-budget film. Your photographs exhibit {$spicy-content-with} atmospheric composition that emphasize intense emotions using selective focus, and warm and cold colors with high quality studio lighting. YOUR PHOTO: {prompt}
92 Quiet Luxury Photo YOUR CONTEXT: You are an editorial fashion photographer specializing in the Old Money aesthetic. Your photograph showcases {$spicy-content-with} a sophisticated, clean composition using natural, soft morning light. The color palette is muted and neutral, focusing on beige, cream, navy blue, and forest green. The scene feels expensive and timeless, set in a luxury location. YOUR PHOTOGRAPH: {prompt}
93 Retro 80s YOUR CONTEXT: Your illustration is made with brightly colored bold lines and touches of cracked paint, while having {$spicy-content-with} a 1980s graphic design aesthetic. YOUR ILLUSTRATION: {prompt}
94 Risograph Print YOUR CONTEXT: You are an underground indie press printer. Your illustration is a tactile Risograph-style print, featuring {$spicy-content-with} vibrant fluorescent ink and a heavy grainy texture. It uses a punchy palette of neon pink, intense yellow, and deep teal. YOUR MULTI SUBJECT PRINT: {prompt}
95 Shadow Fantasy Illustration YOUR CONTEXT: You are a dark fantasy comic book artist. Your drawing is a high-contrast illustration characterized by {$spicy-content-with} massive areas of solid black ink and minimal white highlights. Inspired by "Mignola-esque" aesthetics, it uses chunky, angular silhouettes and geometric shadows to define form rather than internal lines. The atmosphere is ancient, heavy, and supernatural. YOUR DRAWING: {prompt}
96 Simple 3D Render YOUR CONTEXT: You are a 3d artist. Your render is a high-gloss, illustration featuring {$spicy-content-with} ultra-reflective surfaces. The style is dark with dark backgrounds and iridescent highlights. YOUR RENDER: {prompt}
97 Spotlight Stage Photo YOUR CONTEXT: You are a professional stage and theater photographer capturing dramatic performances. Your photographs exhibit {$spicy-content-with} a single, high-intensity overhead spotlight that creates a sharp circular pool of light, dramatic chiaroscuro contrast that isolates the subject against a pitch-black background, and visible volumetric light beams with subtle dust motes for an authentic theatrical atmosphere. YOUR PHOTO: {prompt}
98 Street Documentary PhotoPhoto YOUR CONTEXT: You are a documentary photographer chronicling urban life. Your photograph showcases {$spicy-content-with} candid moments in natural street environments. The image employs a natural color balance, modest contrast, and a shallow depth of field to isolate subjects within bustling scenes. Emphasis is placed on authentic emotion, body language, and the interplay of ambient city light (neon signs, street lamps). Minimal post‑processing preserves the raw, honest feel—only slight exposure adjustments and noise reduction are applied to maintain the immediacy of the moment. YOUR PHOTOGRAPH: {prompt}
99 Studio Anime YOUR CONTEXT: You are a Japanese anime artist. Your Japanese animation includes {$spicy-content-with} warm artwork inspired by Studio Ghibli, with detailed characters and a whimsical aesthetic, full of emotion. YOUR DRAWING: {prompt}
100 Synthwave Photo YOUR CONTEXT: You are a photographer inspired by the retro-futuristic visuals of the 1980s. Your photograph showcases {$spicy-content-with} Synthwave aesthetic, characterized by neon colors and geometric shapes, it includes vibrant pinks, purples, and blues, with a glowing effect. Grids, scanlines, and a retro-futuristic atmosphere. The photograph feels like a scene from a classic 80s sci-fi film. Real people against a computer-generated background. YOUR PHOTOGRAPH: {prompt}
101 Teal and Orange PhotoPhoto YOUR CONTEXT: You are a color specialist photographer. Your photograph showcases {$spicy-content-with} a satured teal and orange colors. The image features cool teal tones in the shadows and highlights, balanced by warm orange tones in the midtones and skin tones, creating a vibrant photography with high contrast and vivid colors. YOUR PHOTOGRAPH: {prompt}
102 Tilt Shift / Toy PhotoPhoto YOUR CONTEXT: You are a photographer who enjoys using tilt‑shift lenses to turn real‑world scenes into miniature toy models. Your photographs exhibit {$spicy-content-with} crisp, high‑resolution details captured in an exaggerated perspective, with a very narrow focus plane, and a dreamy, toy‑like atmosphere that creates the classic “miniature” illusion. YOUR PHOTO: {prompt}
103 Unsettling YOUR CONTEXT: You are an illustrator of dark and disturbing themes. Your illustration includes {$spicy-content-with} stippling textures, dramatic raking side lighting, and a dread-filled atmosphere that is oppressive and claustrophobic. High contrast emphasizes form and shadow, employing cinematic chiaroscuro for depth. YOUR DRAWING: {prompt}
104 Vintage Comic YOUR CONTEXT: You are a 1950s artist. Your illustration in a worn sheet of paper features {$spicy-content-with} a vintage 1950s comic book style made with hand-drawn lines and painted in solid white, red, blue, and black. YOUR ILLUSTRATION: {prompt}
105 Vintage Illustration YOUR CONTEXT: You are a sci-fi pulp magazine illustrator from the 1940s. Your drawing is a lush, painted-style comic cover including {$spicy-content-with} dramatic, heroic lighting and a vivid "Technicolor" palette of saturated oranges, deep teals, and bright yellows. It features soft airbrushed textures, muscular anatomy, and a grainy film-stock finish that evokes a sense of retro-futuristic adventure. YOUR DRAWING: {prompt}
106 Vintage PhotoPhoto YOUR CONTEXT: You are an 80s photographer who enjoys informal shots. Your worn vintage photographs exhibit {$spicy-content-with} a minimalist, amateurish composition, warm desaturated tones and a soft focus that creates a cozy atmosphere. YOUR PHOTO: {prompt}
107 Vintage VGA Monitor YOUR CONTEXT: You have an old VGA CRT monitor sitting on a wooden desk, and a window is reflected on its screen. The monitor's casing has a raised black inscription in the upper left corner that reads "VGA". The screen has a slight curve and sweep lines typical of CRT displays. WHAT'S INSIDE YOUR MONITOR'S SCREEN: A low-resolution, 2D pixel art platform videogame form early 1990s, featuring: {prompt}
108 Vintage Polaroid Photo YOUR CONTEXT: You are a nostalgic photographer who embraces instant film’s tactile charm. Your Polaroid photograph is using vintage‑style film. The photograph features the iconic square format including {$spicy-content-with} a soft focus, a subtle vignetting, and the characteristic pastel color shift of instant film. Showing candid moments or intimate still‑lifes. The photograph retains the authentic border, emulsion texture, and occasional “film‑like” imperfections (light leaks, scratches). YOUR PHOTOGRAPH: {prompt}
109 Whimsical Watercolor YOUR CONTEXT: You are an illustrator specializing in gentle, enchanting narratives for children's books. Your drawing is a whimsical watercolor image, featuring {$spicy-content-with} loose, fluid watercolor washes, delicate linework, and playful, anthropomorphic elements within a lush natural setting. The style is soft and inviting, with a vibrant yet gentle color palette that captures the magic of innocence. YOUR DRAWING: {prompt}
110 Wide Angle / Peephole YOUR CONTEXT: Your peephole shows {$spicy-content-with} a shot from a high angle, inside a fisheye lens distortion glass circle, highly distorted with a wide angle. YOUR PEEPHOLE: {prompt}
111 Witchcore Photo YOUR CONTEXT: You are a photographer drawn to the mystical and earthy aesthetic of Witchcore. Your photograph showcases {$spicy-content-with} a moody, atmospheric scene steeped in natural elements. The image employs a natural color balance, with a focus on deep greens, browns, and blacks. Emphasis is placed on textures: moss, wood, crystals, and flowing fabrics. The photograph has a raw, organic feel, with mystical ambiance. YOUR PHOTOGRAPHY: {prompt}
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
File diff suppressed because one or more lines are too long
+471
View File
@@ -0,0 +1,471 @@
import { app } from "../../scripts/app.js";
// CONSTANTS
const GLOBAL_HIDE_SETTING_ID = "SATA_UtilityNode.GlobalPreviewHide";
const PREVIEW_MACHINE_CLASS = "Preview_Machine";
const SAVE_MACHINE_CLASS = "Save_Machine";
// Track hover state
const hoveredNodeIds = new Set();
// Inject stylesheet for Nodes 2.0 (Vue DOM) and Legacy V1 DOM preview masking
function injectHiderCSS() {
if (document.getElementById("sata-preview-hider-styles")) return;
const style = document.createElement("style");
style.id = "sata-preview-hider-styles";
style.textContent = `
/* Mask only the image container itself in-place */
.sata-image-masked {
position: relative !important;
min-height: 100px !important;
background-color: #1a1a1a !important;
border-radius: 6px !important;
border: 1px dashed #444 !important;
box-sizing: border-box !important;
overflow: hidden !important;
transition: background-color 0.15s ease;
}
/* Hide all img/canvas/video children inside the masked image container */
.sata-image-masked img,
.sata-image-masked canvas,
.sata-image-masked video {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
transition: opacity 0.15s ease;
}
/* Center placeholder text cleanly over the masked image container */
.sata-image-masked::after {
content: "Hidden (Hover to Reveal)" !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: #888 !important;
font-size: 13px !important;
font-weight: bold !important;
font-family: sans-serif !important;
text-align: center !important;
white-space: nowrap !important;
pointer-events: none !important;
opacity: 1 !important;
visibility: visible !important;
z-index: 10 !important;
}
/* NATIVE CSS HOVER REVEAL: Works across Nodes 2.0 Vue Nodes & Legacy LiteGraph DOM */
.sata-image-masked:hover img,
.sata-image-masked:hover canvas,
.sata-image-masked:hover video,
[data-node-id]:hover .sata-image-masked img,
[data-node-id]:hover .sata-image-masked canvas,
[data-node-id]:hover .sata-image-masked video,
.comfy-vue-node:hover .sata-image-masked img,
.comfy-vue-node:hover .sata-image-masked canvas,
.comfy-vue-node:hover .sata-image-masked video,
.node:hover .sata-image-masked img,
.node:hover .sata-image-masked canvas,
.node:hover .sata-image-masked video,
.lgraphnode:hover .sata-image-masked img,
.lgraphnode:hover .sata-image-masked canvas,
.lgraphnode:hover .sata-image-masked video {
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
}
.sata-image-masked:hover::after,
[data-node-id]:hover .sata-image-masked::after,
.comfy-vue-node:hover .sata-image-masked::after,
.node:hover .sata-image-masked::after,
.lgraphnode:hover .sata-image-masked::after {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
}
.sata-image-masked:hover,
[data-node-id]:hover .sata-image-masked,
.comfy-vue-node:hover .sata-image-masked,
.node:hover .sata-image-masked,
.lgraphnode:hover .sata-image-masked {
background-color: transparent !important;
border: none !important;
}
`;
document.head.appendChild(style);
}
// Helper to get settings for both ComfyUI V1 and V2
function getSettingValue(id, defaultValue) {
if (app.extensionManager && app.extensionManager.setting) {
const val = app.extensionManager.setting.get(id);
return val !== undefined ? val : defaultValue;
} else if (app.ui && app.ui.settings) {
return app.ui.settings.getSettingValue(id, defaultValue);
}
return defaultValue;
}
app.registerExtension({
name: "SATA_UtilityNode.Preview_Machine",
settings: [
{
id: GLOBAL_HIDE_SETTING_ID,
name: "SATA Utility: Global Hide Previews",
type: "boolean",
defaultValue: false,
onChange: (value) => {
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
}
],
async setup() {
injectHiderCSS();
setupGlobalMouseTracker();
},
async nodeCreated(node) {
if (isTargetNode(node)) {
setupNodeHider(node);
}
},
async loadedGraphNode(node) {
if (isTargetNode(node)) {
setupNodeHider(node);
}
}
});
function isTargetNode(node) {
if (!node || !node.comfyClass) return false;
return node.comfyClass === "PreviewImage" ||
node.comfyClass === "SaveImage" ||
node.comfyClass === PREVIEW_MACHINE_CLASS ||
node.comfyClass === SAVE_MACHINE_CLASS;
}
function shouldHideNode(node) {
const globalHide = getSettingValue(GLOBAL_HIDE_SETTING_ID, false);
if (globalHide) return true;
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
if (hideWidget) {
return !!hideWidget.value;
}
if (node.comfyClass === PREVIEW_MACHINE_CLASS || node.comfyClass === SAVE_MACHINE_CLASS) {
return true;
}
return false;
}
function isMouseOverNode(node) {
if (!node || !node.pos || !node.size) return false;
if (app.canvas && app.canvas.node_over === node) return true;
if (!app.canvas || !app.canvas.graph_mouse) return false;
const mouse = app.canvas.graph_mouse;
const x = mouse[0];
const y = mouse[1];
return x >= node.pos[0] &&
x <= (node.pos[0] + node.size[0]) &&
y >= node.pos[1] &&
y <= (node.pos[1] + node.size[1]);
}
function isNodeHovered(node) {
if (!node) return false;
if (app.canvas && app.canvas.node_over === node) return true;
if (hoveredNodeIds.has(node.id)) return true;
return isMouseOverNode(node);
}
function shouldHideNow(node) {
const shouldHide = shouldHideNode(node);
const hovered = isNodeHovered(node);
return shouldHide && !hovered;
}
function getNodeDOMElement(node) {
if (!node) return null;
if (node.DOMelement) return node.DOMelement;
if (node.element) return node.element;
const id = String(node.id);
return document.querySelector(`[data-node-id="${id}"]`) ||
document.querySelector(`[data-id="${id}"]`) ||
document.getElementById(`node-${id}`) ||
document.querySelector(`.node-${id}`) ||
document.querySelector(`.comfy-vue-node[data-id="${id}"]`);
}
function hasImages(node) {
if (!node) return false;
if (node.imgs && node.imgs.length > 0) return true;
const el = getNodeDOMElement(node);
if (el) {
const img = el.querySelector("img, canvas.comfy-img-preview, .comfy-img-preview, .node-image-preview, .comfy-vue-image-preview");
if (img) return true;
}
if (node.widgets) {
for (const w of node.widgets) {
if (w.type === "image" || w.name === "preview" || w.imageEl || w.element?.querySelector?.("img")) {
return true;
}
}
}
return false;
}
function getImageContainers(node) {
const containers = new Set();
const el = getNodeDOMElement(node);
if (el) {
// Find existing image preview containers inside the node DOM
const previewEls = el.querySelectorAll(
".comfy-img-preview, .node-image-preview, .comfy-vue-image-preview, .node-widget-image, [class*='ImagePreview'], [class*='image-preview'], [class*='node-image']"
);
previewEls.forEach(p => containers.add(p));
// Direct img elements parent wrappers
const imgs = el.querySelectorAll("img, canvas.comfy-img-preview");
imgs.forEach(img => {
if (img.parentElement && img.parentElement !== el) {
containers.add(img.parentElement);
} else {
containers.add(img);
}
});
}
if (node.widgets) {
for (const widget of node.widgets) {
if (widget.element && (widget.type === "image" || widget.name === "preview" || widget.element.querySelector("img"))) {
containers.add(widget.element);
}
if (widget.imageEl) {
containers.add(widget.imageEl.parentElement || widget.imageEl);
}
}
}
return Array.from(containers);
}
function setupGlobalMouseTracker() {
let rafPending = false;
const checkAndUpdate = () => {
if (!app.graph || !app.graph._nodes) return;
let changed = false;
for (const node of app.graph._nodes) {
if (!isTargetNode(node)) continue;
const currentlyHovered = isMouseOverNode(node);
const wasHovered = hoveredNodeIds.has(node.id);
if (currentlyHovered !== wasHovered) {
if (currentlyHovered) {
hoveredNodeIds.add(node.id);
} else {
hoveredNodeIds.delete(node.id);
}
changed = true;
}
syncNodeDOMVisibility(node);
}
if (changed && app.graph) {
app.graph.setDirtyCanvas(true, true);
}
};
window.addEventListener("mousemove", () => {
if (!rafPending) {
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
checkAndUpdate();
});
}
});
window.addEventListener("mouseout", (e) => {
if (e.relatedTarget === null) {
hoveredNodeIds.clear();
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
});
}
function syncNodeDOMVisibility(node) {
if (!node) return;
const el = getNodeDOMElement(node);
if (el && !el._sata_mouse_bound) {
el._sata_mouse_bound = true;
el.addEventListener("mouseenter", () => {
hoveredNodeIds.add(node.id);
syncNodeDOMVisibility(node);
if (app.graph) app.graph.setDirtyCanvas(true, true);
});
el.addEventListener("mouseleave", () => {
hoveredNodeIds.delete(node.id);
syncNodeDOMVisibility(node);
if (app.graph) app.graph.setDirtyCanvas(true, true);
});
}
// Apply DOM masking when images exist AND hide_preview is active
const nodeHasImages = hasImages(node);
const hide = nodeHasImages && shouldHideNode(node);
const containers = getImageContainers(node);
containers.forEach(container => {
if (hide) {
container.classList.add("sata-image-masked");
} else {
container.classList.remove("sata-image-masked");
}
});
}
function setupNodeHider(node) {
if (node._sata_hider_v12) return;
node._sata_hider_v12 = true;
injectHiderCSS();
const patchWidgets = () => {
if (!node.widgets) return;
for (const widget of node.widgets) {
if (widget._sata_patched) continue;
if (widget.type === "image" || widget.name === "preview") {
if (widget.draw && typeof widget.draw === 'function') {
const origDraw = widget.draw.bind(widget);
widget.draw = function (ctx, nodeRef, widgetWidth, y, widgetHeight) {
const targetNode = nodeRef || node;
if (hasImages(targetNode) && shouldHideNow(targetNode)) {
ctx.save();
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(0, y, widgetWidth, widgetHeight);
ctx.restore();
return;
}
return origDraw(ctx, targetNode, widgetWidth, y, widgetHeight);
};
widget._sata_patched = true;
}
if (widget.drawWidget && typeof widget.drawWidget === 'function') {
const origDrawWidget = widget.drawWidget.bind(widget);
widget.drawWidget = function (...args) {
if (hasImages(node) && shouldHideNow(node)) {
return;
}
return origDrawWidget(...args);
};
widget._sata_patched = true;
}
}
}
syncNodeDOMVisibility(node);
};
patchWidgets();
const origOnDrawBackground = node.onDrawBackground;
node.onDrawBackground = function (ctx) {
patchWidgets();
syncNodeDOMVisibility(node);
if (origOnDrawBackground) {
origOnDrawBackground.apply(this, arguments);
}
};
const origOnDrawForeground = node.onDrawForeground;
node.onDrawForeground = function (ctx) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
syncNodeDOMVisibility(this);
if (hasImages(this) && shouldHideNow(this)) {
// Find where settings widgets end so we only mask the image area below widgets
let startY = 30;
if (this.widgets && this.widgets.length > 0) {
const lastWidget = this.widgets[this.widgets.length - 1];
if (lastWidget.last_y !== undefined) {
const h = (lastWidget.computeSize ? lastWidget.computeSize(this.size[0])[1] : 20) || 20;
startY = lastWidget.last_y + h + 6;
} else {
startY = 30 + this.widgets.length * 24;
}
}
const maskHeight = Math.max(0, this.size[1] - startY);
if (maskHeight > 10) {
ctx.save();
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(4, startY, this.size[0] - 8, maskHeight - 4);
// Fallback for pure LiteGraph 2D Canvas rendering (when no DOM element exists)
const el = getNodeDOMElement(this);
if (!el || !el.querySelector(".sata-image-masked")) {
ctx.fillStyle = "#888";
ctx.font = "bold 13px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Hidden", this.size[0] / 2, startY + maskHeight / 2 - 8);
ctx.font = "10px Arial";
ctx.fillStyle = "#666";
ctx.fillText("(Hover to Reveal)", this.size[0] / 2, startY + maskHeight / 2 + 10);
}
ctx.restore();
}
}
};
const origOnExecuted = node.onExecuted;
node.onExecuted = function (message) {
if (origOnExecuted) {
origOnExecuted.apply(this, arguments);
}
patchWidgets();
syncNodeDOMVisibility(node);
setTimeout(() => syncNodeDOMVisibility(node), 20);
setTimeout(() => syncNodeDOMVisibility(node), 100);
setTimeout(() => syncNodeDOMVisibility(node), 300);
if (app.graph) app.graph.setDirtyCanvas(true, true);
};
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
if (hideWidget) {
const origCallback = hideWidget.callback;
hideWidget.callback = function (value) {
if (origCallback) origCallback.call(this, value);
syncNodeDOMVisibility(node);
if (app.graph) app.graph.setDirtyCanvas(true, true);
};
}
}
+121 -31
View File
@@ -1,30 +1,78 @@
import { app } from "../../../scripts/app.js"; import { app } from "../../../scripts/app.js";
// Helper to get settings for both ComfyUI V1 and V2
function getSettingValue(id, defaultValue) {
if (app.extensionManager && app.extensionManager.setting) {
const val = app.extensionManager.setting.get(id);
return val !== undefined ? val : defaultValue;
} else if (app.ui && app.ui.settings) {
return app.ui.settings.getSettingValue(id, defaultValue);
}
return defaultValue;
}
function fuzzyMatch(text, query) {
const textLower = text.toLowerCase();
const queryLower = query.toLowerCase();
if (!queryLower) return { match: true, score: 0 };
let queryIdx = 0;
let score = 0;
let lastMatchIdx = -1;
for (let i = 0; i < textLower.length; i++) {
if (textLower[i] === queryLower[queryIdx]) {
if (lastMatchIdx !== -1) {
score += (i - lastMatchIdx - 1);
}
lastMatchIdx = i;
queryIdx++;
if (queryIdx === queryLower.length) {
score += (textLower.length - i - 1) * 0.1;
return { match: true, score: score };
}
}
}
return { match: false, score: Infinity };
}
// Register extension // Register extension
app.registerExtension({ app.registerExtension({
name: "SATA_UtilityNode.PromptAutocomplete", name: "SATA_UtilityNode.PromptAutocomplete",
async setup() { settings: [
// Add settings {
app.ui.settings.addSetting({ id: "SATA_UtilityNode.PromptAutocomplete.Trigger",
id: "SATA.PromptAutocomplete.Trigger",
name: "Prompt Autocomplete Trigger Character", name: "Prompt Autocomplete Trigger Character",
type: "text", type: "text",
defaultValue: "#", defaultValue: "#",
}); },
{
app.ui.settings.addSetting({ id: "SATA_UtilityNode.PromptAutocomplete.Global",
id: "SATA.PromptAutocomplete.Global",
name: "Prompt Autocomplete Global Mode (All Text Widgets)", name: "Prompt Autocomplete Global Mode (All Text Widgets)",
type: "boolean", type: "boolean",
defaultValue: false, defaultValue: false,
}
],
async setup() {
const self = this;
document.addEventListener("focusin", (e) => {
if (e.target && (e.target.tagName === "TEXTAREA" || e.target.tagName === "INPUT")) {
if (e.target.dataset.hasAutocomplete) return;
if (!self.snippetsCache) {
self.loadSnippets();
}
e.target.dataset.hasAutocomplete = "true";
e.target.addEventListener("keydown", (evt) => self.handleKeydown(evt, e.target, null));
e.target.addEventListener("input", (evt) => self.handleInput(evt, e.target, null));
}
}); });
}, },
async nodeCreated(node) { async initAutocompleteForNode(node) {
// Check if we should attach to this node const isTargetNode = node.comfyClass === "PromptAutocomplete" || node.comfyClass === "Prompt_Machine";
const isTargetNode = node.comfyClass === "PromptAutocomplete"; const isGlobal = getSettingValue("SATA_UtilityNode.PromptAutocomplete.Global", false);
const isGlobal = app.ui.settings.getSettingValue("SATA.PromptAutocomplete.Global", false);
if (!isTargetNode && !isGlobal) return; if (!isTargetNode && !isGlobal) return;
@@ -42,6 +90,14 @@ app.registerExtension({
}); });
}, },
async nodeCreated(node) {
this.initAutocompleteForNode(node);
},
async loadedGraphNode(node) {
this.initAutocompleteForNode(node);
},
snippetsCache: null, snippetsCache: null,
async loadSnippets() { async loadSnippets() {
@@ -96,15 +152,33 @@ app.registerExtension({
if (widget.inputEl) { if (widget.inputEl) {
setupListener(widget.inputEl); setupListener(widget.inputEl);
} else { } else {
// If not, it might be created later. We can hook `onInput` or similar, // If checking widget.draw:
// but `inputEl` is usually created when the node is drawn or added. // Standard LiteGraph widgets might not have an instance-level draw method, relying on the class prototype or default.
// Let's poll or hook draw? Hooking draw is safer to catch it. // If we blindly assign widget.draw = ..., we obscure the default behavior (disappearing widget).
widget.draw = function (ctx, node, widgetWidth, y, widgetHeight) { // So we only wrap if originalDraw exists (or is on the prototype? widget points to instance).
const result = originalDraw?.apply(this, arguments);
if (this.inputEl) { // Safer approach: check if draw is defined on the instance or try to get it from prototype?
setupListener(this.inputEl); // Actually, if we just want to catch inputEl creation, maybe polling is safer for standard widgets
// if we suspect they don't have a direct 'draw' method we can wrap nicely.
if (originalDraw) {
widget.draw = function (ctx, node, widgetWidth, y, widgetHeight) {
const result = originalDraw.apply(this, arguments);
if (this.inputEl) {
setupListener(this.inputEl);
}
return result;
} }
return result; } else {
// Fallback: poll for inputEl a few times
const poll = setInterval(() => {
if (widget.inputEl) {
setupListener(widget.inputEl);
clearInterval(poll);
}
}, 200);
// Stop polling after 5 seconds to save resources
setTimeout(() => clearInterval(poll), 5000);
} }
} }
}, },
@@ -119,7 +193,7 @@ app.registerExtension({
triggerChar: "#", triggerChar: "#",
handleInput(e, input, widget) { handleInput(e, input, widget) {
const trigger = app.ui.settings.getSettingValue("SATA.PromptAutocomplete.Trigger", "#"); const trigger = getSettingValue("SATA_UtilityNode.PromptAutocomplete.Trigger", "#");
this.triggerChar = trigger; this.triggerChar = trigger;
const val = input.value; const val = input.value;
@@ -274,14 +348,22 @@ app.registerExtension({
updateCategories(query) { updateCategories(query) {
const files = Object.keys(this.snippetsCache || {}); const files = Object.keys(this.snippetsCache || {});
// Filter by query const matches = [];
// We display stripped names (no extension) files.forEach(f => {
this.filteredItems = files.map(f => ({ const display = f.replace(/\.(csv|json)$/, "");
type: "category", const res = fuzzyMatch(display, query);
value: f, if (res.match) {
display: f.replace(/\.(csv|json)$/, "") matches.push({
})).filter(item => item.display.toLowerCase().includes(query.toLowerCase())); type: "category",
value: f,
display: display,
score: res.score
});
}
});
matches.sort((a, b) => a.score - b.score);
this.filteredItems = matches;
this.selectedIndex = 0; this.selectedIndex = 0;
this.renderPopup(); this.renderPopup();
}, },
@@ -290,13 +372,20 @@ app.registerExtension({
if (!this.currentCategory) return; if (!this.currentCategory) return;
const items = this.snippetsCache[this.currentCategory] || []; const items = this.snippetsCache[this.currentCategory] || [];
// Filter const matches = [];
let matches = items.filter(i => i.toLowerCase().includes(query.toLowerCase())); items.forEach(i => {
const res = fuzzyMatch(i, query);
if (res.match) {
matches.push({ value: i, score: res.score });
}
});
matches.sort((a, b) => a.score - b.score);
const sortedItems = matches.map(m => m.value).slice(0, 100);
// Add "Random" option at top // Add "Random" option at top
this.filteredItems = [ this.filteredItems = [
{ type: "random", value: "RANDOM", display: "🎲 Random" }, { type: "random", value: "RANDOM", display: "🎲 Random" },
...matches.map(i => ({ type: "item", value: i, display: i })) ...sortedItems.map(i => ({ type: "item", value: i, display: i }))
]; ];
this.selectedIndex = 0; this.selectedIndex = 0;
@@ -385,6 +474,7 @@ app.registerExtension({
}, },
selectItem(input) { selectItem(input) {
if (!input) return;
const item = this.filteredItems[this.selectedIndex]; const item = this.filteredItems[this.selectedIndex];
if (!item) return; if (!item) return;
+191 -91
View File
@@ -1,101 +1,201 @@
// prompt_machine_frontend.js // prompt_machine_frontend.js
import { app } from "../../../scripts/app.js"; import { app } from "../../../scripts/app.js";
const PROMPT_MACHINE_TOOLTIPS = {
subject: "Core hero or subject and physical details (age, hair, eyes, outfit, accessories).\nExample: 'A young female astronomer with messy raven hair and grey eyes, wearing a fitted leather jacket.'",
pose_and_expression: "Body posture, facial expression, action, and primary gestures.\nExample: 'Leaning over an ancient star map with a subtle smile, holding a brass compass in her right hand.'",
secondary_subject: "Supporting characters, companion animals, or secondary focal objects.\nExample: 'A sleek white robotic cat sitting on the desk beside her, watching the holograms.'",
background_and_environment: "Immediate physical surroundings, wider environment, and background setting.\nExample: 'Inside a high-tech glass dome observatory with scattered parchment maps, overlooking a star-filled galaxy.'",
lighting: "Lighting sources, direction, intensity, reflections, shadows, and mood.\nExample: 'Soft cyan holographic glow illuminating her face, warm golden sunlight streaming through the side window.'",
style: "Visual medium, photographic style, aesthetic textures, and dominant color palette.\nExample: 'Cinematic 35mm film photography, fine grain, shallow depth of field. Deep navy blues and warm amber tones.'",
creative_hook: "One surreal, impossible, or striking visual concept.\nExample: 'Constellations floating like tiny glowing fireflies in the air inside the room.'",
technical: "Camera equipment, lens specs, shot type, and framing (Optional).\nExample: 'Shot on 85mm f/1.4 lens, eye-level medium close-up, sharp focus on subject, soft background bokeh.'",
json_output: "Output structured JSON mapping instead of a formatted text paragraph."
};
function setupPromptMachine(node) {
if (!node.widgets) return;
node.widgets.forEach(w => {
if (PROMPT_MACHINE_TOOLTIPS[w.name]) {
w.tooltip = PROMPT_MACHINE_TOOLTIPS[w.name];
if (w.options) {
w.options.tooltip = PROMPT_MACHINE_TOOLTIPS[w.name];
}
const updateInputEl = () => {
if (w.inputEl) {
w.inputEl.title = PROMPT_MACHINE_TOOLTIPS[w.name];
const firstLine = PROMPT_MACHINE_TOOLTIPS[w.name].split('\n')[0];
if (!w.inputEl.placeholder) {
w.inputEl.placeholder = firstLine;
}
w.inputEl.readOnly = false;
w.inputEl.disabled = false;
}
};
updateInputEl();
// Retry if inputEl creates asynchronously
if (!w.inputEl) {
setTimeout(updateInputEl, 100);
setTimeout(updateInputEl, 500);
}
}
});
}
function setupPromptStyleMachine(node) {
if (node._sata_style_initialized) return;
const csvWidget = node.widgets?.find(w => w.name === "csv_file");
let nameWidget = node.widgets?.find(w => w.name === "name");
if (!csvWidget || !nameWidget) {
return;
}
node._sata_style_initialized = true;
const initialSavedName = nameWidget.value || "None";
// Convert widget to combo dropdown gracefully if it's not already
if (nameWidget.type !== "combo") {
const index = node.widgets.indexOf(nameWidget);
if (index !== -1) {
node.widgets.splice(index, 1);
nameWidget = node.addWidget("combo", "name", initialSavedName, () => { }, {
values: [initialSavedName !== "None" ? initialSavedName : "None"]
});
const newIdx = node.widgets.indexOf(nameWidget);
if (newIdx !== -1 && newIdx !== index) {
node.widgets.splice(newIdx, 1);
node.widgets.splice(index, 0, nameWidget);
}
}
}
if (!nameWidget.options) {
nameWidget.options = { values: [initialSavedName] };
}
let namesConfig = [];
// Fetch names from backend for a given CSV
async function loadNames(csvValue, preferredName) {
if (!csvValue || csvValue === "None") return;
try {
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
const data = await resp.json();
namesConfig = (data.names && data.names.length > 0) ? data.names : ["None"];
refreshNames(preferredName);
} catch (err) {
console.error("[PromptMachineFrontend] Failed to load names:", err);
}
}
// Refresh dropdown with available names while preserving target selection
function refreshNames(preferredName) {
const available = namesConfig.length > 0 ? namesConfig : ["None"];
nameWidget.options.values = available;
const targetVal = preferredName !== undefined ? preferredName : nameWidget.value;
if (available.includes(targetVal)) {
nameWidget.value = targetVal;
} else {
nameWidget.value = available[0] || "None";
}
// Force update previews for current name
updatePreviews(nameWidget.value);
if (app.graph) {
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
if (app.graph.change) app.graph.change();
}
}
// Hook CSV change (user triggered)
const oldCsvCallback = csvWidget.callback;
csvWidget.callback = function (value) {
if (oldCsvCallback) oldCsvCallback.apply(this, arguments);
loadNames(value); // refresh without preferred name (defaults to first available)
};
// --- Preview Logic ---
let previewEl = node.widgets?.find(w => w.name === "preview_text")?.element;
if (!previewEl) {
previewEl = document.createElement("div");
Object.assign(previewEl.style, {
marginTop: "10px",
padding: "8px",
background: "rgba(0,0,0,0.4)",
borderRadius: "4px",
fontSize: "14px",
lineHeight: "1.5",
color: "#ddd",
wordBreak: "break-word",
maxHeight: "400px",
overflowY: "auto"
});
node.addDOMWidget("preview_text", "html", previewEl, {
getValue: () => previewEl.innerHTML,
setValue: (v) => { previewEl.innerHTML = v; },
serialize: false
});
}
async function updatePreviews(name) {
const csv = csvWidget.value;
if (!csv || !name || name === "None") {
previewEl.innerHTML = "<i>No preview available</i>";
return;
}
try {
const resp = await fetch(`/sata/prompt_machine/get?csv=${encodeURIComponent(csv)}&name=${encodeURIComponent(name)}`);
const data = await resp.json();
let html = "";
if (data.positive) html += `${data.positive}`;
previewEl.innerHTML = html || "<i>No preview available</i>";
if (node.computeSize && node.setSize) {
node.setSize(node.computeSize());
}
if (app.graph) {
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
if (app.graph.change) 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.apply(this, arguments);
updatePreviews(value);
};
// Initial sync preserving saved value
loadNames(csvWidget.value, initialSavedName);
}
app.registerExtension({ app.registerExtension({
name: "SATA_UtilityNode.PromptMachineFrontend", name: "SATA_UtilityNode.PromptMachineFrontend",
async nodeCreated(node) { async nodeCreated(node) {
if (node.comfyClass !== "Prompt_Machine") return; if (node.comfyClass === "Prompt_Machine") {
setupPromptMachine(node);
// Widgets } else if (node.comfyClass === "Prompt_Style_Machine") {
const csvWidget = node.widgets.find(w => w.name === "csv_file"); setupPromptStyleMachine(node);
const oldNameWidget = node.widgets.find(w => w.name === "name");
if (!csvWidget || !oldNameWidget) {
console.warn("[PromptMachineFrontend] Missing widgets, check backend definition.");
return;
} }
},
// Remove the old text input for 'name' async loadedGraphNode(node) {
const index = node.widgets.indexOf(oldNameWidget); if (node.comfyClass === "Prompt_Machine") {
node.widgets.splice(index, 1); setupPromptMachine(node);
} else if (node.comfyClass === "Prompt_Style_Machine") {
// Add proper dropdown setupPromptStyleMachine(node);
const nameWidget = node.addWidget("combo", "name", "None", () => { }, {
values: ["None"]
});
// Store available names
let namesConfig = [];
// Fetch names from backend for a given CSV
async function loadNames(csvValue) {
if (!csvValue) return;
try {
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
const data = await resp.json();
namesConfig = data.names || ["None"];
refreshNames();
} catch (err) {
console.error("[PromptMachineFrontend] Failed to load names:", err);
}
} }
// Refresh dropdown with available names
function refreshNames() {
const available = namesConfig.length > 0 ? namesConfig : ["None"];
nameWidget.options.values = available;
if (!available.includes(nameWidget.value)) {
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) {
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).then(() => {
updatePreviews(nameWidget.value);
});
} }
}); });
+164 -137
View File
@@ -1,146 +1,173 @@
import { app } from "../../../scripts/app.js"; import { app } from "../../../scripts/app.js";
function setupResolutionNode(node) {
if (node._sata_res_initialized) return;
if (node.comfyClass !== "Resolution_Machine" && node.comfyClass !== "Latent_Machine") return;
// Find widgets
const modelWidget = node.widgets?.find(w => w.name === "model");
const dimensionWidget = node.widgets?.find(w => w.name === "dimension");
const resolutionWidget = node.widgets?.find(w => w.name === "resolution");
const widthWidget = node.widgets?.find(w => w.name === "width") || node.widgets?.find(w => w.name === "custom_width");
const heightWidget = node.widgets?.find(w => w.name === "height") || node.widgets?.find(w => w.name === "custom_height");
if (!modelWidget || !dimensionWidget || !resolutionWidget || !widthWidget || !heightWidget) {
return;
}
node._sata_res_initialized = true;
// Store resolution config
let resolutionsConfig = {};
// Fetch resolution config from backend
async function loadConfig() {
try {
const resp = await fetch("/SATA_UtilityNode/resolutions_config");
resolutionsConfig = await resp.json();
refreshResolutions(false);
} catch (err) {
console.error("[Resolution_Machine] Failed to load config:", err);
}
}
// --- Utility: refresh resolution dropdown when model/dimension changes ---
function refreshResolutions(resetSelection = true) {
const selectedModel = modelWidget.value;
const selectedDimension = dimensionWidget.value;
let available = [];
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
const buckets = resolutionsConfig.models[selectedModel];
buckets.forEach(bucket => {
if (resolutionsConfig.resolutions && resolutionsConfig.resolutions[bucket] && resolutionsConfig.resolutions[bucket][selectedDimension]) {
available.push(...Object.keys(resolutionsConfig.resolutions[bucket][selectedDimension]));
}
});
}
// Always include "Custom" option
if (!available.includes("Custom")) {
available.push("Custom");
}
resolutionWidget.options.values = available;
if (resetSelection && !available.includes(resolutionWidget.value)) {
resolutionWidget.value = available[0] || "Custom";
}
checkCustomMode();
}
function checkCustomMode() {
const isCustom = resolutionWidget.value === "Custom";
if (isCustom) {
widthWidget.type = "INT";
heightWidget.type = "INT";
delete widthWidget.computeSize;
delete heightWidget.computeSize;
widthWidget.disabled = false;
heightWidget.disabled = false;
} else {
widthWidget.type = "hidden";
heightWidget.type = "hidden";
widthWidget.computeSize = () => [0, -4];
heightWidget.computeSize = () => [0, -4];
applyResolutionPreset();
}
if (node.computeSize && node.setSize) {
node.setSize(node.computeSize());
}
if (app.graph) {
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
if (app.graph.change) app.graph.change();
}
}
// --- Utility: apply resolution preset to width/height ---
function applyResolutionPreset() {
const selectedModel = modelWidget.value;
const selectedDimension = dimensionWidget.value;
const selectedResolution = resolutionWidget.value;
if (selectedResolution === "Custom") {
return;
}
let resolutionData = null;
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
const buckets = resolutionsConfig.models[selectedModel];
for (const bucket of buckets) {
if (resolutionsConfig.resolutions &&
resolutionsConfig.resolutions[bucket] &&
resolutionsConfig.resolutions[bucket][selectedDimension] &&
resolutionsConfig.resolutions[bucket][selectedDimension][selectedResolution]) {
resolutionData = resolutionsConfig.resolutions[bucket][selectedDimension][selectedResolution];
break;
}
}
}
if (resolutionData) {
widthWidget.value = resolutionData.width;
heightWidget.value = resolutionData.height;
}
}
// --- Hook model change ---
const oldModelCallback = modelWidget.callback;
modelWidget.callback = function (value) {
if (oldModelCallback) oldModelCallback.apply(this, arguments);
refreshResolutions(true);
};
// --- Hook dimension change ---
const oldDimensionCallback = dimensionWidget.callback;
dimensionWidget.callback = function (value) {
if (oldDimensionCallback) oldDimensionCallback.apply(this, arguments);
refreshResolutions(true);
};
// --- Hook resolution change ---
const oldResolutionCallback = resolutionWidget.callback;
resolutionWidget.callback = function (value) {
if (oldResolutionCallback) oldResolutionCallback.apply(this, arguments);
checkCustomMode();
};
// --- Add Custom Button (once) ---
if (!node.widgets.some(w => w.name === "Custom")) {
node.addWidget("button", "Custom", null, () => {
widthWidget.disabled = false;
heightWidget.disabled = false;
if (resolutionWidget.options && resolutionWidget.options.values) {
if (!resolutionWidget.options.values.includes("Custom")) {
resolutionWidget.options.values.push("Custom");
}
}
resolutionWidget.value = "Custom";
checkCustomMode();
});
}
loadConfig();
}
app.registerExtension({ app.registerExtension({
name: "SATA_UtilityNode.Resolution_Machine", name: "SATA_UtilityNode.Resolution_Machine",
async nodeCreated(node) { async nodeCreated(node) {
if (node.comfyClass !== "Resolution_Machine") return; setupResolutionNode(node);
},
// Find widgets async loadedGraphNode(node) {
const modelWidget = node.widgets.find(w => w.name === "model"); setupResolutionNode(node);
const resolutionWidget = node.widgets.find(w => w.name === "resolution");
const widthWidget = node.widgets.find(w => w.name === "custom_width");
const heightWidget = node.widgets.find(w => w.name === "custom_height");
if (!modelWidget || !resolutionWidget || !widthWidget || !heightWidget) {
console.warn("[Resolution_Machine] Missing widgets, check backend definition.");
return;
}
// Store resolution config
let resolutionsConfig = {};
// Fetch resolution config from backend
async function loadConfig() {
try {
const resp = await fetch("/SATA_UtilityNode/resolutions_config");
resolutionsConfig = await resp.json();
} catch (err) {
console.error("[Resolution_Machine] Failed to load config:", err);
}
}
await loadConfig();
// --- Utility: refresh resolution dropdown when model changes ---
function refreshResolutions(selectedModel) {
const available = resolutionsConfig[selectedModel]
? Object.keys(resolutionsConfig[selectedModel])
: ["Custom (manual)"];
resolutionWidget.options.values = available;
// Pick safe default if current value is not valid
if (!available.includes(resolutionWidget.value)) {
resolutionWidget.value = available[0];
}
app.graph.change();
applyResolution(); // auto-apply width/height on refresh
}
// --- Utility: apply resolution to width/height ---
function applyResolution() {
const selectedModel = modelWidget.value;
const selectedResolution = resolutionWidget.value;
if (!resolutionsConfig[selectedModel]) return;
const resolutionData = resolutionsConfig[selectedModel][selectedResolution];
if (selectedResolution !== "Custom (manual)" && resolutionData) {
// Auto-set width/height
widthWidget.value = resolutionData.width;
heightWidget.value = resolutionData.height;
// Lock fields
widthWidget.disabled = true;
heightWidget.disabled = true;
} else {
// Allow manual editing
widthWidget.disabled = false;
heightWidget.disabled = false;
}
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) {
if (oldModelCallback) oldModelCallback(value);
refreshResolutions(value);
};
// --- Hook resolution change ---
const oldResolutionCallback = resolutionWidget.callback;
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();
} }
}); });
+135 -31
View File
@@ -1,40 +1,144 @@
import { app } from "../../../scripts/app.js";
const SETTING_ID = "SATA_UtilityNode.EnableTouchpadPanZoom";
app.registerExtension({
name: "SATA_UtilityNode.TouchpadPanZoom",
settings: [
{
id: SETTING_ID,
name: "Enable Touchpad Pan/Zoom (SATA UtilityNode)",
type: "boolean",
defaultValue: true,
onChange: (value) => {
const btn = document.getElementById("sata-touchpad-toggle-btn");
if (btn) {
btn.textContent = value ? "Touchpad: ON" : "Touchpad: OFF";
btn.style.opacity = value ? "1.0" : "0.5";
}
}
}
],
async setup() {
// Create a toggle button in the ComfyUI menu (Legacy V1)
const menu = document.querySelector(".comfy-menu") || document.querySelector(".comfyui-menu");
if (menu) {
const toggleBtn = document.createElement("button");
toggleBtn.id = "sata-touchpad-toggle-btn";
// Function to get current value securely
const getVal = () => {
if (app.extensionManager && app.extensionManager.setting) {
const val = app.extensionManager.setting.get(SETTING_ID);
return val !== undefined ? val : true;
} else if (app.ui && app.ui.settings) {
return app.ui.settings.getSettingValue(SETTING_ID, true);
}
return true;
};
// Function to set value
const setVal = (newVal) => {
if (app.extensionManager && app.extensionManager.setting) {
app.extensionManager.setting.set(SETTING_ID, newVal);
} else if (app.ui && app.ui.settings) {
app.ui.settings.setSettingValue(SETTING_ID, newVal);
}
// Update button visuals
toggleBtn.textContent = newVal ? "Touchpad: ON" : "Touchpad: OFF";
toggleBtn.style.opacity = newVal ? "1.0" : "0.5";
};
const initialVal = getVal();
toggleBtn.textContent = initialVal ? "Touchpad: ON" : "Touchpad: OFF";
toggleBtn.style.opacity = initialVal ? "1.0" : "0.5";
// Basic styling to match comfy UI buttons if classes aren't enough
toggleBtn.className = "comfy-settings-btn";
toggleBtn.onclick = () => {
const currentVal = getVal();
setVal(!currentVal);
};
// Try to place it nicely in the menu
const settingsBtn = document.getElementById("comfy-settings-button");
if (settingsBtn && settingsBtn.parentNode === menu) {
menu.insertBefore(toggleBtn, settingsBtn);
} else {
menu.appendChild(toggleBtn);
}
}
}
});
/** /**
* Enhanced smooth scrolling and zooming for touchpad gestures * Enhanced smooth scrolling and zooming for touchpad gestures
* Overrides LGraphCanvas.prototype.processMouseWheel * Overrides LGraphCanvas.prototype.processMouseWheel if available
*/ */
LGraphCanvas.prototype.processMouseWheel = function (/** @type {WheelEvent} */ event) { if (typeof LGraphCanvas !== "undefined" && LGraphCanvas.prototype) {
if (!this.graph || !this.allow_dragcanvas) return; const originalProcessMouseWheel = LGraphCanvas.prototype.processMouseWheel;
const { clientX: x, clientY: y, deltaX, deltaY, ctrlKey, metaKey } = event; LGraphCanvas.prototype.processMouseWheel = function (/** @type {WheelEvent} */ event) {
let isEnabled = true;
if (app.extensionManager && app.extensionManager.setting) {
const val = app.extensionManager.setting.get(SETTING_ID);
if (val !== undefined) isEnabled = val;
} else if (app.ui && app.ui.settings) {
isEnabled = app.ui.settings.getSettingValue(SETTING_ID, true);
}
// Check if the event occurred inside the canvas viewport if (isEnabled === false) {
if (this.viewport) { if (originalProcessMouseWheel) {
const [vx, vy, vw, vh] = this.viewport; return originalProcessMouseWheel.apply(this, arguments);
const insideX = x >= vx && x < vx + vw; }
const insideY = y >= vy && y < vy + vh; return false;
if (!(insideX && insideY)) return; }
}
const currentScale = this.ds.scale; if (!this.graph || !this.allow_dragcanvas || !this.ds) return;
// Configurable sensitivity const rect = this.canvas ? this.canvas.getBoundingClientRect() : null;
const ZOOM_SENSITIVITY = this.zoomSensitivity || 0.005; if (!rect) return;
const PAN_SENSITIVITY = this.panSensitivity || 1.0; const x = event.clientX - rect.left;
const MIN_SCALE = 0.1; const y = event.clientY - rect.top;
const MAX_SCALE = 4.0; const { deltaX, deltaY, ctrlKey, metaKey } = event;
if (ctrlKey || metaKey) { // Check if the event occurred inside the canvas viewport
// Zoom mode if (this.viewport) {
const zoomDirection = Math.sign(deltaY); const [vx, vy, vw, vh] = this.viewport;
const zoomAmount = zoomDirection * Math.abs(deltaY) * ZOOM_SENSITIVITY; const insideX = x >= vx && x < vx + vw;
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, currentScale - zoomAmount)); const insideY = y >= vy && y < vy + vh;
this.ds.changeScale(newScale, [x, y]); if (!(insideX && insideY)) return;
} else { }
// Pan mode
this.ds.mouseDrag(-deltaX * PAN_SENSITIVITY, -deltaY * PAN_SENSITIVITY);
}
this.graph.change(); const currentScale = this.ds.scale;
event.preventDefault();
return false; // Configurable sensitivity
}; const ZOOM_SENSITIVITY = this.zoomSensitivity || 0.005;
const PAN_SENSITIVITY = this.panSensitivity || 1.0;
const MIN_SCALE = 0.1;
const MAX_SCALE = 4.0;
if (ctrlKey || metaKey) {
// Zoom mode
const zoomDirection = Math.sign(deltaY);
const zoomAmount = zoomDirection * Math.abs(deltaY) * ZOOM_SENSITIVITY;
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, currentScale - zoomAmount));
if (this.ds.changeScale) {
this.ds.changeScale(newScale, [x, y]);
}
} else {
// Pan mode
if (this.ds.mouseDrag) {
this.ds.mouseDrag(-deltaX * PAN_SENSITIVITY, -deltaY * PAN_SENSITIVITY);
}
}
if (this.graph.change) {
this.graph.change();
}
event.preventDefault();
return false;
};
}
+163 -25
View File
@@ -1,78 +1,165 @@
import json
import os
import torch import torch
import torch.fft import torch.fft
import random import random
import comfy.model_management
NODE_NAME = "Latent_Machine"
# Path to JSON config (root of SATA_UtilityNode folder)
CONFIG_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "asset", "resolutions.json"
)
def load_config():
if not os.path.exists(CONFIG_PATH):
raise FileNotFoundError(f"[{NODE_NAME}] resolutions.json not found at {CONFIG_PATH}")
with open(CONFIG_PATH, "r", encoding="utf-8-sig") as f:
return json.load(f)
class Latent_Machine: class Latent_Machine:
""" """
Creates an empty latent initialized with Power-Law (1/f) noise, Perlin-like noise, or Plasma noise. 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. Supports 4-channel (SD1.5/SDXL) and 16-channel (Flux/SD3) latents.
Supports recommended model resolutions out of the box.
""" """
def __init__(self): def __init__(self):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") pass
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(cls):
config = load_config()
# all models
models = list(config["models"].keys())
default_model = models[0] if models else "Unknown"
# Collect ALL possible resolutions to pass validation
all_resolutions = set(["Custom"])
if "resolutions" in config:
for bucket in config["resolutions"].values():
for dim in bucket.values():
all_resolutions.update(dim.keys())
resolutions_list = sorted(list(all_resolutions))
return { return {
"required": { "required": {
"model": (models, {"default": default_model}),
"dimension": (["Square", "Portrait", "Landscape"],),
"resolution": (resolutions_list,),
"width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}), "width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"height": ("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}), "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": ([ "noise_type": ([
"Gaussian (White): Sharp Architecture, Text, intricate mechanics", "Gaussian (White): Sharp Architecture, Text, intricate mechanics",
"Pink (1/f): Photorealism, Portraits, Nature", "Pink (1/f): Photorealism, Portraits, Nature",
"Brown (1/f²): Anime, Digital Art, Bokeh/Backgrounds", "Brown (1/f²): Anime, Digital Art, Bokeh/Backgrounds",
"Blue (High-Frequency): High-Detail, Textures, low-step/distilled models",
"Perlin : Fantasy landscapes, Fluids, Hair/Fabric", "Perlin : Fantasy landscapes, Fluids, Hair/Fabric",
"Plasma : Sci-Fi, Abstract, Alien terrain" "Plasma : Sci-Fi, Abstract, Alien terrain"
],), ],),
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}), "intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"high_contrast": ("BOOLEAN", {"default": False}),
} }
} }
RETURN_TYPES = ("LATENT",) RETURN_TYPES = ("LATENT", "IMAGE")
RETURN_NAMES = ("LATENT", "IMAGE")
FUNCTION = "generate_noise" FUNCTION = "generate_noise"
CATEGORY = "SATA_UtilityNode" CATEGORY = "SATA_UtilityNode"
def generate_noise(self, width, height, batch_size, model_type, noise_type, intensity, seed): def get_resolution_dimensions(self, model, dimension, resolution, width, height):
# Set seed for reproducibility try:
torch.manual_seed(seed) config = load_config()
random.seed(seed) except Exception:
return (width, height)
if model not in config.get("models", {}):
return (width, height)
if resolution == "Custom":
return (width, height)
buckets = config["models"][model]
# Determine channels based on model type resolution_data = None
if "16ch" in model_type: for bucket in buckets:
if bucket in config.get("resolutions", {}):
if dimension in config["resolutions"][bucket]:
if resolution in config["resolutions"][bucket][dimension]:
resolution_data = config["resolutions"][bucket][dimension][resolution]
break
if not resolution_data:
return (width, height)
return (resolution_data["width"], resolution_data["height"])
def generate_noise(self, model="SD 1.5", dimension="Square", resolution="Custom", width=512, height=512, batch_size=1, noise_type="Gaussian (White): Sharp Architecture, Text, intricate mechanics", intensity=1.0, seed=0, high_contrast=False):
# Check if this is an old node layout (shifted arguments due to ComfyUI's positional widget serialization)
if isinstance(model, int):
actual_width = model
actual_height = dimension
actual_batch_size = resolution
actual_model = width # The 4th argument 'width' holds the old 'model_type' (str)
actual_noise_type = height
actual_intensity = batch_size
actual_seed = noise_type
actual_high_contrast = False
else:
actual_model = model
actual_width, actual_height = self.get_resolution_dimensions(model, dimension, resolution, width, height)
actual_batch_size = batch_size
actual_noise_type = noise_type
actual_intensity = intensity
actual_seed = seed
actual_high_contrast = high_contrast
self.device = comfy.model_management.get_torch_device()
# Set seed for reproducibility
torch.manual_seed(actual_seed)
random.seed(actual_seed)
# Determine channels based on model
model_lower = actual_model.lower()
if any(k in model_lower for k in ["flux", "qwen", "sd3", "lumina", "wan", "auraflow", "z-image", "mochi", "ltx", "hunyuanvideo"]):
c = 16 c = 16
else: else:
c = 4 c = 4
# Latent dimensions (compressed by 8) # Latent dimensions (compressed by 8)
h = height // 8 h = actual_height // 8
w = width // 8 w = actual_width // 8
# Generate base noise # Generate base noise
if "Gaussian" in noise_type: if "Gaussian" in actual_noise_type:
noise = torch.randn((batch_size, c, h, w), device=self.device) noise = torch.randn((actual_batch_size, c, h, w), device=self.device)
elif "Perlin" in noise_type: elif "Perlin" in actual_noise_type:
# Multi-Octave Value Noise Approximation # Multi-Octave Value Noise Approximation
noise = self.generate_perlin_approx(batch_size, c, h, w) noise = self.generate_perlin_approx(actual_batch_size, c, h, w)
elif "Blue" in actual_noise_type:
# FFT-based Blue Noise (High-Frequency)
noise = self.generate_blue_noise(actual_batch_size, c, h, w)
else: else:
# FFT-based Power Law Noise (Pink, Brown, Plasma) # FFT-based Power Law Noise (Pink, Brown, Plasma)
if "Pink" in noise_type: if "Pink" in actual_noise_type:
alpha = 1.0 alpha = 1.0
elif "Brown" in noise_type: elif "Brown" in actual_noise_type:
alpha = 2.0 alpha = 2.0
elif "Plasma" in noise_type: elif "Plasma" in actual_noise_type:
alpha = 3.0 # Very smooth alpha = 3.0 # Very smooth
else: else:
alpha = 0.0 # Fallback to white alpha = 0.0 # Fallback to white
noise = self.generate_power_law_noise(batch_size, c, h, w, alpha) noise = self.generate_power_law_noise(actual_batch_size, c, h, w, alpha)
# Normalize standard deviation to match expected latent variance (approx 1.0 for standard Gaussian) # Normalize standard deviation to match expected latent variance (approx 1.0 for standard Gaussian)
# This ensures intensity works consistently across different noise types # This ensures intensity works consistently across different noise types
@@ -80,10 +167,33 @@ class Latent_Machine:
if current_std > 1e-6: if current_std > 1e-6:
noise = noise / current_std noise = noise / current_std
# Apply offset noise for high contrast
if actual_high_contrast:
noise = noise + 0.1
# Apply intensity # Apply intensity
noise = noise * intensity noise = noise * actual_intensity
return ({"samples": noise},) # Prepare Image output for visualization/masking
# Slice the first 3 channels and normalize to [0, 1]
img_tensor = noise[:, :3, :, :]
if img_tensor.shape[1] < 3:
# If for some weird reason it's less than 3 channels, pad it
padding = torch.zeros((actual_batch_size, 3 - img_tensor.shape[1], h, w), device=self.device)
img_tensor = torch.cat([img_tensor, padding], dim=1)
# Normalize min-max
img_min = img_tensor.min()
img_max = img_tensor.max()
if img_max > img_min:
img_tensor = (img_tensor - img_min) / (img_max - img_min)
else:
img_tensor = torch.zeros_like(img_tensor)
# Move to (B, H, W, C) for ComfyUI Image format
img_tensor = img_tensor.movedim(1, -1)
return ({"samples": noise}, img_tensor)
def generate_power_law_noise(self, batch_size, c, h, w, alpha): def generate_power_law_noise(self, batch_size, c, h, w, alpha):
# Generate White Noise (Standard Gaussian) # Generate White Noise (Standard Gaussian)
@@ -146,3 +256,31 @@ class Latent_Machine:
noise += torch.randn((batch_size, c, h, w), device=self.device) * 0.05 noise += torch.randn((batch_size, c, h, w), device=self.device) * 0.05
return noise return noise
def generate_blue_noise(self, batch_size, c, h, w, beta=1.5):
# 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)
frequency_magnitude = torch.sqrt(dy**2 + dx**2)
# Apply High-Pass Scaling: Amplitude ~ f^(beta/2)
scale = frequency_magnitude ** (beta / 2.0)
# Zero out DC component
scale[0, 0] = 0
fft_structured = fft_noise * scale
# Convert back to Spatial Domain (Inverse FFT)
blue_noise = torch.fft.ifft2(fft_structured).real
return blue_noise
+20
View File
@@ -0,0 +1,20 @@
from nodes import PreviewImage
class Preview_Machine(PreviewImage):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"hide_preview": ("BOOLEAN", {"default": True, "tooltip": "Hide image preview until mouse hovers over the node."}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
CATEGORY = "SATA_UtilityNode"
FUNCTION = "save_images"
def save_images(self, images, hide_preview=True, prompt=None, extra_pnginfo=None):
return super().save_images(images, filename_prefix="Preview_Machine", prompt=prompt, extra_pnginfo=extra_pnginfo)
+1
View File
@@ -10,6 +10,7 @@ PROMPT_DIR = os.path.join(os.path.dirname(__file__), "..", "prompt")
def list_prompt_files(): def list_prompt_files():
try: try:
if not os.path.exists(PROMPT_DIR): if not os.path.exists(PROMPT_DIR):
os.makedirs(PROMPT_DIR, exist_ok=True)
return [] return []
return sorted([f for f in os.listdir(PROMPT_DIR) if f.endswith(".csv") or f.endswith(".json")]) return sorted([f for f in os.listdir(PROMPT_DIR) if f.endswith(".csv") or f.endswith(".json")])
except Exception as e: except Exception as e:
+157 -27
View File
@@ -18,7 +18,8 @@ def read_names_from_csv(filename):
"""Return list of names (stripped) from CSV's 'name' column.""" """Return list of names (stripped) from CSV's 'name' column."""
if not filename: if not filename:
return [] return []
path = os.path.join(CSV_DIR, filename) safe_filename = os.path.basename(str(filename).strip())
path = os.path.join(CSV_DIR, safe_filename)
if not os.path.exists(path): if not os.path.exists(path):
print(f"[PromptMachine] CSV not found: {path}") print(f"[PromptMachine] CSV not found: {path}")
return [] return []
@@ -26,11 +27,11 @@ def read_names_from_csv(filename):
with open(path, "r", encoding="utf-8-sig") as f: with open(path, "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
if not reader.fieldnames: if not reader.fieldnames:
print(f"[PromptMachine] CSV has no header: {filename}") print(f"[PromptMachine] CSV has no header: {safe_filename}")
return [] return []
field_map = {fn.strip().lower(): fn for fn in reader.fieldnames} field_map = {fn.strip().lower(): fn for fn in reader.fieldnames}
if "name" not in field_map: if "name" not in field_map:
print(f"[PromptMachine] CSV missing 'name' column: {filename}") print(f"[PromptMachine] CSV missing 'name' column: {safe_filename}")
return [] return []
name_field = field_map["name"] name_field = field_map["name"]
names = [] names = []
@@ -38,20 +39,23 @@ def read_names_from_csv(filename):
raw = row.get(name_field) raw = row.get(name_field)
if raw is None: if raw is None:
continue continue
val = raw.strip() val = str(raw).strip()
if val: if val:
names.append(val) names.append(val)
return names return names
except Exception as e: except Exception as e:
print(f"[PromptMachine] read_names_from_csv error for {filename}: {e}") print(f"[PromptMachine] read_names_from_csv error for {safe_filename}: {e}")
return [] return []
def read_prompt_row(csv_file, name): def read_prompt_row(csv_file, name):
"""Return (positive, negative, note) for a given csv_file and name. """Return (positive, negative, note) for a given csv_file and name.
Matching is whitespace-trimmed and exact on content (case-sensitive by default).""" Matching is whitespace-trimmed and exact on content (case-sensitive by default)."""
if not csv_file or csv_file == "None" or not name or name == "None": if not csv_file or not isinstance(csv_file, str) or csv_file == "None":
return ("", "", "") return ("", "", "")
path = os.path.join(CSV_DIR, csv_file) if not name or not isinstance(name, str) or name == "None":
return ("", "", "")
safe_csv_file = os.path.basename(str(csv_file).strip())
path = os.path.join(CSV_DIR, safe_csv_file)
if not os.path.exists(path): if not os.path.exists(path):
print(f"[PromptMachine] CSV file not found: {path}") print(f"[PromptMachine] CSV file not found: {path}")
return ("", "", "") return ("", "", "")
@@ -59,11 +63,11 @@ def read_prompt_row(csv_file, name):
with open(path, "r", encoding="utf-8-sig") as f: with open(path, "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
if not reader.fieldnames: if not reader.fieldnames:
print(f"[PromptMachine] CSV has no header: {csv_file}") print(f"[PromptMachine] CSV has no header: {safe_csv_file}")
return ("", "", "") return ("", "", "")
field_map = {fn.strip().lower(): fn for fn in reader.fieldnames} field_map = {fn.strip().lower(): fn for fn in reader.fieldnames}
if "name" not in field_map: if "name" not in field_map:
print(f"[PromptMachine] 'name' column missing in CSV: {csv_file}") print(f"[PromptMachine] 'name' column missing in CSV: {safe_csv_file}")
return ("", "", "") return ("", "", "")
name_field = field_map["name"] name_field = field_map["name"]
pos_field = field_map.get("positive", "positive") pos_field = field_map.get("positive", "positive")
@@ -75,17 +79,17 @@ def read_prompt_row(csv_file, name):
cell = row.get(name_field) cell = row.get(name_field)
if cell is None: if cell is None:
continue continue
if cell.strip() == target: if str(cell).strip() == target:
pos = (row.get(pos_field) or "").strip() pos = str(row.get(pos_field) or "").strip()
neg = (row.get(neg_field) or "").strip() neg = str(row.get(neg_field) or "").strip()
note = (row.get(note_field) or "").strip() note = str(row.get(note_field) or "").strip()
return (pos, neg, note) return (pos, neg, note)
except Exception as e: except Exception as e:
print(f"[PromptMachine] read_prompt_row error for {csv_file}, {name}: {e}") print(f"[PromptMachine] read_prompt_row error for {safe_csv_file}, {name}: {e}")
return ("", "", "") return ("", "", "")
class Prompt_Machine: class Prompt_Style_Machine:
""" """
NOTE: name is a free STRING (not a static dropdown) to avoid 'Value not in list' validation errors. NOTE: name is a free STRING (not a static dropdown) to avoid 'Value not in list' validation errors.
The frontend helper will still provide dropdown-like selection and write the chosen name into this STRING. The frontend helper will still provide dropdown-like selection and write the chosen name into this STRING.
@@ -108,23 +112,147 @@ class Prompt_Machine:
# make 'name' a free text field (STRING) — avoids list validation problems # make 'name' a free text field (STRING) — avoids list validation problems
"name": ("STRING", {"default": default_name}), "name": ("STRING", {"default": default_name}),
}, },
"optional": { "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": ""}),
}
} }
RETURN_TYPES = ("STRING", "STRING", "STRING") RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("positive", "negative", "note") RETURN_NAMES = ("prompt",)
FUNCTION = "get_prompts" FUNCTION = "get_prompts"
CATEGORY = "SATA_UtilityNode" CATEGORY = "SATA_UtilityNode"
def get_prompts(self, csv_file, name, positive_preview=None, negative_preview=None, note_preview=None): def get_prompts(self, csv_file, name):
pos, neg, note = read_prompt_row(csv_file, name) pos, neg, note = read_prompt_row(csv_file, name)
return (pos, neg, note) return (pos,)
class Prompt_Machine:
"""
Prompt Machine (8-Category Framework):
Assembles information-dense scene descriptions in natural prose following an inside-out spatial sequence.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"subject": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Core hero or subject and physical details (age, hair, eyes, outfit, accessories).\nExample: 'A young female astronomer with messy raven hair and grey eyes, wearing a fitted leather jacket.'"
}),
"pose_and_expression": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Body posture, facial expression, action, and primary gestures.\nExample: 'Leaning over an ancient star map with a subtle smile, holding a brass compass in her right hand.'"
}),
"secondary_subject": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Supporting characters, companion animals, or secondary focal objects.\nExample: 'A sleek white robotic cat sitting on the desk beside her, watching the holograms.'"
}),
"background_and_environment": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Immediate physical surroundings, wider environment, and background setting.\nExample: 'Inside a high-tech glass dome observatory with scattered parchment maps, overlooking a star-filled galaxy.'"
}),
"lighting": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Lighting sources, direction, intensity, reflections, shadows, and mood.\nExample: 'Soft cyan holographic glow illuminating her face, warm golden sunlight streaming through the side window.'"
}),
"style": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Visual medium, photographic style, aesthetic textures, and dominant color palette.\nExample: 'Cinematic 35mm film photography, fine grain, shallow depth of field. Deep navy blues and warm amber tones.'"
}),
"creative_hook": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "One surreal, impossible, or striking visual concept.\nExample: 'Constellations floating like tiny glowing fireflies in the air inside the room.'"
}),
"technical": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": True,
"tooltip": "Camera equipment, lens specs, shot type, and framing (Optional).\nExample: 'Shot on 85mm f/1.4 lens, eye-level medium close-up, sharp focus on subject, soft background bokeh.'"
}),
"json_output": ("BOOLEAN", {
"default": False,
"tooltip": "Output structured JSON mapping instead of a formatted text paragraph."
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "get_prompt"
CATEGORY = "SATA_UtilityNode"
def get_prompt(self, subject="", pose_and_expression="", secondary_subject="",
background_and_environment="", lighting="", style="",
creative_hook="", technical="", json_output=False, **kwargs):
# Support fallback for legacy parameter names if older workflows pass them
if not subject and kwargs.get("appearance"):
subject = kwargs["appearance"]
if not secondary_subject and kwargs.get("secondary_subjects"):
secondary_subject = kwargs["secondary_subjects"]
if not background_and_environment:
env_parts = [kwargs.get("surroundings", ""), kwargs.get("environment_and_background", "")]
valid_parts = [" ".join(str(p).strip().split()) for p in env_parts if p is not None and str(p).strip()]
if valid_parts:
background_and_environment = " ".join(valid_parts)
if not lighting and kwargs.get("lighting_and_atmosphere"):
lighting = kwargs["lighting_and_atmosphere"]
if kwargs.get("materials_and_colors"):
mat = " ".join(str(kwargs["materials_and_colors"]).strip().split())
if mat:
style_str = " ".join(str(style).strip().split()) if style else ""
style = f"{style_str} {mat}".strip() if style_str else mat
raw_fields = [
("subject", subject),
("pose_and_expression", pose_and_expression),
("secondary_subject", secondary_subject),
("background_and_environment", background_and_environment),
("lighting", lighting),
("style", style),
("creative_hook", creative_hook),
("technical", technical),
]
cleaned_fields = []
for name, val in raw_fields:
if val is None:
continue
normalized = " ".join(str(val).strip().split())
if normalized:
cleaned_fields.append((name, normalized))
if json_output:
import json
data = {k: v for k, v in cleaned_fields}
return (json.dumps(data, indent=2),)
sentences = []
for name, text in cleaned_fields:
# Strip trailing separators like commas, semicolons, colons, or dashes before period
trimmed = text.rstrip(" ,;:-")
if trimmed:
if trimmed[-1] not in ".!?":
trimmed += "."
sentences.append(trimmed)
paragraph = " ".join(sentences)
return (paragraph,)
# ---------------- REST API ---------------- # ---------------- REST API ----------------
@@ -139,7 +267,8 @@ async def list_csvs(request):
@PromptServer.instance.routes.get("/sata/prompt_machine/names") @PromptServer.instance.routes.get("/sata/prompt_machine/names")
async def list_names(request): async def list_names(request):
"""Return names from a selected CSV""" """Return names from a selected CSV"""
csv_file = request.query.get("csv") raw_csv = request.query.get("csv", "")
csv_file = os.path.basename(raw_csv.strip()) if raw_csv else ""
names = read_names_from_csv(csv_file) if csv_file else [] names = read_names_from_csv(csv_file) if csv_file else []
return web.json_response({"names": names}) return web.json_response({"names": names})
@@ -150,7 +279,8 @@ async def get_prompt(request):
Return the prompt triple for immediate frontend consumption. Return the prompt triple for immediate frontend consumption.
Example: /sata/prompt_machine/get?csv=characters.csv&name=Bob Example: /sata/prompt_machine/get?csv=characters.csv&name=Bob
""" """
csv_file = request.query.get("csv") raw_csv = request.query.get("csv", "")
name = request.query.get("name") csv_file = os.path.basename(raw_csv.strip()) if raw_csv else ""
name = request.query.get("name", "")
pos, neg, note = read_prompt_row(csv_file, name) pos, neg, note = read_prompt_row(csv_file, name)
return web.json_response({"positive": pos, "negative": neg, "note": note}) return web.json_response({"positive": pos, "negative": neg, "note": note})
+57 -27
View File
@@ -13,7 +13,7 @@ CONFIG_PATH = os.path.join(
def load_config(): def load_config():
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
raise FileNotFoundError(f"[{NODE_NAME}] resolutions.json not found at {CONFIG_PATH}") raise FileNotFoundError(f"[{NODE_NAME}] resolutions.json not found at {CONFIG_PATH}")
with open(CONFIG_PATH, "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8-sig") as f:
return json.load(f) return json.load(f)
@@ -23,27 +23,29 @@ class Resolution_Machine:
config = load_config() config = load_config()
# all models # all models
models = list(config.keys()) models = list(config["models"].keys())
default_model = models[0] if models else "Unknown" default_model = models[0] if models else "Unknown"
# union of all resolutions across all models # Collect ALL possible resolutions to pass validation
all_resolutions = set() all_resolutions = set(["Custom"])
for model_res in config.values(): if "resolutions" in config:
all_resolutions.update(model_res.keys()) for bucket in config["resolutions"].values():
resolutions = sorted(list(all_resolutions)) for dim in bucket.values():
all_resolutions.update(dim.keys())
default_resolution = list(config[default_model].keys())[0]
# We can't sort mixed types effortlessly if any, but they are strings.
# Sorting helps UI consistency if fallback happens, though JS overrides it.
resolutions_list = sorted(list(all_resolutions))
return { return {
"required": { "required": {
"model": (models, {"default": default_model}), "model": (models, {"default": default_model}),
"resolution": (resolutions, {"default": default_resolution}), "dimension": (["Square", "Portrait", "Landscape"],),
"resolution": (resolutions_list,),
"custom_width": ("INT", {"default": 512, "min": 1, "max": 8192}), "custom_width": ("INT", {"default": 512, "min": 1, "max": 8192}),
"custom_height": ("INT", {"default": 512, "min": 1, "max": 8192}), "custom_height": ("INT", {"default": 512, "min": 1, "max": 8192}),
"megapixel": (["None", "0.5MP", "1.0MP", "1.5MP", "2.0MP", "4.0MP"], {"default": "None"}),
}, },
"optional": {
"dimension_preview": ("STRING", {"multiline": False, "forceInput": False, "default": ""}),
}
} }
RETURN_TYPES = ("INT", "INT") RETURN_TYPES = ("INT", "INT")
@@ -51,27 +53,55 @@ class Resolution_Machine:
FUNCTION = "get_resolution" FUNCTION = "get_resolution"
CATEGORY = "SATA_UtilityNode" CATEGORY = "SATA_UtilityNode"
def get_resolution(self, model, resolution, custom_width, custom_height, dimension_preview=None): def get_resolution(self, model, dimension, resolution, custom_width, custom_height, megapixel="None"):
config = load_config() config = load_config()
if model not in config: if model not in config["models"]:
raise ValueError(f"[{NODE_NAME}] Unknown model: {model}") raise ValueError(f"[{NODE_NAME}] Unknown model: {model}")
resolutions = config[model] if resolution == "Custom":
w, h = custom_width, custom_height
if resolution not in resolutions:
# fallback to first available resolution for safety
resolution = list(resolutions.keys())[0]
print(f"[{NODE_NAME}] Warning: invalid resolution, defaulting to {resolution}")
if resolution == "Custom (manual)":
width, height = custom_width, custom_height
else: else:
entry = resolutions[resolution] # Search for resolution string in the specific bucket/dimension
width, height = entry["width"], entry["height"] # Iterate over tiers to find the bucket that has this model?
# Actually config["models"][model] gives list of buckets.
buckets = config["models"][model]
resolution_data = None
for bucket in buckets:
# Check if this bucket has the selected dimension
if bucket in config["resolutions"]:
if dimension in config["resolutions"][bucket]:
if resolution in config["resolutions"][bucket][dimension]:
resolution_data = config["resolutions"][bucket][dimension][resolution]
break
if not resolution_data:
# Fallback: maybe the user switched model and the resolution is now invalid, or it is custom manual
# If obscure mismatch, default to custom values provided
print(f"[{NODE_NAME}] Warning: resolution '{resolution}' not found for model '{model}' dim '{dimension}', using Custom")
w, h = custom_width, custom_height
else:
w, h = resolution_data["width"], resolution_data["height"]
if megapixel != "None":
mp_map = {
"0.5MP": 500000,
"1.0MP": 1000000,
"1.5MP": 1500000,
"2.0MP": 2000000,
"4.0MP": 4000000,
}
if megapixel in mp_map:
P = mp_map[megapixel]
R = float(w) / float(h)
import math
w_new = int(round(math.sqrt(P * R) / 8.0) * 8.0)
h_new = int(round(math.sqrt(P / R) / 8.0) * 8.0)
w = max(8, w_new)
h = max(8, h_new)
return (width, height) return (w, h)
# --- REST endpoint for frontend config fetch --- # --- REST endpoint for frontend config fetch ---
@@ -11,23 +11,8 @@ import folder_paths
import re import re
def parse_name(ckpt_name):
path = ckpt_name
filename = path.split("/")[-1]
filename = filename.split(".")[:-1]
filename = ".".join(filename)
return filename
def calculate_sha256(file_path):
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def handle_whitespace(string: str): def handle_whitespace(string: str):
return string.strip().replace("\n", " ").replace("\r", " ").replace("\t", " ") return string.strip().replace("\n", " ").replace("\r", " ").replace("\t", " ")
@@ -56,7 +41,7 @@ def make_filename(template: str):
return get_timestamp("%Y-%m-%d-%H%M%S") if filename == "" else filename return get_timestamp("%Y-%m-%d-%H%M%S") if filename == "" else filename
def resolve_placeholders(template: str, prompt=None) -> str: def resolve_placeholders(template: str, prompt=None, extra_pnginfo=None) -> str:
"""Resolve placeholders of the form %node_name/field% by looking into extra_pnginfo and prompt. """Resolve placeholders of the form %node_name/field% by looking into extra_pnginfo and prompt.
This is a best-effort resolver: it attempts several lookup strategies and falls back to leaving This is a best-effort resolver: it attempts several lookup strategies and falls back to leaving
@@ -65,18 +50,58 @@ def resolve_placeholders(template: str, prompt=None) -> str:
if not template or '%' not in template: if not template or '%' not in template:
return template return template
# Build title/type to node ID mapping from extra_pnginfo
node_title_to_id = {}
node_type_to_ids = {}
if extra_pnginfo and isinstance(extra_pnginfo, dict) and "workflow" in extra_pnginfo:
workflow = extra_pnginfo["workflow"]
if isinstance(workflow, dict) and "nodes" in workflow:
for node in workflow["nodes"]:
if isinstance(node, dict):
node_id = str(node.get("id"))
node_title = node.get("title")
node_type = node.get("type")
if node_title:
node_title_to_id[node_title] = node_id
if node_type:
if node_type not in node_type_to_ids:
node_type_to_ids[node_type] = []
node_type_to_ids[node_type].append(node_id)
def lookup(key: str): def lookup(key: str):
# key is like 'node_name/field' or 'field'
if not key: if not key:
return None return None
# Try prompt mapping
node_ref = None
field = key
if '/' in key:
node_ref, field = key.split('/', 1)
node_id = None
if node_ref:
if prompt and node_ref in prompt:
node_id = node_ref
elif node_ref in node_title_to_id:
node_id = node_title_to_id[node_ref]
elif node_ref in node_type_to_ids:
node_id = node_type_to_ids[node_ref][0]
if node_id and prompt and node_id in prompt:
node_data = prompt[node_id]
if isinstance(node_data, dict) and "inputs" in node_data:
inputs = node_data["inputs"]
if field in inputs:
return inputs[field]
# Fallback: check if the key is directly in prompt or if it is just a field name in some node
if prompt and isinstance(prompt, dict): if prompt and isinstance(prompt, dict):
if key in prompt: if key in prompt:
return prompt[key] return prompt[key]
if '/' in key: for nid, ndata in prompt.items():
_, field = key.split('/', 1) if isinstance(ndata, dict) and "inputs" in ndata:
if field in prompt: inputs = ndata.get("inputs", {})
return prompt[field] if field in inputs:
return inputs[field]
return None return None
@@ -98,7 +123,7 @@ def resolve_placeholders(template: str, prompt=None) -> str:
return pattern.sub(repl, template) return pattern.sub(repl, template)
class ImageSaveWithMetadata: class Save_Machine:
def __init__(self): def __init__(self):
self.output_dir = folder_paths.output_directory self.output_dir = folder_paths.output_directory
@@ -107,29 +132,26 @@ class ImageSaveWithMetadata:
return { return {
"required": { "required": {
"images": ("IMAGE", ), "images": ("IMAGE", ),
"path_and_filename": ("STRING", {"default": "%time", "multiline": False}), "path_and_filename": ("STRING", {"default": "%time", "tooltip": "Supports %date, %time, and custom prompt %placeholders%"}),
"extension": ((['png', 'jpeg', 'webp']),), "extension": ((['png', 'jpeg', 'webp']),),
}, "hide_preview": ("BOOLEAN", {"default": True, "tooltip": "Hide image preview until mouse hovers over the node."}),
"optional": {
"custom_string": ("STRING", {"default": '', "multiline": True}),
"lossless_webp": ("BOOLEAN", {"default": True}),
"quality_jpeg_or_webp": ("INT", {"default": 100, "min": 1, "max": 100}),
}, },
"hidden": { "hidden": {
"prompt": "PROMPT", "prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
}, },
} }
RETURN_TYPES = () RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("file_path",)
FUNCTION = "save_files" FUNCTION = "save_files"
OUTPUT_NODE = True OUTPUT_NODE = True
CATEGORY = "SATA_UtilityNode" CATEGORY = "SATA_UtilityNode"
def save_files(self, images, path_and_filename, extension, custom_string=None, def save_files(self, images, path_and_filename, extension, hide_preview=True, prompt=None, extra_pnginfo=None):
quality_jpeg_or_webp=100, lossless_webp=True, prompt=None):
# Resolve placeholders first (uses prompt) # Resolve placeholders first (uses prompt)
try: try:
resolved = resolve_placeholders(str(path_and_filename), prompt) resolved = resolve_placeholders(str(path_and_filename), prompt, extra_pnginfo)
except Exception: except Exception:
resolved = str(path_and_filename) resolved = str(path_and_filename)
@@ -152,8 +174,6 @@ class ImageSaveWithMetadata:
os.makedirs(output_path, exist_ok=True) os.makedirs(output_path, exist_ok=True)
comment_parts = [] comment_parts = []
if custom_string:
comment_parts.append(handle_whitespace(custom_string))
if prompt is not None: if prompt is not None:
try: try:
prompt_text = json.dumps(prompt) prompt_text = json.dumps(prompt)
@@ -165,20 +185,25 @@ class ImageSaveWithMetadata:
# Resolve any %node/field% placeholders in filename and path using prompt # Resolve any %node/field% placeholders in filename and path using prompt
try: try:
filename = resolve_placeholders(filename, prompt) filename = resolve_placeholders(filename, prompt, extra_pnginfo)
path = resolve_placeholders(path, prompt) path = resolve_placeholders(path, prompt, extra_pnginfo)
except Exception: except Exception:
# Best-effort: if resolution fails, continue with original templates # Best-effort: if resolution fails, continue with original templates
pass pass
# We always use quality=100 and lossless_webp=True
filenames = self.save_images(images, output_path, filename, comment, extension, filenames = self.save_images(images, output_path, filename, comment, extension,
quality_jpeg_or_webp, lossless_webp, prompt) 100, True, prompt, extra_pnginfo)
subfolder = os.path.normpath(path) subfolder = os.path.normpath(path)
ui_images = [ {"filename": fn, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'} for fn in filenames ] ui_images = [ {"filename": fn, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'} for fn in filenames ]
return {"ui": {"images": ui_images}}
# Return the absolute path of the first saved image (or output directory if none)
first_file_path = os.path.join(output_path, filenames[0]) if filenames else output_path
return {"ui": {"images": ui_images}, "result": (first_file_path,)}
def save_images(self, images, output_path, filename_prefix, comment, extension, quality_jpeg_or_webp, lossless_webp, prompt=None) -> list: def save_images(self, images, output_path, filename_prefix, comment, extension, quality_jpeg_or_webp, lossless_webp, prompt=None, extra_pnginfo=None) -> list:
img_count = 1 img_count = 1
paths = [] paths = []
@@ -238,7 +263,9 @@ class ImageSaveWithMetadata:
except Exception: except Exception:
metadata.add_text("prompt", str(prompt)) metadata.add_text("prompt", str(prompt))
# No extra_pnginfo support: only include the prompt (handled above) if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
outname = find_unique_name(base_name, 'png') outname = find_unique_name(base_name, 'png')
img.save(os.path.join(output_path, outname), pnginfo=metadata, optimize=True) img.save(os.path.join(output_path, outname), pnginfo=metadata, optimize=True)
@@ -254,11 +281,19 @@ class ImageSaveWithMetadata:
img.save(file_path, optimize=True, quality=quality_jpeg_or_webp, lossless=lossless_webp if ext_name=='webp' else False) img.save(file_path, optimize=True, quality=quality_jpeg_or_webp, lossless=lossless_webp if ext_name=='webp' else False)
# Insert EXIF user comment with our metadata # Insert EXIF user comment with our metadata
if comment: if comment or extra_pnginfo or prompt:
try: try:
exif_dict = {}
if comment:
exif_dict["comment"] = comment
if prompt:
exif_dict["prompt"] = prompt
if extra_pnginfo:
exif_dict["workflow"] = extra_pnginfo.get("workflow", {})
exif_bytes = piexif.dump({ exif_bytes = piexif.dump({
"Exif": { "Exif": {
piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(comment, encoding="unicode") piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(json.dumps(exif_dict), encoding="unicode")
}, },
}) })
piexif.insert(exif_bytes, file_path) piexif.insert(exif_bytes, file_path)
+267 -122
View File
@@ -1,52 +1,129 @@
import os
import torch import torch
import cv2 import torch.nn.functional as F
import numpy as np
import folder_paths import folder_paths
import comfy.utils import comfy.utils
from comfy import model_management from comfy import model_management
from spandrel import ModelLoader from spandrel import ModelLoader
def resize_tensor_opencv(tensor_chw, target_width, target_height, supersample='true', factor=2.0): def generate_blue_noise(batch_size, c, h, w, device, beta=1.5):
# Generate White Noise (Standard Gaussian)
white_noise = torch.randn((batch_size, c, h, w), device=device)
# Convert to Frequency Domain (FFT)
fft_noise = torch.fft.fft2(white_noise)
# Create Frequency Grid
y = torch.fft.fftfreq(h, device=device)
x = torch.fft.fftfreq(w, device=device)
dy, dx = torch.meshgrid(y, x, indexing='ij')
# Calculate distance from center (frequency magnitude)
frequency_magnitude = torch.sqrt(dy**2 + dx**2)
# Apply High-Pass Scaling: Amplitude ~ f^(beta/2)
scale = frequency_magnitude ** (beta / 2.0)
# Zero out DC component
scale[0, 0] = 0
fft_structured = fft_noise * scale
# Convert back to Spatial Domain (Inverse FFT)
blue_noise = torch.fft.ifft2(fft_structured).real
# Normalize standard deviation to ~1.0
std = blue_noise.std()
if std > 1e-6:
blue_noise = blue_noise / std
return blue_noise
def _make_gaussian_kernel(kernel_size, sigma, device):
"""Build a separable 2D Gaussian kernel tensor (1,1,k,k) on device."""
k = kernel_size
coords = torch.arange(k, dtype=torch.float32, device=device) - k // 2
gauss_1d = torch.exp(-0.5 * (coords / sigma) ** 2)
gauss_1d = gauss_1d / gauss_1d.sum()
kernel_2d = gauss_1d[:, None] * gauss_1d[None, :]
return kernel_2d.view(1, 1, k, k)
def fast_gaussian_blur_bchw(tensor_bchw, kernel_size=9, sigma=3.0):
""" """
CPU-based resizing using OpenCV for 'rescale' mode. Fast per-channel GPU Gaussian blur via depthwise convolution.
Input: tensor_chw -> torch.FloatTensor (C,H,W) in [0,1] Input/Output: BCHW float tensor. Kernel is always created on the same device.
Output: torch.FloatTensor (C,H,W) in [0,1], guaranteed 3-channel RGB
""" """
new_width = max(1, int(target_width)) device = tensor_bchw.device
new_height = max(1, int(target_height)) k = kernel_size
kernel = _make_gaussian_kernel(k, sigma, device) # on same device
C = tensor_bchw.shape[1]
kernel_c = kernel.expand(C, 1, k, k).to(device) # guarantee same device
pad = k // 2
return F.conv2d(tensor_bchw.contiguous(), kernel_c, padding=pad, groups=C)
interp = cv2.INTER_LANCZOS4 if factor > 1.0 else cv2.INTER_AREA
np_img = tensor_chw.permute(1, 2, 0).detach().cpu().numpy() def resize_bchw(tensor_bchw, target_h, target_w):
np_img = (np.clip(np_img, 0.0, 1.0) * 255.0).astype(np.uint8) """
Fast GPU resize — stays on device, no antialias overhead.
Bilinear for downscale (smoother), bicubic for upscale (sharper).
"""
in_h, in_w = tensor_bchw.shape[2], tensor_bchw.shape[3]
if in_h == target_h and in_w == target_w:
return tensor_bchw
downscaling = (target_h < in_h) or (target_w < in_w)
mode = 'bilinear' if downscaling else 'bicubic'
return F.interpolate(tensor_bchw, size=(target_h, target_w), mode=mode, align_corners=False)
if np_img.ndim == 2:
np_img = cv2.cvtColor(np_img, cv2.COLOR_GRAY2RGB)
elif np_img.shape[2] == 1:
np_img = cv2.cvtColor(np_img, cv2.COLOR_GRAY2RGB)
elif np_img.shape[2] == 4:
np_img = cv2.cvtColor(np_img, cv2.COLOR_RGBA2RGB)
elif np_img.shape[2] > 3:
np_img = np_img[:, :, :3]
if np_img.shape[0] == 0 or np_img.shape[1] == 0: # ─────────────────────────────────────────────────────────────────────────────
raise ValueError("Input image for OpenCV resize has zero width or height.") # Architecture optimization profiles for spandrel models.
# Keys match the inner nn.Module class name (type(model.model).__name__).
#
# tile : starting tile size for tiled inference (OOM fallback halves it)
# fp16_model : cast model weights to FP16 before inference (Tensor Core boost)
# compile : torch.compile mode — 'reduce-overhead' for fixed-shape tiling
# ─────────────────────────────────────────────────────────────────────────────
ARCH_PROFILES = {
# ── Fast CNN / RRDB models — full FP16 support, large tiles ──────────────
"RRDBNet": {"tile": 768, "fp16_model": True, "compile": "reduce-overhead"}, # ESRGAN, Real-ESRGAN
"SRVGGNetCompact": {"tile": 896, "fp16_model": True, "compile": "reduce-overhead"}, # Real-ESRGAN Compact
"SPAN": {"tile": 896, "fp16_model": True, "compile": "reduce-overhead"}, # Swift, very fast
"PLKSR": {"tile": 768, "fp16_model": False, "compile": "reduce-overhead"}, # Lightweight
"RealPLKSR": {"tile": 768, "fp16_model": False, "compile": "reduce-overhead"}, # Lightweight photo
"RCAN": {"tile": 768, "fp16_model": True, "compile": "reduce-overhead"}, # Residual channel attn
"SAFMN": {"tile": 896, "fp16_model": True, "compile": "reduce-overhead"}, # Very fast
"DITN": {"tile": 768, "fp16_model": True, "compile": "reduce-overhead"},
"CRAFT": {"tile": 768, "fp16_model": True, "compile": "reduce-overhead"},
"NAFNet": {"tile": 768, "fp16_model": True, "compile": "reduce-overhead"},
"OmniSR": {"tile": 640, "fp16_model": True, "compile": "reduce-overhead"},
"RGT": {"tile": 640, "fp16_model": True, "compile": "reduce-overhead"},
# ── Transformer / attention models — autocast only, conservative tiles ────
"DAT": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"}, # Dual Attention Transformer
"DAT_S": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
"HAT": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"}, # Hybrid Attention
"HAT_L": {"tile": 480, "fp16_model": False, "compile": "reduce-overhead"}, # HAT Large — very heavy
"SwinIR": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"}, # Swin Transformer IR
"Swin2SR": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
"SAN": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"}, # Second-order Attn
"DRCT": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
"ATD": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
"GRL": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
"FDAT": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
"AuraSR": {"tile": 512, "fp16_model": False, "compile": "reduce-overhead"},
}
if supersample == 'true': # Fallback profile for any architecture not listed above.
ss_width = max(1, new_width * 8) # Uses supports_half from spandrel for fp16_model, 512 tile as safe default.
ss_height = max(1, new_height * 8) _DEFAULT_PROFILE = {"tile": 512, "fp16_model": None, "compile": "reduce-overhead"}
np_img = cv2.resize(np_img, (ss_width, ss_height), interpolation=interp)
if np_img.shape[0] == 0 or np_img.shape[1] == 0:
raise ValueError("Image became empty after supersample resize.")
resized = cv2.resize(np_img, (new_width, new_height), interpolation=interp)
tensor_out = torch.from_numpy(resized).float().div(255.0).permute(2, 0, 1)
return tensor_out
class Upscale_Machine: class Upscale_Machine:
# Session-level cache: model object id -> torch.compile'd inner module
# Avoids recompiling the same model on repeated runs in the same session.
_compiled_cache: dict = {}
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
@@ -55,8 +132,7 @@ class Upscale_Machine:
"upscale_model": (folder_paths.get_filename_list("upscale_models"), {"default": None}), "upscale_model": (folder_paths.get_filename_list("upscale_models"), {"default": None}),
"chained_model": (["None"] + folder_paths.get_filename_list("upscale_models"), {"default": "None"}), "chained_model": (["None"] + folder_paths.get_filename_list("upscale_models"), {"default": "None"}),
"rescale_factor": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 16.0, "step": 0.01}), "rescale_factor": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 16.0, "step": 0.01}),
"supersample": (["true", "false"],), "frequency_split": ("BOOLEAN", {"default": True}),
"rounding_modulus": ("INT", {"default": 8, "min": 1, "max": 1024, "step": 1}),
} }
} }
@@ -66,10 +142,7 @@ class Upscale_Machine:
CATEGORY = "SATA_UtilityNode" CATEGORY = "SATA_UtilityNode"
def load_model(self, model_name): def load_model(self, model_name):
"""Load ESRGAN/RealESRGAN/AESRGAN from the project's upscale_models folder. """Load ESRGAN/RealESRGAN/AESRGAN from the project's upscale_models folder."""
Only legacy PyTorch/spandrel models are supported by this simplified node.
"""
if not model_name: if not model_name:
raise ValueError("No upscale model selected or provided.") raise ValueError("No upscale model selected or provided.")
@@ -83,44 +156,119 @@ class Upscale_Machine:
model.eval() model.eval()
return model return model
def upscale_with_model(self, upscale_model, image_bhwc): def _get_arch_profile(self, upscale_model):
""" """
Run tiled upscale with spandrel model. Detects the model architecture and returns its optimisation profile.
Input: image_bhwc -> (B,H,W,C) float [0,1] Detection priority:
Output: (B,C,H',W') float [0,1] 1. Inner nn.Module class name (most specific)
2. spandrel supports_half flag (fallback for unknown architectures)
""" """
device = model_management.get_torch_device() inner_cls = type(upscale_model.model).__name__
upscale_model.to(device) profile = ARCH_PROFILES.get(inner_cls)
if profile is None:
# Unknown architecture — use spandrel flag for fp16 decision
fp16_ok = getattr(upscale_model, "supports_half", False)
profile = {**_DEFAULT_PROFILE, "fp16_model": fp16_ok}
return inner_cls, profile
in_img = image_bhwc.movedim(-1, -3).to(device) def _get_compiled_model(self, upscale_model, compile_mode):
"""
tile = 512 Returns a torch.compile()'d version of the model's inner nn.Module.
overlap = 32 Cached by object id — compiles once per session per loaded model.
oom = True Falls back silently to eager mode if compile fails or Triton is missing.
while oom: """
inner = upscale_model.model
cache_key = id(inner)
if cache_key not in Upscale_Machine._compiled_cache:
# Check for triton before attempting compile, as torch.compile is lazy
# and will otherwise crash on the first tile execution on Windows.
try: try:
steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps( import triton
in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap has_triton = True
) except ImportError:
pbar = comfy.utils.ProgressBar(steps) has_triton = False
s = comfy.utils.tiled_scale(
in_img,
lambda a: upscale_model(a),
tile_x=tile,
tile_y=tile,
overlap=overlap,
upscale_amount=getattr(upscale_model, "scale", 4),
pbar=pbar
)
oom = False
except model_management.OOM_EXCEPTION as e:
tile //= 2
if tile < 128:
raise e
upscale_model.cpu() if has_triton:
s = torch.clamp(s, min=0.0, max=1.0) try:
return s compiled = torch.compile(inner, mode=compile_mode, dynamic=False)
Upscale_Machine._compiled_cache[cache_key] = compiled
except Exception as ex:
print(f"[Upscale_Machine] torch.compile unavailable ({ex}), using eager mode.")
Upscale_Machine._compiled_cache[cache_key] = inner
else:
print("[Upscale_Machine] Triton not installed (Windows), skipping torch.compile.")
Upscale_Machine._compiled_cache[cache_key] = inner
return Upscale_Machine._compiled_cache[cache_key]
def upscale_with_model(self, upscale_model, image_bchw, device, pbar=None):
"""
Architecture-aware tiled upscale:
- Detects model type and loads optimisation profile
- FP16 weights (Tensor Cores) for CNN models that support it
- torch.autocast for mixed-precision on every tile (all architectures)
- torch.compile fused graph (cached per session, falls back safely)
Input: image_bchw -> (B,C,H,W) float32 [0,1]
Output: (B,C,H',W') float32 [0,1]
"""
arch_name, profile = self._get_arch_profile(upscale_model)
spandrel_supports_half = getattr(upscale_model, "supports_half", False)
use_fp16 = profile["fp16_model"] and spandrel_supports_half
tile = profile["tile"]
comp_mode = profile["compile"]
device_type = device.type if hasattr(device, "type") else str(device).split(":")[0]
print(f"[Upscale_Machine] {arch_name} | "
f"tile={tile} | fp16={'yes' if use_fp16 else 'no'} | compile={comp_mode}")
upscale_model.to(device)
if use_fp16:
upscale_model.half()
in_tensor = image_bchw.to(device).half()
else:
in_tensor = image_bchw.to(device).float()
# Compile inner nn.Module once per session
compiled_fn = self._get_compiled_model(upscale_model, comp_mode)
try:
overlap = 32
oom = True
while oom:
try:
if not pbar:
steps = in_tensor.shape[0] * comfy.utils.get_tiled_scale_steps(
in_tensor.shape[3], in_tensor.shape[2], tile_x=tile, tile_y=tile, overlap=overlap
)
local_pbar = comfy.utils.ProgressBar(steps)
else:
local_pbar = pbar
import contextlib
# If model doesn't support fp16 (like DAT/HAT), autocast to fp16 will also cause NaNs.
# We only autocast if use_fp16 is True to be perfectly safe.
ctx = torch.autocast(device_type=device_type, dtype=torch.float16) if use_fp16 else contextlib.nullcontext()
with ctx:
s = comfy.utils.tiled_scale(
in_tensor,
lambda a: compiled_fn(a),
tile_x=tile,
tile_y=tile,
overlap=overlap,
upscale_amount=getattr(upscale_model, "scale", 4),
pbar=local_pbar
)
oom = False
except model_management.OOM_EXCEPTION as e:
model_management.soft_empty_cache()
tile //= 2
if tile < 128:
raise e
finally:
upscale_model.cpu().float() # Always restore to FP32 on CPU
return torch.clamp(s.float(), min=0.0, max=1.0)
def _round_to_modulus(self, value, modulus): def _round_to_modulus(self, value, modulus):
if modulus is None or modulus <= 1: if modulus is None or modulus <= 1:
@@ -128,68 +276,65 @@ class Upscale_Machine:
return max(modulus, int(round(value / modulus)) * modulus) return max(modulus, int(round(value / modulus)) * modulus)
def upscale(self, image, upscale_model, chained_model="None", rounding_modulus=8, supersample='true', def upscale(self, image, upscale_model, chained_model="None", rounding_modulus=8, supersample='true',
rescale_factor=2.0): rescale_factor=2.0, frequency_split=True):
if image.ndim != 4: if image.ndim != 4:
raise ValueError("Expected IMAGE tensor with 4 dims (B,H,W,C).") raise ValueError("Expected IMAGE tensor with 4 dims (B,H,W,C).")
device = model_management.get_torch_device()
original_height = int(image.shape[1]) original_height = int(image.shape[1])
original_width = int(image.shape[2]) original_width = int(image.shape[2])
target_width = self._round_to_modulus(original_width * rescale_factor, rounding_modulus) target_w = self._round_to_modulus(original_width * rescale_factor, rounding_modulus)
target_height = self._round_to_modulus(original_height * rescale_factor, rounding_modulus) target_h = self._round_to_modulus(original_height * rescale_factor, rounding_modulus)
current_bhwc = image # Move to GPU once as BCHW — stay there the entire pipeline
current_bchw = image.movedim(-1, 1).to(device)
if current_bchw.shape[1] == 1:
current_bchw = current_bchw.repeat(1, 3, 1, 1)
elif current_bchw.shape[1] > 3:
current_bchw = current_bchw[:, :3, :, :]
# First upscale # Keep a clean GPU copy of original for frequency split baseline
original_bchw = current_bchw.clone()
# ── First upscale model ───────────────────────────────────────────────
if upscale_model: if upscale_model:
up_model = self.load_model(upscale_model) up_model = self.load_model(upscale_model)
up_bchw = self.upscale_with_model(up_model, current_bhwc) current_bchw = self.upscale_with_model(up_model, current_bchw, device)
current_bhwc = up_bchw.movedim(1, -1).contiguous() # tiled_scale may return CPU tensor — pin back to GPU
current_bchw = current_bchw.to(device)
current_bchw = resize_bchw(current_bchw, target_h, target_w)
# Downscale to target size before chaining # ── Chained model ─────────────────────────────────────────────────────
batch_downscaled = [] has_chain = chained_model and chained_model != "None"
for i in range(current_bhwc.shape[0]): if has_chain:
chw = current_bhwc[i].movedim(-1, 0) # (H,W,C) -> (C,H,W)
chw = torch.clamp(chw, 0.0, 1.0)
resized_chw = resize_tensor_opencv(
chw, target_width=target_width, target_height=target_height,
supersample=supersample, factor=rescale_factor
)
if resized_chw.shape[0] == 1:
resized_chw = resized_chw.repeat(3, 1, 1)
elif resized_chw.shape[0] > 3:
resized_chw = resized_chw[:3, :, :]
batch_downscaled.append(resized_chw.unsqueeze(0))
current_bhwc = torch.cat(batch_downscaled, dim=0)
current_bhwc = current_bhwc.movedim(1, -1).contiguous()
# Second upscale (chained model) if selected
if chained_model and chained_model != "None":
# chained model is a model name
chain_model = self.load_model(chained_model) chain_model = self.load_model(chained_model)
chain_bchw = self.upscale_with_model(chain_model, current_bhwc) current_bchw = self.upscale_with_model(chain_model, current_bchw, device)
current_bhwc = chain_bchw.movedim(1, -1).contiguous() # tiled_scale may return CPU tensor — pin back to GPU
current_bchw = current_bchw.to(device)
current_bchw = resize_bchw(current_bchw, target_h, target_w)
# If no chained model, ensure output is at target size images_out = current_bchw
if not (chained_model and chained_model != "None"):
# Already downscaled after first upscale, so just output
images_out = current_bhwc
else:
# After chained model, do a final resize to target size
batch_out = []
for i in range(current_bhwc.shape[0]):
chw = current_bhwc[i].movedim(-1, 0)
chw = torch.clamp(chw, 0.0, 1.0)
resized_chw = resize_tensor_opencv(
chw, target_width=target_width, target_height=target_height,
supersample=supersample, factor=rescale_factor
)
if resized_chw.shape[0] == 1:
resized_chw = resized_chw.repeat(3, 1, 1)
elif resized_chw.shape[0] > 3:
resized_chw = resized_chw[:3, :, :]
batch_out.append(resized_chw.unsqueeze(0))
out_bchw = torch.cat(batch_out, dim=0)
images_out = out_bchw.movedim(1, -1).contiguous()
return (images_out,) # ── Frequency-Split SR ────────────────────────────────────────────────
if frequency_split and upscale_model:
# Ensure both tensors are on the same device before any arithmetic
bicubic_bchw = resize_bchw(original_bchw.to(device), target_h, target_w)
images_out = images_out.to(device)
sr_low = fast_gaussian_blur_bchw(images_out)
sr_high = images_out - sr_low
bic_low = fast_gaussian_blur_bchw(bicubic_bchw)
images_out = torch.clamp(bic_low + sr_high, 0.0, 1.0)
# ── Blue Noise realism (chained only) ─────────────────────────────────
if has_chain:
images_out = images_out.to(device)
B, C, H, W = images_out.shape
noise = generate_blue_noise(B, C, H, W, device)
images_out = torch.clamp(images_out + noise * 0.15, 0.0, 1.0)
# Convert back to BHWC for ComfyUI
return (images_out.movedim(1, -1).contiguous(),)
+2 -34
View File
@@ -1,26 +1,10 @@
[project] [project]
name = "sata_utilitynode" name = "sata_utilitynode"
description = "A collection of utility nodes including image upscale,touchpad support and CSV-driven prompt machine for ComfyUI." description = "A collection of utility nodes including image upscale,touchpad support and CSV-driven prompt machine for ComfyUI."
version = "1.0.0" version = "2.0.0"
license = {file = "LICENSE"} 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"] dependencies = ["opencv-python>=4.5.0", "spandrel", "piexif"]
[project.urls] [project.urls]
Repository = "https://github.com/SatadalAI/SATA_UtilityNode" Repository = "https://github.com/SatadalAI/SATA_UtilityNode"
@@ -28,22 +12,6 @@ Repository = "https://github.com/SatadalAI/SATA_UtilityNode"
Documentation = "https://github.com/SatadalAI/SATA_UtilityNode/wiki" Documentation = "https://github.com/SatadalAI/SATA_UtilityNode/wiki"
"Bug Tracker" = "https://github.com/SatadalAI/SATA_UtilityNode/issues" "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.5.0"
license = "MIT"
[project.urls]
Repository = "https://github.com/SatadalAI/SATA_UtilityNode"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy] [tool.comfy]
PublisherId = "SatadalAI" PublisherId = "SatadalAI"
DisplayName = "SATA_UtilityNode" DisplayName = "SATA_UtilityNode"