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:
Will Miao
2026-09-25 23:29:30 +08:00
parent 8a80f82d93
commit c48feeddb6
17 changed files with 357 additions and 78 deletions
+44 -41
View File
@@ -1263,39 +1263,8 @@ async def test_get_model_civitai_url_falls_back_when_host_setting_is_not_a_strin
}
class TestHfGroupKey:
"""Tests for _extract_hf_group_key and _extract_group_key."""
# --- _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 ---
class TestSourceGroupKey:
"""Tests for _extract_group_key (CivitAI id, then site-native source identity)."""
def test_group_key_civitai_only(self):
"""CivitAI modelId returned as int."""
@@ -1303,30 +1272,64 @@ class TestHfGroupKey:
assert BaseModelService._extract_group_key(item) == 123
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"}
assert BaseModelService._extract_group_key(item) == "hf:user/repo"
assert BaseModelService._extract_group_key(item) is None
def test_group_key_civitai_preferred(self):
"""CivitAI modelId takes precedence over hf_url."""
"""CivitAI modelId takes precedence over any source identity."""
item = {
"civitai": {"modelId": 456},
"hf_url": "https://huggingface.co/other/repo",
"source_url": "https://tensor.art/models/789",
}
assert BaseModelService._extract_group_key(item) == 456
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({"some": "data"}) is None
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 = {
"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: