diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 4635b55..a2a6d79 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -4211,6 +4211,99 @@ test('auto base model keeps workflow path context when a delayed search result d assert.match(info.message, /To change: Choose a different option in the Model field/); }); +test('auto base model uses active widgets instead of workflow model metadata', () => { + const getWorkflowModelReferenceText = eval(`(${extractMethod(searchPanelMethodsSource, 'getWorkflowModelReferenceText')})`); + const getDominantWorkflowBaseModel = eval(`(${extractMethod(searchPanelMethodsSource, 'getDominantWorkflowBaseModel')})`); + const getBaseModelIndependentSearchType = eval(`(${extractMethod(searchPanelMethodsSource, 'getBaseModelIndependentSearchType')})`); + const getMissingAutoBaseModelInfo = eval(`(${extractMethod(searchPanelMethodsSource, 'getMissingAutoBaseModelInfo')})`); + const workflow = { + nodes: [ + { + id: 761, + type: 'UNETLoader', + widgets_values: [ + 'KREA2\\Krea2_Turbo_convrot_int8mixed.safetensors', + 'default' + ], + widgets_values_named: { + unet_name: 'KREA2\\Krea2_Turbo_convrot_int8mixed.safetensors', + weight_dtype: 'default' + }, + properties: { + models: [{ + name: 'z_image_turbo_bf16.safetensors', + url: 'https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors', + directory: 'diffusion_models' + }] + } + } + ], + extra: { + z_image_url: 'https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/config.json' + } + }; + const dialog = { + getCurrentWorkflow() { + return workflow; + }, + getWorkflowSignatureData() { + return { + nodes: [{ + bypassed: false, + widgets_values: [{ + index: 0, + value: 'KREA2\\Krea2_Turbo_convrot_int8mixed.safetensors' + }] + }], + definitions: {} + }; + }, + getResolvedWorkflowBaseModelScores() { + return new Map(); + }, + getBaseModelIndependentSearchType, + getMissingLocalBaseModel() { + return ''; + }, + resolveBaseModelAlias() { + return ''; + }, + resolveBaseModelAliasExact() { + return ''; + }, + resolveBaseModelAliasFromPath() { + return ''; + }, + getBaseModelAliases() { + return [ + { + value: 'Z-Image', + aliases: ['zimage', 'z image', 'z-image', 'z_image', 'zImageTurbo', 'z image turbo'] + }, + { value: 'Krea 2', aliases: ['krea', 'krea 2'] } + ]; + }, + normalizeBaseModelToken(value = '') { + return normalizeSearchToken(value); + }, + getWorkflowModelReferenceText, + getDominantWorkflowBaseModel, + getMissingAutoBaseModelInfo + }; + + const referenceText = getWorkflowModelReferenceText.call(dialog); + assert.match(referenceText, /Krea2_Turbo/); + assert.doesNotMatch(referenceText, /z_image/); + assert.equal(getDominantWorkflowBaseModel.call(dialog), 'Krea 2'); + + const info = getMissingAutoBaseModelInfo.call(dialog, { + name: 'Text Refusal Reduction Krea2.safetensors', + category: 'loras' + }); + assert.equal(info.value, 'Krea 2'); + assert.equal(info.source, 'workflow fallback'); +}); + test('base model path mapping ignores conflicting full-path base model', () => { const { normalizeBaseModelToken, diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 024577d..83c906f 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -251,26 +251,77 @@ export const searchPanelMethods = { getWorkflowModelReferenceText() { const workflow = this.getCurrentWorkflow?.(); if (!workflow) return ''; + const values = []; - const visit = (value, key = '') => { + const visitValue = (value) => { if (typeof value === 'string') { if (/^urn:/i.test(value.trim())) { return; } - if (this.hasModelExtension(value) || /model|checkpoint|unet|diffusion/i.test(key)) { - values.push(value); - } + values.push(value); return; } if (Array.isArray(value)) { - value.forEach(item => visit(item, key)); + value.forEach(visitValue); return; } if (value && typeof value === 'object') { - Object.entries(value).forEach(([childKey, childValue]) => visit(childValue, childKey)); + Object.values(value).forEach(visitValue); } }; - visit(workflow); + + const collectNormalizedNodes = (nodes = []) => { + if (!Array.isArray(nodes)) return; + nodes.forEach(node => { + if (!node || node.bypassed) return; + const widgetValues = Array.isArray(node.widgets_values) + ? node.widgets_values + : []; + widgetValues.forEach(widget => { + if (widget && typeof widget === 'object' && 'value' in widget) { + visitValue(widget.value); + } + }); + }); + }; + + const signatureData = this.getWorkflowSignatureData?.(workflow); + if (signatureData && typeof signatureData === 'object') { + collectNormalizedNodes(signatureData.nodes); + Object.values(signatureData.definitions || {}).forEach(definition => { + if (Array.isArray(definition)) { + definition.forEach(item => collectNormalizedNodes(item?.nodes)); + } else { + collectNormalizedNodes(definition?.nodes); + } + }); + } + + // Older workflow snapshots may only expose named widget values. Keep + // this fallback limited to model-like keys so node metadata and URLs + // from properties.models cannot affect the base-model inference. + if (!values.length) { + const modelKeyPattern = /model|lora|checkpoint|unet|vae|clip|encoder|control|upscale|embedding|gligen|sam/i; + const collectNamedValues = (nodes = []) => { + if (!Array.isArray(nodes)) return; + nodes.forEach(node => { + if (!node || node.mode === 4 || !node.widgets_values_named) return; + Object.entries(node.widgets_values_named).forEach(([key, value]) => { + if (modelKeyPattern.test(key)) visitValue(value); + }); + }); + }; + collectNamedValues(workflow.nodes); + const definitions = workflow.definitions; + Object.values(definitions || {}).forEach(definition => { + if (Array.isArray(definition)) { + definition.forEach(item => collectNamedValues(item?.nodes)); + } else { + collectNamedValues(definition?.nodes); + } + }); + } + return values.join(' '); },