diff --git a/README.md b/README.md index cea200b..4e69aed 100644 --- a/README.md +++ b/README.md @@ -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` + + --- @@ -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 📏** --- diff --git a/js/smart_prompt_controller.js b/js/smart_prompt_controller.js index 5f9b005..3473c9d 100644 --- a/js/smart_prompt_controller.js +++ b/js/smart_prompt_controller.js @@ -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); }; }, diff --git a/js/smart_resize.js b/js/smart_resize.js new file mode 100644 index 0000000..0e7c414 --- /dev/null +++ b/js/smart_resize.js @@ -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); + } + }, +}); diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json index f5ba9cd..e75d3c2 100644 --- a/locales/en/nodeDefs.json +++ b/locales/en/nodeDefs.json @@ -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" }, diff --git a/smart_batch_resize.py b/smart_batch_resize.py index 9fefc56..9cea9cd 100644 --- a/smart_batch_resize.py +++ b/smart_batch_resize.py @@ -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}") diff --git a/smart_prompt_controller.py b/smart_prompt_controller.py index f120010..6aa6322 100644 --- a/smart_prompt_controller.py +++ b/smart_prompt_controller.py @@ -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", }