mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
feat(download): support ModelScope repositories in the URL downloader
ModelScope became a linkable source, but downloading from it was impossible:
the URL picker only recognised huggingface.co, the file listing hit a
huggingface-only endpoint, the resolve URL was hardcoded, and the default
path template always wrote into a `huggingface/` directory.
Move the download knowledge into the providers so the handlers stay generic:
- `ModelSource` gains `list_files()`, `file_download_url()`,
`default_revision` and `default_subdir`. `HuggingFaceSource` keeps the Hub
tree API (`/api/models/{id}/tree/{rev}`, LFS-aware sizes, `main`).
`ModelScopeSource` uses `/api/v1/models/{id}/repo/files?Revision=master`
— which reports real byte sizes for LFS files, so no HEAD probe is needed,
and which only accepts `master` (an HF-imported repo still 404s on `main`)
— and downloads through `/models/{id}/resolve/{rev}/{path}`. That URL
redirects to a CDN target carrying a time-limited `auth_key`, so it is
rebuilt on every request and never cached, which is also what keeps
resumable Range requests working.
- `hf_handlers.py`/`HfHandler` become `model_source_handlers.py`/
`ModelSourceHandler` with `list_model_source_files` and
`download_model_source`. New routes `/api/lm/model-source-files` and
`/api/lm/download-model-source`; the old `/api/lm/hf-repo-files` and
`/api/lm/download-hf-model` paths stay as aliases, and a payload without
`platform` still means Hugging Face, so existing callers are unaffected.
- A downloaded sidecar now records `source_platform` + `source_url` (with the
`hf_url` alias only for Hugging Face) instead of always writing `hf_url`,
and `use_default_paths` files ModelScope downloads under
`modelscope/<owner>/<repo>`. The now-unused shared HF aiohttp session and
its shutdown hook are gone; providers open short-lived sessions.
- Frontend: `detectUrlType` returns the platform-neutral
`model-source-repo` / `model-source-file` plus an explicit `platform`, the
DownloadManager's `hf*` state and methods are renamed to `source*`, every
`source === 'huggingface'` check becomes `isExternalModelSource()`, and
batch groups are keyed by `platform:repo` so the same `owner/name` on two
sites renders as two groups. A bare `owner/name` still means Hugging Face.
- `is_valid_source_id()` centralises repo-id validation (exactly
`owner/name`, no traversal, no leading dot). This also fixes the old HF
download check that rejected any dot in the name, i.e. legitimate repos
such as `black-forest-labs/FLUX.1-dev`.
Verified against the live APIs: the example repo lists 8 weight files with
correct sizes, and a ranged GET of the built resolve URL returns 206 after
following the redirect to the CDN. Backend 2853 passed; frontend 1143 JS +
91 Vue passed. The nine locales carry the refreshed download copy in the
next commit.
This commit is contained in:
@@ -203,10 +203,18 @@ export const DOWNLOAD_ENDPOINTS = {
|
||||
exampleImagesMissing: '/api/lm/download-example-images' // Download only missing example images
|
||||
};
|
||||
|
||||
// Hugging Face API endpoints
|
||||
// External model source endpoints (Hugging Face / ModelScope).
|
||||
// The hf-* paths are the historical names, kept as server-side aliases.
|
||||
export const MODEL_SOURCE_ENDPOINTS = {
|
||||
repoFiles: '/api/lm/model-source-files',
|
||||
download: '/api/lm/download-model-source',
|
||||
sources: '/api/lm/model-sources',
|
||||
};
|
||||
|
||||
/** @deprecated use MODEL_SOURCE_ENDPOINTS */
|
||||
export const HF_ENDPOINTS = {
|
||||
repoFiles: '/api/lm/hf-repo-files',
|
||||
download: '/api/lm/download-hf-model',
|
||||
repoFiles: MODEL_SOURCE_ENDPOINTS.repoFiles,
|
||||
download: MODEL_SOURCE_ENDPOINTS.download,
|
||||
};
|
||||
|
||||
// WebSocket endpoints
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isValidModelType,
|
||||
DOWNLOAD_ENDPOINTS,
|
||||
HF_ENDPOINTS,
|
||||
MODEL_SOURCE_ENDPOINTS,
|
||||
WS_ENDPOINTS
|
||||
} from './apiConfig.js';
|
||||
import { resetAndReload } from './modelApiFactory.js';
|
||||
@@ -1367,30 +1368,52 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchHfRepoFiles(repo, revision = 'main') {
|
||||
/**
|
||||
* List the downloadable weight files of an external repository.
|
||||
* @param {string} repo - `owner/name`
|
||||
* @param {string} [platform] - `huggingface` (default) or `modelscope`
|
||||
* @param {string} [revision] - branch; each site has its own default
|
||||
*/
|
||||
async fetchModelSourceFiles(repo, platform = 'huggingface', revision = '') {
|
||||
try {
|
||||
const params = new URLSearchParams({ repo, revision });
|
||||
const response = await fetch(`${HF_ENDPOINTS.repoFiles}?${params}`);
|
||||
const params = new URLSearchParams({ repo, platform });
|
||||
if (revision) params.set('revision', revision);
|
||||
const response = await fetch(`${MODEL_SOURCE_ENDPOINTS.repoFiles}?${params}`);
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Failed to fetch HF repo files');
|
||||
throw new Error(err.error || 'Failed to fetch repository files');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching HF repo files:', error);
|
||||
console.error('Error fetching repository files:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async downloadHfModel({ repo, filename, revision, modelRoot, relativePath, useDefaultPaths, download_id }) {
|
||||
/** Backwards-compatible Hugging Face wrapper. */
|
||||
async fetchHfRepoFiles(repo, revision = 'main') {
|
||||
return this.fetchModelSourceFiles(repo, 'huggingface', revision);
|
||||
}
|
||||
|
||||
async downloadModelSource({
|
||||
platform = 'huggingface',
|
||||
repo,
|
||||
filename,
|
||||
revision,
|
||||
modelRoot,
|
||||
relativePath,
|
||||
useDefaultPaths,
|
||||
download_id,
|
||||
}) {
|
||||
try {
|
||||
const response = await fetch(HF_ENDPOINTS.download, {
|
||||
const response = await fetch(MODEL_SOURCE_ENDPOINTS.download, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
platform,
|
||||
repo,
|
||||
filename,
|
||||
revision: revision || 'main',
|
||||
revision: revision || '',
|
||||
model_root: modelRoot,
|
||||
relative_path: relativePath || '',
|
||||
use_default_paths: useDefaultPaths || false,
|
||||
@@ -1404,11 +1427,25 @@ export class BaseModelApiClient {
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error downloading HF model:', error);
|
||||
console.error('Error downloading model:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Backwards-compatible Hugging Face wrapper. */
|
||||
async downloadHfModel({ repo, filename, revision, modelRoot, relativePath, useDefaultPaths, download_id }) {
|
||||
return this.downloadModelSource({
|
||||
platform: 'huggingface',
|
||||
repo,
|
||||
filename,
|
||||
revision: revision || 'main',
|
||||
modelRoot,
|
||||
relativePath,
|
||||
useDefaultPaths,
|
||||
download_id,
|
||||
});
|
||||
}
|
||||
|
||||
_buildQueryParams(baseParams, pageState) {
|
||||
const params = new URLSearchParams(baseParams);
|
||||
const isExcludedView = pageState.viewMode === 'excluded';
|
||||
|
||||
Reference in New Issue
Block a user