feat(builder): load workflow sweep sessions
Load Session restores Workflow Sweep arrays from meta.configs_json (workflowConfigToArray) and tells the user when an older sweep session has no saved configs. Builder Start shows 'attached'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
13422104f4
commit
3eae054c11
@@ -0,0 +1,55 @@
|
||||
// workflowConfigToArray lives in an ES module that imports ComfyUI's frontend, so this test slices the
|
||||
// pure helper functions out of the source and evaluates them (same trick as other tests that avoid the DOM).
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const src = fs.readFileSync(path.join(__dirname, '../../web/conf_builder/conf-builder-workflow-sweep.js'), 'utf8');
|
||||
|
||||
function grab(name) {
|
||||
const re = new RegExp('(?:export )?function ' + name + '\\(');
|
||||
const m = re.exec(src);
|
||||
assert.ok(m, 'function ' + name + ' not found');
|
||||
let i = src.indexOf('{', m.index), depth = 0, j = i;
|
||||
for (; j < src.length; j++) {
|
||||
if (src[j] === '{') depth++;
|
||||
else if (src[j] === '}') { depth--; if (depth === 0) break; }
|
||||
}
|
||||
return src.slice(m.index, j + 1).replace(/^export /, '');
|
||||
}
|
||||
|
||||
const code = ['defaultWorkflowState', 'ensureWorkflowState', 'newWorkflowArray', 'workflowConfigToArray'].map(grab).join('\n');
|
||||
const { workflowConfigToArray } = new Function(code + '\nreturn { workflowConfigToArray };')();
|
||||
|
||||
const cfg = {
|
||||
model_type: 'workflow', name: 'My sweep', workflow: { '3': { class_type: 'KSampler', inputs: { seed: 1 } } },
|
||||
workflow_source: 'wf.json', workflow_output: { node_id: '9', output_index: 0 }, workflow_run_other_outputs: true,
|
||||
workflow_bindings: [{ id: 'b1', kind: 'literal', targets: [{ node_id: '3', input: 'seed' }], values: [1, 2] }],
|
||||
positive: [['a cat']], negative: 'bad', _prompt_source: 'custom',
|
||||
};
|
||||
|
||||
test('restores workflow, bindings, output and custom prompts', () => {
|
||||
const a = workflowConfigToArray(cfg, 0);
|
||||
assert.strictEqual(a.kind, 'workflow');
|
||||
assert.strictEqual(a.name, 'My sweep');
|
||||
assert.deepStrictEqual(a.workflow.api_prompt, cfg.workflow);
|
||||
assert.strictEqual(a.workflow.source_name, 'wf.json');
|
||||
assert.deepStrictEqual(a.workflow.output, { node_id: '9', output_index: 0 });
|
||||
assert.strictEqual(a.workflow.run_other_outputs, true);
|
||||
assert.deepStrictEqual(a.workflow.bindings, cfg.workflow_bindings);
|
||||
assert.strictEqual(a.use_custom_prompts, true);
|
||||
assert.deepStrictEqual(a.positive_prompt_groups, [['a cat']]);
|
||||
assert.strictEqual(a.negative_prompt, 'bad');
|
||||
});
|
||||
|
||||
test('global prompts are not copied into the array; odd input never throws', () => {
|
||||
const a = workflowConfigToArray({ ...cfg, _prompt_source: 'global' }, 2);
|
||||
assert.strictEqual(a.use_custom_prompts, false);
|
||||
assert.deepStrictEqual(a.positive_prompt_groups, []);
|
||||
const b = workflowConfigToArray(null, 1);
|
||||
assert.strictEqual(b.kind, 'workflow');
|
||||
assert.strictEqual(b.name, 'Workflow 2');
|
||||
assert.strictEqual(b.workflow.api_prompt, null);
|
||||
assert.deepStrictEqual(b.workflow.bindings, []);
|
||||
});
|
||||
@@ -8,6 +8,17 @@ import { app } from "../../../scripts/app.js";
|
||||
// Cache-busting timestamp (regenerated on every page load)
|
||||
const CACHE_BUST = Date.now();
|
||||
|
||||
// Small info toast (falls back to alert when the frontend has no toast extension)
|
||||
function notify(detail) {
|
||||
try {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({ severity: "info", summary: "USCG", detail, life: 8000 });
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fall through */ }
|
||||
alert(detail);
|
||||
}
|
||||
|
||||
// Module storage
|
||||
let utilities, uiComponents, configManagement, distributionModule;
|
||||
|
||||
@@ -418,7 +429,12 @@ app.registerExtension({
|
||||
if (configs && !Array.isArray(configs) && configs.configs) {
|
||||
configs = configs.configs;
|
||||
}
|
||||
let loadedArrays = utilities.convertConfigsToConfigArrays(configs);
|
||||
// Workflow Sweep entries (model_type "workflow") restore through workflowConfigToArray;
|
||||
// standard entries keep the existing path. Original order is preserved.
|
||||
const isWfCfg = (c) => c && c.model_type === "workflow";
|
||||
const wfCfgs = configs.filter(isWfCfg);
|
||||
const stdCfgs = configs.filter(c => !isWfCfg(c));
|
||||
let loadedArrays = stdCfgs.length ? utilities.convertConfigsToConfigArrays(stdCfgs) : [];
|
||||
const normalize = (str) => str.replace(/\\/g, "/");
|
||||
|
||||
loadedArrays.forEach(arr => {
|
||||
@@ -486,6 +502,15 @@ app.registerExtension({
|
||||
if (arr.kind === undefined) arr.kind = "standard";
|
||||
});
|
||||
|
||||
if (wfCfgs.length) {
|
||||
const { workflowConfigToArray } = await import("./conf-builder-workflow-sweep.js");
|
||||
const wfArrays = wfCfgs.map((c, i) => workflowConfigToArray(c, i));
|
||||
const merged = [];
|
||||
let si = 0, wi = 0;
|
||||
configs.forEach(c => merged.push(isWfCfg(c) ? wfArrays[wi++] : loadedArrays[si++]));
|
||||
loadedArrays = merged.filter(Boolean);
|
||||
}
|
||||
|
||||
this.state.config_arrays = loadedArrays;
|
||||
|
||||
// Restore global prompts from loaded configs
|
||||
@@ -515,6 +540,11 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
// A sweep session saved before its configs were stored: nothing to restore, say so
|
||||
if (meta.workflow_sweep && !meta.configs_json) {
|
||||
notify("This session was made by a Workflow Sweep before its configs were saved. Load the workflow JSON again into a Workflow Sweep array; finished jobs are skipped by fingerprint.");
|
||||
}
|
||||
|
||||
this.state.session_name = sessionName;
|
||||
this.saveState();
|
||||
this.renderUI();
|
||||
|
||||
@@ -656,6 +656,8 @@ export function convertConfigsToConfigArrays(configs) {
|
||||
};
|
||||
|
||||
configs.forEach((config, idx) => {
|
||||
// Workflow Sweep entries are restored by workflowConfigToArray (loadSession); never build a standard array from one
|
||||
if (config && config.model_type === "workflow") return;
|
||||
const loraValue = config.lora;
|
||||
let loras = [];
|
||||
let hasCombined = false;
|
||||
|
||||
@@ -53,6 +53,30 @@ export function newWorkflowArray(index) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of config_builder_node._workflow_array_to_config: a saved workflow config entry (manifest
|
||||
* meta.configs_json) back into a Builder array of kind "workflow". Never throws on odd input.
|
||||
*/
|
||||
export function workflowConfigToArray(cfg, idx = 0) {
|
||||
const arr = newWorkflowArray(idx + 1);
|
||||
cfg = (cfg && typeof cfg === "object") ? cfg : {};
|
||||
if (cfg.name) arr.name = String(cfg.name);
|
||||
arr.workflow = {
|
||||
source_name: String(cfg.workflow_source || ""),
|
||||
api_prompt: (cfg.workflow && typeof cfg.workflow === "object" && Object.keys(cfg.workflow).length) ? cfg.workflow : null,
|
||||
output: (cfg.workflow_output && typeof cfg.workflow_output === "object") ? cfg.workflow_output : {},
|
||||
run_other_outputs: !!cfg.workflow_run_other_outputs,
|
||||
bindings: Array.isArray(cfg.workflow_bindings) ? cfg.workflow_bindings : [],
|
||||
};
|
||||
if (cfg._prompt_source === "custom" && Array.isArray(cfg.positive)) {
|
||||
arr.positive_prompt_groups = cfg.positive;
|
||||
arr.negative_prompt = typeof cfg.negative === "string" ? cfg.negative : "";
|
||||
arr.use_custom_prompts = true;
|
||||
}
|
||||
ensureWorkflowState(arr);
|
||||
return arr;
|
||||
}
|
||||
|
||||
function el(tag, props = {}, css = "") {
|
||||
const e = document.createElement(tag);
|
||||
Object.assign(e, props);
|
||||
|
||||
Reference in New Issue
Block a user