Skip Missing Models refresh for strength-only widget changes
This commit is contained in:
@@ -537,6 +537,33 @@ test('node context integration preserves existing menu hooks and refreshes after
|
||||
]);
|
||||
});
|
||||
|
||||
test('strength widget changes skip proactive node context analysis', () => {
|
||||
const configureNodeContextMenu = eval(`(${extractMethod(modelResolverSource, 'configureNodeContextMenu')})`);
|
||||
const isWorkflowStrengthWidgetName = eval(
|
||||
`(${extractMethod(workflowStateMethodsSource, 'isWorkflowStrengthWidgetName')})`
|
||||
);
|
||||
const calls = [];
|
||||
class NodeType {}
|
||||
const resolver = {
|
||||
getResolvedModelsForNodeContextMenu: () => [],
|
||||
scheduleNodeContextMenuAnalysis: () => calls.push('context-analysis'),
|
||||
dialog: {
|
||||
isWorkflowStrengthWidgetName,
|
||||
scheduleActiveWorkflowRefresh: reason => calls.push(`dialog-refresh:${reason}`),
|
||||
},
|
||||
};
|
||||
|
||||
configureNodeContextMenu.call(resolver, NodeType);
|
||||
const node = new NodeType();
|
||||
node.onWidgetChanged('strength_model');
|
||||
node.onWidgetChanged('lora_2_clip_strength');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'dialog-refresh:node-widget-change',
|
||||
'dialog-refresh:node-widget-change',
|
||||
]);
|
||||
});
|
||||
|
||||
test('background Loaded Models refresh keeps the current view until new data is ready', async () => {
|
||||
const loadLoadedModels = eval(`(${extractMethod(tabsLoadedMethodsSource, 'loadLoadedModels')})`);
|
||||
const workflow = {
|
||||
@@ -671,7 +698,7 @@ test('background Missing Models refresh keeps the current view until new data is
|
||||
cachedWorkflowSignature: 'old-signature',
|
||||
cachedAnalysisData: { missing_models: [], resolved_models: [] },
|
||||
syncWorkflowScopedQueue() {},
|
||||
getWorkflowSignature: () => 'new-signature',
|
||||
getMissingWorkflowSignature: () => 'new-signature',
|
||||
workflowHasNodes: () => true,
|
||||
renderAnalysisProgress() {
|
||||
progressRenderCount += 1;
|
||||
@@ -734,12 +761,14 @@ test('node widget changes request a content-preserving Missing Models refresh',
|
||||
_workflowRefreshGeneration: 1,
|
||||
activeWorkflowRouteKey: 'workflow-a',
|
||||
activeWorkflowSignature: 'old-signature',
|
||||
activeMissingWorkflowSignature: 'old-missing-signature',
|
||||
activeTab: 'missing',
|
||||
contentElement: { style: {} },
|
||||
isVisible: () => true,
|
||||
getActiveWorkflowRouteKey: () => 'workflow-a',
|
||||
getCurrentWorkflow: () => workflow,
|
||||
getWorkflowSignature: () => 'new-signature',
|
||||
getMissingWorkflowSignature: () => 'new-missing-signature',
|
||||
syncWorkflowScopedQueue() {
|
||||
preserveSearchCacheAtSync = this.preserveSearchCacheAcrossNextWorkflowSync;
|
||||
},
|
||||
@@ -763,6 +792,55 @@ test('node widget changes request a content-preserving Missing Models refresh',
|
||||
assert.equal(preserveSearchCacheAtSync, true);
|
||||
});
|
||||
|
||||
test('strength-only widget changes synchronize workflow state without refreshing Missing Models', async () => {
|
||||
const log = { debug() {} };
|
||||
const refreshForActiveWorkflowChange = eval(
|
||||
`(${extractMethod(workflowStateMethodsSource, 'refreshForActiveWorkflowChange')})`
|
||||
);
|
||||
const workflow = {
|
||||
nodes: [{
|
||||
id: 7,
|
||||
type: 'LoraLoader',
|
||||
widgets_values: ['model.safetensors', 1.25],
|
||||
}],
|
||||
};
|
||||
let syncCount = 0;
|
||||
let loadCount = 0;
|
||||
const dialog = {
|
||||
_workflowRefreshGeneration: 1,
|
||||
activeWorkflowRouteKey: 'workflow-a',
|
||||
activeWorkflowSignature: 'old-full-signature',
|
||||
activeMissingWorkflowSignature: 'same-missing-signature',
|
||||
activeTab: 'missing',
|
||||
contentElement: { style: {} },
|
||||
isVisible: () => true,
|
||||
getActiveWorkflowRouteKey: () => 'workflow-a',
|
||||
getCurrentWorkflow: () => workflow,
|
||||
getWorkflowSignature: () => 'new-full-signature',
|
||||
getMissingWorkflowSignature: () => 'same-missing-signature',
|
||||
syncWorkflowScopedQueue() {
|
||||
syncCount += 1;
|
||||
},
|
||||
async loadWorkflowData() {
|
||||
loadCount += 1;
|
||||
},
|
||||
};
|
||||
|
||||
await refreshForActiveWorkflowChange.call(dialog, {
|
||||
reason: 'node-widget-change',
|
||||
expectedRoute: 'workflow-a',
|
||||
previousSignature: 'old-full-signature',
|
||||
attempt: 8,
|
||||
generation: 1,
|
||||
candidateRoute: 'workflow-a',
|
||||
candidateSignature: 'new-full-signature',
|
||||
});
|
||||
|
||||
assert.equal(syncCount, 1);
|
||||
assert.equal(loadCount, 0);
|
||||
assert.equal(dialog.preserveSearchCacheAcrossNextWorkflowSync, true);
|
||||
});
|
||||
|
||||
test('workflow synchronization carries searched results across a strength-only signature change', () => {
|
||||
const syncWorkflowScopedQueue = eval(
|
||||
`(${extractMethod(workflowStateMethodsSource, 'syncWorkflowScopedQueue')})`
|
||||
@@ -784,12 +862,17 @@ test('workflow synchronization carries searched results across a strength-only s
|
||||
preserveSearchCacheAcrossNextWorkflowSync: true,
|
||||
activeWorkflowRouteKey: 'workflow-a',
|
||||
activeWorkflowSignature: 'old-signature',
|
||||
activeMissingWorkflowSignature: 'same-missing-signature',
|
||||
searchResultCache: new Map([['loras:missing.safetensors', searchedState]]),
|
||||
backgroundSearchJobs: new Map(),
|
||||
getCurrentWorkflow: () => workflow,
|
||||
getActiveWorkflowRouteKey: () => 'workflow-a',
|
||||
getWorkflowSignature: () => 'new-signature',
|
||||
getWorkflowScopedQueueKey(route = dialog.activeWorkflowRouteKey, signature = dialog.activeWorkflowSignature) {
|
||||
getMissingWorkflowSignature: () => 'same-missing-signature',
|
||||
getWorkflowScopedQueueKey(
|
||||
route = dialog.activeWorkflowRouteKey,
|
||||
signature = dialog.activeMissingWorkflowSignature || dialog.activeWorkflowSignature
|
||||
) {
|
||||
return `${route}\n${signature}`;
|
||||
},
|
||||
cloneSearchResultCache(cache) {
|
||||
@@ -815,6 +898,7 @@ test('workflow synchronization carries searched results across a strength-only s
|
||||
syncWorkflowScopedQueue.call(dialog, workflow);
|
||||
|
||||
assert.equal(dialog.activeWorkflowSignature, 'new-signature');
|
||||
assert.equal(dialog.activeMissingWorkflowSignature, 'same-missing-signature');
|
||||
assert.equal(
|
||||
dialog.searchResultCache.get('loras:missing.safetensors'),
|
||||
searchedState
|
||||
@@ -1286,6 +1370,47 @@ test('workflow signature tracks EasyLoraStack mode and indexed strengths', () =>
|
||||
]);
|
||||
});
|
||||
|
||||
test('Missing Models signature ignores strength but tracks model identity changes', () => {
|
||||
const getWorkflowSignature = eval(`(${extractMethod(workflowStateMethodsSource, 'getWorkflowSignature')})`);
|
||||
const getMissingWorkflowSignature = eval(`(${extractMethod(workflowStateMethodsSource, 'getMissingWorkflowSignature')})`);
|
||||
const getWorkflowSignatureData = eval(`(${extractMethod(workflowStateMethodsSource, 'getWorkflowSignatureData')})`);
|
||||
const dialog = {
|
||||
capabilities: { node_rules: {} },
|
||||
getWorkflowSignature,
|
||||
getMissingWorkflowSignature,
|
||||
getWorkflowSignatureData,
|
||||
};
|
||||
const makeWorkflow = (modelName, strength) => ({
|
||||
nodes: [{
|
||||
id: 34,
|
||||
type: 'LoraLoader',
|
||||
inputs: [
|
||||
{ name: 'lora_name', widget: { name: 'lora_name' } },
|
||||
{ name: 'strength_model', widget: { name: 'strength_model' } },
|
||||
],
|
||||
outputs: [],
|
||||
widgets_values: [modelName, strength],
|
||||
}],
|
||||
links: [],
|
||||
});
|
||||
const original = makeWorkflow('AnimeEditV2.safetensors', 0.65);
|
||||
const strengthChanged = makeWorkflow('AnimeEditV2.safetensors', 1.25);
|
||||
const modelChanged = makeWorkflow('AnotherModel.safetensors', 1.25);
|
||||
|
||||
assert.notEqual(
|
||||
getWorkflowSignature.call(dialog, original),
|
||||
getWorkflowSignature.call(dialog, strengthChanged)
|
||||
);
|
||||
assert.equal(
|
||||
getMissingWorkflowSignature.call(dialog, original),
|
||||
getMissingWorkflowSignature.call(dialog, strengthChanged)
|
||||
);
|
||||
assert.notEqual(
|
||||
getMissingWorkflowSignature.call(dialog, strengthChanged),
|
||||
getMissingWorkflowSignature.call(dialog, modelChanged)
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow hash refresh ignores node movement and tracks model dependency changes', () => {
|
||||
const getWorkflowSignature = eval(`(${extractMethod(workflowStateMethodsSource, 'getWorkflowSignature')})`);
|
||||
const getWorkflowSignatureData = eval(`(${extractMethod(workflowStateMethodsSource, 'getWorkflowSignatureData')})`);
|
||||
|
||||
@@ -331,7 +331,8 @@ export class ModelResolver {
|
||||
const workflow = app?.graph?.serialize?.();
|
||||
if (!workflow) return { workflow: null, signature: null };
|
||||
|
||||
const signature = this.dialog?.getWorkflowSignature?.(workflow)
|
||||
const signature = this.dialog?.getMissingWorkflowSignature?.(workflow)
|
||||
|| this.dialog?.getWorkflowSignature?.(workflow)
|
||||
|| JSON.stringify(workflow);
|
||||
return { workflow, signature };
|
||||
}
|
||||
@@ -521,7 +522,10 @@ export class ModelResolver {
|
||||
};
|
||||
nodeType.prototype.onWidgetChanged = function() {
|
||||
const result = originalOnWidgetChanged?.apply(this, arguments);
|
||||
owner.scheduleNodeContextMenuAnalysis();
|
||||
const widgetName = arguments[0];
|
||||
if (!owner.dialog?.isWorkflowStrengthWidgetName?.(widgetName)) {
|
||||
owner.scheduleNodeContextMenuAnalysis();
|
||||
}
|
||||
owner.dialog?.scheduleActiveWorkflowRefresh?.('node-widget-change');
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
this._viewportClampFrame = null;
|
||||
this.activeWorkflowRouteKey = this.getActiveWorkflowRouteKey();
|
||||
this.activeWorkflowSignature = null;
|
||||
this.activeMissingWorkflowSignature = null;
|
||||
this._workflowRefreshTimer = null;
|
||||
this._workflowRefreshRetryTimer = null;
|
||||
this._workflowRefreshGeneration = 0;
|
||||
|
||||
@@ -150,7 +150,7 @@ export const lifecycleGraphMethods = {
|
||||
loadToken = `missing-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
this._workflowDataLoadToken = loadToken;
|
||||
|
||||
const workflowSignature = this.getWorkflowSignature(workflow);
|
||||
const workflowSignature = this.getMissingWorkflowSignature(workflow);
|
||||
if (force) {
|
||||
this.invalidateLoadedModelsCacheForActiveWorkflow();
|
||||
}
|
||||
|
||||
@@ -7,17 +7,28 @@ import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
const log = createModuleLogger('workflow_state_methods');
|
||||
|
||||
export const workflowStateMethods = {
|
||||
getWorkflowSignature(workflow) {
|
||||
getWorkflowSignature(workflow, options = {}) {
|
||||
if (!workflow) return null;
|
||||
try {
|
||||
return JSON.stringify(this.getWorkflowSignatureData(workflow));
|
||||
return JSON.stringify(this.getWorkflowSignatureData(workflow, options));
|
||||
} catch (error) {
|
||||
console.warn('Model Resolver: workflow signature generation failed', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getWorkflowSignatureData(workflow) {
|
||||
getMissingWorkflowSignature(workflow) {
|
||||
return this.getWorkflowSignature(workflow, { includeStrength: false });
|
||||
},
|
||||
|
||||
isWorkflowStrengthWidgetName(name = '') {
|
||||
const normalized = String(name || '').trim().replace(/[-\s]/g, '_').toLowerCase();
|
||||
return normalized === 'strength_model'
|
||||
|| normalized === 'strength_clip'
|
||||
|| /(?:^|_)(?:strength|model_strength|clip_strength)$/.test(normalized);
|
||||
},
|
||||
|
||||
getWorkflowSignatureData(workflow, { includeStrength = true } = {}) {
|
||||
const modelExtensionPattern = /\.(ckpt|pt2?|bin|pth|safetensors|pkl|sft|onnx|gguf)(?:$|[?#])/i;
|
||||
const urnPattern = /^urn:air:[^:]+:[^:]+:[^:]+:\d+@\d+$/i;
|
||||
const urlPattern = /^https?:\/\//i;
|
||||
@@ -62,10 +73,12 @@ export const workflowStateMethods = {
|
||||
'modelid',
|
||||
'version_id',
|
||||
'versionid',
|
||||
'strength',
|
||||
'active',
|
||||
'on'
|
||||
]);
|
||||
if (includeStrength) {
|
||||
modelMetadataKeys.add('strength');
|
||||
}
|
||||
const loraStrengthNodeTypes = new Set([
|
||||
'LoraLoader',
|
||||
'LoraLoaderModelOnly',
|
||||
@@ -176,8 +189,10 @@ export const workflowStateMethods = {
|
||||
};
|
||||
const isRelevantScalarKey = (key = '') => {
|
||||
const normalizedKey = normalizeKey(key);
|
||||
return loraStrengthKeys.has(normalizedKey)
|
||||
return (includeStrength && (
|
||||
loraStrengthKeys.has(normalizedKey)
|
||||
|| indexedLoraStrengthPattern.test(normalizedKey)
|
||||
))
|
||||
|| loraStackStateKeys.has(normalizedKey)
|
||||
|| ['active', 'on', 'model_id', 'modelid', 'version_id', 'versionid'].includes(normalizedKey);
|
||||
};
|
||||
@@ -245,7 +260,8 @@ export const workflowStateMethods = {
|
||||
);
|
||||
|
||||
if (
|
||||
normalized === null
|
||||
includeStrength
|
||||
&& normalized === null
|
||||
&& loraStrengthNodeTypes.has(nodeType)
|
||||
&& (
|
||||
normalizeRelevantValue(widgetsValues[index - 1], widgetValueKey(index - 1)) !== null
|
||||
@@ -351,7 +367,10 @@ export const workflowStateMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
getWorkflowScopedQueueKey(route = this.activeWorkflowRouteKey, signature = this.activeWorkflowSignature) {
|
||||
getWorkflowScopedQueueKey(
|
||||
route = this.activeWorkflowRouteKey,
|
||||
signature = this.activeMissingWorkflowSignature || this.activeWorkflowSignature
|
||||
) {
|
||||
if (route && signature) return `${route}\n${signature}`;
|
||||
return route || signature || null;
|
||||
},
|
||||
@@ -598,8 +617,12 @@ export const workflowStateMethods = {
|
||||
const currentWorkflow = workflow || this.getCurrentWorkflow();
|
||||
const nextRoute = this.getActiveWorkflowRouteKey();
|
||||
const nextSignature = this.getWorkflowSignature(currentWorkflow);
|
||||
const nextMissingSignature = this.getMissingWorkflowSignature(currentWorkflow);
|
||||
const previousKey = this.getWorkflowScopedQueueKey();
|
||||
const nextKey = this.getWorkflowScopedQueueKey(nextRoute, nextSignature);
|
||||
const nextKey = this.getWorkflowScopedQueueKey(
|
||||
nextRoute,
|
||||
nextMissingSignature || nextSignature
|
||||
);
|
||||
const preserveSearchCache = Boolean(this.preserveSearchCacheAcrossNextWorkflowSync);
|
||||
const previousSearchCache = preserveSearchCache
|
||||
? this.cloneSearchResultCache(this.searchResultCache, {
|
||||
@@ -624,6 +647,7 @@ export const workflowStateMethods = {
|
||||
if (!nextKey || nextKey === previousKey) {
|
||||
this.activeWorkflowRouteKey = nextRoute;
|
||||
this.activeWorkflowSignature = nextSignature;
|
||||
this.activeMissingWorkflowSignature = nextMissingSignature;
|
||||
this.preserveSearchCacheAcrossNextWorkflowSync = false;
|
||||
return;
|
||||
}
|
||||
@@ -636,6 +660,7 @@ export const workflowStateMethods = {
|
||||
this.clearWorkflowScopedState();
|
||||
this.activeWorkflowRouteKey = nextRoute;
|
||||
this.activeWorkflowSignature = nextSignature;
|
||||
this.activeMissingWorkflowSignature = nextMissingSignature;
|
||||
this.restorePendingQueueForActiveWorkflow();
|
||||
this.restoreAnalysisCacheForActiveWorkflow();
|
||||
this.restoreLoadedModelsCacheForActiveWorkflow();
|
||||
@@ -744,8 +769,11 @@ export const workflowStateMethods = {
|
||||
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const signature = this.getWorkflowSignature(workflow);
|
||||
const missingSignature = this.getMissingWorkflowSignature(workflow);
|
||||
const routeChanged = currentRoute !== this.activeWorkflowRouteKey;
|
||||
const signatureChanged = signature && signature !== this.activeWorkflowSignature;
|
||||
const missingSignatureChanged = missingSignature
|
||||
&& missingSignature !== this.activeMissingWorkflowSignature;
|
||||
const graphStillLooksOld = routeChanged && previousSignature && signature === previousSignature;
|
||||
|
||||
if ((!signature || graphStillLooksOld) && attempt < 8) {
|
||||
@@ -796,6 +824,10 @@ export const workflowStateMethods = {
|
||||
this.syncWorkflowScopedQueue(workflow);
|
||||
|
||||
if (this.activeTab === 'missing') {
|
||||
if (!routeChanged && missingSignature && !missingSignatureChanged) {
|
||||
log.debug('Model Resolver: skipping Missing Models refresh for a strength-only workflow change');
|
||||
return;
|
||||
}
|
||||
if (this.contentElement) this.contentElement.style.overflowY = 'auto';
|
||||
await this.loadWorkflowData(workflow, {
|
||||
preserveContent: !routeChanged && reason === 'node-widget-change'
|
||||
|
||||
Reference in New Issue
Block a user