diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 0d53801..8e1ca66 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -188,7 +188,8 @@ test('auto base model uses Any model for standalone SAM and Ultralytics models', assert.equal(getMissingAutoBaseModel.call(dialog, missing), ''); assert.equal(getSearchBaseModelLabel.call(dialog, 'auto', missing), 'Auto (Any model)'); assert.equal(getSearchBaseModelContext.call(dialog, missing), ''); - assert.match(getMissingAutoBaseModelInfo.call(dialog, missing).message, /Auto uses Any model/); + assert.match(getMissingAutoBaseModelInfo.call(dialog, missing).message, /Selected automatically: Any model/); + assert.match(getMissingAutoBaseModelInfo.call(dialog, missing).message, /no model filter is needed/); } state.selectedBaseModel = 'SDXL 1.0'; @@ -199,6 +200,54 @@ test('auto base model uses Any model for standalone SAM and Ultralytics models', ); }); +test('auto base model keeps workflow path context when a delayed search result disagrees', () => { + const getMissingAutoBaseModelInfo = eval(`(${extractMethod(searchPanelMethodsSource, 'getMissingAutoBaseModelInfo')})`); + const missing = { + original_path: 'KREA2\\KNPV3_1.safetensors', + civitai_search_result: { base_model: 'Flux.1 D' } + }; + const dialog = { + getBaseModelIndependentSearchType() { + return ''; + }, + getCachedSearchSuggestionData() { + return { base_model: 'Flux.1 D' }; + }, + getSavedDownloadTargetSelection() { + return { + subfolder: 'FLUX', + subfolderTouched: false, + subfolderSuggestionAppliedBy: 'auto' + }; + }, + getMissingLocalBaseModel() { + return ''; + }, + resolveBaseModelAlias(value = '') { + if (value === 'Flux.1 D') return 'Flux.1 D'; + if (value === 'Krea 2') return 'Krea 2'; + return ''; + }, + resolveBaseModelAliasExact(value = '') { + return this.resolveBaseModelAlias(value); + }, + resolveBaseModelAliasFromPath(value = '') { + return /(^|[\\/])KREA2([\\/]|$)/i.test(String(value)) ? 'Krea 2' : ''; + }, + getDominantWorkflowBaseModel() { + return 'Flux.1 D'; + } + }; + + const info = getMissingAutoBaseModelInfo.call(dialog, missing); + + assert.equal(info.value, 'Krea 2'); + assert.equal(info.source, 'missing model path'); + assert.match(info.message, /Selected automatically: Krea 2/); + assert.match(info.message, /Why: The model path saved in this workflow/); + assert.match(info.message, /To change: Choose a different option in the Model field/); +}); + test('base model path mapping ignores conflicting full-path base model', () => { const normalizeBaseModelToken = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeBaseModelToken')})`); const getBaseModelTokenVariants = eval(`(${extractMethod(searchPanelMethodsSource, 'getBaseModelTokenVariants')})`); @@ -286,8 +335,26 @@ test('download subfolder tooltip explains automatic suggestion source', () => { const tooltip = getDownloadSubfolderTooltip.call(dialog, { node_id: 1 }, 'loras', 'SDXL/Style'); - assert.match(tooltip, /Auto selected SDXL\\Style/); - assert.match(tooltip, /base model \(SDXL\) and tag \(style\)/); + assert.match(tooltip, /Suggested subfolder: SDXL\\Style/); + assert.match(tooltip, /identified as SDXL and tagged/); +}); + +test('download subfolder tooltip identifies a folder taken from the workflow model path', () => { + const getDownloadSubfolderSuggestionReason = eval(`(${extractMethod(downloadTargetMethodsSource, 'getDownloadSubfolderSuggestionReason')})`); + const dialog = { + normalizeFolderToken(value = '') { + return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); + } + }; + + const reason = getDownloadSubfolderSuggestionReason.call(dialog, { + suggestionSource: 'model_name', + matchedCandidate: 'KREA2' + }, { + original_path: 'KREA2\\KNPV3_1.safetensors' + }); + + assert.match(reason, /model path saved in this workflow.*folder.*KREA2/); }); test('download subfolder tooltip explains Suggest button choice', () => { @@ -333,8 +400,8 @@ test('download subfolder tooltip explains Suggest button choice', () => { const tooltip = getDownloadSubfolderTooltip.call(dialog, { node_id: 1 }, 'loras', 'Pony\\Styles', { saved }); - assert.match(tooltip, /because you clicked Suggest/); - assert.match(tooltip, /path template \(\{base_model\}\/\{first_tag\}\)/); + assert.match(tooltip, /Selected with Suggest: Pony\\Styles/); + assert.match(tooltip, /saved folder rule.*\{base_model\}\/\{first_tag\}/); }); test('post-search subfolder suggestion can prefer path template metadata', () => { @@ -500,6 +567,119 @@ test('search suggestion metadata prefers exact matching base model over weaker a assert.equal(merged.filename, 'snofs_krea_v1.safetensors'); }); +test('search suggestion does not let a delayed incompatible result replace workflow path context', () => { + const getCachedSearchSuggestionData = eval(`(${extractMethod(downloadTargetMethodsSource, 'getCachedSearchSuggestionData')})`); + const getFirstSearchResult = eval(`(${extractMethod(downloadTargetMethodsSource, 'getFirstSearchResult')})`); + const getSearchSuggestionPreferredBaseModel = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSearchSuggestionPreferredBaseModel')})`); + const baseModelMatchesSearchSuggestionPreference = eval(`(${extractMethod(downloadTargetMethodsSource, 'baseModelMatchesSearchSuggestionPreference')})`); + const getCompatibleCivitaiSearchResult = eval(`(${extractMethod(downloadTargetMethodsSource, 'getCompatibleCivitaiSearchResult')})`); + const getSearchSuggestionResultScore = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSearchSuggestionResultScore')})`); + const state = { + selectedBaseModel: 'auto', + results: { + civitai: { + base_model: 'Flux.1 D', + filename: 'KNPV3_1.safetensors', + match_type: 'similar', + confidence: 80 + } + } + }; + const dialog = { + searchResultCache: new Map([['missing-key', state]]), + getCachedSearchSuggestionData, + getFirstSearchResult, + getSearchSuggestionPreferredBaseModel, + baseModelMatchesSearchSuggestionPreference, + getCompatibleCivitaiSearchResult, + getSearchSuggestionResultScore, + getMissingSearchKey() { + return 'missing-key'; + }, + getSavedDownloadTargetSelection() { + return null; + }, + getMissingLocalBaseModel() { + return ''; + }, + resolveBaseModelAliasFromPath(value = '') { + return /(^|[\\/])KREA2([\\/]|$)/i.test(String(value)) ? 'Krea 2' : ''; + }, + normalizeBaseModelToken(value = '') { + return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); + }, + resolveBaseModelAlias(value = '') { + const token = this.normalizeBaseModelToken(value); + if (token === 'krea2') return 'Krea 2'; + if (token === 'flux1d') return 'Flux.1 D'; + return ''; + }, + resolveBaseModelAliasExact(value = '') { + return this.resolveBaseModelAlias(value); + }, + getSourceResultDownloadCategory() { + return 'loras'; + } + }; + const missing = { + category: 'loras', + original_path: 'KREA2\\KNPV3_1.safetensors', + civitai_search_result: { base_model: 'Flux.1 D' } + }; + + assert.equal(getSearchSuggestionPreferredBaseModel.call(dialog, missing), 'Krea 2'); + assert.deepEqual(getCompatibleCivitaiSearchResult.call(dialog, missing), {}); + assert.deepEqual(getCachedSearchSuggestionData.call(dialog, missing), {}); +}); + +test('subfolder suggestion ignores hidden local matches below the visible confidence threshold', () => { + const getSuggestedModelSubfolderCandidates = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSuggestedModelSubfolderCandidates')})`); + const getSuggestedExistingSubfolderByModelName = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSuggestedExistingSubfolderByModelName')})`); + const dialog = { + getSuggestedModelSubfolderCandidates, + getSuggestedExistingSubfolderByModelName, + getBestLocalMatch(missing, minConfidence) { + return (missing.matches || []) + .filter(match => Number(match.confidence) >= minConfidence) + .sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0))[0] || null; + }, + getCompatibleCivitaiSearchResult() { + return {}; + }, + getCachedSearchSuggestionData() { + return {}; + }, + normalizeFolderToken(value = '') { + return String(value || '') + .toLowerCase() + .replace(/[\\/]+/g, ' ') + .replace(/[^a-z0-9]+/g, ''); + } + }; + const missing = { + original_path: 'KNPV3_1.safetensors', + name: 'KNPV3_1.safetensors', + matches: [{ + confidence: 55, + model: { + filename: 'KNPV4.1_pre.safetensors', + relative_path: 'FLUX\\KREA\\concept\\KNPV4.1_pre.safetensors' + } + }] + }; + const folders = [{ + value: 'FLUX', + segments: ['FLUX'], + normalizedSegments: ['flux'] + }]; + + const candidates = getSuggestedModelSubfolderCandidates.call(dialog, missing); + const suggestion = getSuggestedExistingSubfolderByModelName.call(dialog, missing, folders); + + assert.equal(candidates.some(candidate => candidate.normalized === 'flux'), false); + assert.equal(suggestion, null); +}); + test('auto-link 100 percent applies visible exact matches without re-analyzing', async () => { const autoResolve100Percent = eval(`(${extractMethod(resolveDownloadMethodsSource, 'autoResolve100Percent')})`); const getExactLocalMatchSelections = eval(`(${extractMethod(resolveDownloadMethodsSource, 'getExactLocalMatchSelections')})`); diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index c1cc91f..3109f44 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -445,6 +445,9 @@ export const downloadTargetMethods = { }, getDownloadCategoryTooltip(missing = {}, selectedCategory = '', options = {}) { + const formatFolderTooltip = (heading, value, reason, action = 'Choose another option in the Folder field.') => ( + `${heading}: ${value}\n\nWhy: ${reason}\n\nTo change: ${action}` + ); const category = this.normalizeDownloadCategory(selectedCategory || 'checkpoints'); const label = this.getCategoryDisplayName(category); const saved = options.saved || this.getSavedDownloadTargetSelection(missing); @@ -452,27 +455,47 @@ export const downloadTargetMethods = { const preserveSavedCategory = options.preserveSavedCategory ?? this.shouldPreserveSavedDownloadCategory(missing, saved, inferredCategory); if (preserveSavedCategory) { - return `Folder is set to ${label} because you manually changed it for this missing model. Model Resolver remembers that choice for this workflow entry.`; + return formatFolderTooltip( + 'Selected manually', + label, + 'You changed this folder earlier, so Model Resolver kept your choice for this workflow.' + ); } const nodeCategory = this.getMissingNodeTypeDownloadCategory(missing); if (nodeCategory && this.normalizeDownloadCategory(nodeCategory) === category) { const nodeType = missing.locate_node_type || missing.promoted_inner_node_type || missing.node_type || 'this node'; - return `Auto selected ${label} because the node type (${nodeType}) uses this kind of model folder.`; + return formatFolderTooltip( + 'Selected automatically', + label, + `The workflow node “${nodeType}” uses this type of model.` + ); } const rawMissingCategory = missing.category || missing.directory || ''; const missingCategory = rawMissingCategory ? this.normalizeDownloadCategory(rawMissingCategory) : ''; if (missingCategory && missingCategory === category) { - return `Auto selected ${label} from the model category detected in the workflow: ${rawMissingCategory}.`; + return formatFolderTooltip( + 'Selected automatically', + label, + `The workflow identifies this as a ${this.getCategoryDisplayName(missingCategory)} model.` + ); } const defaultCategory = this.normalizeDownloadCategory(options.defaultCategory || 'checkpoints'); if (defaultCategory && defaultCategory === category) { - return `Auto selected ${label} from the download source or default folder for this result.`; + return formatFolderTooltip( + 'Selected automatically', + label, + 'This is the folder recommended by the selected download source or your default settings.' + ); } - return `Auto selected ${label} as the best available download folder. If this is wrong, choose another folder manually.`; + return formatFolderTooltip( + 'Selected automatically', + label, + 'This is the closest matching download folder available for this model.' + ); }, getSubfolderSuggestionTrackingPatch(suggestion = null, appliedBy = '') { @@ -497,28 +520,40 @@ export const downloadTargetMethods = { }; }, - getDownloadSubfolderSuggestionReason(suggestion = {}) { + getDownloadSubfolderSuggestionReason(suggestion = {}, missing = {}) { const source = suggestion?.suggestionSource || ''; if (source === 'template') { - const template = suggestion.template ? ` (${suggestion.template})` : ''; - return `your path template${template} matched the available model metadata`; + const template = suggestion.template ? ` “${suggestion.template}”` : ''; + return `Your saved folder rule${template} produced this subfolder from the available model information.`; } if (source === 'lora_metadata') { const baseModel = suggestion.matchedBaseModel || ''; const tag = suggestion.matchedTag || ''; if (baseModel && tag) { - return `this LoRA's base model (${baseModel}) and tag (${tag}) match an existing folder`; + return `The LoRA is identified as ${baseModel} and tagged “${tag}”; an existing folder matches both.`; } if (baseModel) { - return `this LoRA's base model (${baseModel}) matches an existing folder`; + return `The LoRA is identified as ${baseModel}, and an existing folder has the same name.`; } - return `this LoRA's metadata matches an existing folder`; + return `The available LoRA information matches an existing folder.`; } if (source === 'model_name') { const candidate = suggestion.matchedCandidate || ''; + const candidateToken = this.normalizeFolderToken(candidate); + const workflowPathParts = String(missing?.original_path || '') + .split(/[/\\]+/) + .map(part => part.trim()) + .filter(Boolean); + const workflowDirectoryParts = workflowPathParts.slice(0, -1); + const matchingWorkflowDirectory = workflowDirectoryParts.find(part => ( + candidateToken && this.normalizeFolderToken(part) === candidateToken + )); + if (matchingWorkflowDirectory) { + return `The model path saved in this workflow already contains the folder “${matchingWorkflowDirectory}”, and the same download folder exists on this computer.`; + } return candidate - ? `the model name or filename (${candidate}) matches an existing folder` - : `the model name or filename matches an existing folder`; + ? `Part of the model name, “${candidate}”, matches an existing download folder.` + : `The model name matches an existing download folder.`; } return ''; }, @@ -560,6 +595,9 @@ export const downloadTargetMethods = { }, getDownloadSubfolderTooltip(missing = {}, selectedCategory = '', selectedSubfolder = '', options = {}) { + const formatSubfolderTooltip = (heading, value, reason, finalLabel = 'To change', finalText = 'Type or choose another subfolder.') => ( + `${heading}: ${value}\n\nWhy: ${reason}\n\n${finalLabel}: ${finalText}` + ); const category = this.normalizeDownloadCategory(selectedCategory || 'checkpoints'); const categoryLabel = this.getCategoryDisplayName(category); const saved = options.saved || this.getSavedDownloadTargetSelection(missing); @@ -568,33 +606,67 @@ export const downloadTargetMethods = { const mode = this.getDownloadPathMode(); const savedSuggestion = this.getSavedDownloadSubfolderSuggestion(saved, subfolder, baseDirectory); const currentSuggestion = savedSuggestion || this.getCurrentDownloadSubfolderSuggestion(missing, category, subfolder, baseDirectory); - const suggestionReason = this.getDownloadSubfolderSuggestionReason(currentSuggestion); + const suggestionReason = this.getDownloadSubfolderSuggestionReason(currentSuggestion, missing); if (saved?.subfolderTouched) { if (!subfolder) { - return `Subfolder is empty because you manually cleared it for this missing model. The file will download directly into the ${categoryLabel} folder.`; + return formatSubfolderTooltip( + 'Subfolder', + 'None', + 'You cleared this field manually, so Model Resolver kept it empty.', + 'Save location', + `The file will be saved directly in the ${categoryLabel} folder.` + ); } if (saved?.subfolderSuggestionAppliedBy === 'button' && suggestionReason) { - return `Subfolder is set to ${subfolder} because you clicked Suggest. The suggestion was chosen because ${suggestionReason}. Model Resolver remembers that choice for this workflow entry.`; + return formatSubfolderTooltip( + 'Selected with Suggest', + subfolder, + suggestionReason + ); } - return `Subfolder is set to ${subfolder} because you manually changed it for this missing model. Model Resolver remembers that choice for this workflow entry.`; + return formatSubfolderTooltip( + 'Selected manually', + subfolder, + 'You changed this subfolder earlier, so Model Resolver kept your choice for this workflow.' + ); } if (subfolder) { - return suggestionReason - ? `Auto selected ${subfolder} because ${suggestionReason}. If this is wrong, type or pick another subfolder manually.` - : `Auto selected ${subfolder} as the best available subfolder for this model. If this is wrong, type or pick another subfolder manually.`; + return formatSubfolderTooltip( + 'Suggested subfolder', + subfolder, + suggestionReason || 'This is the closest matching existing folder for the available model information.' + ); } if (mode === 'manual') { - return `Subfolder is empty because Subfolder fill mode is Manual. Type a path or click Suggest to choose one for this download.`; + return formatSubfolderTooltip( + 'Subfolder', + 'None', + 'Automatic subfolder suggestions are set to Manual in Options.', + 'To add one', + 'Type a subfolder or click Suggest.' + ); } if (!this.isAutoFillSubfolderEnabled()) { - return `Subfolder is empty because Auto-fill subfolder is disabled. Type a path or click Suggest to choose one for this download.`; + return formatSubfolderTooltip( + 'Subfolder', + 'None', + 'Automatic subfolder suggestions are disabled in Options.', + 'To add one', + 'Type a subfolder or click Suggest.' + ); } - return `No subfolder was selected automatically because Model Resolver did not find a reliable metadata, tag, filename, or existing-folder match. The file will download directly into the ${categoryLabel} folder.`; + return formatSubfolderTooltip( + 'Subfolder', + 'None', + 'Model Resolver did not find enough reliable information to choose one.', + 'Save location', + `The file will be saved directly in the ${categoryLabel} folder.` + ); }, refreshDownloadTargetHelp(container, missing, categoryEl = null, subfolderEl = null) { @@ -1231,9 +1303,10 @@ export const downloadTargetMethods = { getDownloadPathMetadata(missing = {}, source = {}) { const sourceData = source && typeof source === 'object' ? source : {}; const searchSuggestion = this.getCachedSearchSuggestionData(missing); + const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {}; const merged = { ...(missing?.civitai_info || {}), - ...(missing?.civitai_search_result || {}), + ...compatibleCivitaiSearch, ...(missing?.download_source || {}), ...(searchSuggestion || {}), ...sourceData @@ -1269,9 +1342,10 @@ export const downloadTargetMethods = { getDownloadMetadata(missing = {}, source = {}, options = {}) { const sourceData = source && typeof source === 'object' ? source : {}; const searchSuggestion = this.getCachedSearchSuggestionData(missing); + const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {}; const merged = { ...(missing?.civitai_info || {}), - ...(missing?.civitai_search_result || {}), + ...compatibleCivitaiSearch, ...(missing?.download_source || {}), ...(searchSuggestion || {}), ...sourceData @@ -1453,13 +1527,17 @@ export const downloadTargetMethods = { if (selectedBaseModel && selectedBaseModel !== 'auto' && selectedBaseModel !== 'none') { addCandidate(selectedBaseModel); } - addCandidate(missing?.civitai_search_result?.base_model); - addCandidate(missing?.civitai_search_result?.baseModel); - addCandidate(missing?.civitai_info?.base_model); - addCandidate(missing?.civitai_info?.baseModel); addCandidate(missing?.download_source?.base_model); addCandidate(missing?.download_source?.baseModel); + addCandidate(missing?.civitai_info?.base_model); + addCandidate(missing?.civitai_info?.baseModel); addCandidate(this.getMissingLocalBaseModel?.(missing, 70)); + const savedTarget = this.getSavedDownloadTargetSelection?.(missing) || {}; + if (savedTarget.subfolderTouched) { + addCandidate(this.resolveBaseModelAliasFromPath?.(savedTarget.subfolder)); + } + addCandidate(this.resolveBaseModelAliasFromPath?.(missing?.original_path)); + addCandidate(this.resolveBaseModelAliasFromPath?.(missing?.name)); for (const candidate of candidates) { const canonical = this.resolveBaseModelAlias?.(candidate) @@ -1489,6 +1567,22 @@ export const downloadTargetMethods = { return Boolean(preferredToken && resultToken && preferredToken === resultToken); }, + getCompatibleCivitaiSearchResult(missing = {}) { + const result = missing?.civitai_search_result; + if (!result || typeof result !== 'object') return {}; + + const preferredBaseModel = this.getSearchSuggestionPreferredBaseModel(missing); + const resultBaseModel = result.base_model || result.baseModel || ''; + if ( + preferredBaseModel + && resultBaseModel + && !this.baseModelMatchesSearchSuggestionPreference(resultBaseModel, preferredBaseModel) + ) { + return {}; + } + return result; + }, + getSearchSuggestionResultScore(source = '', result = {}, preferredBaseModel = '') { if (!result || typeof result !== 'object') return -Infinity; @@ -1574,6 +1668,12 @@ export const downloadTargetMethods = { result: this.getFirstSearchResult(results[source]) })) .filter(candidate => candidate.result && typeof candidate.result === 'object') + .filter(candidate => { + if (!preferredBaseModel) return true; + const resultBaseModel = candidate.result.base_model || candidate.result.baseModel || ''; + return !resultBaseModel + || this.baseModelMatchesSearchSuggestionPreference(resultBaseModel, preferredBaseModel); + }) .map(candidate => ({ ...candidate, score: this.getSearchSuggestionResultScore(candidate.source, candidate.result, preferredBaseModel) @@ -1626,10 +1726,11 @@ export const downloadTargetMethods = { } const searchSuggestion = this.getCachedSearchSuggestionData(missing); + const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {}; const civitaiData = { ...(missing?.civitai_info || {}), ...(searchSuggestion || {}), - ...(missing?.civitai_search_result || {}), + ...compatibleCivitaiSearch, ...(missing?.download_source || {}) }; const baseModel = civitaiData.base_model || ''; @@ -1685,11 +1786,15 @@ export const downloadTargetMethods = { getSuggestedModelSubfolderCandidates(missing = {}) { const source = missing.download_source || {}; const civitaiInfo = missing.civitai_info || {}; - const civitaiSearch = missing.civitai_search_result || {}; + const civitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {}; const searchSuggestion = this.getCachedSearchSuggestionData(missing); const localMatches = Array.isArray(missing.matches) ? missing.matches : []; - const bestLocalMatch = localMatches - .filter(match => match && typeof match === 'object') + const bestLocalMatch = this.getBestLocalMatch?.(missing, 70) || localMatches + .filter(match => ( + match + && typeof match === 'object' + && Number(match.confidence) >= 70 + )) .sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0))[0] || null; const bestLocalModel = bestLocalMatch?.model || {}; const bestLocalPath = bestLocalModel.relative_path diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 1592fbd..61eba92 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -226,24 +226,28 @@ export const searchPanelMethods = { }, getMissingAutoBaseModelInfo(missing = {}) { + const formatAutoMessage = (selected, reason, action = 'Choose a different option in the Model field.') => ( + `Selected automatically: ${selected}\n\nWhy: ${reason}\n\nTo change: ${action}` + ); const independentType = this.getBaseModelIndependentSearchType(missing); if (independentType) { return { value: '', source: 'independent model type', usesAnyModel: true, - message: `${independentType} models are not tied to a base model, so Auto uses Any model.` + message: formatAutoMessage( + 'Any model', + `${independentType} models are not tied to one base model, so no model filter is needed.` + ) }; } const searchSuggestion = this.getCachedSearchSuggestionData?.(missing) || {}; const directCandidates = [ - { value: missing?.civitai_info?.base_model, source: 'model metadata' }, - { value: missing?.civitai_info?.baseModel, source: 'model metadata' }, - { value: missing?.civitai_search_result?.base_model, source: 'search result metadata' }, - { value: missing?.civitai_search_result?.baseModel, source: 'search result metadata' }, { value: missing?.download_source?.base_model, source: 'selected download source' }, - { value: missing?.download_source?.baseModel, source: 'selected download source' } + { value: missing?.download_source?.baseModel, source: 'selected download source' }, + { value: missing?.civitai_info?.base_model, source: 'model metadata' }, + { value: missing?.civitai_info?.baseModel, source: 'model metadata' } ]; for (const { value, source } of directCandidates) { @@ -252,15 +256,23 @@ export const searchPanelMethods = { return { value: canonical, source, - message: `Auto selected ${canonical} from ${source}.` + message: formatAutoMessage( + canonical, + source === 'selected download source' + ? `The download source you selected identifies this model as ${canonical}.` + : `The model information saved with this workflow identifies it as ${canonical}.` + ) }; } } const savedTarget = this.getSavedDownloadTargetSelection?.(missing) || {}; + const explicitlySelectedSubfolder = savedTarget.subfolderTouched + ? savedTarget.subfolder + : ''; const pathCandidates = [ { value: this.getMissingLocalBaseModel(missing, 70), source: 'best local match' }, - { value: savedTarget.subfolder, source: 'selected subfolder' } + { value: explicitlySelectedSubfolder, source: 'selected subfolder' } ]; for (const { value, source } of pathCandidates) { @@ -270,22 +282,12 @@ export const searchPanelMethods = { return { value: canonical, source, - message: `Auto selected ${canonical} from the ${source}.` - }; - } - } - - const cachedCandidates = [ - { value: searchSuggestion.base_model, source: 'cached search result' }, - { value: searchSuggestion.baseModel, source: 'cached search result' } - ]; - for (const { value, source } of cachedCandidates) { - const canonical = this.resolveBaseModelAlias(value); - if (canonical) { - return { - value: canonical, - source, - message: `Auto selected ${canonical} from ${source} metadata.` + message: formatAutoMessage( + canonical, + source === 'best local match' + ? `A reliable local match is stored in a folder associated with ${canonical}.` + : `The subfolder you selected is associated with ${canonical}.` + ) }; } } @@ -301,7 +303,32 @@ export const searchPanelMethods = { return { value: canonical, source, - message: `Auto selected ${canonical} from the ${source}.` + message: formatAutoMessage( + canonical, + source === 'missing model path' + ? `The model path saved in this workflow points to a folder associated with ${canonical}.` + : source === 'missing model name' + ? `The model name saved in this workflow contains a name associated with ${canonical}.` + : `A previously found model path points to a folder associated with ${canonical}.` + ) + }; + } + } + + const cachedCandidates = [ + { value: searchSuggestion.base_model, source: 'cached search result' }, + { value: searchSuggestion.baseModel, source: 'cached search result' } + ]; + for (const { value, source } of cachedCandidates) { + const canonical = this.resolveBaseModelAlias(value); + if (canonical) { + return { + value: canonical, + source, + message: formatAutoMessage( + canonical, + `A compatible search result identifies this model as ${canonical}.` + ) }; } } @@ -311,14 +338,22 @@ export const searchPanelMethods = { return { value: workflowFallback, source: 'workflow fallback', - message: `This model was not recognized in the Base Models list, so Auto is using the workflow-wide fallback: ${workflowFallback}. If this looks wrong, open Options and update the local database / Base Models list.` + message: formatAutoMessage( + workflowFallback, + `This model could not be identified directly, so Model Resolver used the most common model type in this workflow.`, + 'Choose another model here, or update the Base Models list in Options.' + ) }; } return { value: '', source: 'none', - message: 'Auto could not detect a base model for this entry. Open Options and update the local database / Base Models list, or choose a model manually.' + message: formatAutoMessage( + 'No model filter', + 'There is not enough reliable information to identify the model type.', + 'Choose a model here, or update the Base Models list in Options.' + ) }; }, @@ -326,10 +361,10 @@ export const searchPanelMethods = { const state = this.getSearchState(missing); const selected = state.selectedBaseModel || this.getDefaultSearchBaseModel(); if (selected === 'none') { - return 'Search will use Any model and will not filter by base model.'; + return 'Selected: Any model\n\nWhy: Searches will not be limited to one model type.\n\nTo change: Choose Auto or a specific model.'; } if (selected && selected !== 'auto') { - return `Search model filter is manually set to ${selected}.`; + return `Selected manually: ${selected}\n\nWhy: Searches are limited to this model type.\n\nTo change: Choose Auto, Any model, or another model.`; } return this.getMissingAutoBaseModelInfo(missing).message; },