Improve seed and loop controls
This commit is contained in:
@@ -91,6 +91,11 @@ Basic loop wiring:
|
|||||||
If omitted, the end node uses the start collector internally.
|
If omitted, the end node uses the start collector internally.
|
||||||
5. After the loop finishes, use `For Loop End.collected` as the combined output.
|
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:
|
`collection_mode` controls how values are stored:
|
||||||
|
|
||||||
- `auto_batch`: concatenates image tensors or latent samples when possible,
|
- `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.
|
appearance remains stable.
|
||||||
|
|
||||||
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
|
`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
|
`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
|
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
|
- `follow_main`: always follows the final generator's main `seed` input and
|
||||||
ignores the entered axis seed.
|
ignores the entered axis seed.
|
||||||
- `fixed`: always uses 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:
|
For normal prompt iteration, `SxCP Seed Locker` is usually simpler:
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -258,6 +258,7 @@ class SxCPForLoopStart:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"total": ("INT", {"default": 2, "min": 1, "max": 100000, "step": 1}),
|
"total": ("INT", {"default": 2, "min": 1, "max": 100000, "step": 1}),
|
||||||
|
"skip": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
f"initial_value{index}": (ANY_TYPE,) for index in range(1, MAX_CARRY_VALUES + 1)
|
f"initial_value{index}": (ANY_TYPE,) for index in range(1, MAX_CARRY_VALUES + 1)
|
||||||
@@ -276,9 +277,10 @@ class SxCPForLoopStart:
|
|||||||
FUNCTION = "start"
|
FUNCTION = "start"
|
||||||
CATEGORY = "prompt_builder/loop"
|
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()
|
_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
|
collected = initial_collected
|
||||||
initial_values = {
|
initial_values = {
|
||||||
"initial_value0": index,
|
"initial_value0": index,
|
||||||
@@ -287,7 +289,7 @@ class SxCPForLoopStart:
|
|||||||
for carry_index in range(1, MAX_CARRY_VALUES + 1):
|
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}")
|
initial_values[f"initial_value{carry_index + 1}"] = kwargs.get(f"initial_value{carry_index}")
|
||||||
graph = GraphBuilder()
|
graph = GraphBuilder()
|
||||||
graph.node("SxCPWhileLoopStart", condition=total, **initial_values)
|
graph.node("SxCPWhileLoopStart", condition=index < int(total), **initial_values)
|
||||||
return {
|
return {
|
||||||
"result": tuple(["stub", index, collected] + [kwargs.get(f"initial_value{index}") for index in range(1, MAX_CARRY_VALUES + 1)]),
|
"result": tuple(["stub", index, collected] + [kwargs.get(f"initial_value{index}") for index in range(1, MAX_CARRY_VALUES + 1)]),
|
||||||
"expand": graph.finalize(),
|
"expand": graph.finalize(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user