mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
perf(modelscope): fetch a repository's model card once per run
A collection repository publishes many model files under a single source id, but enrichment re-read the README and the model-detail payload for every one of them: eight checkpoints meant sixteen HTTP requests, each detail payload being 10-22 KB of JSON. Add `ModelSourceCache`, created by `execute_skill()` for the duration of a run and passed to the provider through a new optional `cache` argument on `fetch_model_card_context()`. The agent caches the README (repository-wide and provider-agnostic), and ModelScope caches its detail payload under a provider-namespaced key. Only successful reads are memoised, so a transient failure is still retried for the next file, and the per-file selection is redone from the cached payload so a checkpoint never inherits a sibling's example images. Nothing is retained across runs — a model card can change at any time — and download URLs are not routed through the cache. Measured over the eight checkpoints of one ModelScope repository: 16 requests before, 2 after. To keep the two concerns separable, `_build_card_context()` now turns a detail payload into a `ModelCardContext` as a pure function.
This commit is contained in:
@@ -24,6 +24,10 @@ which redirects to a CDN URL carrying a time-limited ``auth_key``.
|
||||
Requesting the resolve URL fresh on every attempt (which the shared
|
||||
downloader does, including for resumable Range requests) keeps that key
|
||||
valid; the CDN URL must never be cached.
|
||||
|
||||
The README and the detail payload both describe the whole repository rather
|
||||
than one file, so a per-run ``ModelSourceCache`` keeps them from being read
|
||||
again for every checkpoint of a collection repository.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -32,7 +36,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from .base import (
|
||||
ModelCardContext,
|
||||
@@ -43,6 +47,9 @@ from .base import (
|
||||
filter_weight_files,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - typing only
|
||||
from .base import ModelSourceCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_URL_PATTERN = re.compile(
|
||||
@@ -106,7 +113,11 @@ class ModelScopeSource(ModelSource):
|
||||
return ""
|
||||
|
||||
async def fetch_model_card_context(
|
||||
self, source_id: str, filename: str = ""
|
||||
self,
|
||||
source_id: str,
|
||||
filename: str = "",
|
||||
*,
|
||||
cache: Optional["ModelSourceCache"] = None,
|
||||
) -> ModelCardContext:
|
||||
"""Read the model-detail API that backs the ModelScope model page.
|
||||
|
||||
@@ -121,30 +132,44 @@ class ModelScopeSource(ModelSource):
|
||||
``stats.fileList``, which means the images returned belong to the
|
||||
exact ``.safetensors`` being enriched — essential for collection
|
||||
repositories, where every checkpoint has its own sample image.
|
||||
|
||||
The detail payload describes the whole repository and is therefore
|
||||
shared across every file in it, so it is read through *cache* when the
|
||||
caller supplies one; only the per-file selection is redone.
|
||||
"""
|
||||
|
||||
data = await self._fetch_detail(source_id, cache=cache)
|
||||
if data is None:
|
||||
return ModelCardContext()
|
||||
return _build_card_context(data, filename)
|
||||
|
||||
async def _fetch_detail(
|
||||
self,
|
||||
source_id: str,
|
||||
*,
|
||||
cache: Optional["ModelSourceCache"] = None,
|
||||
) -> Optional[dict[str, Any]]:
|
||||
"""Fetch (or reuse) the model-detail payload for *source_id*."""
|
||||
|
||||
cache_key = (self.platform, "detail", source_id)
|
||||
if cache is not None and cache_key in cache.provider:
|
||||
return cache.provider[cache_key]
|
||||
|
||||
status, payload = await fetch_json(
|
||||
f"https://modelscope.cn/api/v1/models/{source_id}"
|
||||
)
|
||||
if status != 200 or not isinstance(payload, dict):
|
||||
logger.debug("ModelScope detail API returned HTTP %s for %s", status, source_id)
|
||||
return ModelCardContext()
|
||||
logger.debug(
|
||||
"ModelScope detail API returned HTTP %s for %s", status, source_id
|
||||
)
|
||||
return None
|
||||
data = payload.get("Data")
|
||||
if not isinstance(data, dict):
|
||||
return ModelCardContext()
|
||||
return None
|
||||
|
||||
context = ModelCardContext(
|
||||
description=_clean_text(data.get("Description")),
|
||||
base_model=_first_string(data.get("BaseModel")),
|
||||
base_model_aliases=_base_model_aliases(data),
|
||||
official_tags=_official_tags(data.get("OfficialTags")),
|
||||
)
|
||||
|
||||
versions = _matching_versions(data.get("MuseInfo"), filename)
|
||||
if versions:
|
||||
context.example_images = _cover_image_urls(versions)
|
||||
context.trigger_words = _version_trigger_words(versions)
|
||||
return context
|
||||
if cache is not None:
|
||||
cache.provider[cache_key] = data
|
||||
return data
|
||||
|
||||
async def list_files(
|
||||
self, source_id: str, revision: str = ""
|
||||
@@ -220,6 +245,28 @@ def _first_string(value: Any) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _build_card_context(data: dict[str, Any], filename: str) -> ModelCardContext:
|
||||
"""Turn a model-detail payload into a :class:`ModelCardContext`.
|
||||
|
||||
Separated from the HTTP fetch so the repository-wide payload can be cached
|
||||
across the files of a collection repository while the per-file selection
|
||||
is still redone for each one.
|
||||
"""
|
||||
|
||||
context = ModelCardContext(
|
||||
description=_clean_text(data.get("Description")),
|
||||
base_model=_first_string(data.get("BaseModel")),
|
||||
base_model_aliases=_base_model_aliases(data),
|
||||
official_tags=_official_tags(data.get("OfficialTags")),
|
||||
)
|
||||
|
||||
versions = _matching_versions(data.get("MuseInfo"), filename)
|
||||
if versions:
|
||||
context.example_images = _cover_image_urls(versions)
|
||||
context.trigger_words = _version_trigger_words(versions)
|
||||
return context
|
||||
|
||||
|
||||
def _base_model_aliases(data: dict[str, Any]) -> list[str]:
|
||||
"""Return the site's own names for the base model.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user