fix(workflow): accept non-string widget values and support GlobalSeed node in gen-params (#1026)

This commit is contained in:
Will Miao
2026-07-18 22:57:20 +08:00
parent 74a7d12ca4
commit d15a8aa9a2
4 changed files with 93 additions and 8 deletions

View File

@@ -3471,7 +3471,7 @@ class NodeRegistryHandler:
status=400, status=400,
) )
if not isinstance(value, str) or not value: if value is None or (isinstance(value, str) and not value):
return web.json_response( return web.json_response(
{"success": False, "error": "Missing value parameter"}, status=400 {"success": False, "error": "Missing value parameter"}, status=400
) )
@@ -3578,7 +3578,7 @@ class NodeRegistryHandler:
status=400, status=400,
) )
if not isinstance(value, str) or not value: if value is None or (isinstance(value, str) and not value):
return web.json_response( return web.json_response(
{"success": False, "error": "Missing value parameter"}, status=400 {"success": False, "error": "Missing value parameter"}, status=400
) )

View File

@@ -141,6 +141,20 @@ const PARAM_TO_WIDGET_CANDIDATES = {
scheduler: ['scheduler'], scheduler: ['scheduler'],
}; };
// ---------------------------------------------------------------------------
// Node-type-specific widget name overrides.
// Keys are ComfyUI node class names (e.g. "GlobalSeed //Inspire").
// Values are partial PARAM_TO_WIDGET_CANDIDATES maps; the per-node candidates
// are tried *before* the global ones. Only the params listed here are
// overridden — every other param still uses the global candidates.
// ---------------------------------------------------------------------------
const NODE_TYPE_WIDGET_OVERRIDES = {
// Inspire Pack — Global Seed node stores the seed in a widget named "value"
'GlobalSeed //Inspire': {
seed: ['value'],
},
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Parse a combined sampler+scheduler value (space-separated or underscore) // Parse a combined sampler+scheduler value (space-separated or underscore)
// e.g., "Euler a Karras", "DPM++ 2M beta", "er_sde_beta" // e.g., "Euler a Karras", "DPM++ 2M beta", "er_sde_beta"
@@ -235,7 +249,7 @@ function resolveSamplerScheduler(rawValue) {
// Find which gen params can be sent to a given node, matching by widget names // Find which gen params can be sent to a given node, matching by widget names
// Returns array of { widgetName, value } objects // Returns array of { widgetName, value } objects
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function findMatchingWidgets(nodeWidgetNames, resolvedParams) { function findMatchingWidgets(nodeWidgetNames, resolvedParams, nodeType) {
if (!nodeWidgetNames || !Array.isArray(nodeWidgetNames) || nodeWidgetNames.length === 0) { if (!nodeWidgetNames || !Array.isArray(nodeWidgetNames) || nodeWidgetNames.length === 0) {
return []; return [];
} }
@@ -243,6 +257,26 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
const widgetSet = new Set(nodeWidgetNames.map(w => String(w).toLowerCase())); const widgetSet = new Set(nodeWidgetNames.map(w => String(w).toLowerCase()));
const updates = []; const updates = [];
// Resolve node-type-specific overrides (if any)
const typeOverrides =
nodeType && typeof nodeType === 'string'
? (NODE_TYPE_WIDGET_OVERRIDES[nodeType] || {})
: {};
/**
* Build the effective candidate list for a parameter:
* type-specific overrides (if any) come first, then the global candidates.
*/
function getCandidates(key) {
const global = PARAM_TO_WIDGET_CANDIDATES[key] || [key];
const extra = typeOverrides[key];
if (extra && Array.isArray(extra) && extra.length > 0) {
// Prepend type-specific candidates; keep global as fallback
return [...extra, ...global];
}
return global;
}
// Simple numeric/string params: seed, steps, cfg // Simple numeric/string params: seed, steps, cfg
const simpleParams = [ const simpleParams = [
{ key: 'seed', value: resolvedParams.seed }, { key: 'seed', value: resolvedParams.seed },
@@ -251,7 +285,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
]; ];
for (const { key, value } of simpleParams) { for (const { key, value } of simpleParams) {
if (value === undefined || value === null || value === '') continue; if (value === undefined || value === null || value === '') continue;
const candidates = PARAM_TO_WIDGET_CANDIDATES[key] || [key]; const candidates = getCandidates(key);
for (const candidate of candidates) { for (const candidate of candidates) {
if (widgetSet.has(candidate.toLowerCase())) { if (widgetSet.has(candidate.toLowerCase())) {
updates.push({ widgetName: candidate, value }); updates.push({ widgetName: candidate, value });
@@ -262,7 +296,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
// Sampler // Sampler
if (resolvedParams.sampler) { if (resolvedParams.sampler) {
const candidates = PARAM_TO_WIDGET_CANDIDATES.sampler; const candidates = getCandidates('sampler');
for (const candidate of candidates) { for (const candidate of candidates) {
if (widgetSet.has(candidate.toLowerCase())) { if (widgetSet.has(candidate.toLowerCase())) {
updates.push({ widgetName: candidate, value: resolvedParams.sampler }); updates.push({ widgetName: candidate, value: resolvedParams.sampler });
@@ -273,7 +307,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
// Scheduler // Scheduler
if (resolvedParams.scheduler) { if (resolvedParams.scheduler) {
const candidates = PARAM_TO_WIDGET_CANDIDATES.scheduler; const candidates = getCandidates('scheduler');
for (const candidate of candidates) { for (const candidate of candidates) {
if (widgetSet.has(candidate.toLowerCase())) { if (widgetSet.has(candidate.toLowerCase())) {
updates.push({ widgetName: candidate, value: resolvedParams.scheduler }); updates.push({ widgetName: candidate, value: resolvedParams.scheduler });
@@ -290,6 +324,7 @@ export {
SCHEDULER_SUFFIXES, SCHEDULER_SUFFIXES,
SCHEDULER_ONLY_VALUES, SCHEDULER_ONLY_VALUES,
PARAM_TO_WIDGET_CANDIDATES, PARAM_TO_WIDGET_CANDIDATES,
NODE_TYPE_WIDGET_OVERRIDES,
parseCombinedSamplerName, parseCombinedSamplerName,
resolveSamplerScheduler, resolveSamplerScheduler,
findMatchingWidgets, findMatchingWidgets,

View File

@@ -1144,8 +1144,8 @@ export async function sendGenParamsToWorkflow(genParams) {
const node = targetNodes[nodeKey]; const node = targetNodes[nodeKey];
if (!node) continue; if (!node) continue;
const widgetNames = node.widget_names || []; const widgetNames = getWidgetNames(node);
const updates = findMatchingWidgets(widgetNames, raw); const updates = findMatchingWidgets(widgetNames, raw, node.type_name);
if (updates.length === 0) { if (updates.length === 0) {
showToast(`Node "${node.title || node.type}" has no matching widgets for these parameters`, {}, 'warning'); showToast(`Node "${node.title || node.type}" has no matching widgets for these parameters`, {}, 'warning');

View File

@@ -9,6 +9,7 @@ import {
parseCombinedSamplerName, parseCombinedSamplerName,
resolveSamplerScheduler, resolveSamplerScheduler,
findMatchingWidgets, findMatchingWidgets,
NODE_TYPE_WIDGET_OVERRIDES,
} from '../../../static/js/utils/genParamsMapper.js'; } from '../../../static/js/utils/genParamsMapper.js';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -243,4 +244,53 @@ describe('findMatchingWidgets', () => {
const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved); const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
expect(updates.map(u => u.widgetName)).toEqual(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler']); expect(updates.map(u => u.widgetName)).toEqual(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler']);
}); });
// --- node-type-specific overrides ---
it('matches GlobalSeed //Inspire value widget for seed param', () => {
const updates = findMatchingWidgets(
['value', 'mode', 'action', 'last_seed'],
{ seed: 42 },
'GlobalSeed //Inspire'
);
expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({ widgetName: 'value', value: 42 });
});
it('ignores nodeType when it does not match any override entry', () => {
const updates = findMatchingWidgets(
['value', 'mode', 'action', 'last_seed'],
{ seed: 42 },
'SomeOtherNode'
);
expect(updates).toEqual([]);
});
it('still falls back to global candidates when override candidates do not match', () => {
// GlobalSeed override does not include steps — should use global candidate "steps"
const updates = findMatchingWidgets(
['steps', 'cfg', 'sampler_name'],
{ steps: 20 },
'GlobalSeed //Inspire'
);
expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({ widgetName: 'steps', value: 20 });
});
it('prefers overrides when both override and global candidates match', () => {
// If a hypothetical node has both "value" and "seed" widgets AND a
// GlobalSeed override, the override candidate "value" should take precedence
const updates = findMatchingWidgets(
['seed', 'noise_seed', 'value', 'mode'],
{ seed: 99 },
'GlobalSeed //Inspire'
);
expect(updates).toHaveLength(1);
expect(updates[0].widgetName).toBe('value');
});
it('omits nodeType argument and still matches via global candidates', () => {
const updates = findMatchingWidgets(['seed', 'steps', 'cfg'], { seed: 7 });
expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({ widgetName: 'seed', value: 7 });
});
}); });