mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
fix(metadata): keep the saved trigger-word order on refresh
`civitai.trainedWords` is an ordered array, and the order is what gets pasted into a prompt: "Copy Trigger Words" and the insert-into-node action join it as-is. The refresh merge unioned the stored words with the freshly fetched ones via `list(set(...))`, so any metadata refresh silently shuffled a user's ordering into an arbitrary one. Now that the UI exposes reordering, that would look like the feature losing the change at random. Merge in order instead: stored words first (in their saved order), then newly discovered ones, duplicates dropped. `_merge_ordered_unique` keeps the behaviour easy to assert, and the existing merge test keeps passing because it compares the result as a set.
This commit is contained in:
@@ -19,6 +19,28 @@ from .model_sources import has_external_source
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_ordered_unique(existing: Iterable[str], new: Iterable[str]) -> list[str]:
|
||||||
|
"""Concatenate two word lists, dropping duplicates without reordering.
|
||||||
|
|
||||||
|
Trigger word order is meaningful: the sequence stored in
|
||||||
|
``civitai.trainedWords`` is the order used when building prompts, and users
|
||||||
|
can reorder it in the UI. A plain ``set`` union used to shuffle that order on
|
||||||
|
every metadata refresh, so existing words are kept first (in their saved
|
||||||
|
order) and newly discovered ones are appended.
|
||||||
|
"""
|
||||||
|
|
||||||
|
merged: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
|
||||||
|
for word in list(existing) + list(new):
|
||||||
|
if word in seen:
|
||||||
|
continue
|
||||||
|
seen.add(word)
|
||||||
|
merged.append(word)
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
class MetadataProviderProtocol(Protocol):
|
class MetadataProviderProtocol(Protocol):
|
||||||
"""Subset of metadata provider interface consumed by the sync service."""
|
"""Subset of metadata provider interface consumed by the sync service."""
|
||||||
|
|
||||||
@@ -115,9 +137,10 @@ class MetadataSyncService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "trainedWords" in existing_civitai:
|
if "trainedWords" in existing_civitai:
|
||||||
existing_trained = existing_civitai.get("trainedWords", [])
|
existing_trained = existing_civitai.get("trainedWords", []) or []
|
||||||
new_trained = civitai_metadata.get("trainedWords", [])
|
new_trained = civitai_metadata.get("trainedWords", []) or []
|
||||||
merged_trained = list(set(existing_trained + new_trained))
|
# Order preserving merge: the saved order drives prompt order.
|
||||||
|
merged_trained = _merge_ordered_unique(existing_trained, new_trained)
|
||||||
merged_civitai["trainedWords"] = merged_trained
|
merged_civitai["trainedWords"] = merged_trained
|
||||||
|
|
||||||
local_metadata["civitai"] = merged_civitai
|
local_metadata["civitai"] = merged_civitai
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from py.services.connectivity_guard import OFFLINE_COOLDOWN_ERROR, OFFLINE_FRIENDLY_MESSAGE
|
from py.services.connectivity_guard import OFFLINE_COOLDOWN_ERROR, OFFLINE_FRIENDLY_MESSAGE
|
||||||
from py.services.errors import RateLimitError
|
from py.services.errors import RateLimitError
|
||||||
from py.services.metadata_sync_service import MetadataSyncService
|
from py.services.metadata_sync_service import MetadataSyncService, _merge_ordered_unique
|
||||||
|
|
||||||
|
|
||||||
class DummySettings:
|
class DummySettings:
|
||||||
@@ -112,6 +112,49 @@ async def test_update_model_metadata_merges_and_persists():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_ordered_unique_keeps_first_seen_order():
|
||||||
|
assert _merge_ordered_unique(["b", "a"], ["a", "c", "b", "d"]) == [
|
||||||
|
"b",
|
||||||
|
"a",
|
||||||
|
"c",
|
||||||
|
"d",
|
||||||
|
]
|
||||||
|
assert _merge_ordered_unique([], ["x"]) == ["x"]
|
||||||
|
assert _merge_ordered_unique(["x"], []) == ["x"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_model_metadata_preserves_trained_word_order():
|
||||||
|
"""Trigger word order (prompt order) must survive a metadata refresh."""
|
||||||
|
|
||||||
|
helpers = build_service()
|
||||||
|
|
||||||
|
local = {
|
||||||
|
"civitai": {"trainedWords": ["zeta style", "alpha", "beta"]},
|
||||||
|
"model_name": "Local",
|
||||||
|
}
|
||||||
|
remote = {
|
||||||
|
"source": "api",
|
||||||
|
"trainedWords": ["beta", "gamma", "alpha"],
|
||||||
|
"model": {"name": "Remote Model"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await helpers.service.update_model_metadata(
|
||||||
|
"path/to/model.metadata.json",
|
||||||
|
local,
|
||||||
|
remote,
|
||||||
|
helpers.default_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Saved order first, newly discovered words appended, duplicates dropped
|
||||||
|
assert result["civitai"]["trainedWords"] == [
|
||||||
|
"zeta style",
|
||||||
|
"alpha",
|
||||||
|
"beta",
|
||||||
|
"gamma",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_model_metadata_propagates_civitai_autov3():
|
async def test_update_model_metadata_propagates_civitai_autov3():
|
||||||
helpers = build_service()
|
helpers = build_service()
|
||||||
|
|||||||
Reference in New Issue
Block a user