polish: SPC result display, SmartResize presets, README rewrite, error messages

This commit is contained in:
rubz
2026-06-30 10:02:55 +02:00
parent 4231615d52
commit f54122f895
6 changed files with 146 additions and 32 deletions
+21 -16
View File
@@ -2,13 +2,13 @@
Custom nodes for ComfyUI - image resizing, dataset prep & prompt automation for LoRA training workflows.
All nodes appear under the **Craftopia** category in the node menu.
All nodes appear under the **CraftKit** category in the node menu.
---
## Nodes
### 🎛️ Smart Prompt Controller
### 📋 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.
**Typical usecases:**
@@ -44,7 +44,7 @@ Use this as a **pipeline node** — IMAGE in, IMAGE out. No files are saved to d
| Input | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | Single image or batch |
| longest_side | INT | 1536 | Target size for longest side |
| longest_side | INT | 1536 | Target size for longest side (quick presets: 512 / 768 / 1024 / 1536) |
| 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 |
@@ -56,23 +56,28 @@ Use this as a **pipeline node** — IMAGE in, IMAGE out. No files are saved to d
---
### 📁 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.
Load **all images from a folder**, resize each one by longest side, and save into a subfolder. Original filenames are always preserved — the node adds an optional suffix and/or resolution to the filename.
Use this for **bulk preprocessing** — e.g. preparing a LoRA dataset from a folder of high-res images.
Processes and saves directly to disk with the **original filename preserved**. The output folder is automatically named with the resolution (e.g. `resized_1024`) so you always know what's inside. Filenames are also passed as outputs so you can use them further in your pipeline.
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.
| 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 |
| input_folder | STRING | — | Source folder path (use Browse button or paste manually) |
| longest_side | INT | 1024 | Target size for longest side |
| 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 |
| suffix_resolution | BOOLEAN | true | Append resolution to filename — e.g. `photo_1024.png` |
| suffix_custom | STRING | — | Custom text added after filename — e.g. `photo_headshot.png` |
| folder_resolution | BOOLEAN | false | Append resolution to subfolder name — e.g. `resized_1024` |
| folder_custom | STRING | resized | Subfolder name inside the input folder |
| skip_if_exists | BOOLEAN | true | Skip files that already exist in the output folder |
| delimiter | STRING | _ | Separator between filename parts |
**Outputs:** `images` (list), `filenames` (list), `output_paths` (list), `count`
**Outputs:** `images` (list), `count`
<img src="assets/screenshot-smart-batch-resize.png" width="700">
---
@@ -114,12 +119,12 @@ Also useful for LoRA dataset prep — using mixed resolutions in your training s
cd ComfyUI/custom_nodes
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 Resize 📐**
- `CraftKit` → **Smart Batch Resize 📁**
- `CraftKit` → **Smart Resolution Multiplier 📏**
---
+43 -12
View File
@@ -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);
};
},
+76
View File
@@ -0,0 +1,76 @@
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 = 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);
}
},
});
+2 -2
View File
@@ -47,9 +47,9 @@
}
},
"SmartPromptController": {
"display_name": "Smart Prompt Controller 🎛️",
"display_name": "Smart Prompt Controller 📋",
"inputs": {
"counter": { "name": "Counter" },
"counter": { "name": "counter" },
"prompt_list_1": { "name": "Prompt list 1" },
"prompt_list_2": { "name": "Prompt list 2" },
"prompt_list_3": { "name": "Prompt list 3" },
+2
View File
@@ -130,6 +130,8 @@ class SmartBatchResize:
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}")
+2 -2
View File
@@ -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.
"""
@@ -94,5 +94,5 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SmartPromptController": "Smart Prompt Controller \ud83c\udf9b\ufe0f",
"SmartPromptController": "Smart Prompt Controller \ud83d\udccb",
}