polish: SPC result display, SmartResize presets, README rewrite, error messages
This commit is contained in:
@@ -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 📏**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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" },
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user