17 Commits
Author SHA1 Message Date
Satadal Dhara 5eda61901a v2.0: Architecture-aware upscaling, dynamic precision/tiling, GPU-bound frequency splitting, and Triton safety checks 2026-07-20 16:03:08 +05:30
Satadal Dhara a15e3169b1 feat: Comprehensive node updates (resolutions, prompts, latent, upscale) 2026-07-19 18:18:32 +05:30
Satadal Dhara 7eb8a036c7 Optimize custom nodes, fix memory leaks, and add V2 settings compatibility 2026-06-18 07:49:44 +05:30
Satadal Dhara 8417902269 feat: implement SATA UtilityNode suite with advanced upscale, latent, prompt, and save processing nodes 2026-05-10 16:44:42 +05:30
Satadal Dhara 2c3e98461c Clean up __init__.py by removing comment
Removed commented out header from __init__.py
2026-05-05 15:55:33 +05:30
Satadal Dhara d0dd3acbcc Clean up pyproject.toml by removing comments
Removed commented-out classifiers for operating systems and GPU support.
2026-05-05 15:53:31 +05:30
Satadal Dhara cb6396596c new_node_added 2025-12-24 22:24:58 +05:30
Satadal Dhara b4cd890c9e updated node. 2025-12-17 21:02:05 +05:30
Satadal Dhara a9969a7009 resolution updated 2025-12-16 12:57:07 +05:30
Satadal Dhara 2c94b1a61b version_update 2025-11-27 21:30:50 +05:30
Satadal Dhara c130c72d7a Merge pull request #1 from ComfyNodePRs/publish
Add Github Action for Publishing to Comfy Registry
2025-11-27 21:21:41 +05:30
Satadal Dhara c5c2c6693c Merge pull request #2 from ComfyNodePRs/pyproject
Add pyproject.toml for Custom Node Registry
2025-11-27 21:19:14 +05:30
Satadal Dhara 680e1413af Merge branch 'main' into pyproject 2025-11-27 21:18:52 +05:30
Satadal Dhara 62a857abd8 clean_up 2025-11-27 20:32:00 +05:30
Satadal Dhara de4b4aece9 added_new_node 2025-11-27 20:13:59 +05:30
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") 67f5446069 chore(pyproject): Add pyproject.toml for Custom Node Registry 2025-10-22 20:12:47 +00:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") 97ff05a208 chore(publish): Add Github Action for Publishing to Comfy Registry 2025-10-22 20:12:46 +00:00
20 changed files with 3163 additions and 414 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'SatadalAI' }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+44 -23
View File
@@ -1,7 +1,7 @@
# SATA Utility Node Suite for ComfyUI # SATA Utility Node Suite for ComfyUI
**Author:** Satadal **Author:** Satadal
**Version:** 1.2.0 **Version:** 1.5.0
**License:** MIT **License:** MIT
**Repository:** https://github.com/SatadalAI/SATA_UtilityNode **Repository:** https://github.com/SatadalAI/SATA_UtilityNode
@@ -12,12 +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 (Six-Slot):** 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 (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.
--- ---
@@ -26,17 +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.
- **Dual Output:** Returns both the processed image and a help/documentation link.
### 💬 Prompt Machine ### 💬 Prompt Machine (Six-Slot Framework)
- **Structured Prompting:** Dedicated input boxes for `subject`, `style`, `lighting`, `composition`, `mood`, and `technical` details to prevent "word salad".
- **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!
- **Natural Language & Comma Separated:** Dynamically formats outputs for FLUX (Natural Language) or SDXL (Comma Separated).
### 💬 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,31 +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
- **Advanced Noise Generation:** Initialize latents with specific noise patterns:
- **Gaussian (White):** Standard noise, good for sharp details.
- **Blue (High-Frequency):** Enhances detail and textures.
- **Pink (1/f) / Brown (1/f²):** Photorealism, nature, anime backgrounds.
- **Perlin / Plasma:** Fluid landscapes, sci-fi, abstract 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).
- **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.
### 👁️ 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
+16 -10
View File
@@ -1,28 +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.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,
"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",
"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
+248
View File
@@ -0,0 +1,248 @@
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();
// 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() {
setupGlobalMouseTracker();
},
async nodeCreated(node) {
if (isTargetNode(node)) {
setupNodeHider(node);
}
},
async loadedGraphNode(node) {
if (isTargetNode(node)) {
setupNodeHider(node);
}
}
});
function isTargetNode(node) {
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 (node.comfyClass === PREVIEW_MACHINE_CLASS) {
return true;
}
if (node.comfyClass === SAVE_MACHINE_CLASS) {
const w = node.widgets?.find(w => w.name === "hide_preview");
if (w) {
if (w.value === true) return true;
if (w.value === false && !globalHide) return false;
}
return true;
}
return globalHide;
}
function isMouseOverNode(node) {
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 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;
}
}
if (changed) {
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 setupNodeHider(node) {
if (node._sata_hider_v6) return;
node._sata_hider_v6 = true;
// Function to check if node should be hidden right now
const shouldHideNow = () => {
const shouldHide = shouldHideNode(node);
const hovered = hoveredNodeIds.has(node.id);
return shouldHide && !hovered;
};
// Find and patch all image-related widgets on the node
const patchWidgets = () => {
if (!node.widgets) return;
for (const widget of node.widgets) {
// Check if this is an image widget (they typically have draw/drawWidget methods)
if (widget._sata_patched) continue;
// Patch the draw method if it exists
if (widget.draw && typeof widget.draw === 'function') {
const origDraw = widget.draw.bind(widget);
widget.draw = function (ctx, node, widgetWidth, y, widgetHeight) {
if (shouldHideNow()) {
// Draw hidden placeholder instead
ctx.save();
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(0, y, widgetWidth, widgetHeight);
ctx.fillStyle = "#888";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Hidden", widgetWidth / 2, y + widgetHeight / 2 - 8);
ctx.font = "9px Arial";
ctx.fillStyle = "#666";
ctx.fillText("(Hover to Reveal)", widgetWidth / 2, y + widgetHeight / 2 + 8);
ctx.restore();
return;
}
return origDraw(ctx, node, widgetWidth, y, widgetHeight);
};
widget._sata_patched = true;
}
// Also patch drawWidget if it exists (some widgets use this)
if (widget.drawWidget && typeof widget.drawWidget === 'function') {
const origDrawWidget = widget.drawWidget.bind(widget);
widget.drawWidget = function (...args) {
if (shouldHideNow()) {
// Skip drawing - the onDrawForeground will handle the overlay
return;
}
return origDrawWidget(...args);
};
widget._sata_patched = true;
}
}
};
// Patch widgets initially and on any update
patchWidgets();
// Also intercept onDrawBackground to catch widget additions
const origOnDrawBackground = node.onDrawBackground;
node.onDrawBackground = function (ctx) {
patchWidgets(); // Re-check for new widgets
if (origOnDrawBackground) {
origOnDrawBackground.apply(this, arguments);
}
};
// Draw overlay in onDrawForeground as backup
const origOnDrawForeground = node.onDrawForeground;
node.onDrawForeground = function (ctx) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
if (shouldHideNow()) {
ctx.save();
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(0, 0, this.size[0], this.size[1]);
ctx.fillStyle = "#888";
ctx.font = "bold 14px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Hidden", this.size[0] / 2, this.size[1] / 2 - 10);
ctx.font = "10px Arial";
ctx.fillStyle = "#666";
ctx.fillText("(Hover to Reveal)", this.size[0] / 2, this.size[1] / 2 + 10);
ctx.restore();
}
};
// Widget callback for Save_Machine
if (node.comfyClass === SAVE_MACHINE_CLASS) {
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);
if (app.graph) app.graph.setDirtyCanvas(true, true);
};
}
}
}
+145 -70
View File
@@ -1,30 +1,63 @@
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 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 +75,14 @@ app.registerExtension({
}); });
}, },
async nodeCreated(node) {
this.initAutocompleteForNode(node);
},
async loadedGraphNode(node) {
this.initAutocompleteForNode(node);
},
snippetsCache: null, snippetsCache: null,
async loadSnippets() { async loadSnippets() {
@@ -59,7 +100,7 @@ app.registerExtension({
const contentData = await contentResp.json(); const contentData = await contentResp.json();
this.snippetsCache[file] = contentData.items || []; this.snippetsCache[file] = contentData.items || [];
} }
console.log("[PromptAutocomplete] Loaded snippets:", Object.keys(this.snippetsCache));
} catch (err) { } catch (err) {
console.error("[PromptAutocomplete] Failed to load snippets:", err); console.error("[PromptAutocomplete] Failed to load snippets:", err);
} }
@@ -77,12 +118,12 @@ app.registerExtension({
// Hook into the input element creation/usage // Hook into the input element creation/usage
// NOTE: ComfyUI's customtext widget often creates a textarea. // NOTE: ComfyUI's customtext widget often creates a textarea.
// We can try to find the textarea in the DOM if it's active, or hook the input callback. // We can try to find the textarea in the DOM if it's active, or hook the input callback.
// A more robust way for ComfyUI widgets: // A more robust way for ComfyUI widgets:
// The widget has an `inputEl` property if it's a DOM widget. // The widget has an `inputEl` property if it's a DOM widget.
// Let's try to hook the `inputEl` if it exists, or wait for it. // Let's try to hook the `inputEl` if it exists, or wait for it.
// Helper to setup the listener // Helper to setup the listener
function setupListener(input) { function setupListener(input) {
if (input.dataset.hasAutocomplete) return; if (input.dataset.hasAutocomplete) return;
@@ -96,21 +137,39 @@ 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);
} }
} }
}, },
// --- Autocomplete Logic --- // --- Autocomplete Logic ---
popup: null, popup: null,
active: false, active: false,
currentCategory: null, // null = showing categories, string = showing items currentCategory: null, // null = showing categories, string = showing items
@@ -119,7 +178,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;
@@ -127,7 +186,7 @@ app.registerExtension({
// Simple check: is the character before cursor the trigger? // Simple check: is the character before cursor the trigger?
// Or are we in a "trigger session"? // Or are we in a "trigger session"?
// If we just typed the trigger // If we just typed the trigger
if (e.data === trigger) { if (e.data === trigger) {
this.showPopup(input, cursor); this.showPopup(input, cursor);
@@ -144,11 +203,11 @@ app.registerExtension({
} }
const query = val.substring(lastTriggerIndex + 1, cursor); const query = val.substring(lastTriggerIndex + 1, cursor);
// Check if we have a category selected (format: #category:query) // Check if we have a category selected (format: #category:query)
// But wait, the plan said: Select Category -> Then Items. // But wait, the plan said: Select Category -> Then Items.
// So maybe we just filter categories first. // So maybe we just filter categories first.
if (this.currentCategory) { if (this.currentCategory) {
// We are inside a category // We are inside a category
// Check if the query contains the category prefix? // Check if the query contains the category prefix?
@@ -161,17 +220,17 @@ app.registerExtension({
// But how do we represent that in text? // But how do we represent that in text?
// Maybe we don't change text, just internal state? // Maybe we don't change text, just internal state?
// Standard way: #category:item // Standard way: #category:item
// Let's parse the text to see where we are. // Let's parse the text to see where we are.
// If text is "#cat", we filter categories. // If text is "#cat", we filter categories.
// If text is "#cat:ite", we filter items in "cat". // If text is "#cat:ite", we filter items in "cat".
const parts = query.split(":"); const parts = query.split(":");
if (parts.length > 1) { if (parts.length > 1) {
// We have a category selected // We have a category selected
const catName = parts[0]; const catName = parts[0];
const itemQuery = parts.slice(1).join(":"); // rest is item query const itemQuery = parts.slice(1).join(":"); // rest is item query
// Check if catName is valid // Check if catName is valid
if (this.snippetsCache[catName + ".csv"] || this.snippetsCache[catName + ".json"] || this.snippetsCache[catName]) { if (this.snippetsCache[catName + ".csv"] || this.snippetsCache[catName + ".json"] || this.snippetsCache[catName]) {
// It's a valid category (roughly). // It's a valid category (roughly).
@@ -184,7 +243,7 @@ app.registerExtension({
} }
} }
} }
// If no colon, or invalid category, we are filtering categories // If no colon, or invalid category, we are filtering categories
this.currentCategory = null; this.currentCategory = null;
this.updateCategories(query); this.updateCategories(query);
@@ -192,17 +251,17 @@ app.registerExtension({
} else { } else {
// No category selected yet // No category selected yet
// Check for colon // Check for colon
const parts = query.split(":"); const parts = query.split(":");
if (parts.length > 1) { if (parts.length > 1) {
// Maybe user typed "style:" manually? // Maybe user typed "style:" manually?
const catName = parts[0]; const catName = parts[0];
let key = Object.keys(this.snippetsCache).find(k => k.replace(/\.(csv|json)$/, "") === catName); let key = Object.keys(this.snippetsCache).find(k => k.replace(/\.(csv|json)$/, "") === catName);
if (key) { if (key) {
this.currentCategory = key; this.currentCategory = key;
this.updateItems(parts.slice(1).join(":")); this.updateItems(parts.slice(1).join(":"));
return; return;
} }
} }
this.updateCategories(query); this.updateCategories(query);
} }
@@ -274,14 +333,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();
}, },
@@ -289,14 +356,21 @@ app.registerExtension({
updateItems(query) { updateItems(query) {
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;
@@ -328,7 +402,7 @@ app.registerExtension({
if (idx === this.selectedIndex) { if (idx === this.selectedIndex) {
div.style.backgroundColor = "#444"; div.style.backgroundColor = "#444";
div.style.color = "#fff"; div.style.color = "#fff";
// Show preview if it's a long item // Show preview if it's a long item
if (item.type === "item" && item.value.length > 30) { if (item.type === "item" && item.value.length > 30) {
this.showPreview(item.value); this.showPreview(item.value);
@@ -341,10 +415,10 @@ app.registerExtension({
this.selectedIndex = idx; this.selectedIndex = idx;
this.selectItem(this.activeInput); // We need reference to input this.selectItem(this.activeInput); // We need reference to input
}; };
this.popup.appendChild(div); this.popup.appendChild(div);
}); });
// Keep active input ref // Keep active input ref
const activeElement = document.activeElement; const activeElement = document.activeElement;
if (activeElement && activeElement.tagName === "TEXTAREA") { if (activeElement && activeElement.tagName === "TEXTAREA") {
@@ -370,7 +444,7 @@ app.registerExtension({
}); });
document.body.appendChild(this.previewBox); document.body.appendChild(this.previewBox);
} }
this.previewBox.textContent = text; this.previewBox.textContent = text;
const rect = this.popup.getBoundingClientRect(); const rect = this.popup.getBoundingClientRect();
this.previewBox.style.left = `${rect.right + 5}px`; this.previewBox.style.left = `${rect.right + 5}px`;
@@ -385,6 +459,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;
@@ -402,16 +477,16 @@ app.registerExtension({
this.currentCategory = item.value; this.currentCategory = item.value;
// Don't close popup, just update to items // Don't close popup, just update to items
// We need to update the input value first // We need to update the input value first
// Replace everything from trigger to cursor with trigger + textToInsert // Replace everything from trigger to cursor with trigger + textToInsert
const before = val.substring(0, lastTriggerIndex + 1); // includes trigger const before = val.substring(0, lastTriggerIndex + 1); // includes trigger
const after = val.substring(cursor); const after = val.substring(cursor);
input.value = before + textToInsert + after; input.value = before + textToInsert + after;
// Move cursor // Move cursor
const newCursor = lastTriggerIndex + 1 + textToInsert.length; const newCursor = lastTriggerIndex + 1 + textToInsert.length;
input.setSelectionRange(newCursor, newCursor); input.setSelectionRange(newCursor, newCursor);
// Trigger input event manually to update state // Trigger input event manually to update state
this.handleInput({ data: null }, input, null); this.handleInput({ data: null }, input, null);
return; return;
@@ -430,13 +505,13 @@ app.registerExtension({
// Replace // Replace
const before = val.substring(0, lastTriggerIndex); // remove trigger const before = val.substring(0, lastTriggerIndex); // remove trigger
const after = val.substring(cursor); const after = val.substring(cursor);
// If we are replacing a category query like #style:something, we need to find the start of # // If we are replacing a category query like #style:something, we need to find the start of #
// Actually, if we are in item mode, the text is "#style:query". // Actually, if we are in item mode, the text is "#style:query".
// We want to replace the whole "#style:query" with "textToInsert". // We want to replace the whole "#style:query" with "textToInsert".
input.value = before + textToInsert + after; input.value = before + textToInsert + after;
// Move cursor // Move cursor
const newCursor = before.length + textToInsert.length; const newCursor = before.length + textToInsert.length;
input.setSelectionRange(newCursor, newCursor); input.setSelectionRange(newCursor, newCursor);
+65 -6
View File
@@ -4,7 +4,7 @@ import { app } from "../../../scripts/app.js";
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_Style_Machine") return;
// Widgets // Widgets
const csvWidget = node.widgets.find(w => w.name === "csv_file"); const csvWidget = node.widgets.find(w => w.name === "csv_file");
@@ -19,10 +19,16 @@ app.registerExtension({
const index = node.widgets.indexOf(oldNameWidget); const index = node.widgets.indexOf(oldNameWidget);
node.widgets.splice(index, 1); node.widgets.splice(index, 1);
// Add proper dropdown // Add proper dropdown (this appends to the end)
const nameWidget = node.addWidget("combo", "name", "None", () => {}, { const nameWidget = node.addWidget("combo", "name", "None", () => { }, {
values: ["None"] values: ["None"]
}); });
// Move the new widget back to the original position
// Remove from the end (where addWidget put it)
node.widgets.splice(node.widgets.indexOf(nameWidget), 1);
// Insert at the correct index
node.widgets.splice(index, 0, nameWidget);
// Store available names // Store available names
let namesConfig = []; let namesConfig = [];
@@ -34,7 +40,7 @@ app.registerExtension({
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`); const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
const data = await resp.json(); const data = await resp.json();
namesConfig = data.names || ["None"]; namesConfig = data.names || ["None"];
console.log("[PromptMachineFrontend] Loaded names:", namesConfig);
refreshNames(); refreshNames();
} catch (err) { } catch (err) {
console.error("[PromptMachineFrontend] Failed to load names:", err); console.error("[PromptMachineFrontend] Failed to load names:", err);
@@ -50,17 +56,70 @@ app.registerExtension({
nameWidget.value = available[0]; // safe fallback nameWidget.value = available[0]; // safe fallback
} }
// Force update previews for the new name
updatePreviews(nameWidget.value);
app.graph.change(); app.graph.change();
} }
// Hook CSV change // Hook CSV change
const oldCsvCallback = csvWidget.callback; const oldCsvCallback = csvWidget.callback;
csvWidget.callback = function(value) { csvWidget.callback = function (value) {
if (oldCsvCallback) oldCsvCallback(value); if (oldCsvCallback) oldCsvCallback(value);
loadNames(value); // refresh dynamically loadNames(value); // refresh dynamically
}; };
// --- Preview Logic ---
const 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; }
});
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();
let html = "";
if (data.positive) html += `${data.positive}`;
previewEl.innerHTML = html || "<i>No preview available</i>";
// Adjust node size to fit content
node.setSize(node.computeSize());
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 // Initial sync
loadNames(csvWidget.value); loadNames(csvWidget.value).then(() => {
updatePreviews(nameWidget.value);
});
} }
}); });
+106 -38
View File
@@ -4,15 +4,16 @@ app.registerExtension({
name: "SATA_UtilityNode.Resolution_Machine", name: "SATA_UtilityNode.Resolution_Machine",
async nodeCreated(node) { async nodeCreated(node) {
if (node.comfyClass !== "Resolution_Machine") return; if (node.comfyClass !== "Resolution_Machine" && node.comfyClass !== "Latent_Machine") return;
// Find widgets // Find widgets
const modelWidget = node.widgets.find(w => w.name === "model"); 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 resolutionWidget = node.widgets.find(w => w.name === "resolution");
const widthWidget = node.widgets.find(w => w.name === "custom_width"); 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 === "custom_height"); const heightWidget = node.widgets.find(w => w.name === "height") || node.widgets.find(w => w.name === "custom_height");
if (!modelWidget || !resolutionWidget || !widthWidget || !heightWidget) { if (!modelWidget || !dimensionWidget || !resolutionWidget || !widthWidget || !heightWidget) {
console.warn("[Resolution_Machine] Missing widgets, check backend definition."); console.warn("[Resolution_Machine] Missing widgets, check backend definition.");
return; return;
} }
@@ -25,7 +26,7 @@ app.registerExtension({
try { try {
const resp = await fetch("/SATA_UtilityNode/resolutions_config"); const resp = await fetch("/SATA_UtilityNode/resolutions_config");
resolutionsConfig = await resp.json(); resolutionsConfig = await resp.json();
console.log("[Resolution_Machine] Loaded config:", resolutionsConfig);
} catch (err) { } catch (err) {
console.error("[Resolution_Machine] Failed to load config:", err); console.error("[Resolution_Machine] Failed to load config:", err);
} }
@@ -33,11 +34,27 @@ app.registerExtension({
await loadConfig(); await loadConfig();
// --- Utility: refresh resolution dropdown when model changes --- // --- Utility: refresh resolution dropdown when model/dimension changes ---
function refreshResolutions(selectedModel) { function refreshResolutions() {
const available = resolutionsConfig[selectedModel] const selectedModel = modelWidget.value;
? Object.keys(resolutionsConfig[selectedModel]) const selectedDimension = dimensionWidget.value;
: ["Custom (manual)"];
let available = [];
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
const buckets = resolutionsConfig.models[selectedModel];
buckets.forEach(bucket => {
// Check if bucket exists and has the selected dimension
if (resolutionsConfig.resolutions && resolutionsConfig.resolutions[bucket] && resolutionsConfig.resolutions[bucket][selectedDimension]) {
available.push(...Object.keys(resolutionsConfig.resolutions[bucket][selectedDimension]));
}
});
}
// Always include "Custom" option so that switching model/dimension dropdowns doesn't kick the user out of custom mode
if (!available.includes("Custom")) {
available.push("Custom");
}
resolutionWidget.options.values = available; resolutionWidget.options.values = available;
@@ -46,52 +63,103 @@ app.registerExtension({
resolutionWidget.value = available[0]; resolutionWidget.value = available[0];
} }
console.log(`[Resolution_Machine] Resolutions for ${selectedModel}:`, available); // If we switched to a preset (not Custom), lock fields
app.graph.change(); // If we defaulted to Custom (e.g. empty available), unlock
applyResolution(); // auto-apply width/height on refresh checkCustomMode();
} }
// --- Utility: apply resolution to width/height --- function checkCustomMode() {
function applyResolution() { if (resolutionWidget.value === "Custom") {
const selectedModel = modelWidget.value; widthWidget.type = "INT";
const selectedResolution = resolutionWidget.value; heightWidget.type = "INT";
delete widthWidget.computeSize;
if (!resolutionsConfig[selectedModel]) return; delete heightWidget.computeSize;
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; widthWidget.disabled = false;
heightWidget.disabled = false; heightWidget.disabled = false;
} else {
widthWidget.type = "hidden";
heightWidget.type = "hidden";
widthWidget.computeSize = () => [0, -4];
heightWidget.computeSize = () => [0, -4];
applyResolutionPreset();
}
node.setSize(node.computeSize());
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;
} }
app.graph.change(); let resolutionData = null;
// Lookup
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
const buckets = resolutionsConfig.models[selectedModel];
for (const bucket of buckets) {
if (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 --- // --- Hook model change ---
const oldModelCallback = modelWidget.callback; const oldModelCallback = modelWidget.callback;
modelWidget.callback = function(value) { modelWidget.callback = function (value) {
if (oldModelCallback) oldModelCallback(value); if (oldModelCallback) oldModelCallback(value);
refreshResolutions(value); refreshResolutions();
};
// --- Hook dimension change ---
const oldDimensionCallback = dimensionWidget.callback;
dimensionWidget.callback = function (value) {
if (oldDimensionCallback) oldDimensionCallback(value);
refreshResolutions();
}; };
// --- Hook resolution change --- // --- Hook resolution change ---
const oldResolutionCallback = resolutionWidget.callback; const oldResolutionCallback = resolutionWidget.callback;
resolutionWidget.callback = function(value) { resolutionWidget.callback = function (value) {
if (oldResolutionCallback) oldResolutionCallback(value); if (oldResolutionCallback) oldResolutionCallback(value);
applyResolution(); checkCustomMode();
}; };
// --- Add Custom Button ---
// Rename "Swap Dimensions" -> "Custom"
// Note: Node logic adds this button. Previous JS added it. We are replacing previous JS logic.
node.addWidget("button", "Custom", null, () => {
// Unlock widgets
widthWidget.disabled = false;
heightWidget.disabled = false;
// Ensure "Custom" option exists and is selected
if (!resolutionWidget.options.values.includes("Custom")) {
resolutionWidget.options.values.push("Custom");
}
resolutionWidget.value = "Custom";
checkCustomMode();
app.graph.change();
});
// --- Initial sync --- // --- Initial sync ---
refreshResolutions(modelWidget.value); refreshResolutions();
} }
}); });
+96 -1
View File
@@ -1,11 +1,106 @@
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
const menu = document.querySelector(".comfy-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
*/ */
const originalProcessMouseWheel = LGraphCanvas.prototype.processMouseWheel;
LGraphCanvas.prototype.processMouseWheel = function (/** @type {WheelEvent} */ 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);
}
if (isEnabled === false) {
if (originalProcessMouseWheel) {
return originalProcessMouseWheel.apply(this, arguments);
}
return false;
}
if (!this.graph || !this.allow_dragcanvas) return; if (!this.graph || !this.allow_dragcanvas) return;
const { clientX: x, clientY: y, deltaX, deltaY, ctrlKey, metaKey } = event; const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const { deltaX, deltaY, ctrlKey, metaKey } = event;
// Check if the event occurred inside the canvas viewport // Check if the event occurred inside the canvas viewport
if (this.viewport) { if (this.viewport) {
+286
View File
@@ -0,0 +1,286 @@
import json
import os
import torch
import torch.fft
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:
"""
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 recommended model resolutions out of the box.
"""
def __init__(self):
pass
@classmethod
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 {
"required": {
"model": (models, {"default": default_model}),
"dimension": (["Square", "Portrait", "Landscape"],),
"resolution": (resolutions_list,),
"width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
"noise_type": ([
"Gaussian (White): Sharp Architecture, Text, intricate mechanics",
"Pink (1/f): Photorealism, Portraits, Nature",
"Brown (1/f²): Anime, Digital Art, Bokeh/Backgrounds",
"Blue (High-Frequency): High-Detail, Textures, low-step/distilled models",
"Perlin : Fantasy landscapes, Fluids, Hair/Fabric",
"Plasma : Sci-Fi, Abstract, Alien terrain"
],),
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"high_contrast": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("LATENT", "IMAGE")
RETURN_NAMES = ("LATENT", "IMAGE")
FUNCTION = "generate_noise"
CATEGORY = "SATA_UtilityNode"
def get_resolution_dimensions(self, model, dimension, resolution, width, height):
try:
config = load_config()
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]
resolution_data = None
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
else:
c = 4
# Latent dimensions (compressed by 8)
h = actual_height // 8
w = actual_width // 8
# Generate base noise
if "Gaussian" in actual_noise_type:
noise = torch.randn((actual_batch_size, c, h, w), device=self.device)
elif "Perlin" in actual_noise_type:
# Multi-Octave Value Noise Approximation
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:
# FFT-based Power Law Noise (Pink, Brown, Plasma)
if "Pink" in actual_noise_type:
alpha = 1.0
elif "Brown" in actual_noise_type:
alpha = 2.0
elif "Plasma" in actual_noise_type:
alpha = 3.0 # Very smooth
else:
alpha = 0.0 # Fallback to white
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)
# This ensures intensity works consistently across different noise types
current_std = noise.std()
if current_std > 1e-6:
noise = noise / current_std
# Apply offset noise for high contrast
if actual_high_contrast:
noise = noise + 0.1
# Apply intensity
noise = noise * actual_intensity
# 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):
# Generate White Noise (Standard Gaussian)
white_noise = torch.randn((batch_size, c, h, w), device=self.device)
# Convert to Frequency Domain (FFT)
fft_noise = torch.fft.fft2(white_noise)
# Create Frequency Grid
y = torch.fft.fftfreq(h, device=self.device)
x = torch.fft.fftfreq(w, device=self.device)
dy, dx = torch.meshgrid(y, x, indexing='ij')
# Calculate distance from center (frequency magnitude)
# Add epsilon to avoid division by zero at DC component
frequency_magnitude = torch.sqrt(dy**2 + dx**2) + 1e-8
# Apply Power Law Scaling: Amplitude ~ 1 / f^(alpha/2)
scale = 1.0 / (frequency_magnitude ** (alpha / 2.0))
# Zero out the DC component (mean brightness) to keep it neutral
scale[0, 0] = 0
fft_structured = fft_noise * scale
# Convert back to Spatial Domain (Inverse FFT)
structured_noise = torch.fft.ifft2(fft_structured).real
return structured_noise
def generate_perlin_approx(self, batch_size, c, h, w):
"""
Generates a Perlin-like noise by blending upsampled noise from multiple octaves.
This is faster than calculating actual Perlin noise per pixel on the CPU/GPU.
"""
noise = torch.zeros((batch_size, c, h, w), device=self.device)
# Octaves: scale down, generate noise, scale up
octaves = [2, 4, 8, 16]
weights = [0.5, 0.25, 0.125, 0.0625] # 1/f amplitude falloff
total_weight = 0
for scale, weight in zip(octaves, weights):
# Calculate small dimensions
sh, sw = max(1, h // scale), max(1, w // scale)
# Generate random noise at this scale
small_noise = torch.randn((batch_size, c, sh, sw), device=self.device)
# Upsample to full size using bicubic interpolation for smoothness
upsampled = torch.nn.functional.interpolate(
small_noise, size=(h, w), mode='bicubic', align_corners=False
)
noise += upsampled * weight
total_weight += weight
# Add a bit of fine-grained white noise for texture
noise += torch.randn((batch_size, c, h, w), device=self.device) * 0.05
return noise
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
+19
View File
@@ -0,0 +1,19 @@
from nodes import PreviewImage
class Preview_Machine(PreviewImage):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
CATEGORY = "SATA_UtilityNode"
FUNCTION = "save_images"
def save_images(self, images, 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:
+56 -8
View File
@@ -49,7 +49,9 @@ def read_names_from_csv(filename):
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 ("", "", "")
if not name or not isinstance(name, str) or name == "None":
return ("", "", "") return ("", "", "")
path = os.path.join(CSV_DIR, csv_file) path = os.path.join(CSV_DIR, csv_file)
if not os.path.exists(path): if not os.path.exists(path):
@@ -85,7 +87,7 @@ def read_prompt_row(csv_file, name):
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.
@@ -107,19 +109,65 @@ 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": {}
} }
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): def get_prompts(self, csv_file, name):
print(f"[PromptMachine] get_prompts - csv_file={csv_file}, name={name}")
pos, neg, note = read_prompt_row(csv_file, name) pos, neg, note = read_prompt_row(csv_file, name)
print(f"[PromptMachine] get_prompts -> pos_len={len(pos)}, neg_len={len(neg)}, note_len={len(note)}")
return (pos, neg, note) return (pos,)
class Prompt_Machine:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"subject": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
"style": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
"lighting": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
"composition": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
"mood": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
"technical": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
"json_output": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "get_prompt"
CATEGORY = "SATA_UtilityNode"
def get_prompt(self, subject, style, lighting, composition, mood, technical, json_output):
if json_output:
import json
data = {
"subject": subject.strip(),
"style": style.strip(),
"lighting": lighting.strip(),
"composition": composition.strip(),
"mood": mood.strip(),
"technical": technical.strip()
}
# Remove empty fields to keep JSON clean
data = {k: v for k, v in data.items() if v}
return (json.dumps(data, indent=2),)
else:
parts = []
if subject.strip(): parts.append(subject.strip())
if style.strip(): parts.append(style.strip())
if lighting.strip(): parts.append(lighting.strip())
if composition.strip(): parts.append(composition.strip())
if mood.strip(): parts.append(mood.strip())
if technical.strip(): parts.append(technical.strip())
return (", ".join(parts),)
# ---------------- REST API ---------------- # ---------------- REST API ----------------
+59 -26
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,24 +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"}),
},
} }
RETURN_TYPES = ("INT", "INT") RETURN_TYPES = ("INT", "INT")
@@ -48,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): 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"]
print(f"[{NODE_NAME}] Model={model}, Resolution={resolution}, Output=({width}, {height})") if megapixel != "None":
return (width, height) 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 (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,25 @@ 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']),),
}, },
"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, 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 +173,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 +184,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 +262,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 +280,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(),)
+10 -6
View File
@@ -1,14 +1,18 @@
[project] [project]
name = "comfyui_sata_utilitynode" name = "sata_utilitynode"
description = "ComfyUI utility nodes collection providing features like prompt machine, resolution machine, and upscale machine to enhance workflow efficiency." description = "A collection of utility nodes including image upscale,touchpad support and CSV-driven prompt machine for ComfyUI."
version = "1.2.0" version = "1.5.0"
license = "MIT" license = {file = "LICENSE"}
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"
# Used by Comfy Registry https://comfyregistry.org # Used by Comfy Registry https://registry.comfy.org
Documentation = "https://github.com/SatadalAI/SATA_UtilityNode/wiki"
"Bug Tracker" = "https://github.com/SatadalAI/SATA_UtilityNode/issues"
[tool.comfy] [tool.comfy]
PublisherId = "SatadalAI" PublisherId = "SatadalAI"
DisplayName = "SATA_UtilityNode" DisplayName = "SATA_UtilityNode"
Icon = "" Icon = ""
+1 -3
View File
@@ -1,5 +1,3 @@
opencv-python>=4.5.0 opencv-python>=4.5.0
spandrel spandrel
nvidia-tensorrt>=8.0 piexif
onnx>=1.12.0
onnxruntime-gpu>=1.12.0