refactor(search): preserve workflow path context and standardize tooltips
This commit is contained in:
@@ -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')})`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user