mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
38d4c59b4c
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.
226 lines
6.9 KiB
Python
226 lines
6.9 KiB
Python
"""Registry and metadata helpers for external model sources.
|
|
|
|
The registry is the single place the rest of the codebase asks "which site
|
|
is this URL from?", "what is this model's source?", and "can we enrich it?".
|
|
Import from :mod:`py.services.model_sources` rather than this module
|
|
directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict, Mapping, Optional
|
|
|
|
from .base import GROUP_PREFIXES, ModelSource, SourceRef, clean_source_url
|
|
from .huggingface import HuggingFaceSource
|
|
from .modelscope import ModelScopeSource
|
|
from .tensorart import TensorArtSource
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
#: Order matters only for disambiguation; the URL patterns are disjoint.
|
|
_SOURCES: tuple[ModelSource, ...] = (
|
|
HuggingFaceSource(),
|
|
ModelScopeSource(),
|
|
TensorArtSource(),
|
|
)
|
|
|
|
_BY_PLATFORM: Dict[str, ModelSource] = {s.platform: s for s in _SOURCES}
|
|
|
|
#: Metadata keys that carry the canonical external-source identity.
|
|
SOURCE_PLATFORM_FIELD = "source_platform"
|
|
SOURCE_URL_FIELD = "source_url"
|
|
#: Legacy field kept as a read/write alias for Hugging Face models so that
|
|
#: older sidecars, cached rows, and third-party consumers keep working.
|
|
LEGACY_HF_URL_FIELD = "hf_url"
|
|
|
|
|
|
def list_sources() -> list[ModelSource]:
|
|
"""Return every known model source."""
|
|
|
|
return list(_SOURCES)
|
|
|
|
|
|
def get_source(platform: Optional[str]) -> Optional[ModelSource]:
|
|
"""Return the source registered for *platform*, or ``None``."""
|
|
|
|
if not platform or not isinstance(platform, str):
|
|
return None
|
|
return _BY_PLATFORM.get(platform.strip().lower())
|
|
|
|
|
|
def source_label(platform: Optional[str], default: str = "") -> str:
|
|
"""Return the human-readable label for *platform*."""
|
|
|
|
source = get_source(platform)
|
|
return source.label if source else default
|
|
|
|
|
|
def downloadable_sources() -> list[ModelSource]:
|
|
"""Return the sources whose repositories can be downloaded directly."""
|
|
|
|
return [source for source in _SOURCES if source.supports_download]
|
|
|
|
|
|
def get_download_source(platform: Optional[str]) -> Optional[ModelSource]:
|
|
"""Return the source for *platform*, but only when it supports downloads."""
|
|
|
|
source = get_source(platform)
|
|
if source is None or not source.supports_download:
|
|
return None
|
|
return source
|
|
|
|
|
|
def detect_source(url: Optional[str], *, strict: bool = False) -> Optional[SourceRef]:
|
|
"""Return the :class:`SourceRef` for *url*, or ``None`` if unsupported."""
|
|
|
|
if not url or not isinstance(url, str):
|
|
return None
|
|
for source in _SOURCES:
|
|
ref = source.ref(url, strict=strict)
|
|
if ref is not None:
|
|
return ref
|
|
return None
|
|
|
|
|
|
def resolve_source_ref(metadata: Mapping[str, Any]) -> Optional[SourceRef]:
|
|
"""Return the source reference described by a model's metadata.
|
|
|
|
Handles all three storage states found in the wild:
|
|
|
|
1. ``source_url`` + ``source_platform`` (current format)
|
|
2. ``hf_url`` only (legacy Hugging Face storage)
|
|
3. ``hf_url`` plus a newer ``source_url`` (both written by older builds)
|
|
"""
|
|
|
|
if not isinstance(metadata, Mapping):
|
|
return None
|
|
|
|
platform = clean_source_url(metadata.get(SOURCE_PLATFORM_FIELD)).lower()
|
|
url = clean_source_url(metadata.get(SOURCE_URL_FIELD))
|
|
legacy = clean_source_url(metadata.get(LEGACY_HF_URL_FIELD))
|
|
|
|
source = get_source(platform)
|
|
if url:
|
|
if source is not None:
|
|
ref = source.ref(url)
|
|
if ref is not None:
|
|
return ref
|
|
ref = detect_source(url)
|
|
if ref is not None:
|
|
return ref
|
|
# Unknown platform but a URL is present: keep it addressable.
|
|
return SourceRef(platform=platform or "unknown", source_id="", url=url)
|
|
|
|
if legacy:
|
|
return detect_source(legacy)
|
|
return None
|
|
|
|
|
|
def normalize_metadata_source(metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Normalise the external-source fields on *metadata* in place.
|
|
|
|
Guarantees that ``source_url``/``source_platform`` are present and
|
|
consistent, and that ``hf_url`` mirrors ``source_url`` for Hugging Face
|
|
models (never for other platforms, so a stale alias can't make a
|
|
ModelScope model look like a Hugging Face one).
|
|
|
|
Returns the same dict for convenient chaining.
|
|
"""
|
|
|
|
if not isinstance(metadata, dict):
|
|
return metadata
|
|
|
|
platform = clean_source_url(metadata.get(SOURCE_PLATFORM_FIELD)).lower()
|
|
url = clean_source_url(metadata.get(SOURCE_URL_FIELD))
|
|
legacy = clean_source_url(metadata.get(LEGACY_HF_URL_FIELD))
|
|
|
|
source = get_source(platform)
|
|
ref: Optional[SourceRef] = None
|
|
|
|
if url:
|
|
ref = source.ref(url) if source is not None else None
|
|
if ref is None:
|
|
ref = detect_source(url)
|
|
elif legacy:
|
|
ref = detect_source(legacy)
|
|
|
|
if ref is not None and ref.source_id:
|
|
platform = ref.platform
|
|
url = ref.url or url
|
|
|
|
if platform:
|
|
metadata[SOURCE_PLATFORM_FIELD] = platform
|
|
else:
|
|
metadata.setdefault(SOURCE_PLATFORM_FIELD, "")
|
|
|
|
metadata[SOURCE_URL_FIELD] = url
|
|
|
|
# Keep the legacy alias in sync, but only for Hugging Face.
|
|
if url and platform == "huggingface":
|
|
metadata[LEGACY_HF_URL_FIELD] = url
|
|
elif LEGACY_HF_URL_FIELD in metadata and platform and platform != "huggingface":
|
|
metadata[LEGACY_HF_URL_FIELD] = ""
|
|
elif legacy and not url:
|
|
metadata[LEGACY_HF_URL_FIELD] = legacy
|
|
|
|
return metadata
|
|
|
|
|
|
def has_external_source(item: Mapping[str, Any]) -> bool:
|
|
"""Return ``True`` when *item* is linked to any external model site."""
|
|
|
|
if not isinstance(item, Mapping):
|
|
return False
|
|
return bool(
|
|
clean_source_url(item.get(SOURCE_URL_FIELD))
|
|
or clean_source_url(item.get(LEGACY_HF_URL_FIELD))
|
|
)
|
|
|
|
|
|
def get_source_platform(item: Mapping[str, Any]) -> str:
|
|
"""Return the platform id stored on *item* (may be empty)."""
|
|
|
|
if not isinstance(item, Mapping):
|
|
return ""
|
|
platform = clean_source_url(item.get(SOURCE_PLATFORM_FIELD)).lower()
|
|
if platform:
|
|
return platform
|
|
ref = resolve_source_ref(item)
|
|
return ref.platform if ref else ""
|
|
|
|
|
|
def source_group_key(item: Mapping[str, Any]) -> Optional[str]:
|
|
"""Return the version-group key for *item*, or ``None``.
|
|
|
|
Hugging Face keeps the historical ``hf:{owner}/{repo}`` shape; other
|
|
platforms use their own short prefix (see :data:`GROUP_PREFIXES`).
|
|
"""
|
|
|
|
ref = resolve_source_ref(item)
|
|
if ref is None or not ref.source_id:
|
|
return None
|
|
source = get_source(ref.platform)
|
|
if source is None:
|
|
return None
|
|
return source.group_key(ref.source_id)
|
|
|
|
|
|
__all__ = [
|
|
"GROUP_PREFIXES",
|
|
"LEGACY_HF_URL_FIELD",
|
|
"SOURCE_PLATFORM_FIELD",
|
|
"SOURCE_URL_FIELD",
|
|
"detect_source",
|
|
"downloadable_sources",
|
|
"get_download_source",
|
|
"get_source",
|
|
"get_source_platform",
|
|
"has_external_source",
|
|
"list_sources",
|
|
"normalize_metadata_source",
|
|
"resolve_source_ref",
|
|
"source_group_key",
|
|
"source_label",
|
|
]
|