Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7caae4a3e | ||
|
|
4b85129857 | ||
|
|
c5ccc7799d | ||
|
|
155144a018 | ||
|
|
6a3dbaea98 | ||
|
|
c0609cfc85 | ||
|
|
f54122f895 | ||
|
|
4231615d52 | ||
|
|
2008eccd71 | ||
|
|
9bf268bf1b | ||
|
|
e218330869 | ||
|
|
fd90def305 | ||
|
|
cb7da507e4 | ||
|
|
cf94f2aa8e | ||
|
|
7b49cbe603 |
@@ -0,0 +1,20 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -1,3 +1,11 @@
|
||||
# Local Obsidian notes
|
||||
project-craftkit.md
|
||||
how-to-publish-comfyui-node.md
|
||||
fable-review-2026-07-02.md
|
||||
|
||||
# Local design source files
|
||||
assets/*.ai
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 CraftopiaStudio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,15 +1,20 @@
|
||||
# ComfyUI-Craftopia
|
||||
<img src="assets/banner.svg" width="100%" alt="ComfyUI-CraftKit">
|
||||
|
||||
Custom nodes for ComfyUI — image resizing, dataset prep & prompt automation for LoRA training workflows.
|
||||
# ComfyUI-CraftKit
|
||||
|
||||
All nodes appear under the **Craftopia** category in the node menu.
|
||||
Custom nodes for ComfyUI - image resizing, dataset prep & prompt automation for LoRA training workflows.
|
||||
|
||||
All nodes appear under the **CraftKit** category in the node menu.
|
||||
|
||||
---
|
||||
|
||||
## Nodes
|
||||
|
||||
### 🎛️ Smart Prompt Controller
|
||||
Cycle through up to 4 prompt lists using a single incrementing counter. Counts lines automatically, selects the right prompt, and outputs which list is active — ideal for driving Switch nodes that control aspect ratio, latent size, or other per-category settings.
|
||||
### 📋 Smart Prompt Controller
|
||||
|
||||
<img src="assets/screenshot-smart-prompt-controller.png" width="100%">
|
||||
|
||||
Cycle through up to 4 prompt lists using a single incrementing index. Counts lines automatically, selects the right prompt, and outputs which list is active - ideal for driving Switch nodes that control aspect ratio, latent size, or other per-category settings.
|
||||
|
||||
**Typical usecases:**
|
||||
- LoRA dataset generation with multiple pose categories (headshots, halfbody, fullbody, tall portrait), each with their own prompt list and aspect ratio
|
||||
@@ -17,79 +22,124 @@ Cycle through up to 4 prompt lists using a single incrementing counter. Counts l
|
||||
|
||||
| Input | Type | Description |
|
||||
|---|---|---|
|
||||
| counter | INT | Auto-incrementing counter (1-indexed) |
|
||||
| prompt_list_1 | STRING | First prompt list — one prompt per line (forceInput) |
|
||||
| prompt_list_2 | STRING | Second prompt list (optional, forceInput) |
|
||||
| prompt_list_3 | STRING | Third prompt list (optional, forceInput) |
|
||||
| prompt_list_4 | STRING | Fourth prompt list (optional, forceInput) |
|
||||
| Index | INT | Current position across all lists (1-indexed). Set "control after generate" to `increment` to auto-advance every run |
|
||||
| Prompt list 1 | STRING | First prompt list, one prompt per line |
|
||||
| Prompt list 2 | STRING | Second prompt list (optional) |
|
||||
| Prompt list 3 | STRING | Third prompt list (optional) |
|
||||
| Prompt list 4 | STRING | Fourth prompt list (optional) |
|
||||
|
||||
**Outputs:** `prompt` (selected line as STRING), `list_index` (active list number as INT → feed to Switch nodes)
|
||||
**Outputs:** Prompt (selected line as STRING), Active list (active list number as INT → feed to Smart Profile Switch or Switch nodes)
|
||||
|
||||
The node displays a status label showing the current position: `List 1 — 4/5 | total: 8`
|
||||
|
||||
<img src="assets/screenshot-smart-prompt-controller.png" width="700">
|
||||
---
|
||||
|
||||
### 🧩 Smart Profile Switch
|
||||
|
||||
<img src="assets/screenshot-smart-profile-switch.png" width="100%">
|
||||
|
||||
Maps an active index (1–4) to a label, width, and height. Pairs directly with Smart Prompt Controller's `active_list` output to switch resolution and filename label per category in one node — no separate Aspect Ratio + Primitive + Switch combo needed.
|
||||
|
||||
**Typical usecases:**
|
||||
- Driving `EmptyLatentImage` / `EmptySD3LatentImage` width/height per prompt category
|
||||
- Feeding a filename suffix into `SaveImageExtended` or similar so each category's output is labeled automatically
|
||||
- Replacing a CR Aspect Ratio + 4× Primitive + LatentSwitch chain with a single node
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| Index | INT | 1 | Active profile (1–4). Connect to Active list from Smart Prompt Controller |
|
||||
| Label 1 … Label 4 | STRING | Headshots / Halfbody / Fullbody / Tall | Label for each profile, used as filename suffix |
|
||||
| Width 1 … Width 4 | INT | 1024 / 832 / 768 / 640 | Width for each profile |
|
||||
| Height 1 … Height 4 | INT | 1024 / 1216 / 1344 / 1536 | Height for each profile |
|
||||
|
||||
**Outputs:** Label (STRING), Width (INT), Height (INT)
|
||||
|
||||
The node displays a status label showing the active slot: `Slot 2: Halfbody | 832 × 1216`
|
||||
|
||||
---
|
||||
|
||||
### 📐 Smart Resize
|
||||
|
||||
<img src="assets/screenshot-smart-resize.png" width="100%">
|
||||
|
||||
Resize any image (or batch) so the **longest side** equals `longest_side`, with aspect ratio preserved.
|
||||
|
||||
Use this as a **pipeline node** — IMAGE in, IMAGE out. No files are saved to disk.
|
||||
|
||||
**Typical usecases:**
|
||||
- Downscale a SeedVR (or other upscaler) result back to your target training size — the upscaler adds fine detail, Smart Resize brings it to the right dimensions for LoRA training
|
||||
- Downscale a SeedVR2 (or other upscaler) result back to your target training size — the upscaler adds fine detail, Smart Resize brings it to the right dimensions for LoRA training
|
||||
- Any workflow step that needs a clean resize before the next node
|
||||
- Normalize mixed-resolution batches to a consistent size
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| image | IMAGE | — | Single image or batch |
|
||||
| longest_side | INT | 1536 | Target size for longest side |
|
||||
| multiple_of | INT | 8 | Snap dimensions to this multiple (8 = SD/Flux compatible) |
|
||||
| interpolation | ENUM | lanczos | lanczos / bicubic / bilinear / nearest |
|
||||
| upscale_if_smaller | BOOLEAN | false | Also upscale images already smaller than longest_side |
|
||||
| Image | IMAGE | — | Single image or batch |
|
||||
| Longest side (px) | INT | 1536 | Target size for longest side. Quick presets: 512 / 768 / 1024 / 1536 — or type any custom value directly into the field |
|
||||
| Round to multiple of | INT | 8 | Snap dimensions to this multiple (8 = SD/Flux compatible) |
|
||||
| Interpolation method | ENUM | lanczos | lanczos / bicubic / bilinear / nearest |
|
||||
| Upscale if smaller | BOOLEAN | true | Upscale images smaller than longest_side. Turn off to only ever downscale, never upscale |
|
||||
|
||||
**Outputs:** `image`, `width`, `height`
|
||||
|
||||
<img src="assets/screenshot-smart-resize.png" width="700">
|
||||
**Outputs:** Image, Width, Height
|
||||
|
||||
---
|
||||
|
||||
### 📁 Smart Batch Resize
|
||||
Load **all images from a folder**, resize each one by longest side, and save with the original filename + a suffix into a subfolder. Original filenames are always preserved.
|
||||
|
||||
Use this for **bulk preprocessing** — e.g. preparing a LoRA dataset from a folder of high-res images.
|
||||
<img src="assets/screenshot-smart-batch-resize.png" width="100%">
|
||||
|
||||
Load **all images from a folder**, resize each one by longest side, and save into a subfolder. Build clean dataset filenames from a prefix, the original name, and/or a sequential counter — combined with an optional resolution suffix.
|
||||
|
||||
Use this for **bulk preprocessing** — e.g. preparing a LoRA dataset from a folder of high-res images. It works as a **standalone node**: it needs no upstream input and no downstream connection to do its job — drop it on the canvas, point it at a folder, hit **Run Batch**, and it reads, resizes, and saves everything itself. Connecting the `images`/`count` outputs is entirely optional, e.g. for previewing results.
|
||||
|
||||
Includes a **Browse folder** button to pick the input folder directly from the node, and quick presets (512 / 768 / 1024 / 1536) for the longest side — or type any custom value directly into the field.
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| input_folder | STRING | D:/images/input | Source folder path |
|
||||
| longest_side | INT | 1536 | Target size for longest side |
|
||||
| filename_suffix | STRING | _Small | Appended to each output filename before the extension |
|
||||
| output_subfolder | STRING | resized | Subfolder created inside input_folder |
|
||||
| multiple_of | INT | 8 | Snap dimensions to this multiple |
|
||||
| interpolation | ENUM | lanczos | lanczos / bicubic / bilinear / nearest |
|
||||
| skip_if_exists | BOOLEAN | false | Skip files that already exist in output |
|
||||
| Input folder | STRING | — | Source folder path (use Browse button or paste manually) |
|
||||
| Longest side (px) | INT | 1024 | Target size for longest side. Quick presets: 512 / 768 / 1024 / 1536 — or type any custom value directly into the field |
|
||||
| Round to multiple of | INT | 8 | Snap dimensions to this multiple |
|
||||
| Interpolation method | ENUM | lanczos | lanczos / bicubic / bilinear / nearest |
|
||||
| Filename prefix | STRING | — | Label prepended to filename — e.g. `headshot` → `headshot_photo_001_1024.jpg` |
|
||||
| Keep original filename | BOOLEAN | true | Include the original filename in the output name |
|
||||
| Add counter | BOOLEAN | false | Add a sequential 3-digit counter to each filename (001, 002, ...) |
|
||||
| Counter start | INT | 1 | Starting number for the counter |
|
||||
| Add resolution to filename | BOOLEAN | true | Append resolution to filename — e.g. `photo_1024.png` |
|
||||
| Create resolution subfolder | BOOLEAN | false | Append resolution to subfolder name — e.g. `resized_1024` |
|
||||
| Custom output subfolder | STRING | resized | Subfolder name inside the input folder |
|
||||
| Skip existing files | BOOLEAN | true | Skip files that already exist in the output folder |
|
||||
| Filename delimiter | STRING | _ | Separator between filename parts |
|
||||
|
||||
**Outputs:** `images` (list), `filenames` (list), `output_paths` (list), `count`
|
||||
**Outputs:** Images (list), Count
|
||||
|
||||
---
|
||||
|
||||
### 📏 Smart Resolution Multiplier
|
||||
|
||||
<img src="assets/screenshot-smart-resolution-multiplier.png" width="100%">
|
||||
|
||||
Multiply image dimensions by a factor and output the results as integers.
|
||||
|
||||
SeedVR takes a single `resolution` INT (the longest side) — not a width and height separately. Standard math nodes give you a FLOAT or require multiple steps to get there. This node does it cleanly in one step: give it your image and a multiplier, and it outputs `width`, `height`, and `resolution` (longest side) ready to connect directly to SeedVR's `max_resolution` input.
|
||||
SeedVR2 takes a single `resolution` INT (the longest side) — not a width and height separately. Standard math nodes give you a FLOAT or require multiple steps to get there. This node does it cleanly in one step: give it your image and a multiplier, and it outputs `width`, `height`, and `resolution` (longest side) ready to connect directly to SeedVR2's `resolution` input.
|
||||
|
||||
Also useful for LoRA dataset prep — using mixed resolutions in your training set generally produces better results than training on a single fixed size. Smart Resolution Multiplier makes it easy to dynamically calculate the right target size per image rather than hardcoding a value.
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| image | IMAGE | — | Source image |
|
||||
| multiplier | FLOAT | 2.0 | Multiply width and height by this factor |
|
||||
| multiple_of | INT | 8 | Snap dimensions to this multiple |
|
||||
| Image | IMAGE | — | Source image — its current width/height are read directly, no separate Get Image Size node needed |
|
||||
| Multiplier | FLOAT | 2.0 | Multiply width and height by this factor |
|
||||
| Round to multiple of | INT | 8 | Snap dimensions to this multiple |
|
||||
|
||||
**Outputs:** `width`, `height`, `resolution` (longest side as INT → directly into SeedVR)
|
||||
**Outputs:** Width, Height, Resolution (longest side as INT → directly into SeedVR2)
|
||||
|
||||
<img src="assets/screenshot-smart-resolution-multiplier.png" width="700">
|
||||
---
|
||||
|
||||
## Example workflows
|
||||
|
||||
Ready-to-load workflows are included in [`example_workflows/`](example_workflows):
|
||||
|
||||
- **`smart_prompt_controller_demo.json`** — Smart Prompt Controller + Smart Profile Switch driving 4 prompt categories with auto-switching resolution and filename label
|
||||
- **`seedvr2_smartresize_demo.json`** — Smart Resolution Multiplier → SeedVR2 upscale → Smart Resize, a clean pattern for upscaling without hardcoding target resolution
|
||||
- **`smart_batch_resize_demo.json`** — minimal Smart Batch Resize setup with a Note explaining outputs and settings
|
||||
|
||||
---
|
||||
|
||||
@@ -98,26 +148,29 @@ Also useful for LoRA dataset prep — using mixed resolutions in your training s
|
||||
- ComfyUI's built-in `Resize Images by Longer Edge` [BETA] has no Lanczos and no `multiple_of` snapping
|
||||
- `JWImageResizeByLongerSide` (comfyui-various) has no Lanczos in the official release
|
||||
- No existing node combines batch folder loading + longest-side resize + original filename preservation
|
||||
- No existing node outputs a ready-to-use `resolution` INT for SeedVR
|
||||
- No existing node cycles through multiple prompt lists with automatic list switching and counter
|
||||
- No existing node outputs a ready-to-use `resolution` INT for SeedVR2
|
||||
- No existing node cycles through multiple prompt lists with automatic list switching and an index
|
||||
- No existing node switches label + width + height together per category in one place
|
||||
- Smart Prompt Controller + Smart Profile Switch handle **uneven category sizes automatically** — 5 headshot prompts and 10 fullbody prompts cycle correctly without padding lists to match or manually tracking where one category ends and the next begins
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
**ComfyUI Manager:** search for `ComfyUI-Craftopia` and install.
|
||||
**ComfyUI Manager:** search for `ComfyUI-CraftKit` and install.
|
||||
|
||||
**Manual:**
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/CraftopiaStudio/ComfyUI-Craftopia
|
||||
git clone https://github.com/CraftopiaStudio/ComfyUI-CraftKit
|
||||
```
|
||||
Restart ComfyUI. All nodes appear under **Craftopia** in the node menu:
|
||||
Restart ComfyUI. All nodes appear under **CraftKit** in the node menu:
|
||||
|
||||
- `Craftopia` → **Smart Prompt Controller 🎛️**
|
||||
- `Craftopia` → **Smart Resize 📐**
|
||||
- `Craftopia` → **Smart Batch Resize 📁**
|
||||
- `Craftopia` → **Smart Resolution Multiplier 📏**
|
||||
- `CraftKit` → **Smart Prompt Controller 📋**
|
||||
- `CraftKit` → **Smart Profile Switch 🧩**
|
||||
- `CraftKit` → **Smart Resize 📐**
|
||||
- `CraftKit` → **Smart Batch Resize 📁**
|
||||
- `CraftKit` → **Smart Resolution Multiplier 📏**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,11 +1,122 @@
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
from .smart_resize import NODE_CLASS_MAPPINGS as A, NODE_DISPLAY_NAME_MAPPINGS as A_NAMES
|
||||
from .smart_batch_resize import NODE_CLASS_MAPPINGS as B, NODE_DISPLAY_NAME_MAPPINGS as B_NAMES
|
||||
from .smart_resolution_multiplier import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES
|
||||
from .smart_prompt_controller import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES
|
||||
from .smart_prompt_controller import NODE_CLASS_MAPPINGS as A, NODE_DISPLAY_NAME_MAPPINGS as A_NAMES
|
||||
from .smart_profile_switch import NODE_CLASS_MAPPINGS as B, NODE_DISPLAY_NAME_MAPPINGS as B_NAMES
|
||||
from .smart_resize import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES
|
||||
from .smart_batch_resize import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES
|
||||
from .smart_resolution_multiplier import NODE_CLASS_MAPPINGS as E, NODE_DISPLAY_NAME_MAPPINGS as E_NAMES
|
||||
|
||||
NODE_CLASS_MAPPINGS = {**A, **B, **C, **D}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES}
|
||||
NODE_CLASS_MAPPINGS = {**A, **B, **C, **D, **E}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES, **E_NAMES}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
|
||||
@PromptServer.instance.routes.get("/craftkit/browse_folder")
|
||||
async def browse_folder(request):
|
||||
import subprocess, sys
|
||||
|
||||
def _open_dialog():
|
||||
if sys.platform != "win32":
|
||||
return ""
|
||||
ps = r"""
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class CraftKitFolderPicker
|
||||
{
|
||||
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
||||
private class FileOpenDialogRCW { }
|
||||
|
||||
[ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IFileDialog
|
||||
{
|
||||
[PreserveSig] int Show(IntPtr parent);
|
||||
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
||||
void SetFileTypeIndex(uint iFileType);
|
||||
void GetFileTypeIndex(out uint piFileType);
|
||||
void Advise(IntPtr pfde, out uint pdwCookie);
|
||||
void Unadvise(uint dwCookie);
|
||||
void SetOptions(uint fos);
|
||||
void GetOptions(out uint fos);
|
||||
void SetDefaultFolder(IntPtr psi);
|
||||
void SetFolder(IntPtr psi);
|
||||
void GetFolder(out IntPtr ppsi);
|
||||
void GetCurrentSelection(out IntPtr ppsi);
|
||||
void SetFileName(string pszName);
|
||||
void GetFileName(out IntPtr pszName);
|
||||
void SetTitle(string pszTitle);
|
||||
void SetOkButtonLabel(string pszText);
|
||||
void SetFileNameLabel(string pszLabel);
|
||||
void GetResult(out IShellItemLocal ppsi);
|
||||
void AddPlace(IntPtr psi, uint fdap);
|
||||
void SetDefaultExtension(string pszDefaultExtension);
|
||||
void Close(int hr);
|
||||
void SetClientGuid(ref Guid guid);
|
||||
void ClearClientData();
|
||||
void SetFilter(IntPtr pFilter);
|
||||
}
|
||||
|
||||
[ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IShellItemLocal
|
||||
{
|
||||
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
||||
void GetParent(out IShellItemLocal ppsi);
|
||||
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
||||
void Compare(IShellItemLocal psi, uint hint, out int piOrder);
|
||||
}
|
||||
|
||||
public static string ShowDialog(IntPtr owner, string title)
|
||||
{
|
||||
var dialog = (IFileDialog)new FileOpenDialogRCW();
|
||||
dialog.SetOptions(0x20u | 0x40u); // FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM
|
||||
if (!string.IsNullOrEmpty(title)) dialog.SetTitle(title);
|
||||
int hr = dialog.Show(owner);
|
||||
if (hr != 0) return null;
|
||||
IShellItemLocal item;
|
||||
dialog.GetResult(out item);
|
||||
IntPtr pszPath;
|
||||
item.GetDisplayName(0x80058000u, out pszPath); // SIGDN_FILESYSPATH
|
||||
string path = Marshal.PtrToStringAuto(pszPath);
|
||||
Marshal.FreeCoTaskMem(pszPath);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$r = ''
|
||||
$o = New-Object System.Windows.Forms.Form
|
||||
$o.TopMost = $true
|
||||
$o.ShowInTaskbar = $false
|
||||
$o.FormBorderStyle = 'None'
|
||||
$o.Width = 1; $o.Height = 1; $o.Opacity = 0
|
||||
$o.StartPosition = 'CenterScreen'
|
||||
$o.Add_Shown({
|
||||
$o.Activate()
|
||||
$path = [CraftKitFolderPicker]::ShowDialog($o.Handle, 'Select input folder')
|
||||
if ($path) { $script:r = $path }
|
||||
$o.Close()
|
||||
})
|
||||
$o.ShowDialog() | Out-Null
|
||||
$r
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", ps],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
folder = await loop.run_in_executor(pool, _open_dialog)
|
||||
|
||||
if folder and __import__("os").path.isdir(folder):
|
||||
return web.json_response({"ok": True, "path": folder})
|
||||
return web.json_response({"ok": False, "cancelled": True})
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1198 318">
|
||||
<!-- Generator: Adobe Illustrator 30.6.0, SVG Export Plug-In . SVG Version: 2.1.4 Build 109) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #7c7c82;
|
||||
font-size: 18px;
|
||||
letter-spacing: .11em;
|
||||
}
|
||||
|
||||
.st0, .st1, .st2 {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.st0, .st2 {
|
||||
font-family: Inter-Medium, Inter;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #e6e6e9;
|
||||
font-family: SegoeUI-Bold, 'Segoe UI';
|
||||
font-size: 69px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0em;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #252527;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: #473333;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: url(#linear-gradient1);
|
||||
}
|
||||
|
||||
.st5, .st6 {
|
||||
stroke: #3a3a3d;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
|
||||
.st7 {
|
||||
fill: #f28f41;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #c9c9cc;
|
||||
font-size: 27px;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="293.25" y1="555.26" x2="293.25" y2="554.26" gradientTransform="translate(-350133 175462) scale(1196 -316)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#1e1e20"/>
|
||||
<stop offset="1" stop-color="#141415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient1" x1="293.6" x2="293.6" xlink:href="#linear-gradient"/>
|
||||
</defs>
|
||||
<g id="Layer_2">
|
||||
<rect class="st6" x="1" y="1" width="1196" height="316" rx="20" ry="20"/>
|
||||
</g>
|
||||
<g id="Layer_3">
|
||||
<g>
|
||||
<path class="st5" d="M1015.83,43.69c-63.58,0-115.31,51.73-115.31,115.31s51.73,115.31,115.31,115.31,115.31-51.73,115.31-115.31-51.73-115.31-115.31-115.31Z"/>
|
||||
<path class="st3" d="M1178.92,159c0-75.96-52.19-139.95-122.6-158h-80.98c-70.41,18.05-122.6,82.04-122.6,158s52.19,139.95,122.6,158h80.98c70.4-18.05,122.6-82.05,122.6-158ZM1015.83,274.31c-63.58,0-115.31-51.73-115.31-115.31s51.73-115.31,115.31-115.31,115.31,51.73,115.31,115.31-51.73,115.31-115.31,115.31Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st4" d="M1006.29,92.19v-33.47c-47.89,4.39-86.19,42.43-90.95,90.21h33.76c4.6-29.27,27.84-52.37,57.19-56.74Z"/>
|
||||
<path class="st4" d="M948.94,169.07h-33.59c4.76,47.78,43.06,85.82,90.95,90.21v-32.41c-29.7-4.43-53.16-28.03-57.35-57.8Z"/>
|
||||
</g>
|
||||
<path class="st4" d="M1006.29,53.56V20.1c-68.91,4.93-124.13,59.99-129.32,128.83h33.21c4.8-50.62,45.38-90.95,96.11-95.37Z"/>
|
||||
<path class="st4" d="M910.18,169.07h-33.3c4.7,69.34,60.14,124.94,129.4,129.9v-34.52c-50.73-4.42-91.31-44.75-96.11-95.37Z"/>
|
||||
<g>
|
||||
<path class="st7" d="M1026.42,259.13c35.79-3.86,66-26.54,80.55-57.91h-32.11c8.4-11.77,13.34-26.16,13.34-41.69s-4.95-29.92-13.34-41.69h32.59c-14.35-31.92-44.84-55.07-81.03-58.97v29.43c-3.32-.47-6.71-.72-10.16-.72-39.67,0-71.95,32.27-71.95,71.95s32.27,71.95,71.95,71.95c3.45,0,6.84-.25,10.16-.72v28.37ZM1005.05,94.5c-2.69,3.65-5.12,8.71-7.22,15.12-1.44,4.38-2.66,9.23-3.66,14.43h-16.67c6.17-14.47,15.99-25.25,27.55-29.55ZM991.04,157.1h-20.44c.25-10.12,2.03-19.67,5.01-28.18h17.71c-1.38,8.73-2.16,18.28-2.27,28.18ZM993.31,190.14h-17.71c-2.98-8.5-4.76-18.06-5.01-28.18h20.44c.11,9.9.89,19.45,2.27,28.18ZM990.37,97.65c-3.66,3.11-7.07,6.9-10.15,11.34-3.16,4.56-5.83,9.62-8,15.06h-12.87c7.32-11.7,18.15-21,31.01-26.4ZM956.59,128.92h13.87c-2.88,8.74-4.5,18.29-4.73,28.18h-16.49c.36-10.12,2.97-19.67,7.35-28.18ZM949.23,161.97h16.49c.23,9.89,1.85,19.44,4.73,28.18h-13.87c-4.38-8.5-6.99-18.06-7.35-28.18ZM959.36,195.01h12.87c2.17,5.44,4.84,10.5,8,15.06,3.07,4.44,6.48,8.23,10.15,11.34-12.86-5.4-23.69-14.7-31.01-26.4ZM977.5,195.01h16.67c1,5.2,2.22,10.05,3.66,14.43,2.1,6.41,4.53,11.47,7.22,15.12-11.56-4.3-21.38-15.08-27.55-29.55ZM1013.83,226.03c-5.62-2.6-11.27-13.94-14.69-31.03h14.69v31.03ZM1013.83,190.14h-15.58c-1.37-8.33-2.23-17.82-2.34-28.18h17.92v28.18ZM1013.83,157.1h-17.92c.12-10.36.97-19.84,2.34-28.18h15.58v28.18ZM1013.83,124.05h-14.69c3.42-17.09,9.07-28.43,14.69-31.03v31.03ZM1060.3,195.01h12.87c-7.32,11.7-18.15,21-31.01,26.4,3.66-3.11,7.07-6.9,10.15-11.34,3.16-4.56,5.83-9.62,8-15.06ZM1056.92,190.14h-17.71c1.38-8.73,2.16-18.28,2.27-28.18h20.44c-.25,10.12-2.03,19.67-5.01,28.18ZM1041.48,157.1c-.11-9.9-.89-19.45-2.27-28.18h17.71c2.98,8.5,4.76,18.06,5.01,28.18h-20.44ZM1060.3,124.05c-2.17-5.44-4.84-10.5-8-15.06-3.07-4.44-6.48-8.23-10.15-11.34,12.86,5.4,23.69,14.69,31.01,26.4h-12.87ZM1055.03,124.05h-16.67c-1-5.2-2.22-10.05-3.66-14.43-2.1-6.41-4.53-11.47-7.22-15.12,11.56,4.3,21.38,15.08,27.55,29.55ZM1055.03,195.01c-6.17,14.47-15.99,25.26-27.55,29.55,2.69-3.65,5.12-8.71,7.22-15.12,1.44-4.38,2.66-9.23,3.66-14.43h16.67ZM1075.94,190.14h-13.87c2.88-8.74,4.5-18.29,4.73-28.18h16.49c-.36,10.12-2.97,19.67-7.35,28.18ZM1083.29,157.1h-16.49c-.23-9.89-1.85-19.44-4.73-28.18h13.87c4.38,8.5,6.99,18.06,7.35,28.18ZM1018.7,93.03c5.62,2.6,11.27,13.94,14.69,31.03h-14.69v-31.03ZM1018.7,128.92h15.58c1.37,8.33,2.23,17.82,2.34,28.18h-17.92v-28.18ZM1018.7,161.97h17.92c-.12,10.36-.97,19.84-2.34,28.18h-15.58v-28.18ZM1018.7,226.03v-31.03h14.69c-3.42,17.09-9.07,28.43-14.69,31.03Z"/>
|
||||
<path class="st7" d="M1113.1,117.84h36.74c-16.82-53.73-65.25-93.6-123.42-97.76v33.61c39.08,4,71.93,29.35,86.68,64.16Z"/>
|
||||
<path class="st7" d="M1026.42,264.31v34.67c58.16-4.16,106.6-44.03,123.42-97.76h-37.22c-14.96,34.26-47.52,59.13-86.19,63.09Z"/>
|
||||
</g>
|
||||
<path class="st3" d="M805.08,159c0-62.85,27.66-119.35,71.44-158h-59.95c-35.65,44.79-55.02,99.9-55.02,158s19.37,113.21,55.02,158h59.95c-43.78-38.65-71.44-95.15-71.44-158Z"/>
|
||||
<g>
|
||||
<path class="st3" d="M1197,51.44v-30.44c0-11.05-8.95-20-20-20h-21.86c16.43,14.5,30.58,31.51,41.86,50.44Z"/>
|
||||
<path class="st3" d="M1155.14,317h21.86c11.05,0,20-8.95,20-20v-30.44c-11.28,18.92-25.43,35.93-41.86,50.44Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<text class="st1" transform="translate(77 187)"><tspan x="0" y="0">CraftKit</tspan></text>
|
||||
<text class="st2" transform="translate(81 237)"><tspan x="0" y="0">Smart resize, batch processing & prompt control for LoRA workflows</tspan></text>
|
||||
<text class="st0" transform="translate(81 279)"><tspan x="0" y="0">SMART RESIZE · BATCH RESIZE · RESOLUTION · PROMPT CONTROLLER</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 751 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 7,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Note",
|
||||
"pos": [0, -240],
|
||||
"size": {"0": 1840, "1": 190},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["CraftKit × SeedVR2 — Smart Upscale\n\nPattern:\n1. SmartResolutionMultiplier calculates the 2× target resolution from the input image automatically\n2. ImageScaleBy pre-downscales the input (0.75×) so SeedVR2 has room to work\n3. SeedVR2 upscales to the computed target resolution (connected from SmartResolutionMultiplier → resolution)\n4. SmartResize normalizes the output to a consistent longest side\n\nChange multiplier on SmartResolutionMultiplier to control the upscale factor. Change longest_side on SmartResize to control output size."],
|
||||
"color": "#2a3a2a",
|
||||
"bgcolor": "#1a2a1a"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [0, -10],
|
||||
"size": {"0": 280, "1": 320},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "image", "type": "COMBO", "widget": {"name": "image"}, "link": null},
|
||||
{"name": "upload", "type": "IMAGEUPLOAD", "widget": {"name": "upload"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "IMAGE", "type": "IMAGE", "links": [1, 2], "slot_index": 0},
|
||||
{"name": "MASK", "type": "MASK", "links": null, "slot_index": 1}
|
||||
],
|
||||
"properties": {"cnr_id": "comfy-core", "Node name for S&R": "LoadImage"},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "SmartResolutionMultiplier",
|
||||
"pos": [320, -10],
|
||||
"size": {"0": 320, "1": 150},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "image", "type": "IMAGE", "link": 1}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "width", "type": "INT", "links": null, "slot_index": 0},
|
||||
{"name": "height", "type": "INT", "links": null, "slot_index": 1},
|
||||
{"name": "resolution", "type": "INT", "links": [3], "slot_index": 2}
|
||||
],
|
||||
"properties": {"cnr_id": "comfyui-craftkit", "Node name for S&R": "SmartResolutionMultiplier"},
|
||||
"widgets_values": [2.0, 8]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "ImageScaleBy",
|
||||
"pos": [320, 180],
|
||||
"size": {"0": 270, "1": 130},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "image", "type": "IMAGE", "link": 2}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "IMAGE", "type": "IMAGE", "links": [4], "slot_index": 0}
|
||||
],
|
||||
"title": "Pre-downscale (0.75×)",
|
||||
"properties": {"cnr_id": "comfy-core", "Node name for S&R": "ImageScaleBy"},
|
||||
"widgets_values": ["lanczos", 0.75]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "SeedVR2LoadDiTModel",
|
||||
"pos": [680, -10],
|
||||
"size": {"0": 380, "1": 260},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "torch_compile_args", "shape": 7, "type": "TORCH_COMPILE_ARGS", "link": null},
|
||||
{"name": "model", "type": "COMBO", "widget": {"name": "model"}, "link": null},
|
||||
{"name": "device", "type": "COMBO", "widget": {"name": "device"}, "link": null},
|
||||
{"name": "blocks_to_swap", "shape": 7, "type": "INT", "widget": {"name": "blocks_to_swap"}, "link": null},
|
||||
{"name": "swap_io_components", "shape": 7, "type": "BOOLEAN", "widget": {"name": "swap_io_components"}, "link": null},
|
||||
{"name": "offload_device", "shape": 7, "type": "COMBO", "widget": {"name": "offload_device"}, "link": null},
|
||||
{"name": "cache_model", "shape": 7, "type": "BOOLEAN", "widget": {"name": "cache_model"}, "link": null},
|
||||
{"name": "attention_mode", "shape": 7, "type": "COMBO", "widget": {"name": "attention_mode"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "SEEDVR2_DIT", "type": "SEEDVR2_DIT", "links": [5], "slot_index": 0}
|
||||
],
|
||||
"properties": {"cnr_id": "seedvr2_videoupscaler", "Node name for S&R": "SeedVR2LoadDiTModel"},
|
||||
"widgets_values": ["seedvr2_ema_3b_fp8_e4m3fn.safetensors", "cuda:0", 0, false, "none", false, "sdpa"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SeedVR2LoadVAEModel",
|
||||
"pos": [680, 300],
|
||||
"size": {"0": 380, "1": 300},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "torch_compile_args", "shape": 7, "type": "TORCH_COMPILE_ARGS", "link": null},
|
||||
{"name": "model", "type": "COMBO", "widget": {"name": "model"}, "link": null},
|
||||
{"name": "device", "type": "COMBO", "widget": {"name": "device"}, "link": null},
|
||||
{"name": "encode_tiled", "shape": 7, "type": "BOOLEAN", "widget": {"name": "encode_tiled"}, "link": null},
|
||||
{"name": "encode_tile_size", "shape": 7, "type": "INT", "widget": {"name": "encode_tile_size"}, "link": null},
|
||||
{"name": "encode_tile_overlap", "shape": 7, "type": "INT", "widget": {"name": "encode_tile_overlap"}, "link": null},
|
||||
{"name": "decode_tiled", "shape": 7, "type": "BOOLEAN", "widget": {"name": "decode_tiled"}, "link": null},
|
||||
{"name": "decode_tile_size", "shape": 7, "type": "INT", "widget": {"name": "decode_tile_size"}, "link": null},
|
||||
{"name": "decode_tile_overlap", "shape": 7, "type": "INT", "widget": {"name": "decode_tile_overlap"}, "link": null},
|
||||
{"name": "tile_debug", "shape": 7, "type": "COMBO", "widget": {"name": "tile_debug"}, "link": null},
|
||||
{"name": "offload_device", "shape": 7, "type": "COMBO", "widget": {"name": "offload_device"}, "link": null},
|
||||
{"name": "cache_model", "shape": 7, "type": "BOOLEAN", "widget": {"name": "cache_model"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "SEEDVR2_VAE", "type": "SEEDVR2_VAE", "links": [6], "slot_index": 0}
|
||||
],
|
||||
"properties": {"cnr_id": "seedvr2_videoupscaler", "Node name for S&R": "SeedVR2LoadVAEModel"},
|
||||
"widgets_values": ["ema_vae_fp16.safetensors", "cuda:0", true, 1024, 128, true, 1024, 128, "false", "cpu", false]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SeedVR2VideoUpscaler",
|
||||
"pos": [1100, -10],
|
||||
"size": {"0": 380, "1": 560},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "image", "type": "IMAGE", "link": 4},
|
||||
{"name": "dit", "type": "SEEDVR2_DIT", "link": 5},
|
||||
{"name": "vae", "type": "SEEDVR2_VAE", "link": 6},
|
||||
{"name": "seed", "type": "INT", "widget": {"name": "seed"}, "link": null},
|
||||
{"name": "resolution", "type": "INT", "widget": {"name": "resolution"}, "link": 3},
|
||||
{"name": "max_resolution", "type": "INT", "widget": {"name": "max_resolution"}, "link": null},
|
||||
{"name": "batch_size", "type": "INT", "widget": {"name": "batch_size"}, "link": null},
|
||||
{"name": "uniform_batch_size", "type": "BOOLEAN", "widget": {"name": "uniform_batch_size"}, "link": null},
|
||||
{"name": "color_correction", "type": "COMBO", "widget": {"name": "color_correction"}, "link": null},
|
||||
{"name": "temporal_overlap", "shape": 7, "type": "INT", "widget": {"name": "temporal_overlap"}, "link": null},
|
||||
{"name": "prepend_frames", "shape": 7, "type": "INT", "widget": {"name": "prepend_frames"}, "link": null},
|
||||
{"name": "input_noise_scale", "shape": 7, "type": "FLOAT", "widget": {"name": "input_noise_scale"}, "link": null},
|
||||
{"name": "latent_noise_scale", "shape": 7, "type": "FLOAT", "widget": {"name": "latent_noise_scale"}, "link": null},
|
||||
{"name": "offload_device", "shape": 7, "type": "COMBO", "widget": {"name": "offload_device"}, "link": null},
|
||||
{"name": "enable_debug", "shape": 7, "type": "BOOLEAN", "widget": {"name": "enable_debug"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "IMAGE", "type": "IMAGE", "links": [7], "slot_index": 0}
|
||||
],
|
||||
"properties": {"cnr_id": "seedvr2_videoupscaler", "Node name for S&R": "SeedVR2VideoUpscaler"},
|
||||
"widgets_values": [42, "fixed", 0, 1, false, "lab", 0, 0, 0.0, 0.0, "cpu", false]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "SmartResize",
|
||||
"pos": [1520, -10],
|
||||
"size": {"0": 320, "1": 240},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "image", "type": "IMAGE", "link": 7}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "image", "type": "IMAGE", "links": [], "slot_index": 0},
|
||||
{"name": "width", "type": "INT", "links": [], "slot_index": 1},
|
||||
{"name": "height", "type": "INT", "links": [], "slot_index": 2}
|
||||
],
|
||||
"properties": {"cnr_id": "comfyui-craftkit", "Node name for S&R": "SmartResize"},
|
||||
"widgets_values": [1024, 8, "lanczos", false]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "Note",
|
||||
"pos": [1520, 260],
|
||||
"size": {"0": 320, "1": 310},
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["Outputs:\n\n→ image (SmartResize)\n connect to PreviewImage or SaveImage\n\nTweak:\n• multiplier on SmartResolutionMultiplier\n 2.0 = double, 1.5 = 50% bigger\n• scale_by on Pre-downscale\n lower = more room for SeedVR2 to\n reconstruct detail\n• longest_side on SmartResize\n target output size after upscale\n\nModel options:\n• 3b fp8 — fast, less VRAM\n• 7b fp16 — best quality, high VRAM"],
|
||||
"color": "#2a3a2a",
|
||||
"bgcolor": "#1a2a1a"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 2, 0, 3, 0, "IMAGE"],
|
||||
[2, 2, 0, 4, 0, "IMAGE"],
|
||||
[3, 3, 2, 7, 4, "INT"],
|
||||
[4, 4, 0, 7, 0, "IMAGE"],
|
||||
[5, 5, 0, 7, 1, "SEEDVR2_DIT"],
|
||||
[6, 6, 0, 7, 2, "SEEDVR2_VAE"],
|
||||
[7, 7, 0, 8, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Input",
|
||||
"bounding": [-20, -30, 640, 390],
|
||||
"color": "#4a2d5a",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "SeedVR2 Models",
|
||||
"bounding": [660, -30, 420, 640],
|
||||
"color": "#1e3d5a",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Upscale + Output",
|
||||
"bounding": [1080, -30, 800, 630],
|
||||
"color": "#1e4a4a",
|
||||
"font_size": 24
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {"scale": 0.6, "offset": [-20, 230]}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "SmartBatchResize",
|
||||
"pos": [60, 80],
|
||||
"size": [340, 366],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"links": [1],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "INT",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SmartBatchResize"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
1024,
|
||||
8,
|
||||
"lanczos",
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
true,
|
||||
false,
|
||||
"resized",
|
||||
true,
|
||||
"_"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PreviewImage",
|
||||
"pos": [460, 80],
|
||||
"size": [320, 280],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "Note",
|
||||
"pos": [460, 390],
|
||||
"size": [340, 400],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"text": "Smart Batch Resize — how it works\n\nThe node ALWAYS writes resized images to disk — connecting the outputs is optional.\n\nOutputs:\n📷 images → batch tensor, feed into PreviewImage or any IMAGE input\n🔢 count → number of images actually processed\n\nKey settings:\n📁 Input folder — pick with Browse folder, or paste a path\n📐 Longest side — target size, aspect ratio is always preserved\n🔢 Round to multiple of — snap dimensions (use 8 for SD/Flux)\n🏷️ Filename prefix / Keep original filename / Add counter — build the output filename\n📂 Create resolution subfolder / Custom output subfolder — where resized files land (default: input_folder/resized/)\n⏭️ Skip existing files — don't reprocess files already in the output folder\n\n▶️ Run Batch = same as the regular Run button — it just runs this node. No separate output folder needed: results are saved inside the input folder itself (in the resized/ subfolder).\n\nTip: leave the prefix empty and 'Keep original filename' on to preserve names, or turn it off and use a prefix + counter for a clean renumbered batch."
|
||||
},
|
||||
"widgets_values": [
|
||||
"Smart Batch Resize — how it works\n\nThe node ALWAYS writes resized images to disk — connecting the outputs is optional.\n\nOutputs:\n📷 images → batch tensor, feed into PreviewImage or any IMAGE input\n🔢 count → number of images actually processed\n\nKey settings:\n📁 Input folder — pick with Browse folder, or paste a path\n📐 Longest side — target size, aspect ratio is always preserved\n🔢 Round to multiple of — snap dimensions (use 8 for SD/Flux)\n🏷️ Filename prefix / Keep original filename / Add counter — build the output filename\n📂 Create resolution subfolder / Custom output subfolder — where resized files land (default: input_folder/resized/)\n⏭️ Skip existing files — don't reprocess files already in the output folder\n\n▶️ Run Batch = same as the regular Run button — it just runs this node. No separate output folder needed: results are saved inside the input folder itself (in the resized/ subfolder).\n\nTip: leave the prefix empty and 'Keep original filename' on to preserve names, or turn it off and use a prefix + counter for a clean renumbered batch."
|
||||
],
|
||||
"color": "#2a3a2a",
|
||||
"bgcolor": "#1a2a1a"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 2, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 7,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Note",
|
||||
"pos": [0, -240],
|
||||
"size": {"0": 1090, "1": 180},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["CraftKit — Smart Prompt Controller + Smart Profile Switch\n\nSet index to 1 and control_after_generate to 'increment' — cycles through all 16 prompts automatically, one per generation.\n\nSmartPromptController picks the right prompt line and tells SmartProfileSwitch which profile is active (1–4).\nSmartProfileSwitch switches the resolution and label to match. Connect the outputs on the right to your sampler pipeline."],
|
||||
"color": "#2a3a2a",
|
||||
"bgcolor": "#1a2a1a"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [0, -20],
|
||||
"size": {"0": 340, "1": 210},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "STRING", "widget": {"name": "value"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "STRING", "type": "STRING", "links": [1], "slot_index": 0}
|
||||
],
|
||||
"title": "Headshot Prompts",
|
||||
"properties": {"cnr_id": "comfy-core", "ver": "0.17.0", "Node name for S&R": "PrimitiveStringMultiline"},
|
||||
"widgets_values": ["01 headshot portrait, face and neck only, neutral expression, soft studio lighting, clean background\n02 headshot portrait, slight smile, diffused window light, plain white background\n03 headshot portrait, professional look, direct gaze, high-key studio lighting\n04 headshot portrait, relaxed expression, warm natural daylight, simple neutral background"],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [360, -20],
|
||||
"size": {"0": 340, "1": 210},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "STRING", "widget": {"name": "value"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "STRING", "type": "STRING", "links": [2], "slot_index": 0}
|
||||
],
|
||||
"title": "Halfbody Prompts",
|
||||
"properties": {"cnr_id": "comfy-core", "ver": "0.17.0", "Node name for S&R": "PrimitiveStringMultiline"},
|
||||
"widgets_values": ["05 half-body portrait, shoulders and chest visible, neutral expression, studio lighting, grey background\n06 half-body portrait, slight smile, arms relaxed at sides, soft window light from left\n07 half-body portrait, confident pose, looking slightly to the side, clean background\n08 half-body portrait, arms crossed, composed expression, even studio lighting"],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [0, 210],
|
||||
"size": {"0": 340, "1": 210},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "STRING", "widget": {"name": "value"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "STRING", "type": "STRING", "links": [3], "slot_index": 0}
|
||||
],
|
||||
"title": "Fullbody Prompts",
|
||||
"properties": {"cnr_id": "comfy-core", "ver": "0.17.0", "Node name for S&R": "PrimitiveStringMultiline"},
|
||||
"widgets_values": ["09 full-body portrait, standing straight, neutral pose, studio lighting, clean background\n10 full-body portrait, casual relaxed pose, hands in pockets, soft outdoor light\n11 full-body portrait, mid-stride walking pose, urban setting, natural daylight\n12 full-body portrait, dynamic confident pose, weight on one leg, plain backdrop"],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [360, 210],
|
||||
"size": {"0": 340, "1": 210},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "value", "type": "STRING", "widget": {"name": "value"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "STRING", "type": "STRING", "links": [4], "slot_index": 0}
|
||||
],
|
||||
"title": "Tall Portrait Prompts",
|
||||
"properties": {"cnr_id": "comfy-core", "ver": "0.17.0", "Node name for S&R": "PrimitiveStringMultiline"},
|
||||
"widgets_values": ["13 tall portrait, full figure head to toe, neutral standing pose, studio lighting, clean background\n14 tall portrait, elegant standing pose, one hand on hip, soft diffused light\n15 tall portrait, outdoor setting, warm golden hour rim light, natural relaxed pose\n16 tall portrait, indoor setting, leaning against wall, soft directional light"],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SmartPromptController",
|
||||
"pos": [740, -20],
|
||||
"size": {"0": 300, "1": 290},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "prompt_list_1", "type": "STRING", "link": 1},
|
||||
{"name": "prompt_list_2", "type": "STRING", "link": 2},
|
||||
{"name": "prompt_list_3", "type": "STRING", "link": 3},
|
||||
{"name": "prompt_list_4", "type": "STRING", "link": 4},
|
||||
{"name": "index", "type": "INT", "widget": {"name": "index"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "prompt", "type": "STRING", "links": [], "slot_index": 0},
|
||||
{"name": "active_list", "type": "INT", "links": [7], "slot_index": 1}
|
||||
],
|
||||
"properties": {"cnr_id": "comfyui-craftkit", "Node name for S&R": "SmartPromptController"},
|
||||
"widgets_values": [1, "increment"],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "SmartProfileSwitch",
|
||||
"pos": [740, 310],
|
||||
"size": {"0": 300, "1": 530},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "index", "type": "INT", "widget": {"name": "index"}, "link": 7},
|
||||
{"name": "label_1", "type": "STRING", "widget": {"name": "label_1"}, "link": null},
|
||||
{"name": "width_1", "type": "INT", "widget": {"name": "width_1"}, "link": null},
|
||||
{"name": "height_1", "type": "INT", "widget": {"name": "height_1"}, "link": null},
|
||||
{"name": "label_2", "type": "STRING", "widget": {"name": "label_2"}, "link": null},
|
||||
{"name": "width_2", "type": "INT", "widget": {"name": "width_2"}, "link": null},
|
||||
{"name": "height_2", "type": "INT", "widget": {"name": "height_2"}, "link": null},
|
||||
{"name": "label_3", "type": "STRING", "widget": {"name": "label_3"}, "link": null},
|
||||
{"name": "width_3", "type": "INT", "widget": {"name": "width_3"}, "link": null},
|
||||
{"name": "height_3", "type": "INT", "widget": {"name": "height_3"}, "link": null},
|
||||
{"name": "label_4", "type": "STRING", "widget": {"name": "label_4"}, "link": null},
|
||||
{"name": "width_4", "type": "INT", "widget": {"name": "width_4"}, "link": null},
|
||||
{"name": "height_4", "type": "INT", "widget": {"name": "height_4"}, "link": null}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "label", "type": "STRING", "links": [], "slot_index": 0},
|
||||
{"name": "width", "type": "INT", "links": [], "slot_index": 1},
|
||||
{"name": "height", "type": "INT", "links": [], "slot_index": 2}
|
||||
],
|
||||
"properties": {"cnr_id": "comfyui-craftkit", "Node name for S&R": "SmartProfileSwitch"},
|
||||
"widgets_values": [1, "Headshots", 1024, 1024, "Halfbody", 832, 1216, "Fullbody", 768, 1344, "Tall Portrait", 640, 1536],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "Note",
|
||||
"pos": [1080, -20],
|
||||
"size": {"0": 300, "1": 380},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["Connect to your pipeline:\n\n→ prompt (SmartPromptController)\n positive CLIP text encoder\n\n→ label (SmartProfileSwitch)\n filename prefix or suffix\n e.g. SaveImageExtended filename_keys\n\n→ width / height (SmartProfileSwitch)\n EmptyLatentImage or EmptySD3LatentImage\n\n─────────────────\n\nTip: label auto-switches per category every generation — no manual filename changes needed."],
|
||||
"color": "#2a3a2a",
|
||||
"bgcolor": "#1a2a1a"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 3, 0, 7, 0, "STRING"],
|
||||
[2, 4, 0, 7, 1, "STRING"],
|
||||
[3, 5, 0, 7, 2, "STRING"],
|
||||
[4, 6, 0, 7, 3, "STRING"],
|
||||
[7, 7, 1, 8, 0, "INT"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Prompt Lists",
|
||||
"bounding": [-20, -40, 720, 470],
|
||||
"color": "#2d5a3a",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "CraftKit Control",
|
||||
"bounding": [720, -40, 340, 880],
|
||||
"color": "#1e3d5a",
|
||||
"font_size": 24
|
||||
},
|
||||
{
|
||||
"title": "Connect to Your Pipeline",
|
||||
"bounding": [1060, -40, 350, 440],
|
||||
"color": "#1e4a4a",
|
||||
"font_size": 24
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {"scale": 0.7, "offset": [-30, 230]}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Craftopia.SmartBatchResize",
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "SmartBatchResize") return;
|
||||
|
||||
const folderWidget = node.widgets?.find(w => w.name === "input_folder");
|
||||
if (!folderWidget) return;
|
||||
|
||||
const btn = node.addWidget("button", "📁 Browse folder", null, async () => {
|
||||
try {
|
||||
const res = await fetch("/craftkit/browse_folder");
|
||||
const data = await res.json();
|
||||
if (data.ok && data.path) {
|
||||
folderWidget.value = data.path;
|
||||
node.setDirtyCanvas(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[SmartBatchResize] Browse failed:", e);
|
||||
}
|
||||
}, { serialize: false });
|
||||
|
||||
btn.serialize = false;
|
||||
|
||||
// Move Browse button to right after input_folder (index 1)
|
||||
const folderIdx = node.widgets.indexOf(folderWidget);
|
||||
const btnIdx = node.widgets.indexOf(btn);
|
||||
if (btnIdx !== folderIdx + 1) {
|
||||
node.widgets.splice(btnIdx, 1);
|
||||
node.widgets.splice(folderIdx + 1, 0, btn);
|
||||
}
|
||||
|
||||
// Compact preset chips for longest_side. Start from a REAL button widget
|
||||
// (so all of ComfyUI's layout/computeSize/sizing integration is correct)
|
||||
// and only override its draw + mouse. A bare custom widget with our own
|
||||
// computeSize forced the node width and broke the layout on click.
|
||||
const longestSideWidget = node.widgets?.find(w => w.name === "longest_side");
|
||||
if (longestSideWidget) {
|
||||
const PRESETS = [512, 768, 1024, 1536];
|
||||
|
||||
// Per-chip click handled in the button callback (buttons route clicks
|
||||
// to their callback, not to .mouse). We read the pointer position from
|
||||
// the canvas at click time and match it against the drawn chip rects.
|
||||
const presetWidget = node.addWidget("button", "size_presets", null, (value, canvas, node) => {
|
||||
const gm = (canvas || app.canvas)?.graph_mouse;
|
||||
if (!gm) return;
|
||||
const lx = gm[0] - node.pos[0];
|
||||
const ly = gm[1] - node.pos[1];
|
||||
for (const r of presetWidget._rects) {
|
||||
if (lx >= r.x1 && lx <= r.x2 && ly >= r.y1 && ly <= r.y2) {
|
||||
longestSideWidget.value = r.size;
|
||||
longestSideWidget.callback?.(r.size, app.canvas, node);
|
||||
node.setDirtyCanvas(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, { serialize: false });
|
||||
presetWidget.serialize = false;
|
||||
presetWidget._rects = [];
|
||||
|
||||
presetWidget.draw = function (ctx, node, widgetWidth, y, height) {
|
||||
const margin = 14;
|
||||
const gap = 5;
|
||||
const innerW = Math.max(40, widgetWidth - margin * 2);
|
||||
const n = PRESETS.length;
|
||||
const cellW = Math.max(10, (innerW - gap * (n - 1)) / n);
|
||||
const h = Math.min(height - 2, 18);
|
||||
const top = y + (height - h) / 2;
|
||||
const current = longestSideWidget.value;
|
||||
this._rects = [];
|
||||
|
||||
ctx.save();
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = margin + i * (cellW + gap);
|
||||
const active = Number(current) === PRESETS[i];
|
||||
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(x, top, cellW, h, 4);
|
||||
else ctx.rect(x, top, cellW, h);
|
||||
ctx.fillStyle = active ? "#f28f41" : "#2a2a2a";
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = active ? "#f28f41" : "#555";
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = active ? "#111" : "#ccc";
|
||||
ctx.fillText(String(PRESETS[i]), x + cellW / 2, top + h / 2 + 0.5);
|
||||
|
||||
this._rects.push({ x1: x, x2: x + cellW, y1: top, y2: top + h, size: PRESETS[i] });
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Move preset row to right after longest_side
|
||||
const lsIdx = node.widgets.indexOf(longestSideWidget);
|
||||
const pwIdx = node.widgets.indexOf(presetWidget);
|
||||
if (pwIdx !== lsIdx + 1) {
|
||||
node.widgets.splice(pwIdx, 1);
|
||||
node.widgets.splice(lsIdx + 1, 0, presetWidget);
|
||||
}
|
||||
}
|
||||
|
||||
// Override serialize so non-serializable JS widgets (Browse, presets, status,
|
||||
// run batch) are excluded from widgets_values in the saved workflow JSON.
|
||||
// Without this, LiteGraph saves null slots for these widgets which then shift
|
||||
// all Python widget values on load (configure runs before nodeCreated).
|
||||
// Invariant: Python widgets must stay in their original relative order in
|
||||
// this.widgets — JS widgets may be spliced in between, but never reorder
|
||||
// the Python ones, or the positional value assignment on load will silently
|
||||
// misalign.
|
||||
const origSerialize = node.serialize;
|
||||
node.serialize = function () {
|
||||
const data = origSerialize.call(this);
|
||||
if (data.widgets_values) {
|
||||
data.widgets_values = this.widgets
|
||||
.filter(w => w.serialize !== false && w.options?.serialize !== false)
|
||||
.map(w => w.value);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// Canvas-drawn status display
|
||||
const statusWidget = node.addWidget("button", "_sbr_status", null, () => {}, { serialize: false });
|
||||
statusWidget.serialize = false;
|
||||
statusWidget._text = "";
|
||||
|
||||
statusWidget.draw = function (ctx, node, widgetWidth, y, height) {
|
||||
const margin = 14;
|
||||
const innerW = widgetWidth - margin * 2;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(margin, y + 2, innerW, height - 4, 4);
|
||||
else ctx.rect(margin, y + 2, innerW, height - 4);
|
||||
ctx.fillStyle = "#111";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = this._text ? "#3a3a3a" : "#222";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = this._text ? "#f28f41" : "#666";
|
||||
ctx.fillText(this._text || "— RUN TO PROCESS —", widgetWidth / 2, y + height / 2);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Run Batch button
|
||||
const runBtn = node.addWidget("button", "▶ Run Batch", null, () => {
|
||||
app.queuePrompt(0, 1).catch(e => console.error("[SmartBatchResize] Queue failed:", e));
|
||||
}, { serialize: false });
|
||||
runBtn.serialize = false;
|
||||
|
||||
// Show/hide counter_start based on use_counter toggle
|
||||
const useCounterWidget = node.widgets?.find(w => w.name === "use_counter");
|
||||
const counterStartWidget = node.widgets?.find(w => w.name === "counter_start");
|
||||
if (useCounterWidget && counterStartWidget) {
|
||||
const defaultComputeSize = counterStartWidget.computeSize;
|
||||
const updateCounterVisibility = () => {
|
||||
counterStartWidget.computeSize = useCounterWidget.value
|
||||
? defaultComputeSize
|
||||
: () => [0, -4];
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
const origCallback = useCounterWidget.callback;
|
||||
useCounterWidget.callback = function (...args) {
|
||||
origCallback?.call(this, ...args);
|
||||
updateCounterVisibility();
|
||||
};
|
||||
const origOnConfigure = node.onConfigure;
|
||||
node.onConfigure = function (...args) {
|
||||
origOnConfigure?.call(this, ...args);
|
||||
updateCounterVisibility();
|
||||
};
|
||||
updateCounterVisibility();
|
||||
}
|
||||
|
||||
// Hide any customtext widget already present (e.g. from a previously run workflow)
|
||||
const hideCustomText = () => {
|
||||
for (const w of node.widgets) {
|
||||
if (w.type === "customtext") w.computeSize = () => [0, -4];
|
||||
}
|
||||
};
|
||||
hideCustomText();
|
||||
|
||||
// Intercept onExecuted to update status and hide the default customtext output
|
||||
const origOnExecuted = node.onExecuted;
|
||||
node.onExecuted = function (output) {
|
||||
origOnExecuted?.call(this, output);
|
||||
if (output?.text?.[0]) {
|
||||
statusWidget._text = output.text[0];
|
||||
hideCustomText();
|
||||
}
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Craftopia.SmartProfileSwitch",
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "SmartProfileSwitch") return;
|
||||
|
||||
// Slot header dividers — visual only, not serialized
|
||||
const dividers = [];
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const div = node.addWidget("button", `_div_${i}`, null, () => {}, { serialize: false });
|
||||
div.serialize = false;
|
||||
const num = i;
|
||||
div.draw = function (ctx, node, widgetWidth, y, height) {
|
||||
const lx = 14;
|
||||
const label = `SLOT ${num}`;
|
||||
ctx.save();
|
||||
ctx.font = "bold 10px sans-serif";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.fillText(label, lx, y + height / 2);
|
||||
const lineX = lx + ctx.measureText(label).width + 8;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lineX, y + height / 2);
|
||||
ctx.lineTo(widgetWidth - 14, y + height / 2);
|
||||
ctx.strokeStyle = "#333";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
};
|
||||
dividers.push(div);
|
||||
}
|
||||
|
||||
// Status widget — shows active slot after execution
|
||||
const statusWidget = node.addWidget("button", "_sps_status", null, () => {}, { serialize: false });
|
||||
statusWidget.serialize = false;
|
||||
statusWidget._text = "";
|
||||
|
||||
statusWidget.draw = function (ctx, node, widgetWidth, y, height) {
|
||||
const margin = 14;
|
||||
const innerW = widgetWidth - margin * 2;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(margin, y + 2, innerW, height - 4, 4);
|
||||
else ctx.rect(margin, y + 2, innerW, height - 4);
|
||||
ctx.fillStyle = "#111";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = this._text ? "#3a3a3a" : "#222";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = this._text ? "#f28f41" : "#666";
|
||||
ctx.fillText(
|
||||
this._text || "— RUN TO SEE RESULT —",
|
||||
widgetWidth / 2,
|
||||
y + height / 2
|
||||
);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Reorder: index, [div1, label_1, w_1, h_1], ..., [div4, label_4, w_4, h_4], status
|
||||
const indexWidget = node.widgets.find(w => w.name === "index");
|
||||
const slotWidgets = [];
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
slotWidgets.push(
|
||||
dividers[i - 1],
|
||||
node.widgets.find(w => w.name === `label_${i}`),
|
||||
node.widgets.find(w => w.name === `width_${i}`),
|
||||
node.widgets.find(w => w.name === `height_${i}`),
|
||||
);
|
||||
}
|
||||
node.widgets = [indexWidget, ...slotWidgets, statusWidget];
|
||||
|
||||
// Serialize override — keeps non-serializable widgets out of widgets_values
|
||||
const origSerialize = node.serialize;
|
||||
node.serialize = function () {
|
||||
const data = origSerialize.call(this);
|
||||
if (data.widgets_values) {
|
||||
data.widgets_values = this.widgets
|
||||
.filter(w => w.serialize !== false && w.options?.serialize !== false)
|
||||
.map(w => w.value);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// onExecuted — update status, hide default customtext textarea
|
||||
const origOnExecuted = node.onExecuted;
|
||||
node.onExecuted = function (output) {
|
||||
origOnExecuted?.call(this, output);
|
||||
if (output?.text?.[0]) {
|
||||
statusWidget._text = output.text[0];
|
||||
for (const w of node.widgets) {
|
||||
if (w !== statusWidget && w.type === "customtext") {
|
||||
w.computeSize = () => [0, -4];
|
||||
}
|
||||
}
|
||||
}
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -6,24 +6,55 @@ app.registerExtension({
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "SmartPromptController") return;
|
||||
|
||||
// Status widget — no visible label name
|
||||
const widget = node.addWidget("text", "", "", () => {});
|
||||
widget.serializeValue = () => undefined;
|
||||
// Canvas-drawn result display — clearly an output, not an input field
|
||||
const statusWidget = node.addWidget("button", "_spc_result", null, () => {}, { serialize: false });
|
||||
statusWidget.serialize = false;
|
||||
statusWidget._text = "";
|
||||
|
||||
const onExecuted = node.onExecuted;
|
||||
statusWidget.draw = function (ctx, node, widgetWidth, y, height) {
|
||||
const margin = 14;
|
||||
const innerW = widgetWidth - margin * 2;
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(margin, y + 2, innerW, height - 4, 4);
|
||||
else ctx.rect(margin, y + 2, innerW, height - 4);
|
||||
ctx.fillStyle = "#111";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = this._text ? "#3a3a3a" : "#222";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = this._text ? "#f28f41" : "#666";
|
||||
ctx.fillText(
|
||||
this._text || "— RUN TO SEE RESULT —",
|
||||
widgetWidth / 2,
|
||||
y + height / 2
|
||||
);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Intercept onExecuted: update our widget, hide ComfyUI's default textarea
|
||||
const origOnExecuted = node.onExecuted;
|
||||
node.onExecuted = function (output) {
|
||||
const w = node.size[0];
|
||||
const h = node.size[1];
|
||||
|
||||
onExecuted?.apply(this, arguments);
|
||||
origOnExecuted?.call(this, output);
|
||||
|
||||
if (output?.text?.[0]) {
|
||||
widget.value = output.text[0];
|
||||
statusWidget._text = output.text[0];
|
||||
|
||||
// Collapse the default customtext widget ComfyUI adds for ui.text
|
||||
for (const w of node.widgets) {
|
||||
if (w !== statusWidget && w.type === "customtext") {
|
||||
w.computeSize = () => [0, -4];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve node size
|
||||
node.size[0] = Math.max(w, node.size[0]);
|
||||
node.size[1] = Math.max(h, node.size[1]);
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Craftopia.SmartResize",
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "SmartResize") return;
|
||||
|
||||
const longestSideWidget = node.widgets?.find(w => w.name === "longest_side");
|
||||
if (!longestSideWidget) return;
|
||||
|
||||
const PRESETS = [512, 768, 1024, 1536];
|
||||
|
||||
const presetWidget = node.addWidget("button", "size_presets", null, (value, canvas, node) => {
|
||||
const gm = (canvas || app.canvas)?.graph_mouse;
|
||||
if (!gm) return;
|
||||
const lx = gm[0] - node.pos[0];
|
||||
const ly = gm[1] - node.pos[1];
|
||||
for (const r of presetWidget._rects) {
|
||||
if (lx >= r.x1 && lx <= r.x2 && ly >= r.y1 && ly <= r.y2) {
|
||||
longestSideWidget.value = r.size;
|
||||
longestSideWidget.callback?.(r.size, app.canvas, node);
|
||||
node.setDirtyCanvas(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, { serialize: false });
|
||||
presetWidget.serialize = false;
|
||||
presetWidget._rects = [];
|
||||
|
||||
presetWidget.draw = function (ctx, node, widgetWidth, y, height) {
|
||||
const margin = 14;
|
||||
const gap = 5;
|
||||
const innerW = Math.max(40, widgetWidth - margin * 2);
|
||||
const n = PRESETS.length;
|
||||
const cellW = Math.max(10, (innerW - gap * (n - 1)) / n);
|
||||
const h = Math.min(height - 2, 18);
|
||||
const top = y + (height - h) / 2;
|
||||
const current = longestSideWidget.value;
|
||||
this._rects = [];
|
||||
|
||||
ctx.save();
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = margin + i * (cellW + gap);
|
||||
const active = Number(current) === PRESETS[i];
|
||||
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(x, top, cellW, h, 4);
|
||||
else ctx.rect(x, top, cellW, h);
|
||||
ctx.fillStyle = active ? "#f28f41" : "#2a2a2a";
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = active ? "#f28f41" : "#555";
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = active ? "#111" : "#ccc";
|
||||
ctx.fillText(String(PRESETS[i]), x + cellW / 2, top + h / 2 + 0.5);
|
||||
|
||||
this._rects.push({ x1: x, x2: x + cellW, y1: top, y2: top + h, size: PRESETS[i] });
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Override serialize to exclude the non-serializable preset widget from
|
||||
// widgets_values — same drift fix as SmartBatchResize (see that file for details).
|
||||
const origSerialize = node.serialize;
|
||||
node.serialize = function () {
|
||||
const data = origSerialize.call(this);
|
||||
if (data.widgets_values) {
|
||||
data.widgets_values = this.widgets
|
||||
.filter(w => w.serialize !== false && w.options?.serialize !== false)
|
||||
.map(w => w.value);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// Move preset row to right after longest_side
|
||||
const lsIdx = node.widgets.indexOf(longestSideWidget);
|
||||
const pwIdx = node.widgets.indexOf(presetWidget);
|
||||
if (pwIdx !== lsIdx + 1) {
|
||||
node.widgets.splice(pwIdx, 1);
|
||||
node.widgets.splice(lsIdx + 1, 0, presetWidget);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"SmartBatchResize": {
|
||||
"display_name": "Smart Batch Resize 📁",
|
||||
"inputs": {
|
||||
"input_folder": { "name": "Input folder" },
|
||||
"longest_side": { "name": "Longest side (px)" },
|
||||
"multiple_of": { "name": "Round to multiple of" },
|
||||
"interpolation": { "name": "Interpolation method" },
|
||||
"prefix": { "name": "Filename prefix" },
|
||||
"use_original_name": { "name": "Keep original filename" },
|
||||
"use_counter": { "name": "Add counter" },
|
||||
"counter_start": { "name": "Counter start" },
|
||||
"suffix_resolution": { "name": "Add resolution to filename" },
|
||||
"folder_resolution": { "name": "Create resolution subfolder" },
|
||||
"folder_custom": { "name": "Custom output subfolder" },
|
||||
"skip_if_exists": { "name": "Skip existing files" },
|
||||
"delimiter": { "name": "Filename delimiter" }
|
||||
},
|
||||
"outputs": {
|
||||
"0": { "name": "Images" },
|
||||
"1": { "name": "Count" }
|
||||
}
|
||||
},
|
||||
"SmartResize": {
|
||||
"display_name": "Smart Resize 📐",
|
||||
"inputs": {
|
||||
"image": { "name": "Image" },
|
||||
"longest_side": { "name": "Longest side (px)" },
|
||||
"multiple_of": { "name": "Round to multiple of" },
|
||||
"interpolation": { "name": "Interpolation method" },
|
||||
"upscale_if_smaller": { "name": "Upscale if smaller" }
|
||||
},
|
||||
"outputs": {
|
||||
"0": { "name": "Image" },
|
||||
"1": { "name": "Width" },
|
||||
"2": { "name": "Height" }
|
||||
}
|
||||
},
|
||||
"SmartResolutionMultiplier": {
|
||||
"display_name": "Smart Resolution Multiplier 📏",
|
||||
"inputs": {
|
||||
"image": { "name": "Image" },
|
||||
"multiplier": { "name": "Multiplier" },
|
||||
"multiple_of": { "name": "Round to multiple of" }
|
||||
},
|
||||
"outputs": {
|
||||
"0": { "name": "Width" },
|
||||
"1": { "name": "Height" },
|
||||
"2": { "name": "Resolution" }
|
||||
}
|
||||
},
|
||||
"SmartProfileSwitch": {
|
||||
"display_name": "Smart Profile Switch 🧩",
|
||||
"inputs": {
|
||||
"index": { "name": "Index" },
|
||||
"label_1": { "name": "Label 1" },
|
||||
"width_1": { "name": "Width 1" },
|
||||
"height_1": { "name": "Height 1" },
|
||||
"label_2": { "name": "Label 2" },
|
||||
"width_2": { "name": "Width 2" },
|
||||
"height_2": { "name": "Height 2" },
|
||||
"label_3": { "name": "Label 3" },
|
||||
"width_3": { "name": "Width 3" },
|
||||
"height_3": { "name": "Height 3" },
|
||||
"label_4": { "name": "Label 4" },
|
||||
"width_4": { "name": "Width 4" },
|
||||
"height_4": { "name": "Height 4" }
|
||||
},
|
||||
"outputs": {
|
||||
"0": { "name": "Label" },
|
||||
"1": { "name": "Width" },
|
||||
"2": { "name": "Height" }
|
||||
}
|
||||
},
|
||||
"SmartPromptController": {
|
||||
"display_name": "Smart Prompt Controller 📋",
|
||||
"inputs": {
|
||||
"index": { "name": "Index" },
|
||||
"prompt_list_1": { "name": "Prompt list 1" },
|
||||
"prompt_list_2": { "name": "Prompt list 2" },
|
||||
"prompt_list_3": { "name": "Prompt list 3" },
|
||||
"prompt_list_4": { "name": "Prompt list 4" }
|
||||
},
|
||||
"outputs": {
|
||||
"0": { "name": "Prompt" },
|
||||
"1": { "name": "Active list" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "comfyui-craftkit"
|
||||
description = "Smart resize, batch resize, resolution multiplier and prompt controller nodes for ComfyUI"
|
||||
version = "1.0.3"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/CraftopiaStudio/ComfyUI-CraftKit"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "craftopiastudio"
|
||||
DisplayName = "ComfyUI-CraftKit"
|
||||
Icon = "https://raw.githubusercontent.com/CraftopiaStudio/ComfyUI-CraftKit/main/assets/icon.png"
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image as PILImage
|
||||
from PIL import Image as PILImage, ImageOps
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
@@ -29,6 +29,28 @@ def _calc_new_size(w, h, longest_side, multiple_of):
|
||||
return new_w, new_h
|
||||
|
||||
|
||||
def _build_stem(original_stem, prefix, use_original_name, use_counter, counter_index, counter_start, suffix_resolution, longest_side, delimiter):
|
||||
parts = []
|
||||
|
||||
if prefix:
|
||||
parts.append(prefix)
|
||||
|
||||
if use_original_name:
|
||||
parts.append(original_stem)
|
||||
|
||||
if use_counter:
|
||||
number = counter_start + counter_index
|
||||
parts.append(f"{number:03d}")
|
||||
|
||||
if suffix_resolution:
|
||||
parts.append(str(longest_side))
|
||||
|
||||
if not parts:
|
||||
parts.append(original_stem)
|
||||
|
||||
return delimiter.join(parts)
|
||||
|
||||
|
||||
class SmartBatchResize:
|
||||
|
||||
@classmethod
|
||||
@@ -41,7 +63,7 @@ class SmartBatchResize:
|
||||
"multiline": False,
|
||||
"tooltip": "Folder containing images to resize."
|
||||
}),
|
||||
|
||||
|
||||
# RESIZE SETTINGS
|
||||
"longest_side": ("INT", {
|
||||
"default": 1024, "min": 64, "max": 8192, "step": 64,
|
||||
@@ -52,20 +74,33 @@ class SmartBatchResize:
|
||||
"tooltip": "Snap both dimensions to a multiple of this value. Use 8 for SD/Flux."
|
||||
}),
|
||||
"interpolation": (["lanczos", "bicubic", "bilinear", "nearest"], {
|
||||
"default": "lanczos"
|
||||
"default": "lanczos",
|
||||
"tooltip": "Resampling method. Lanczos is sharpest for downscaling."
|
||||
}),
|
||||
|
||||
|
||||
# OUTPUT NAMING
|
||||
"suffix_resolution": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Append resolution to filename. E.g. image_1024.png"
|
||||
}),
|
||||
"suffix_custom": ("STRING", {
|
||||
"prefix": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Custom text to add before resolution. Only used when suffix_resolution is on."
|
||||
"tooltip": "Label prepended to filename. E.g. 'headshot' → headshot_photo_001_1024.jpg"
|
||||
}),
|
||||
|
||||
"use_original_name": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Include the original filename in the output name."
|
||||
}),
|
||||
"use_counter": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Add a sequential 3-digit counter to each filename (001, 002, ...)."
|
||||
}),
|
||||
"counter_start": ("INT", {
|
||||
"default": 1, "min": 0, "max": 99999, "step": 1,
|
||||
"tooltip": "Starting number for the counter. Useful when processing multiple batches."
|
||||
}),
|
||||
"suffix_resolution": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "Append resolution to filename. E.g. photo_1024.png"
|
||||
}),
|
||||
|
||||
# OUTPUT LOCATION
|
||||
"folder_resolution": ("BOOLEAN", {
|
||||
"default": False,
|
||||
@@ -74,9 +109,9 @@ class SmartBatchResize:
|
||||
"folder_custom": ("STRING", {
|
||||
"default": "resized",
|
||||
"multiline": False,
|
||||
"tooltip": "Subfolder name. Resolution is appended if folder_resolution is on."
|
||||
"tooltip": "Subfolder name. Resolution is appended if 'Create resolution subfolder' is on."
|
||||
}),
|
||||
|
||||
|
||||
# OPTIONS
|
||||
"skip_if_exists": ("BOOLEAN", {
|
||||
"default": True,
|
||||
@@ -85,50 +120,40 @@ class SmartBatchResize:
|
||||
"delimiter": ("STRING", {
|
||||
"default": "_",
|
||||
"multiline": False,
|
||||
"tooltip": "Separator between filename and folder parts. E.g. _ or -"
|
||||
"tooltip": "Separator between filename parts. E.g. _ or -"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT")
|
||||
RETURN_NAMES = ("images", "filenames", "output_paths", "count")
|
||||
OUTPUT_IS_LIST = (True, True, True, False)
|
||||
RETURN_TYPES = ("IMAGE", "INT")
|
||||
RETURN_NAMES = ("images", "count")
|
||||
OUTPUT_IS_LIST = (True, False)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Craftopia"
|
||||
CATEGORY = "CraftKit"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, input_folder, **kwargs):
|
||||
# Folder contents aren't a widget input, so ComfyUI can't see when files
|
||||
# are added/removed. Force re-execution every run; skip_if_exists keeps
|
||||
# already-processed files cheap.
|
||||
return float("nan")
|
||||
|
||||
def run(self, input_folder, longest_side, multiple_of, interpolation,
|
||||
suffix_resolution, suffix_custom,
|
||||
prefix, use_original_name, use_counter, counter_start, suffix_resolution,
|
||||
folder_resolution, folder_custom,
|
||||
skip_if_exists, delimiter):
|
||||
|
||||
# Resolve filename suffix
|
||||
suffix_raw = suffix_custom.strip()
|
||||
if suffix_resolution:
|
||||
if suffix_raw == "":
|
||||
suffix_raw = f"{delimiter}{longest_side}"
|
||||
else:
|
||||
suffix_raw = f"{suffix_raw}{delimiter}{longest_side}"
|
||||
|
||||
# Ensure delimiter is prepended if there's a suffix
|
||||
if suffix_raw != "" and not suffix_raw.startswith(delimiter):
|
||||
filename_suffix = f"{delimiter}{suffix_raw}"
|
||||
else:
|
||||
filename_suffix = suffix_raw
|
||||
|
||||
# Resolve output subfolder
|
||||
subfolder = folder_custom.strip()
|
||||
|
||||
if subfolder == "":
|
||||
if folder_resolution:
|
||||
subfolder = str(longest_side) # Only resolution
|
||||
else:
|
||||
subfolder = "resized" # Fallback
|
||||
else:
|
||||
if folder_resolution:
|
||||
subfolder = f"{subfolder}{delimiter}{longest_side}"
|
||||
subfolder = str(longest_side) if folder_resolution else "resized"
|
||||
elif folder_resolution:
|
||||
subfolder = f"{subfolder}{delimiter}{longest_side}"
|
||||
|
||||
input_folder = input_folder.strip()
|
||||
if not input_folder:
|
||||
raise ValueError("[SmartBatchResize] No folder selected. Enter a path or use the Browse button.")
|
||||
if not os.path.isdir(input_folder):
|
||||
raise ValueError(f"[SmartBatchResize] Folder not found: {input_folder}")
|
||||
|
||||
@@ -144,17 +169,37 @@ class SmartBatchResize:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
interp = INTERP_MAP[interpolation]
|
||||
images_out, filenames_out, paths_out = [], [], []
|
||||
images_out = []
|
||||
counter_index = 0
|
||||
skipped_count = 0
|
||||
|
||||
for f in files:
|
||||
out_name = f"{f.stem}{filename_suffix}{f.suffix.lower()}"
|
||||
stem = _build_stem(
|
||||
original_stem=f.stem,
|
||||
prefix=prefix.strip(),
|
||||
use_original_name=use_original_name,
|
||||
use_counter=use_counter,
|
||||
counter_index=counter_index,
|
||||
counter_start=counter_start,
|
||||
suffix_resolution=suffix_resolution,
|
||||
longest_side=longest_side,
|
||||
delimiter=delimiter,
|
||||
)
|
||||
out_name = f"{stem}{f.suffix.lower()}"
|
||||
out_path = out_dir / out_name
|
||||
|
||||
if skip_if_exists and out_path.exists():
|
||||
print(f"[SmartBatchResize] Skipped (exists): {out_name}")
|
||||
skipped_count += 1
|
||||
counter_index += 1
|
||||
existing = PILImage.open(out_path).convert("RGB")
|
||||
arr = np.array(existing).astype("float32") / 255.0
|
||||
images_out.append(torch.from_numpy(arr).unsqueeze(0))
|
||||
continue
|
||||
|
||||
img = PILImage.open(f).convert("RGB")
|
||||
img = PILImage.open(f)
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img = img.convert("RGB")
|
||||
w, h = img.size
|
||||
new_w, new_h = _calc_new_size(w, h, longest_side, multiple_of)
|
||||
|
||||
@@ -167,13 +212,14 @@ class SmartBatchResize:
|
||||
|
||||
arr = np.array(img_resized).astype("float32") / 255.0
|
||||
images_out.append(torch.from_numpy(arr).unsqueeze(0))
|
||||
filenames_out.append(out_name)
|
||||
paths_out.append(str(out_path))
|
||||
counter_index += 1
|
||||
|
||||
if not images_out:
|
||||
raise ValueError("[SmartBatchResize] No images processed.")
|
||||
|
||||
return (images_out, filenames_out, paths_out, len(images_out))
|
||||
processed_count = len(images_out) - skipped_count
|
||||
if skipped_count:
|
||||
summary = f"✓ {processed_count} new, {skipped_count} already existed → {subfolder}/"
|
||||
else:
|
||||
summary = f"✓ {processed_count} images saved → {subfolder}/"
|
||||
return {"ui": {"text": [summary]}, "result": (images_out, len(images_out))}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"SmartBatchResize": SmartBatchResize}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
class SmartProfileSwitch:
|
||||
"""
|
||||
CraftKit — Smart Profile Switch 🧩
|
||||
Maps an active index (1–4) to a label, width, and height.
|
||||
Designed to pair with Smart Prompt Controller's active_list output.
|
||||
"""
|
||||
|
||||
_DEFAULTS = [
|
||||
("Headshots", 1024, 1024),
|
||||
("Halfbody", 832, 1216),
|
||||
("Fullbody", 768, 1344),
|
||||
("Tall", 640, 1536),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
required = {
|
||||
"index": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 4,
|
||||
"step": 1,
|
||||
"display": "number",
|
||||
"tooltip": "Active profile (1–4). Connect to active_list from Smart Prompt Controller.",
|
||||
}),
|
||||
}
|
||||
for i, (label, w, h) in enumerate(cls._DEFAULTS, 1):
|
||||
required[f"label_{i}"] = ("STRING", {
|
||||
"default": label,
|
||||
"tooltip": f"Label for profile {i}, used as filename suffix.",
|
||||
})
|
||||
required[f"width_{i}"] = ("INT", {"default": w, "min": 64, "max": 8192, "step": 8})
|
||||
required[f"height_{i}"] = ("INT", {"default": h, "min": 64, "max": 8192, "step": 8})
|
||||
return {"required": required}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "INT")
|
||||
RETURN_NAMES = ("label", "width", "height")
|
||||
FUNCTION = "route"
|
||||
CATEGORY = "CraftKit"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def route(self, index,
|
||||
label_1, width_1, height_1,
|
||||
label_2, width_2, height_2,
|
||||
label_3, width_3, height_3,
|
||||
label_4, width_4, height_4):
|
||||
slots = [
|
||||
(label_1, width_1, height_1),
|
||||
(label_2, width_2, height_2),
|
||||
(label_3, width_3, height_3),
|
||||
(label_4, width_4, height_4),
|
||||
]
|
||||
idx = max(1, min(4, index)) - 1
|
||||
label, width, height = slots[idx]
|
||||
status = f"Slot {idx + 1}: {label} | {width} × {height}"
|
||||
return {"ui": {"text": [status]}, "result": (label, width, height)}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SmartProfileSwitch": SmartProfileSwitch,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SmartProfileSwitch": "Smart Profile Switch \U0001f9e9",
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
class SmartPromptController:
|
||||
"""
|
||||
Craftopia — Smart Prompt Controller 🎛️
|
||||
Craftopia — Smart Prompt Controller 📋
|
||||
Cycles through up to 4 prompt lists using a single incrementing counter.
|
||||
Counts lines automatically and outputs the selected prompt + list number.
|
||||
"""
|
||||
@@ -9,27 +9,28 @@ class SmartPromptController:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"counter": ("INT", {
|
||||
"index": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 99999,
|
||||
"step": 1,
|
||||
"display": "number",
|
||||
"control_after_generate": "increment",
|
||||
"tooltip": "Current position across all lists.",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"prompt_list_1": ("STRING", {"forceInput": True}),
|
||||
"prompt_list_2": ("STRING", {"forceInput": True}),
|
||||
"prompt_list_3": ("STRING", {"forceInput": True}),
|
||||
"prompt_list_4": ("STRING", {"forceInput": True}),
|
||||
"prompt_list_1": ("STRING", {"forceInput": True, "tooltip": "Prompt list, one prompt per line."}),
|
||||
"prompt_list_2": ("STRING", {"forceInput": True, "tooltip": "Optional second prompt list."}),
|
||||
"prompt_list_3": ("STRING", {"forceInput": True, "tooltip": "Optional third prompt list."}),
|
||||
"prompt_list_4": ("STRING", {"forceInput": True, "tooltip": "Optional fourth prompt list."}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT")
|
||||
RETURN_NAMES = ("prompt", "list_index")
|
||||
RETURN_NAMES = ("prompt", "active_list")
|
||||
FUNCTION = "control"
|
||||
CATEGORY = "Craftopia"
|
||||
CATEGORY = "CraftKit"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def _get_lines(self, text):
|
||||
@@ -40,7 +41,7 @@ class SmartPromptController:
|
||||
text = text[0] if text else ""
|
||||
return [l for l in str(text).splitlines() if l.strip()]
|
||||
|
||||
def control(self, counter, prompt_list_1="", prompt_list_2="",
|
||||
def control(self, index, prompt_list_1="", prompt_list_2="",
|
||||
prompt_list_3="", prompt_list_4=""):
|
||||
raw = [prompt_list_1, prompt_list_2, prompt_list_3, prompt_list_4]
|
||||
|
||||
@@ -58,7 +59,7 @@ class SmartPromptController:
|
||||
"result": ("", 0)}
|
||||
|
||||
# Map counter position to list + line
|
||||
position = (counter - 1) % total_all
|
||||
idx = (index - 1) % total_all
|
||||
|
||||
cumulative = 0
|
||||
selected_list_index = 0
|
||||
@@ -66,9 +67,9 @@ class SmartPromptController:
|
||||
selected_prompt = ""
|
||||
|
||||
for list_idx, lines in active:
|
||||
if position < cumulative + len(lines):
|
||||
if idx < cumulative + len(lines):
|
||||
selected_list_index = list_idx
|
||||
selected_line_index = position - cumulative
|
||||
selected_line_index = idx - cumulative
|
||||
selected_prompt = lines[selected_line_index]
|
||||
break
|
||||
cumulative += len(lines)
|
||||
@@ -93,5 +94,5 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SmartPromptController": "Smart Prompt Controller \ud83c\udf9b\ufe0f",
|
||||
"SmartPromptController": "Smart Prompt Controller \ud83d\udccb",
|
||||
}
|
||||
|
||||
@@ -44,10 +44,12 @@ class SmartResize:
|
||||
"default": 8, "min": 1, "max": 64, "step": 1,
|
||||
"tooltip": "Snap both dimensions to a multiple of this value. Use 8 for SD/Flux."
|
||||
}),
|
||||
"interpolation": (["lanczos", "bicubic", "bilinear", "nearest"],),
|
||||
"interpolation": (["lanczos", "bicubic", "bilinear", "nearest"], {
|
||||
"tooltip": "Resampling method. Lanczos is sharpest for downscaling."
|
||||
}),
|
||||
"upscale_if_smaller": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Upscale images that are already smaller than longest_side."
|
||||
"default": True,
|
||||
"tooltip": "Upscale images that are smaller than the target longest side. Turn off to only ever downscale, never upscale."
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -55,7 +57,7 @@ class SmartResize:
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT")
|
||||
RETURN_NAMES = ("image", "width", "height")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Craftopia"
|
||||
CATEGORY = "CraftKit"
|
||||
|
||||
def run(self, image, longest_side, multiple_of, interpolation, upscale_if_smaller):
|
||||
interp = INTERP_MAP[interpolation]
|
||||
|
||||
@@ -2,14 +2,16 @@ class SmartResolutionMultiplier:
|
||||
"""
|
||||
Multiplies image dimensions by a factor.
|
||||
Outputs width, height, and resolution (longest side) as INT.
|
||||
Use resolution output directly with SeedVR max_resolution.
|
||||
Use resolution output directly with SeedVR2's resolution input.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"image": ("IMAGE", {
|
||||
"tooltip": "Source image. Its current width/height are read directly, no separate Get Image Size node needed."
|
||||
}),
|
||||
"multiplier": ("FLOAT", {
|
||||
"default": 2.0,
|
||||
"min": 0.1,
|
||||
@@ -31,7 +33,7 @@ class SmartResolutionMultiplier:
|
||||
RETURN_TYPES = ("INT", "INT", "INT")
|
||||
RETURN_NAMES = ("width", "height", "resolution")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Craftopia"
|
||||
CATEGORY = "CraftKit"
|
||||
|
||||
def run(self, image, multiplier, multiple_of):
|
||||
h, w = image.shape[1], image.shape[2]
|
||||
|
||||