diff --git a/README.md b/README.md index 7950dce..0ea90d1 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,11 @@ Basic loop wiring: If omitted, the end node uses the start collector internally. 5. After the loop finishes, use `For Loop End.collected` as the combined output. +`For Loop Start.skip` skips the first N iterations while keeping the index +stable. For example, `total=10` and `skip=1` runs indexes `1..9`; `skip=5` runs +indexes `5..9`. This is useful when you want to resume a loop without changing +index-derived seeds or row numbers. + `collection_mode` controls how values are stored: - `auto_batch`: concatenates image tensors or latent samples when possible, @@ -205,7 +210,11 @@ expression, and composition can still change while the saved character appearance remains stable. `SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt -builder's optional `seed_config` input. +builder's optional `seed_config` input. When an axis is set to `random`, the +visible seed value is materialized before the workflow queues, and that exact +value is used for the queued prompt. The mode returns to `random` after queueing +so the next run can reroll. Use `Lock Random Seeds Now` on the node when you want +to convert the current random axes into fixed reusable seeds. `SxCP Seed Locker` is the fast version for iteration. Set `base_seed` to a seed you like, choose one `reroll_axis`, and connect its `seed_config`. All other @@ -680,7 +689,10 @@ axis has its own mode plus seed value: - `follow_main`: always follows the final generator's main `seed` input and ignores the entered axis seed. - `fixed`: always uses the entered axis seed. -- `random`: generates a fresh axis seed when the node runs. +- `random`: generates a fresh visible axis seed when the workflow queues. + +The `Lock Random Seeds Now` button turns every current `random` axis into a +visible concrete seed and switches those axes to `fixed`. For normal prompt iteration, `SxCP Seed Locker` is usually simpler: diff --git a/loop_nodes.py b/loop_nodes.py index d940039..357b6eb 100644 --- a/loop_nodes.py +++ b/loop_nodes.py @@ -258,6 +258,7 @@ class SxCPForLoopStart: return { "required": { "total": ("INT", {"default": 2, "min": 1, "max": 100000, "step": 1}), + "skip": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}), }, "optional": { f"initial_value{index}": (ANY_TYPE,) for index in range(1, MAX_CARRY_VALUES + 1) @@ -276,9 +277,10 @@ class SxCPForLoopStart: FUNCTION = "start" CATEGORY = "prompt_builder/loop" - def start(self, total, initial_index=None, initial_collected=None, **kwargs): + def start(self, total, skip=0, initial_index=None, initial_collected=None, **kwargs): _require_graph_builder() - index = 0 if initial_index is None else initial_index + skip = max(0, int(skip)) + index = skip if initial_index is None else max(int(initial_index), skip) collected = initial_collected initial_values = { "initial_value0": index, @@ -287,7 +289,7 @@ class SxCPForLoopStart: for carry_index in range(1, MAX_CARRY_VALUES + 1): initial_values[f"initial_value{carry_index + 1}"] = kwargs.get(f"initial_value{carry_index}") graph = GraphBuilder() - graph.node("SxCPWhileLoopStart", condition=total, **initial_values) + graph.node("SxCPWhileLoopStart", condition=index < int(total), **initial_values) return { "result": tuple(["stub", index, collected] + [kwargs.get(f"initial_value{index}") for index in range(1, MAX_CARRY_VALUES + 1)]), "expand": graph.finalize(), diff --git a/web/seed_control.js b/web/seed_control.js new file mode 100644 index 0000000..109fa28 --- /dev/null +++ b/web/seed_control.js @@ -0,0 +1,140 @@ +import { app } from "../../scripts/app.js"; + +const EXTENSION = "ethanfel.prompt_builder.seed_control"; +const NODE_NAME = "SxCPSeedControl"; +const SEED_AXES = [ + "category", + "subcategory", + "content", + "person", + "scene", + "pose", + "role", + "expression", + "composition", +]; + +function widget(node, name) { + return node.widgets?.find((w) => w.name === name); +} + +function isSeedControlNode(node) { + return node?._sxcpSeedControlNode || node?.comfyClass === NODE_NAME || node?.type === NODE_NAME; +} + +function resizeNode(node) { + const size = node.computeSize?.(); + if (size) node.setSize?.(size); + app.graph?.setDirtyCanvas(true, true); +} + +function randomSeed() { + if (globalThis.crypto?.getRandomValues) { + const values = new Uint32Array(1); + globalThis.crypto.getRandomValues(values); + return values[0]; + } + return Math.floor(Math.random() * 0x100000000); +} + +function setWidgetValue(node, w, value) { + if (!w) return; + const oldValue = w.value; + w.value = value; + w.callback?.(value, app.canvas, node); + node.onWidgetChanged?.(w.name, value, oldValue, w); +} + +function materializeNode(node) { + const changes = []; + for (const axis of SEED_AXES) { + const modeWidget = widget(node, `${axis}_seed_mode`); + const seedWidget = widget(node, `${axis}_seed`); + if (modeWidget?.value !== "random" || !seedWidget) continue; + + const seed = randomSeed(); + changes.push({ + node, + modeWidget, + previousMode: modeWidget.value, + seedWidget, + previousSeed: seedWidget.value, + seed, + }); + setWidgetValue(node, seedWidget, seed); + setWidgetValue(node, modeWidget, "fixed"); + } + if (changes.length) resizeNode(node); + return changes; +} + +function materializeAllForQueue() { + const changes = []; + for (const node of app.graph?._nodes || []) { + if (isSeedControlNode(node)) changes.push(...materializeNode(node)); + } + if (changes.length) app.graph?.setDirtyCanvas(true, true); + return changes; +} + +function restoreRandomModes(changes) { + for (const change of changes) { + setWidgetValue(change.node, change.modeWidget, change.previousMode); + } + if (changes.length) app.graph?.setDirtyCanvas(true, true); +} + +function lockRandomSeeds(node) { + const changes = materializeNode(node); + if (!changes.length) { + alert("No random seed modes to lock."); + return; + } + resizeNode(node); +} + +function setupNode(node) { + node._sxcpSeedControlNode = true; + if (!node._sxcpLockRandomSeedsButton) { + node._sxcpLockRandomSeedsButton = node.addWidget("button", "Lock Random Seeds Now", null, () => lockRandomSeeds(node)); + } + resizeNode(node); +} + +app.registerExtension({ + name: EXTENSION, + + async setup() { + if (app._sxcpSeedControlQueuePatched) return; + const originalQueuePrompt = app.queuePrompt; + if (!originalQueuePrompt) return; + + app._sxcpSeedControlQueuePatched = true; + app.queuePrompt = async function () { + const randomSeedChanges = materializeAllForQueue(); + try { + return await originalQueuePrompt.apply(this, arguments); + } finally { + restoreRandomModes(randomSeedChanges); + } + }; + }, + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== NODE_NAME) return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const result = onNodeCreated?.apply(this, arguments); + setupNode(this); + return result; + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const result = onConfigure?.apply(this, arguments); + queueMicrotask(() => setupNode(this)); + return result; + }; + }, +});