diff --git a/README.md b/README.md index 8998f67..63138cb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ComfyUI-SmartResize +# ComfyUI-Craftopia -Smart image resize nodes for ComfyUI — batch processing, LoRA prep & SeedVR support. +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. @@ -8,6 +8,27 @@ All nodes appear under the **Craftopia** 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. + +**Typical usecases:** +- LoRA dataset generation with multiple pose categories (headshots, halfbody, fullbody, tall portrait), each with their own prompt list and aspect ratio +- Any batch workflow where you need to rotate through different prompt sets and switch settings per set + +| 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) | + +**Outputs:** `prompt` (selected line as STRING), `list_index` (active list number as INT → feed to Switch nodes) + +The node displays a status label showing the current position: `List 1 — 4/5 | total: 8` + +--- + ### 📐 Smart Resize Resize any image (or batch) so the **longest side** equals `max_pixels`, with aspect ratio preserved. @@ -72,20 +93,22 @@ Also useful for LoRA dataset prep — using mixed resolutions in your training s - `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 --- ## Installation -**ComfyUI Manager:** search for `ComfyUI-SmartResize` and install. +**ComfyUI Manager:** search for `ComfyUI-Craftopia` and install. **Manual:** ```bash cd ComfyUI/custom_nodes -git clone https://github.com/CraftopiaStudio/ComfyUI-SmartResize +git clone https://github.com/CraftopiaStudio/ComfyUI-Craftopia ``` Restart ComfyUI. All nodes appear under **Craftopia** in the node menu: +- `Craftopia` → **Smart Prompt Controller 🎛️** - `Craftopia` → **Smart Resize 📐** - `Craftopia` → **Smart Batch Resize 📁** - `Craftopia` → **Smart Size Multiplier 📏** diff --git a/__init__.py b/__init__.py index c3963e3..4f5b719 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,9 @@ +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_size_multiplier import NODE_CLASS_MAPPINGS as C, NODE_DISPLAY_NAME_MAPPINGS as C_NAMES -from .smart_batch_controller import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES +from .smart_prompt_controller import NODE_CLASS_MAPPINGS as D, NODE_DISPLAY_NAME_MAPPINGS as D_NAMES NODE_CLASS_MAPPINGS = {**A, **B, **C, **D} NODE_DISPLAY_NAME_MAPPINGS = {**A_NAMES, **B_NAMES, **C_NAMES, **D_NAMES} diff --git a/js/smart_batch_controller.js b/js/smart_batch_controller.js new file mode 100644 index 0000000..11e5204 --- /dev/null +++ b/js/smart_batch_controller.js @@ -0,0 +1,32 @@ +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "Craftopia.SmartBatchController", + + async nodeCreated(node) { + if (node.comfyClass !== "SmartBatchController") return; + + // Create status widget — empty name so no label shows + const widget = node.addWidget("text", "", "", () => {}); + widget.serializeValue = () => undefined; + + // Listen for execution results + const onExecuted = node.onExecuted; + node.onExecuted = function (output) { + // Store current size before update + const w = node.size[0]; + const h = node.size[1]; + + onExecuted?.apply(this, arguments); + + if (output?.text?.[0]) { + widget.value = output.text[0]; + } + + // Restore size so ComfyUI doesn't shrink the node + 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_prompt_controller.js b/js/smart_prompt_controller.js new file mode 100644 index 0000000..5f9b005 --- /dev/null +++ b/js/smart_prompt_controller.js @@ -0,0 +1,30 @@ +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "Craftopia.SmartPromptController", + + async nodeCreated(node) { + if (node.comfyClass !== "SmartPromptController") return; + + // Status widget — no visible label name + const widget = node.addWidget("text", "", "", () => {}); + widget.serializeValue = () => undefined; + + const onExecuted = node.onExecuted; + node.onExecuted = function (output) { + const w = node.size[0]; + const h = node.size[1]; + + onExecuted?.apply(this, arguments); + + if (output?.text?.[0]) { + widget.value = output.text[0]; + } + + // 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/smart_batch_controller.py b/smart_batch_controller.py deleted file mode 100644 index bf56237..0000000 --- a/smart_batch_controller.py +++ /dev/null @@ -1,86 +0,0 @@ -class SmartBatchController: - """ - Craftopia — Smart Batch Controller 🎛️ - Drives batch switching and prompt index from a single incrementing counter. - Accepts up to 4 prompt lists as STRING inputs and counts lines automatically. - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "counter": ("INT", { - "default": 1, - "min": 1, - "max": 99999, - "step": 1, - "display": "number", - "control_after_generate": "increment", - }), - }, - "optional": { - "batch_1": ("STRING", {"forceInput": True}), - "batch_2": ("STRING", {"forceInput": True}), - "batch_3": ("STRING", {"forceInput": True}), - "batch_4": ("STRING", {"forceInput": True}), - } - } - - RETURN_TYPES = ("INT", "INT", "INT") - RETURN_NAMES = ("select", "prompt_index", "total_all") - FUNCTION = "control" - CATEGORY = "Craftopia" - OUTPUT_NODE = True - - def _count_lines(self, text): - """Count non-empty lines in a prompt list string.""" - if not text: - return 0 - return len([l for l in text.splitlines() if l.strip()]) - - def control(self, counter, batch_1=None, batch_2=None, batch_3=None, batch_4=None): - raw = [batch_1, batch_2, batch_3, batch_4] - - # Build active batch list: (original_index, line_count) - active = [] - for i, text in enumerate(raw): - count = self._count_lines(text) - if count > 0: - active.append((i, count)) - - total_all = sum(t for _, t in active) - - if total_all == 0: - return {"ui": {"text": ["no prompts"]}, "result": (0, 0, 0)} - - # Wrap counter within total_all (1-based counter → 0-based position) - position = (counter - 1) % total_all - - # Find which batch and which prompt within that batch - cumulative = 0 - batch_index = 0 - prompt_index = 0 - - for i, t in active: - if position < cumulative + t: - batch_index = i - prompt_index = position - cumulative # 0-based within batch - break - cumulative += t - - # UI label on node: "B2 — 3 / 7" - batch_number = batch_index + 1 - batch_total = self._count_lines(raw[batch_index]) - prompt_display = prompt_index + 1 # 1-based for display - label = f"B{batch_number} — {prompt_display} / {batch_total} | total: {total_all}" - - return {"ui": {"text": [label]}, "result": (batch_index, prompt_index, total_all)} - - -NODE_CLASS_MAPPINGS = { - "SmartBatchController": SmartBatchController, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "SmartBatchController": "Smart Batch Controller 🎛️", -} diff --git a/smart_prompt_controller.py b/smart_prompt_controller.py new file mode 100644 index 0000000..b6dc0d3 --- /dev/null +++ b/smart_prompt_controller.py @@ -0,0 +1,97 @@ +class SmartPromptController: + """ + 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. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "counter": ("INT", { + "default": 1, + "min": 1, + "max": 99999, + "step": 1, + "display": "number", + "control_after_generate": "increment", + }), + }, + "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}), + } + } + + RETURN_TYPES = ("STRING", "INT") + RETURN_NAMES = ("prompt", "list_index") + FUNCTION = "control" + CATEGORY = "Craftopia" + OUTPUT_NODE = True + + def _get_lines(self, text): + """Return list of non-empty lines from a text block.""" + if not text: + return [] + if isinstance(text, list): + 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="", + prompt_list_3="", prompt_list_4=""): + raw = [prompt_list_1, prompt_list_2, prompt_list_3, prompt_list_4] + + # Collect active lists: (list_index, [lines]) + active = [] + for i, text in enumerate(raw): + lines = self._get_lines(text) + if lines: + active.append((i, lines)) + + total_all = sum(len(lines) for _, lines in active) + + if total_all == 0: + return {"ui": {"text": ["no prompts connected"]}, + "result": ("", 0)} + + # Map counter position to list + line + position = (counter - 1) % total_all + + cumulative = 0 + selected_list_index = 0 + selected_line_index = 0 + selected_prompt = "" + + for list_idx, lines in active: + if position < cumulative + len(lines): + selected_list_index = list_idx + selected_line_index = position - cumulative + selected_prompt = lines[selected_line_index] + break + cumulative += len(lines) + + # Label: "List 1 — 4/5 | total: 8" + list_number = selected_list_index + 1 + list_total = len(self._get_lines(raw[selected_list_index])) + prompt_display = selected_line_index + 1 + label = f"List {list_number} \u2014 {prompt_display}/{list_total} | total: {total_all}" + + return { + "ui": {"text": [label]}, + "result": ( + selected_prompt, + list_number, + ) + } + + +NODE_CLASS_MAPPINGS = { + "SmartPromptController": SmartPromptController, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SmartPromptController": "Smart Prompt Controller \ud83c\udf9b\ufe0f", +}