diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 38ab38e..42ac2b9 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -3830,6 +3830,39 @@ test('automatic Local Database download source stays hidden until that source is assert.equal(shouldDisplayKnownDownloadSource.call(dialog, {}, downloadSource, state), true); }); +test('HuggingFace fallback download source prefers the direct file URL', () => { + const getBestDownloadSourceForMissing = eval(`(${extractMethod(resolveDownloadMethodsSource, 'getBestDownloadSourceForMissing')})`); + const pageUrl = 'https://huggingface.co/Comfy-Org/YuE2/tree/main/checkpoints'; + const downloadUrl = 'https://huggingface.co/Comfy-Org/YuE2/resolve/main/checkpoints/yue2_3b_bf16.safetensors'; + const missing = { + missing_search_key: 'missing-key', + original_path: 'yue2_convrot_int8.safetensors', + category: 'checkpoints', + }; + const dialog = { + searchResultCache: new Map([[ + 'missing-key', + { results: { huggingface: { + source: 'huggingface', + model_id: 'Comfy-Org/YuE2', + filename: 'yue2_3b_bf16.safetensors', + url: pageUrl, + download_url: downloadUrl, + } } } + ]]), + getMissingSearchKey() { + return 'missing-key'; + }, + getFilenameFromPath(value = '') { + return String(value).split(/[\\/]/).at(-1) || ''; + }, + }; + + const source = getBestDownloadSourceForMissing.call(dialog, missing); + + assert.equal(source.url, downloadUrl); +}); + test('inactive missing models require every workflow reference to be inactive', () => { const { isMissingModelInactive } = missingModelStateMethods; @@ -8420,6 +8453,66 @@ test('applying source model details selection updates current download source ha assert.equal(refreshCalls.length, 1); }); +test('applying a HuggingFace file selection keeps the direct URL in the search result', () => { + const applySourceModelDetailsSelection = eval(`(${extractMethod(modelInfoMethodsSource, 'applySourceModelDetailsSelection')})`); + const sha256 = 'c'.repeat(64); + const pageUrl = 'https://huggingface.co/Comfy-Org/YuE2/tree/main/checkpoints'; + const downloadUrl = 'https://huggingface.co/Comfy-Org/YuE2/resolve/main/checkpoints/yue2_3b_bf16.safetensors'; + const missing = { + original_path: 'yue2_convrot_int8.safetensors', + category: 'checkpoints', + civitai_info: {}, + }; + const state = { results: {} }; + const dialog = { + getMissingByKey(key) { + return key === 'missing-key' ? missing : null; + }, + getSearchState() { + return state; + }, + getSourceModelFileHash(value = {}) { + const hashes = value.hashes && typeof value.hashes === 'object' ? value.hashes : {}; + return String(value.sha256 || value.hash || hashes.SHA256 || hashes.sha256 || '').trim().toLowerCase(); + }, + refreshSearchUiForMissing() {}, + refreshSearchBaseModelLabels() {}, + updateBatchFooterButtons() {}, + persistSearchStateForActiveWorkflow() {}, + syncRemoteHashMatchesForResult() {}, + showNotification() {}, + }; + + applySourceModelDetailsSelection.call(dialog, { + source: 'huggingface', + model_id: 'Comfy-Org/YuE2', + version_id: 'main', + name: 'Comfy-Org/YuE2', + filename: 'yue2_3b_bf16.safetensors', + url: pageUrl, + download_url: downloadUrl, + path: 'checkpoints/yue2_3b_bf16.safetensors', + sha256, + hashes: { SHA256: sha256 }, + file_info: { + name: 'yue2_3b_bf16.safetensors', + path: 'checkpoints/yue2_3b_bf16.safetensors', + sha256, + hashes: { SHA256: sha256 }, + }, + }, { + missing_key: 'missing-key', + details_source: 'huggingface', + }); + + assert.equal(state.results.huggingface.url, downloadUrl); + assert.equal(state.results.huggingface.download_url, downloadUrl); + assert.equal(state.results.huggingface.model_url, pageUrl); + assert.equal(missing.download_source.url, downloadUrl); + assert.equal(missing.download_source.model_url, pageUrl); + assert.equal(missing.download_source.sha256, sha256); +}); + test('batch sources advance independently while each source stays sequential', async () => { const runBatch = eval(`(${extractMethod(resolveDownloadMethodsSource, 'searchMissingBatch')})`); const started = []; diff --git a/tests/test_huggingface_show_more_ui.mjs b/tests/test_huggingface_show_more_ui.mjs index 8ff91e4..fa392d1 100644 --- a/tests/test_huggingface_show_more_ui.mjs +++ b/tests/test_huggingface_show_more_ui.mjs @@ -45,7 +45,7 @@ test('HuggingFace download rows expose the Show More action', () => { ); assert.match( resolveDownloadSource, - /if \(hfResult && hfResult\.url\)[\s\S]*?detailsContext:\s*\{[\s\S]*?\.\.\.hfResult,[\s\S]*?details_source:\s*'huggingface'/ + /const hfDownloadUrl = hfResult\?\.download_url \|\| hfResult\?\.url \|\| ''[\s\S]*?if \(hfResult && hfDownloadUrl\)[\s\S]*?detailsContext:\s*\{[\s\S]*?\.\.\.hfResult,[\s\S]*?details_source:\s*'huggingface'/ ); }); @@ -55,6 +55,19 @@ test('HuggingFace details requests keep folder, branch, and token context', () = assert.match(modelInfoSource, /hf_token: tokens\.hf_token \|\| ''/); }); +test('selected HuggingFace details reuse the row and download the direct file URL', () => { + assert.match( + resolveDownloadSource, + /const hfDownloadUrl = hfResult\?\.download_url \|\| hfResult\?\.url \|\| ''/ + ); + assert.match(resolveDownloadSource, /downloadUrl: hfDownloadUrl/); + assert.match( + modelInfoSource, + /const isHuggingFace = sourceKey === 'huggingface'[\s\S]*?url: isHuggingFace \? selection\.download_url : selection\.url/ + ); + assert.match(modelInfoSource, /model_url: sourceResult\.model_url \|\| sourceResult\.url/); +}); + test('a selected HuggingFace variant keeps its repository path', () => { assert.match( modelInfoSource, diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 6427a92..b96a4e4 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -130,7 +130,8 @@ export const resolveDownloadMethods = { { source: 'huggingface', result: first(results.huggingface), - urlKey: 'url', + urlKey: 'download_url', + fallbackUrlKey: 'url', filenameKey: 'filename' }, { @@ -155,7 +156,8 @@ export const resolveDownloadMethods = { for (const candidate of candidates) { const result = candidate.result; - const url = result?.[candidate.urlKey]; + const url = result?.[candidate.urlKey] + || (candidate.fallbackUrlKey ? result?.[candidate.fallbackUrlKey] : ''); if (!url) continue; const rawModelUrl = result.model_url || result.url || url; @@ -3859,7 +3861,8 @@ export const resolveDownloadMethods = { })); } - if (hfResult && hfResult.url) { + const hfDownloadUrl = hfResult?.download_url || hfResult?.url || ''; + if (hfResult && hfDownloadUrl) { const hfRepo = hfResult.repo_id || hfResult.repo || ''; const hfModelUrl = hfRepo ? `https://huggingface.co/${hfRepo}` : getModelCardUrl(hfResult.url); const missingCategory = this.getMissingDownloadCategory?.(missing, 'checkpoints') || missing.category || 'checkpoints'; @@ -3870,7 +3873,7 @@ export const resolveDownloadMethods = { model: hfRepo || hfResult.filename, filename: hfResult.filename, secondary: hfResult.path && hfResult.path !== hfResult.filename ? hfResult.path : '', - downloadUrl: hfResult.url, + downloadUrl: hfDownloadUrl, downloadFilename: hfResult.filename, category: missingCategory, openUrl: hfModelUrl, diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js index ea16062..d703eda 100644 --- a/web/resolver/views/model_info_methods.js +++ b/web/resolver/views/model_info_methods.js @@ -3549,6 +3549,13 @@ export const modelInfoMethods = { } const sourceKey = String(selection.source || contextModel.details_source || contextModel.source || '').toLowerCase(); + const isHuggingFace = sourceKey === 'huggingface'; + const sourcePageUrl = selection.page_url + || selection.pageUrl + || selection.version_url + || selection.versionUrl + || selection.url + || ''; const selectedBaseModel = selection.base_model || contextModel.base_model || ''; const selectionHashes = selection.hashes && typeof selection.hashes === 'object' ? selection.hashes : {}; const selectionFile = selection.file_info || selection.selected_file || selection.file || null; @@ -3565,7 +3572,7 @@ export const modelInfoMethods = { version_name: selection.version_name, type: selection.type, filename: selection.filename, - url: selection.url, + url: isHuggingFace ? selection.download_url : selection.url, download_url: selection.download_url, size: selection.size, base_model: selectedBaseModel, @@ -3578,6 +3585,12 @@ export const modelInfoMethods = { confidence: selection.confidence || 100, searchedAt: new Date().toISOString() }; + if (isHuggingFace) { + sourceResult.repo_id = selection.repo_id || selection.repo || selection.model_id || ''; + sourceResult.version_url = sourcePageUrl; + sourceResult.page_url = sourcePageUrl; + sourceResult.model_url = sourcePageUrl; + } if (Object.keys(selectionHashes).length) sourceResult.hashes = selectionHashes; if (selectionFile && typeof selectionFile === 'object') { sourceResult.file_info = selectionFile; @@ -3591,7 +3604,7 @@ export const modelInfoMethods = { missing.download_source = { ...sourceResult, url: sourceResult.download_url, - model_url: sourceResult.url, + model_url: sourceResult.model_url || sourceResult.url, directory: missing.category || 'checkpoints' };