mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-27 14:04:08 -03:00
fix: stop grouping HF/ModelScope models by repository
A repository is not a model identity: collection repos on Hugging Face and ModelScope host many unrelated models, which were wrongly shown as versions of each other. - Hugging Face models no longer auto-group (the Hub exposes no site-native model id) - ModelScope models group by the site's native published-model id (MuseInfo modelVersion.modelId), extracted during enrichment and persisted on the sidecar as source_model_id/source_version_id; unenriched models stay standalone instead of collapsing a whole repo into one group - TensorArt grouping unchanged (its URL id is already model-level) - Frontend group-key derivation mirrors the new backend semantics
This commit is contained in:
@@ -199,6 +199,15 @@ class PostProcessor:
|
|||||||
if is_source_model and site_version:
|
if is_source_model and site_version:
|
||||||
self._merge_civitai(updates, metadata, name=site_version)
|
self._merge_civitai(updates, metadata, name=site_version)
|
||||||
|
|
||||||
|
# Site-native identity ids (ModelScope's published model/version ids).
|
||||||
|
# They are what version grouping keys off, so they must reach the
|
||||||
|
# sidecar even when nothing else about the card changed.
|
||||||
|
if is_source_model and source_context is not None:
|
||||||
|
if source_context.source_model_id:
|
||||||
|
updates["source_model_id"] = source_context.source_model_id
|
||||||
|
if source_context.source_version_id:
|
||||||
|
updates["source_version_id"] = source_context.source_version_id
|
||||||
|
|
||||||
# gallery images → civitai.images (site example images, YAML frontmatter
|
# gallery images → civitai.images (site example images, YAML frontmatter
|
||||||
# widget entries, and Sample Gallery markdown tables in the README body)
|
# widget entries, and Sample Gallery markdown tables in the README body)
|
||||||
rec_width = llm_output.get("recommended_width") or 0
|
rec_width = llm_output.get("recommended_width") or 0
|
||||||
|
|||||||
@@ -740,18 +740,14 @@ class BaseModelService(ABC):
|
|||||||
|
|
||||||
return annotated
|
return annotated
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_hf_group_key(item: Dict[str, Any]) -> Optional[str]:
|
|
||||||
"""Extract `hf:{owner}/{repo}` from item's ``hf_url``, or None."""
|
|
||||||
key = BaseModelService._extract_source_group_key(item)
|
|
||||||
return key if key and key.startswith("hf:") else None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_source_group_key(item: Dict[str, Any]) -> Optional[str]:
|
def _extract_source_group_key(item: Dict[str, Any]) -> Optional[str]:
|
||||||
"""Return the external-source group key for *item*, or None.
|
"""Return the external-source group key for *item*, or None.
|
||||||
|
|
||||||
Hugging Face keeps the historical ``hf:{owner}/{repo}`` shape; other
|
Only sources with a site-native model identity yield a key:
|
||||||
platforms use their own short prefix (``ms:`` / ``ta:``).
|
ModelScope groups by its published-model id (``ms:{id}``), TensorArt
|
||||||
|
by its numeric model id (``ta:{id}``); Hugging Face models never
|
||||||
|
group (see :meth:`ModelSource.group_key`).
|
||||||
"""
|
"""
|
||||||
return source_group_key(item)
|
return source_group_key(item)
|
||||||
|
|
||||||
@@ -761,8 +757,8 @@ class BaseModelService(ABC):
|
|||||||
|
|
||||||
Preference order:
|
Preference order:
|
||||||
1. CivitAI ``modelId`` (int)
|
1. CivitAI ``modelId`` (int)
|
||||||
2. External model source identity, e.g. ``hf:{owner}/{repo}``,
|
2. External model source identity, e.g. ``ms:{model_id}``,
|
||||||
``ms:{owner}/{repo}``, ``ta:{model_id}`` (str)
|
``ta:{model_id}`` (str)
|
||||||
3. ``None`` (no known grouping source)
|
3. ``None`` (no known grouping source)
|
||||||
"""
|
"""
|
||||||
mid = BaseModelService._extract_model_id(item)
|
mid = BaseModelService._extract_model_id(item)
|
||||||
|
|||||||
@@ -399,10 +399,14 @@ class ModelScanner:
|
|||||||
'skip_metadata_refresh': bool(get_value('skip_metadata_refresh', False)),
|
'skip_metadata_refresh': bool(get_value('skip_metadata_refresh', False)),
|
||||||
# External model source (Hugging Face / ModelScope / TensorArt).
|
# External model source (Hugging Face / ModelScope / TensorArt).
|
||||||
# `source_url` + `source_platform` are canonical; `hf_url` stays in
|
# `source_url` + `source_platform` are canonical; `hf_url` stays in
|
||||||
# sync as a legacy alias (normalised below).
|
# sync as a legacy alias (normalised below). `source_model_id` /
|
||||||
|
# `source_version_id` are the site-native identity ids version
|
||||||
|
# grouping keys off (ModelScope; empty elsewhere).
|
||||||
'source_platform': get_value('source_platform', '') or '',
|
'source_platform': get_value('source_platform', '') or '',
|
||||||
'source_url': get_value('source_url', '') or '',
|
'source_url': get_value('source_url', '') or '',
|
||||||
'hf_url': get_value('hf_url', '') or '',
|
'hf_url': get_value('hf_url', '') or '',
|
||||||
|
'source_model_id': get_value('source_model_id', '') or '',
|
||||||
|
'source_version_id': get_value('source_version_id', '') or '',
|
||||||
}
|
}
|
||||||
normalize_metadata_source(entry)
|
normalize_metadata_source(entry)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, Iterable, Optional
|
from typing import Any, Dict, Iterable, Mapping, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
@@ -123,6 +123,20 @@ class ModelCardContext:
|
|||||||
trigger_words: list[str] = field(default_factory=list)
|
trigger_words: list[str] = field(default_factory=list)
|
||||||
"""Trigger words the site records for the requested model file."""
|
"""Trigger words the site records for the requested model file."""
|
||||||
|
|
||||||
|
source_model_id: str = ""
|
||||||
|
"""Site-native id of the *published model* the requested file belongs to.
|
||||||
|
|
||||||
|
Sites whose repository is not a model identity publish a separate,
|
||||||
|
stable id per model (ModelScope's ``modelVersion.modelId`` — identical
|
||||||
|
across every version of one published model, different between the
|
||||||
|
models of a collection repository). It is the version-grouping key,
|
||||||
|
persisted on the sidecar as ``source_model_id``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_version_id: str = ""
|
||||||
|
"""Site-native id of the published version the requested file belongs to
|
||||||
|
(ModelScope's ``modelVersion.id``), persisted as ``source_version_id``."""
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
"""Return ``True`` when the site contributed nothing extra."""
|
"""Return ``True`` when the site contributed nothing extra."""
|
||||||
|
|
||||||
@@ -139,6 +153,8 @@ class ModelCardContext:
|
|||||||
self.official_tags,
|
self.official_tags,
|
||||||
self.example_images,
|
self.example_images,
|
||||||
self.trigger_words,
|
self.trigger_words,
|
||||||
|
self.source_model_id,
|
||||||
|
self.source_version_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -329,11 +345,20 @@ class ModelSource:
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def group_key(self, source_id: str) -> str:
|
def group_key(self, ref: SourceRef, item: Mapping[str, Any]) -> Optional[str]:
|
||||||
"""Return the version-group key for *source_id*."""
|
"""Return the version-group key for the model described by *item*.
|
||||||
|
|
||||||
|
The default groups by source id (``{prefix}:{owner}/{repo}``), which
|
||||||
|
is only correct when the source id already identifies a single
|
||||||
|
published model. Sources whose repository hosts many unrelated
|
||||||
|
models override this: they either derive the key from a site-native
|
||||||
|
model identity recorded in *item* (ModelScope's ``source_model_id``)
|
||||||
|
or return ``None`` when the platform has no reliable model identity
|
||||||
|
at all (Hugging Face), leaving the model ungrouped.
|
||||||
|
"""
|
||||||
|
|
||||||
prefix = GROUP_PREFIXES.get(self.platform, self.platform)
|
prefix = GROUP_PREFIXES.get(self.platform, self.platform)
|
||||||
return f"{prefix}:{source_id}"
|
return f"{prefix}:{ref.source_id}"
|
||||||
|
|
||||||
async def fetch_model_card(self, source_id: str) -> str:
|
async def fetch_model_card(self, source_id: str) -> str:
|
||||||
"""Fetch the raw model card (README) markdown for *source_id*."""
|
"""Fetch the raw model card (README) markdown for *source_id*."""
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Any, Mapping, Optional
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
ModelSource,
|
ModelSource,
|
||||||
ModelSourceError,
|
ModelSourceError,
|
||||||
|
SourceRef,
|
||||||
fetch_json,
|
fetch_json,
|
||||||
fetch_text,
|
fetch_text,
|
||||||
filter_weight_files,
|
filter_weight_files,
|
||||||
@@ -54,6 +56,16 @@ class HuggingFaceSource(ModelSource):
|
|||||||
def canonical_url(self, source_id: str) -> str:
|
def canonical_url(self, source_id: str) -> str:
|
||||||
return f"https://huggingface.co/{source_id}"
|
return f"https://huggingface.co/{source_id}"
|
||||||
|
|
||||||
|
def group_key(self, ref: SourceRef, item: Mapping[str, Any]) -> Optional[str]:
|
||||||
|
"""Hugging Face models never auto-group.
|
||||||
|
|
||||||
|
A repository is not a model identity — collection repos host many
|
||||||
|
unrelated models — and the Hub exposes no site-native published-model
|
||||||
|
id, so there is no reliable key to group by.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def asset_base_url(self, source_id: str, revision: str = "") -> str:
|
def asset_base_url(self, source_id: str, revision: str = "") -> str:
|
||||||
return f"https://huggingface.co/{source_id}/resolve/{self.resolve_revision(revision)}"
|
return f"https://huggingface.co/{source_id}/resolve/{self.resolve_revision(revision)}"
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,15 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Any, Iterable, Optional
|
from typing import TYPE_CHECKING, Any, Iterable, Mapping, Optional
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
|
GROUP_PREFIXES,
|
||||||
ModelCardContext,
|
ModelCardContext,
|
||||||
ModelSource,
|
ModelSource,
|
||||||
ModelSourceError,
|
ModelSourceError,
|
||||||
|
SourceRef,
|
||||||
|
clean_source_url,
|
||||||
fetch_json,
|
fetch_json,
|
||||||
fetch_text,
|
fetch_text,
|
||||||
filter_weight_files,
|
filter_weight_files,
|
||||||
@@ -111,6 +114,22 @@ class ModelScopeSource(ModelSource):
|
|||||||
def canonical_url(self, source_id: str) -> str:
|
def canonical_url(self, source_id: str) -> str:
|
||||||
return f"{self.base_url}/models/{source_id}"
|
return f"{self.base_url}/models/{source_id}"
|
||||||
|
|
||||||
|
def group_key(self, ref: SourceRef, item: Mapping[str, Any]) -> Optional[str]:
|
||||||
|
"""Group by ModelScope's published-model id, never by repository.
|
||||||
|
|
||||||
|
A collection repository hosts many unrelated published models, so
|
||||||
|
the repo id is not a version-group identity. Only models whose
|
||||||
|
metadata carries the site-native ``source_model_id`` (recorded at
|
||||||
|
enrichment time from ``MuseInfo.versions[].modelVersion.modelId``)
|
||||||
|
group together; unenriched models stay standalone.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_id = clean_source_url(item.get("source_model_id"))
|
||||||
|
if not model_id:
|
||||||
|
return None
|
||||||
|
prefix = GROUP_PREFIXES.get(self.platform, self.platform)
|
||||||
|
return f"{prefix}:{model_id}"
|
||||||
|
|
||||||
def asset_base_url(self, source_id: str, revision: str = "") -> str:
|
def asset_base_url(self, source_id: str, revision: str = "") -> str:
|
||||||
return (
|
return (
|
||||||
f"{self.base_url}/models/{source_id}/resolve/"
|
f"{self.base_url}/models/{source_id}/resolve/"
|
||||||
@@ -350,9 +369,37 @@ def _build_card_context(
|
|||||||
context.version_name = _version_label(versions)
|
context.version_name = _version_label(versions)
|
||||||
context.example_images = _cover_image_urls(versions)
|
context.example_images = _cover_image_urls(versions)
|
||||||
context.trigger_words = _version_trigger_words(versions)
|
context.trigger_words = _version_trigger_words(versions)
|
||||||
|
context.source_model_id, context.source_version_id = _version_identity(
|
||||||
|
versions
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _version_identity(versions: list[dict[str, Any]]) -> tuple[str, str]:
|
||||||
|
"""Return the site-native ``(model id, version id)`` of the first match.
|
||||||
|
|
||||||
|
``modelVersion.modelId`` is identical across every version of one
|
||||||
|
published model and differs between the models of a collection
|
||||||
|
repository, which makes it the version-grouping identity;
|
||||||
|
``modelVersion.id`` identifies the version itself. Both are ints in
|
||||||
|
the payload and are stored as strings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for version in versions:
|
||||||
|
model_version = version.get("modelVersion")
|
||||||
|
if not isinstance(model_version, dict):
|
||||||
|
continue
|
||||||
|
model_id = model_version.get("modelId")
|
||||||
|
version_id = model_version.get("id")
|
||||||
|
if model_id is None and version_id is None:
|
||||||
|
continue
|
||||||
|
return (
|
||||||
|
str(model_id) if model_id is not None else "",
|
||||||
|
str(version_id) if version_id is not None else "",
|
||||||
|
)
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
|
||||||
def _base_model_aliases(data: dict[str, Any]) -> list[str]:
|
def _base_model_aliases(data: dict[str, Any]) -> list[str]:
|
||||||
"""Return the site's own names for the base model.
|
"""Return the site's own names for the base model.
|
||||||
|
|
||||||
|
|||||||
@@ -196,8 +196,13 @@ def get_source_platform(item: Mapping[str, Any]) -> str:
|
|||||||
def source_group_key(item: Mapping[str, Any]) -> Optional[str]:
|
def source_group_key(item: Mapping[str, Any]) -> Optional[str]:
|
||||||
"""Return the version-group key for *item*, or ``None``.
|
"""Return the version-group key for *item*, or ``None``.
|
||||||
|
|
||||||
Hugging Face keeps the historical ``hf:{owner}/{repo}`` shape; other
|
Only sources with a site-native model identity yield a key: TensorArt
|
||||||
platforms use their own short prefix (see :data:`GROUP_PREFIXES`).
|
groups by its numeric model id (``ta:<id>``) and ModelScope by the
|
||||||
|
published-model id recorded at enrichment time (``ms:<id>`` /
|
||||||
|
``msai:<id>``). Hugging Face yields no key at all — a repository is
|
||||||
|
not a model identity — and unenriched ModelScope models stay
|
||||||
|
standalone rather than collapsing a whole collection repository into
|
||||||
|
one group.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ref = resolve_source_ref(item)
|
ref = resolve_source_ref(item)
|
||||||
@@ -206,7 +211,7 @@ def source_group_key(item: Mapping[str, Any]) -> Optional[str]:
|
|||||||
source = get_source(ref.platform)
|
source = get_source(ref.platform)
|
||||||
if source is None:
|
if source is None:
|
||||||
return None
|
return None
|
||||||
return source.group_key(ref.source_id)
|
return source.group_key(ref, item)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class PersistentModelCache:
|
|||||||
"source_platform",
|
"source_platform",
|
||||||
"source_url",
|
"source_url",
|
||||||
"hf_url",
|
"hf_url",
|
||||||
|
"source_model_id",
|
||||||
|
"source_version_id",
|
||||||
)
|
)
|
||||||
_MODEL_UPDATE_COLUMNS: Tuple[str, ...] = _MODEL_COLUMNS[2:]
|
_MODEL_UPDATE_COLUMNS: Tuple[str, ...] = _MODEL_COLUMNS[2:]
|
||||||
_instances: Dict[str, "PersistentModelCache"] = {}
|
_instances: Dict[str, "PersistentModelCache"] = {}
|
||||||
@@ -214,6 +216,8 @@ class PersistentModelCache:
|
|||||||
"source_platform": row["source_platform"] or "",
|
"source_platform": row["source_platform"] or "",
|
||||||
"source_url": row["source_url"] or "",
|
"source_url": row["source_url"] or "",
|
||||||
"hf_url": row["hf_url"] or "",
|
"hf_url": row["hf_url"] or "",
|
||||||
|
"source_model_id": row["source_model_id"] or "",
|
||||||
|
"source_version_id": row["source_version_id"] or "",
|
||||||
}
|
}
|
||||||
# Legacy rows only carry `hf_url`; derive the canonical pair so
|
# Legacy rows only carry `hf_url`; derive the canonical pair so
|
||||||
# every consumer sees the same shape.
|
# every consumer sees the same shape.
|
||||||
@@ -579,6 +583,8 @@ class PersistentModelCache:
|
|||||||
source_platform TEXT DEFAULT '',
|
source_platform TEXT DEFAULT '',
|
||||||
source_url TEXT DEFAULT '',
|
source_url TEXT DEFAULT '',
|
||||||
hf_url TEXT DEFAULT '',
|
hf_url TEXT DEFAULT '',
|
||||||
|
source_model_id TEXT DEFAULT '',
|
||||||
|
source_version_id TEXT DEFAULT '',
|
||||||
PRIMARY KEY (model_type, file_path)
|
PRIMARY KEY (model_type, file_path)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -648,6 +654,8 @@ class PersistentModelCache:
|
|||||||
"source_platform": "TEXT DEFAULT ''",
|
"source_platform": "TEXT DEFAULT ''",
|
||||||
"source_url": "TEXT DEFAULT ''",
|
"source_url": "TEXT DEFAULT ''",
|
||||||
"hf_url": "TEXT DEFAULT ''",
|
"hf_url": "TEXT DEFAULT ''",
|
||||||
|
"source_model_id": "TEXT DEFAULT ''",
|
||||||
|
"source_version_id": "TEXT DEFAULT ''",
|
||||||
"autov3": "TEXT",
|
"autov3": "TEXT",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,6 +743,8 @@ class PersistentModelCache:
|
|||||||
item.get("source_platform") or "",
|
item.get("source_platform") or "",
|
||||||
item.get("source_url") or "",
|
item.get("source_url") or "",
|
||||||
item.get("hf_url") or "",
|
item.get("hf_url") or "",
|
||||||
|
item.get("source_model_id") or "",
|
||||||
|
item.get("source_version_id") or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _insert_model_sql(self) -> str:
|
def _insert_model_sql(self) -> str:
|
||||||
|
|||||||
@@ -522,7 +522,7 @@ export function createModelCard(model, modelType) {
|
|||||||
card.dataset.modelId = modelId;
|
card.dataset.modelId = modelId;
|
||||||
} else {
|
} else {
|
||||||
// For externally-sourced models, derive a group key from the source
|
// For externally-sourced models, derive a group key from the source
|
||||||
// URL for version grouping (hf:user/repo, ms:user/repo, ta:<id>).
|
// identity for version grouping (ms:<model_id>, ta:<id>).
|
||||||
const sourceGroupKey = getModelSourceGroupKey(model);
|
const sourceGroupKey = getModelSourceGroupKey(model);
|
||||||
if (sourceGroupKey) {
|
if (sourceGroupKey) {
|
||||||
card.dataset.modelId = sourceGroupKey;
|
card.dataset.modelId = sourceGroupKey;
|
||||||
|
|||||||
@@ -994,9 +994,9 @@ export function initVersionsTab({
|
|||||||
renderErrorState(container, translate('modals.model.versions.missingModelId', {}, 'This model is missing a Civitai model id.'));
|
renderErrorState(container, translate('modals.model.versions.missingModelId', {}, 'This model is missing a Civitai model id.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// External source group keys (e.g. "hf:user/repo", "ms:user/repo",
|
// External source group keys (e.g. "ms:12345", "ta:8278...") are
|
||||||
// "ta:8278...") are not real CivitAI model IDs — skip the remote API
|
// not real CivitAI model IDs — skip the remote API call and show a
|
||||||
// call and show a helpful message instead.
|
// helpful message instead.
|
||||||
const sourceGroup = parseModelSourceGroupKey(modelId);
|
const sourceGroup = parseModelSourceGroupKey(modelId);
|
||||||
if (sourceGroup) {
|
if (sourceGroup) {
|
||||||
controller.isLoading = false;
|
controller.isLoading = false;
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
* support AI metadata enrichment.
|
* support AI metadata enrichment.
|
||||||
*
|
*
|
||||||
* Models loaded from an older cache may only carry the legacy `hf_url`
|
* Models loaded from an older cache may only carry the legacy `hf_url`
|
||||||
* field; every helper here falls back to it, and to the legacy
|
* field; every helper here falls back to it.
|
||||||
* `hf:user/repo` group key shape.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { translate } from './i18nHelpers.js';
|
import { translate } from './i18nHelpers.js';
|
||||||
@@ -17,6 +16,9 @@ export const MODEL_SOURCES = [
|
|||||||
platform: 'huggingface',
|
platform: 'huggingface',
|
||||||
label: 'Hugging Face',
|
label: 'Hugging Face',
|
||||||
groupPrefix: 'hf',
|
groupPrefix: 'hf',
|
||||||
|
// A repository hosts many unrelated models and the site exposes no
|
||||||
|
// model-level identity, so HF models never auto-group.
|
||||||
|
groupKey: 'none',
|
||||||
supportsEnrichment: true,
|
supportsEnrichment: true,
|
||||||
supportsDownload: true,
|
supportsDownload: true,
|
||||||
defaultRevision: 'main',
|
defaultRevision: 'main',
|
||||||
@@ -36,6 +38,9 @@ export const MODEL_SOURCES = [
|
|||||||
platform: 'modelscope',
|
platform: 'modelscope',
|
||||||
label: 'ModelScope',
|
label: 'ModelScope',
|
||||||
groupPrefix: 'ms',
|
groupPrefix: 'ms',
|
||||||
|
// Group by the site-native published-model id (`source_model_id`),
|
||||||
|
// recorded by enrichment — the repo id is not a model identity.
|
||||||
|
groupKey: 'modelId',
|
||||||
supportsEnrichment: true,
|
supportsEnrichment: true,
|
||||||
supportsDownload: true,
|
supportsDownload: true,
|
||||||
defaultRevision: 'master',
|
defaultRevision: 'master',
|
||||||
@@ -56,6 +61,7 @@ export const MODEL_SOURCES = [
|
|||||||
platform: 'modelscope-ai',
|
platform: 'modelscope-ai',
|
||||||
label: 'ModelScope (International)',
|
label: 'ModelScope (International)',
|
||||||
groupPrefix: 'msai',
|
groupPrefix: 'msai',
|
||||||
|
groupKey: 'modelId',
|
||||||
supportsEnrichment: true,
|
supportsEnrichment: true,
|
||||||
supportsDownload: true,
|
supportsDownload: true,
|
||||||
defaultRevision: 'master',
|
defaultRevision: 'master',
|
||||||
@@ -73,6 +79,8 @@ export const MODEL_SOURCES = [
|
|||||||
platform: 'tensorart',
|
platform: 'tensorart',
|
||||||
label: 'TensorArt',
|
label: 'TensorArt',
|
||||||
groupPrefix: 'ta',
|
groupPrefix: 'ta',
|
||||||
|
// The numeric id in a TensorArt URL already identifies a single model.
|
||||||
|
groupKey: 'repo',
|
||||||
supportsEnrichment: false,
|
supportsEnrichment: false,
|
||||||
supportsDownload: false,
|
supportsDownload: false,
|
||||||
defaultRevision: '',
|
defaultRevision: '',
|
||||||
@@ -152,11 +160,21 @@ export function getModelSourceInfo(model) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Version-group key for a model, matching the backend's `_extract_group_key`.
|
* Version-group key for a model, matching the backend's `_extract_group_key`.
|
||||||
* Returns `''` when the model has no external source.
|
* Returns `''` when the model has no external source, or when its source has
|
||||||
|
* no reliable model identity (Hugging Face, or a ModelScope model that has
|
||||||
|
* not been enriched with the site-native `source_model_id` yet).
|
||||||
*/
|
*/
|
||||||
export function getModelSourceGroupKey(model) {
|
export function getModelSourceGroupKey(model) {
|
||||||
const info = getModelSourceInfo(model);
|
const info = getModelSourceInfo(model);
|
||||||
if (!info || !info.sourceId) return '';
|
if (!info) return '';
|
||||||
|
const strategy = info.groupKey || 'repo';
|
||||||
|
if (strategy === 'none') return '';
|
||||||
|
if (strategy === 'modelId') {
|
||||||
|
const modelId =
|
||||||
|
model && typeof model.source_model_id === 'string' ? model.source_model_id.trim() : '';
|
||||||
|
return modelId ? `${info.groupPrefix}:${modelId}` : '';
|
||||||
|
}
|
||||||
|
if (!info.sourceId) return '';
|
||||||
return `${info.groupPrefix}:${info.sourceId}`;
|
return `${info.groupPrefix}:${info.sourceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,13 +105,39 @@ describe('modelSourceHelpers', () => {
|
|||||||
|
|
||||||
describe('getModelSourceGroupKey', () => {
|
describe('getModelSourceGroupKey', () => {
|
||||||
it('matches the backend group-key shapes', () => {
|
it('matches the backend group-key shapes', () => {
|
||||||
expect(getModelSourceGroupKey({ hf_url: 'https://huggingface.co/u/r' })).toBe('hf:u/r');
|
// TensorArt's numeric id already identifies a single model.
|
||||||
expect(
|
|
||||||
getModelSourceGroupKey({ source_url: 'https://modelscope.cn/models/u/r' })
|
|
||||||
).toBe('ms:u/r');
|
|
||||||
expect(getModelSourceGroupKey({ source_url: 'https://tensor.art/models/123' })).toBe(
|
expect(getModelSourceGroupKey({ source_url: 'https://tensor.art/models/123' })).toBe(
|
||||||
'ta:123'
|
'ta:123'
|
||||||
);
|
);
|
||||||
|
// ModelScope groups by the site-native published-model id.
|
||||||
|
expect(
|
||||||
|
getModelSourceGroupKey({
|
||||||
|
source_url: 'https://modelscope.cn/models/u/r',
|
||||||
|
source_model_id: '555',
|
||||||
|
})
|
||||||
|
).toBe('ms:555');
|
||||||
|
expect(
|
||||||
|
getModelSourceGroupKey({
|
||||||
|
source_url: 'https://www.modelscope.ai/models/u/r',
|
||||||
|
source_model_id: '678',
|
||||||
|
})
|
||||||
|
).toBe('msai:678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty string for sources without a model identity', () => {
|
||||||
|
// Hugging Face repos are not a model identity: never grouped.
|
||||||
|
expect(getModelSourceGroupKey({ hf_url: 'https://huggingface.co/u/r' })).toBe('');
|
||||||
|
// Unenriched ModelScope models stay standalone rather than collapsing
|
||||||
|
// a whole collection repo into one group.
|
||||||
|
expect(
|
||||||
|
getModelSourceGroupKey({ source_url: 'https://modelscope.cn/models/u/r' })
|
||||||
|
).toBe('');
|
||||||
|
expect(
|
||||||
|
getModelSourceGroupKey({
|
||||||
|
source_url: 'https://modelscope.cn/models/u/r',
|
||||||
|
source_model_id: ' ',
|
||||||
|
})
|
||||||
|
).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an empty string without a source', () => {
|
it('returns an empty string without a source', () => {
|
||||||
|
|||||||
@@ -1027,6 +1027,8 @@ def _modelscope_card_payload() -> dict:
|
|||||||
"modelVersion": {
|
"modelVersion": {
|
||||||
"showName": "c1-st1000",
|
"showName": "c1-st1000",
|
||||||
"triggerWords": '["kreaface","kreamodel"]',
|
"triggerWords": '["kreaface","kreamodel"]',
|
||||||
|
"id": 1002,
|
||||||
|
"modelId": 555,
|
||||||
},
|
},
|
||||||
"coverImages": [
|
"coverImages": [
|
||||||
{"url": "https://resources.modelscope.cn/cover-images/b.png"},
|
{"url": "https://resources.modelscope.cn/cover-images/b.png"},
|
||||||
@@ -1141,6 +1143,9 @@ async def test_download_hydrates_the_card_from_the_site(tmp_path, monkeypatch):
|
|||||||
'{"strength_min": 0.5, "strength_max": 1.2, "strength_range": "0.5-1.2"}'
|
'{"strength_min": 0.5, "strength_max": 1.2, "strength_range": "0.5-1.2"}'
|
||||||
)
|
)
|
||||||
assert saved["metadata_source"] == "source:modelscope"
|
assert saved["metadata_source"] == "source:modelscope"
|
||||||
|
# The site-native identity ids are persisted for version grouping.
|
||||||
|
assert saved["source_model_id"] == "555"
|
||||||
|
assert saved["source_version_id"] == "1002"
|
||||||
# No provider answered, so claiming an AI enrichment would be a lie.
|
# No provider answered, so claiming an AI enrichment would be a lie.
|
||||||
assert "llm_enriched_at" not in saved
|
assert "llm_enriched_at" not in saved
|
||||||
|
|
||||||
@@ -1148,6 +1153,7 @@ async def test_download_hydrates_the_card_from_the_site(tmp_path, monkeypatch):
|
|||||||
assert scanner.update_single_model_cache.await_count == 1
|
assert scanner.update_single_model_cache.await_count == 1
|
||||||
cached = scanner.update_single_model_cache.await_args.args[2]
|
cached = scanner.update_single_model_cache.await_args.args[2]
|
||||||
assert cached["model_name"] == "Krea-2-LORA"
|
assert cached["model_name"] == "Krea-2-LORA"
|
||||||
|
assert cached["source_model_id"] == "555"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1263,39 +1263,8 @@ async def test_get_model_civitai_url_falls_back_when_host_setting_is_not_a_strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestHfGroupKey:
|
class TestSourceGroupKey:
|
||||||
"""Tests for _extract_hf_group_key and _extract_group_key."""
|
"""Tests for _extract_group_key (CivitAI id, then site-native source identity)."""
|
||||||
|
|
||||||
# --- _extract_hf_group_key ---
|
|
||||||
|
|
||||||
def test_hf_group_key_valid_url(self):
|
|
||||||
"""Standard HF URL returns hf:user/repo."""
|
|
||||||
item = {"hf_url": "https://huggingface.co/unsloth/qwen-edit"}
|
|
||||||
assert BaseModelService._extract_hf_group_key(item) == "hf:unsloth/qwen-edit"
|
|
||||||
|
|
||||||
def test_hf_group_key_url_with_subpath(self):
|
|
||||||
"""URL with subpath still extracts just owner/repo."""
|
|
||||||
item = {"hf_url": "https://huggingface.co/user/repo/resolve/main/file.safetensors"}
|
|
||||||
assert BaseModelService._extract_hf_group_key(item) == "hf:user/repo"
|
|
||||||
|
|
||||||
def test_hf_group_key_empty_url(self):
|
|
||||||
"""Empty hf_url returns None."""
|
|
||||||
assert BaseModelService._extract_hf_group_key({"hf_url": ""}) is None
|
|
||||||
|
|
||||||
def test_hf_group_key_no_url(self):
|
|
||||||
"""Missing hf_url key returns None."""
|
|
||||||
assert BaseModelService._extract_hf_group_key({}) is None
|
|
||||||
|
|
||||||
def test_hf_group_key_none_url(self):
|
|
||||||
"""None hf_url returns None."""
|
|
||||||
assert BaseModelService._extract_hf_group_key({"hf_url": None}) is None
|
|
||||||
|
|
||||||
def test_hf_group_key_invalid_url(self):
|
|
||||||
"""Malformed HF URL returns None."""
|
|
||||||
assert BaseModelService._extract_hf_group_key({"hf_url": "not-a-url"}) is None
|
|
||||||
assert BaseModelService._extract_hf_group_key({"hf_url": "https://example.com"}) is None
|
|
||||||
|
|
||||||
# --- _extract_group_key ---
|
|
||||||
|
|
||||||
def test_group_key_civitai_only(self):
|
def test_group_key_civitai_only(self):
|
||||||
"""CivitAI modelId returned as int."""
|
"""CivitAI modelId returned as int."""
|
||||||
@@ -1303,30 +1272,64 @@ class TestHfGroupKey:
|
|||||||
assert BaseModelService._extract_group_key(item) == 123
|
assert BaseModelService._extract_group_key(item) == 123
|
||||||
|
|
||||||
def test_group_key_hf_only(self):
|
def test_group_key_hf_only(self):
|
||||||
"""HF-only item returns hf:user/repo string."""
|
"""HF-linked items never group: a repository is not a model identity."""
|
||||||
item = {"hf_url": "https://huggingface.co/user/repo"}
|
item = {"hf_url": "https://huggingface.co/user/repo"}
|
||||||
assert BaseModelService._extract_group_key(item) == "hf:user/repo"
|
assert BaseModelService._extract_group_key(item) is None
|
||||||
|
|
||||||
def test_group_key_civitai_preferred(self):
|
def test_group_key_civitai_preferred(self):
|
||||||
"""CivitAI modelId takes precedence over hf_url."""
|
"""CivitAI modelId takes precedence over any source identity."""
|
||||||
item = {
|
item = {
|
||||||
"civitai": {"modelId": 456},
|
"civitai": {"modelId": 456},
|
||||||
"hf_url": "https://huggingface.co/other/repo",
|
"source_url": "https://tensor.art/models/789",
|
||||||
}
|
}
|
||||||
assert BaseModelService._extract_group_key(item) == 456
|
assert BaseModelService._extract_group_key(item) == 456
|
||||||
|
|
||||||
def test_group_key_neither(self):
|
def test_group_key_neither(self):
|
||||||
"""No CivitAI or HF returns None."""
|
"""No CivitAI or groupable source returns None."""
|
||||||
assert BaseModelService._extract_group_key({}) is None
|
assert BaseModelService._extract_group_key({}) is None
|
||||||
assert BaseModelService._extract_group_key({"some": "data"}) is None
|
assert BaseModelService._extract_group_key({"some": "data"}) is None
|
||||||
|
|
||||||
def test_group_key_civitai_none_model_id(self):
|
def test_group_key_civitai_none_model_id(self):
|
||||||
"""civitai.modelId=None falls through to HF."""
|
"""civitai.modelId=None falls through to the source identity."""
|
||||||
item = {
|
item = {
|
||||||
"civitai": {"modelId": None},
|
"civitai": {"modelId": None},
|
||||||
"hf_url": "https://huggingface.co/user/repo",
|
"source_url": "https://tensor.art/models/789",
|
||||||
}
|
}
|
||||||
assert BaseModelService._extract_group_key(item) == "hf:user/repo"
|
assert BaseModelService._extract_group_key(item) == "ta:789"
|
||||||
|
|
||||||
|
def test_group_key_modelscope_uses_published_model_id(self):
|
||||||
|
"""ModelScope groups under ms:<modelId> once enrichment recorded it."""
|
||||||
|
item = {
|
||||||
|
"source_platform": "modelscope",
|
||||||
|
"source_url": "https://modelscope.cn/models/u/r",
|
||||||
|
"source_model_id": "555",
|
||||||
|
}
|
||||||
|
assert BaseModelService._extract_group_key(item) == "ms:555"
|
||||||
|
|
||||||
|
def test_group_key_modelscope_unenriched_stays_standalone(self):
|
||||||
|
"""Without source_model_id there is no key — never repo-level grouping."""
|
||||||
|
item = {
|
||||||
|
"source_platform": "modelscope",
|
||||||
|
"source_url": "https://modelscope.cn/models/u/r",
|
||||||
|
}
|
||||||
|
assert BaseModelService._extract_group_key(item) is None
|
||||||
|
|
||||||
|
def test_group_key_modelscope_identity_crosses_repos(self):
|
||||||
|
"""Same published-model id groups across repos; same repo does not."""
|
||||||
|
|
||||||
|
def ms_item(repo, model_id):
|
||||||
|
return {
|
||||||
|
"source_platform": "modelscope",
|
||||||
|
"source_url": f"https://modelscope.cn/models/{repo}",
|
||||||
|
"source_model_id": model_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert BaseModelService._extract_group_key(
|
||||||
|
ms_item("alice/collection", "555")
|
||||||
|
) == BaseModelService._extract_group_key(ms_item("bob/mirror", "555"))
|
||||||
|
assert BaseModelService._extract_group_key(
|
||||||
|
ms_item("alice/collection", "555")
|
||||||
|
) != BaseModelService._extract_group_key(ms_item("alice/collection", "777"))
|
||||||
|
|
||||||
|
|
||||||
class TestApplyHashFilters:
|
class TestApplyHashFilters:
|
||||||
|
|||||||
@@ -967,6 +967,8 @@ def _make_cache_entry(**overrides) -> Dict[str, Any]:
|
|||||||
"source_platform": "",
|
"source_platform": "",
|
||||||
"source_url": "",
|
"source_url": "",
|
||||||
"hf_url": "",
|
"hf_url": "",
|
||||||
|
"source_model_id": "",
|
||||||
|
"source_version_id": "",
|
||||||
"license_flags": 113,
|
"license_flags": 113,
|
||||||
"hash_status": "completed",
|
"hash_status": "completed",
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1007,8 @@ async def test_sync_cache_no_change(tmp_path: Path):
|
|||||||
"tags": ["alpha"],
|
"tags": ["alpha"],
|
||||||
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
||||||
"hf_url": "",
|
"hf_url": "",
|
||||||
|
"source_model_id": "",
|
||||||
|
"source_version_id": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = await scanner.sync_cache_from_metadata(
|
changed = await scanner.sync_cache_from_metadata(
|
||||||
@@ -1049,6 +1053,8 @@ async def test_sync_cache_in_place_update(tmp_path: Path):
|
|||||||
"tags": ["beta", "gamma"],
|
"tags": ["beta", "gamma"],
|
||||||
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
||||||
"hf_url": "",
|
"hf_url": "",
|
||||||
|
"source_model_id": "",
|
||||||
|
"source_version_id": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = await scanner.sync_cache_from_metadata(
|
changed = await scanner.sync_cache_from_metadata(
|
||||||
@@ -1094,6 +1100,8 @@ async def test_sync_cache_not_in_cache_delegates(tmp_path: Path):
|
|||||||
"tags": [],
|
"tags": [],
|
||||||
"civitai": {},
|
"civitai": {},
|
||||||
"hf_url": "",
|
"hf_url": "",
|
||||||
|
"source_model_id": "",
|
||||||
|
"source_version_id": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = await scanner.sync_cache_from_metadata(
|
changed = await scanner.sync_cache_from_metadata(
|
||||||
@@ -1147,6 +1155,8 @@ async def test_sync_cache_conditional_resort_skipped(tmp_path: Path, monkeypatch
|
|||||||
"tags": ["alpha"],
|
"tags": ["alpha"],
|
||||||
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
||||||
"hf_url": "",
|
"hf_url": "",
|
||||||
|
"source_model_id": "",
|
||||||
|
"source_version_id": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = await scanner.sync_cache_from_metadata(
|
changed = await scanner.sync_cache_from_metadata(
|
||||||
@@ -1197,6 +1207,8 @@ async def test_sync_cache_conditional_resort_triggered(tmp_path: Path, monkeypat
|
|||||||
"tags": ["alpha"],
|
"tags": ["alpha"],
|
||||||
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
"civitai": {"id": 111, "modelId": 222, "name": "v1"},
|
||||||
"hf_url": "",
|
"hf_url": "",
|
||||||
|
"source_model_id": "",
|
||||||
|
"source_version_id": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = await scanner.sync_cache_from_metadata(
|
changed = await scanner.sync_cache_from_metadata(
|
||||||
|
|||||||
@@ -279,15 +279,39 @@ class TestHelpers:
|
|||||||
assert get_source_platform({"source_platform": "tensorart"}) == "tensorart"
|
assert get_source_platform({"source_platform": "tensorart"}) == "tensorart"
|
||||||
assert get_source_platform({}) == ""
|
assert get_source_platform({}) == ""
|
||||||
|
|
||||||
def test_group_keys_match_legacy_hf_shape(self):
|
def test_group_keys_use_site_native_identity(self):
|
||||||
assert source_group_key({"hf_url": "https://huggingface.co/u/r"}) == "hf:u/r"
|
# Hugging Face has no site-native model identity: never grouped.
|
||||||
assert (
|
assert source_group_key({"hf_url": "https://huggingface.co/u/r"}) is None
|
||||||
source_group_key({"source_url": "https://modelscope.cn/models/u/r"}) == "ms:u/r"
|
# TensorArt's numeric id already identifies a single model.
|
||||||
)
|
|
||||||
assert (
|
assert (
|
||||||
source_group_key({"source_url": "https://tensor.art/models/123"}) == "ta:123"
|
source_group_key({"source_url": "https://tensor.art/models/123"}) == "ta:123"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_modelscope_groups_by_published_model_id(self):
|
||||||
|
# Without an enriched source_model_id the model stays standalone —
|
||||||
|
# never grouped by repo, which would collapse a collection repo.
|
||||||
|
assert (
|
||||||
|
source_group_key({"source_url": "https://modelscope.cn/models/u/r"}) is None
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
source_group_key(
|
||||||
|
{
|
||||||
|
"source_url": "https://modelscope.cn/models/u/r",
|
||||||
|
"source_model_id": "555",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "ms:555"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
source_group_key(
|
||||||
|
{
|
||||||
|
"source_url": "https://www.modelscope.ai/models/u/r",
|
||||||
|
"source_model_id": "678",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "msai:678"
|
||||||
|
)
|
||||||
|
|
||||||
def test_group_key_is_none_without_source(self):
|
def test_group_key_is_none_without_source(self):
|
||||||
assert source_group_key({}) is None
|
assert source_group_key({}) is None
|
||||||
assert source_group_key({"hf_url": "https://example.com/x"}) is None
|
assert source_group_key({"hf_url": "https://example.com/x"}) is None
|
||||||
@@ -457,7 +481,12 @@ def _modelscope_detail_payload() -> dict:
|
|||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"stats": {"fileList": ["Krea-2-LORA_c1-st8000.safetensors"]},
|
"stats": {"fileList": ["Krea-2-LORA_c1-st8000.safetensors"]},
|
||||||
"modelVersion": {"showName": "c1-st8000", "triggerWords": '[""]'},
|
"modelVersion": {
|
||||||
|
"showName": "c1-st8000",
|
||||||
|
"triggerWords": '[""]',
|
||||||
|
"id": 1001,
|
||||||
|
"modelId": 555,
|
||||||
|
},
|
||||||
"coverImages": [
|
"coverImages": [
|
||||||
{"url": "https://resources.modelscope.cn/cover-images/a.png"}
|
{"url": "https://resources.modelscope.cn/cover-images/a.png"}
|
||||||
],
|
],
|
||||||
@@ -467,6 +496,8 @@ def _modelscope_detail_payload() -> dict:
|
|||||||
"modelVersion": {
|
"modelVersion": {
|
||||||
"showName": "c1-st1000",
|
"showName": "c1-st1000",
|
||||||
"triggerWords": '["kreaface","kreamodel"]',
|
"triggerWords": '["kreaface","kreamodel"]',
|
||||||
|
"id": 1002,
|
||||||
|
"modelId": 555,
|
||||||
},
|
},
|
||||||
"coverImages": [
|
"coverImages": [
|
||||||
{"url": "https://resources.modelscope.cn/cover-images/b.png"},
|
{"url": "https://resources.modelscope.cn/cover-images/b.png"},
|
||||||
@@ -533,6 +564,9 @@ class TestFetchModelCardContext:
|
|||||||
# The version label is taken from the file that was matched, not from
|
# The version label is taken from the file that was matched, not from
|
||||||
# whichever version happens to come first in the payload.
|
# whichever version happens to come first in the payload.
|
||||||
assert context.version_name == "c1-st1000"
|
assert context.version_name == "c1-st1000"
|
||||||
|
# The site-native identity ids belong to the matched version too.
|
||||||
|
assert context.source_model_id == "555"
|
||||||
|
assert context.source_version_id == "1002"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_modelscope_version_label_is_empty_for_an_unknown_file(
|
async def test_modelscope_version_label_is_empty_for_an_unknown_file(
|
||||||
@@ -550,6 +584,9 @@ class TestFetchModelCardContext:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert context.version_name == ""
|
assert context.version_name == ""
|
||||||
|
# No version matched, so there is no per-version identity either.
|
||||||
|
assert context.source_model_id == ""
|
||||||
|
assert context.source_version_id == ""
|
||||||
# The repository-wide fields are still published.
|
# The repository-wide fields are still published.
|
||||||
assert context.model_name == "Krea-2-LORA"
|
assert context.model_name == "Krea-2-LORA"
|
||||||
|
|
||||||
|
|||||||
@@ -817,6 +817,75 @@ class TestSiteProvidedContext:
|
|||||||
"https://huggingface.co/user/repo/resolve/main/images/cat.png"
|
"https://huggingface.co/user/repo/resolve/main/images/cat.png"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_site_identity_ids_are_persisted(self, processor):
|
||||||
|
"""source_model_id/source_version_id reach the sidecar for grouping."""
|
||||||
|
context = ModelCardContext(source_model_id="555", source_version_id="1002")
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch("py.metadata_ops.apply_metadata_updates") as mock_apply,
|
||||||
|
mock.patch("py.metadata_ops.download_preview", return_value=None),
|
||||||
|
mock.patch("py.metadata_ops.refresh_cache"),
|
||||||
|
):
|
||||||
|
await processor.process(
|
||||||
|
skill_name="enrich_hf_metadata",
|
||||||
|
model_path="/p.safetensors",
|
||||||
|
llm_output=self.LLM_OUTPUT,
|
||||||
|
metadata=dict(self.MODELSCOPE_METADATA),
|
||||||
|
readme_content="",
|
||||||
|
source_context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
applied = mock_apply.call_args[0][1]
|
||||||
|
assert applied["source_model_id"] == "555"
|
||||||
|
assert applied["source_version_id"] == "1002"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_site_identity_ids_absent_without_context_values(self, processor):
|
||||||
|
"""No identity keys are written when the site did not publish any."""
|
||||||
|
with (
|
||||||
|
mock.patch("py.metadata_ops.apply_metadata_updates") as mock_apply,
|
||||||
|
mock.patch("py.metadata_ops.download_preview", return_value=None),
|
||||||
|
mock.patch("py.metadata_ops.refresh_cache"),
|
||||||
|
):
|
||||||
|
await processor.process(
|
||||||
|
skill_name="enrich_hf_metadata",
|
||||||
|
model_path="/p.safetensors",
|
||||||
|
llm_output=self.LLM_OUTPUT,
|
||||||
|
metadata=dict(self.MODELSCOPE_METADATA),
|
||||||
|
readme_content="",
|
||||||
|
source_context=ModelCardContext(description="summary only"),
|
||||||
|
)
|
||||||
|
|
||||||
|
applied = mock_apply.call_args[0][1]
|
||||||
|
assert "source_model_id" not in applied
|
||||||
|
assert "source_version_id" not in applied
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_site_identity_ids_skipped_for_a_model_with_no_external_source(
|
||||||
|
self, processor
|
||||||
|
):
|
||||||
|
"""A CivitAI-only model must not pick up source identity ids."""
|
||||||
|
context = ModelCardContext(source_model_id="555", source_version_id="1002")
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch("py.metadata_ops.apply_metadata_updates") as mock_apply,
|
||||||
|
mock.patch("py.metadata_ops.download_preview", return_value=None),
|
||||||
|
mock.patch("py.metadata_ops.refresh_cache"),
|
||||||
|
):
|
||||||
|
await processor.process(
|
||||||
|
skill_name="enrich_hf_metadata",
|
||||||
|
model_path="/p.safetensors",
|
||||||
|
llm_output=self.LLM_OUTPUT,
|
||||||
|
metadata={"from_civitai": True},
|
||||||
|
readme_content="",
|
||||||
|
source_context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
applied = mock_apply.call_args[0][1]
|
||||||
|
assert "source_model_id" not in applied
|
||||||
|
assert "source_version_id" not in applied
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user