mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(modelscope): read the model-detail API for card extras
ModelScope's model card is not just README.md: the author's summary (Description), the site-curated tags (OfficialTags), the internal architecture enums (VisionFoundation/SubVisionFoundation) and — per published version — the model filenames with that file's example images (coverImages) and trigger words all live in the model-detail API. AIGC repositories there frequently ship an auto-generated boilerplate README and put the only useful text in Description, so reading just the README yielded almost nothing. Add `ModelSource.fetch_model_card_context()` returning a new `ModelCardContext`, implemented by ModelScopeSource against the public (no API key) detail endpoint. Example images are matched to the model's basename through each version's `stats.fileList`, so every checkpoint in a collection repository gets its own images rather than a sibling's. Consume the context in the post-processor: * example images seed `civitai.images` and, being per-file, take priority in the preview fallback chain * the author summary becomes a paragraph in `modelDescription` and fills `civitai.description` when the LLM returns no short description * site-curated tags are always merged in, which also fixes the official `character-enhancement` being dropped by the prompt's no-hyphen rule * per-file trigger words are used before the repo-wide YAML `instance_prompt` * an explicitly stated strength range is recovered by regex so `usage_tips` is populated even without an LLM The prompt gains a Site-Provided Metadata section so the LLM can prefer the site's first-hand data over its own guesses.
This commit is contained in:
@@ -332,6 +332,190 @@ class TestAssetBaseUrl:
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model card context (site extras kept outside the README)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _modelscope_detail_payload() -> dict:
|
||||
"""A trimmed-but-faithful ModelScope model-detail response.
|
||||
|
||||
Mirrors the shape of ``/api/v1/models/{id}`` for an AIGC LoRA repo whose
|
||||
README is auto-generated boilerplate, so the author summary and the
|
||||
per-file example images are only reachable through this API.
|
||||
"""
|
||||
|
||||
return {
|
||||
"Code": 200,
|
||||
"Data": {
|
||||
"Name": "Krea-2-LORA",
|
||||
"ChineseName": "krea脸模",
|
||||
"Description": "权重0.5-1.2。配合《风格滤镜》lora一起使用。",
|
||||
"BaseModel": ["krea/Krea-2-Turbo"],
|
||||
"License": "Apache License 2.0",
|
||||
"OfficialTags": [
|
||||
{"Tag": "photography", "ChineseName": "写实摄影"},
|
||||
{"Tag": "woman", "ChineseName": "女生"},
|
||||
{"Tag": "photography", "ChineseName": "重复项"},
|
||||
],
|
||||
"MuseInfo": {
|
||||
"versions": [
|
||||
{
|
||||
"stats": {"fileList": ["Krea-2-LORA_c1-st8000.safetensors"]},
|
||||
"modelVersion": {"showName": "c1-st8000", "triggerWords": '[""]'},
|
||||
"coverImages": [
|
||||
{"url": "https://resources.modelscope.cn/cover-images/a.png"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"stats": {"fileList": ["Krea-2-LORA_c1-st1000.safetensors"]},
|
||||
"modelVersion": {
|
||||
"showName": "c1-st1000",
|
||||
"triggerWords": '["kreaface","kreamodel"]',
|
||||
},
|
||||
"coverImages": [
|
||||
{"url": "https://resources.modelscope.cn/cover-images/b.png"},
|
||||
{"url": "https://resources.modelscope.cn/cover-images/c.png"},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestFetchModelCardContext:
|
||||
@pytest.mark.asyncio
|
||||
async def test_modelscope_reads_description_tags_and_base_model(self, monkeypatch):
|
||||
async def fake_fetch_json(url, **_kwargs):
|
||||
assert url == "https://modelscope.cn/api/v1/models/u/r"
|
||||
return 200, _modelscope_detail_payload()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||
)
|
||||
|
||||
context = await ModelScopeSource().fetch_model_card_context("u/r")
|
||||
|
||||
assert context.description == "权重0.5-1.2。配合《风格滤镜》lora一起使用。"
|
||||
assert context.base_model == "krea/Krea-2-Turbo"
|
||||
# OfficialTag values only, de-duplicated, order preserved.
|
||||
assert context.official_tags == ["photography", "woman"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modelscope_matches_example_images_by_filename(self, monkeypatch):
|
||||
async def fake_fetch_json(url, **_kwargs):
|
||||
return 200, _modelscope_detail_payload()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||
)
|
||||
|
||||
context = await ModelScopeSource().fetch_model_card_context(
|
||||
"u/r", "Krea-2-LORA_c1-st1000.safetensors"
|
||||
)
|
||||
|
||||
# Only the requested file's images, never a sibling checkpoint's.
|
||||
assert context.example_images == [
|
||||
"https://resources.modelscope.cn/cover-images/b.png",
|
||||
"https://resources.modelscope.cn/cover-images/c.png",
|
||||
]
|
||||
assert context.trigger_words == ["kreaface", "kreamodel"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modelscope_never_borrows_images_for_an_unknown_file(
|
||||
self, monkeypatch
|
||||
):
|
||||
async def fake_fetch_json(url, **_kwargs):
|
||||
return 200, _modelscope_detail_payload()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||
)
|
||||
|
||||
context = await ModelScopeSource().fetch_model_card_context(
|
||||
"u/r", "other.safetensors"
|
||||
)
|
||||
|
||||
assert context.example_images == []
|
||||
assert context.trigger_words == []
|
||||
# The repo-wide fields are still returned.
|
||||
assert context.base_model == "krea/Krea-2-Turbo"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modelscope_single_version_repo_without_filename(self, monkeypatch):
|
||||
payload = _modelscope_detail_payload()
|
||||
versions = payload["Data"]["MuseInfo"]["versions"]
|
||||
payload["Data"]["MuseInfo"]["versions"] = versions[:1]
|
||||
|
||||
async def fake_fetch_json(url, **_kwargs):
|
||||
return 200, payload
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||
)
|
||||
|
||||
context = await ModelScopeSource().fetch_model_card_context("u/r")
|
||||
|
||||
assert context.example_images == [
|
||||
"https://resources.modelscope.cn/cover-images/a.png"
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modelscope_tolerates_failures_and_odd_payloads(self, monkeypatch):
|
||||
payloads = (None, {"Code": 500}, {"Data": "nope"}, {"Data": {}})
|
||||
for payload in payloads:
|
||||
|
||||
async def fake_fetch_json(url, _payload=payload, **_kwargs):
|
||||
return (0 if _payload is None else 200), _payload
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||
)
|
||||
context = await ModelScopeSource().fetch_model_card_context(
|
||||
"u/r", "a.safetensors"
|
||||
)
|
||||
assert context.is_empty(), payload
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modelscope_reads_stats_from_json_encoded_fallback(self, monkeypatch):
|
||||
payload = {
|
||||
"Data": {
|
||||
"MuseInfo": {
|
||||
"versions": [
|
||||
{
|
||||
"modelVersion": {
|
||||
"showName": "v1",
|
||||
"stats": '{"fileList": ["model.safetensors"]}',
|
||||
"triggerWords": '["hi"]',
|
||||
},
|
||||
"coverImages": [{"url": "https://cdn.example/x.png"}],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async def fake_fetch_json(url, **_kwargs):
|
||||
return 200, payload
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.model_sources.modelscope.fetch_json", fake_fetch_json
|
||||
)
|
||||
|
||||
context = await ModelScopeSource().fetch_model_card_context(
|
||||
"u/r", "model.safetensors"
|
||||
)
|
||||
|
||||
assert context.example_images == ["https://cdn.example/x.png"]
|
||||
assert context.trigger_words == ["hi"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_context_is_empty_for_other_sources(self):
|
||||
assert (await HuggingFaceSource().fetch_model_card_context("u/r")).is_empty()
|
||||
assert (await TensorArtSource().fetch_model_card_context("123")).is_empty()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Download support
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user