feat(ui): merge user tags into auto-tag badges and refresh on tag edit (#918)

- Layer 2 fallback: user tags overlapping with auto-tag categories
  (HIGH/LOW/I2V/T2V/TI2V/Lightning/Turbo) are merged into auto_tags,
  providing manual override when filename-based detection fails.
  Matching is case-insensitive so "high"/"High"/"HIGH" all work.
- Refresh on tag edit: save_metadata and add_tags handlers now return
  recalculated auto_tags in the response; the frontend passes them to
  VirtualScroller.updateSingleItem so badges update immediately without
  requiring a page reload.
- 8 new test cases for Layer 2 fallback and case-insensitive matching.
This commit is contained in:
Will Miao
2026-05-20 22:48:44 +08:00
parent 9ce56dd40c
commit 78303b2a5e
7 changed files with 150 additions and 39 deletions

View File

@@ -255,7 +255,7 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None:
cache_updates.append(metadata)
return True
tags = asyncio.run(
tags, auto_tags = asyncio.run(
service.add_tags(
file_path=str(tmp_path / "model.safetensors"),
new_tags=["new", "existing"],
@@ -265,5 +265,6 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None:
)
assert tags == ["existing", "new"]
assert auto_tags == []
assert manager.saved
assert cache_updates

View File

@@ -43,7 +43,7 @@ async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path)
return True
# Try to add "Test" (different case) - should not be added since "test" already exists
tags = await service.add_tags(
tags, auto_tags = await service.add_tags(
file_path=str(tmp_path / "model.safetensors"),
new_tags=["Test"],
metadata_loader=loader,
@@ -52,6 +52,7 @@ async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path)
# Should still only have "test" (lowercase) in the tags
assert tags == ["test"]
assert auto_tags == [] # no file_name/base_model in metadata, so no auto-detection
assert len(manager.saved) == 1
saved_metadata = manager.saved[0][1]
assert saved_metadata["tags"] == ["test"]
@@ -76,7 +77,7 @@ async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) ->
return True
# Add new tags with mixed case
tags = await service.add_tags(
tags, auto_tags = await service.add_tags(
file_path=str(tmp_path / "model.safetensors"),
new_tags=["NewTag", "ANOTHER_TAG"],
metadata_loader=loader,
@@ -87,6 +88,7 @@ async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) ->
assert "existing" in tags
assert "newtag" in tags
assert "another_tag" in tags
assert auto_tags == []
assert len(manager.saved) == 1
saved_metadata = manager.saved[0][1]
assert "newtag" in saved_metadata["tags"]

View File

@@ -126,6 +126,80 @@ class TestExtractAutoTags:
})
assert set(result) == {"HIGH", "I2V"}
# ── Layer 2: user-defined tags as manual fallback ───────────
def test_user_tags_fallback_when_detection_fails(self):
result = extract_auto_tags({
"file_name": "BOTH-v1.0",
"base_model": "Wan 2.2",
"civitai": {},
"tags": ["HIGH", "I2V", "T2V"],
})
assert set(result) == {"HIGH", "I2V", "T2V"}
def test_user_tags_augment_partial_detection(self):
result = extract_auto_tags({
"file_name": "wan_i2v_hn_v2",
"base_model": "Wan 2.2 I2V",
"civitai": {},
"tags": ["HIGH"],
})
assert set(result) == {"HIGH", "I2V"}
def test_user_tags_non_auto_tag_ignored(self):
result = extract_auto_tags({
"file_name": "model_v1",
"base_model": "Wan 2.2",
"civitai": {},
"tags": ["HIGH", "character", "style", "nsfw"],
})
assert set(result) == {"HIGH"}
def test_user_tags_overrides_non_wan_gate(self):
result = extract_auto_tags({
"file_name": "flux_model_v1",
"base_model": "Flux.1 D",
"civitai": {},
"tags": ["HIGH", "LOW", "Turbo"],
})
assert set(result) == {"HIGH", "LOW", "Turbo"}
def test_user_tags_no_duplication(self):
result = extract_auto_tags({
"file_name": "wan_i2v_high_v3",
"base_model": "Wan 2.2",
"civitai": {},
"tags": ["HIGH", "I2V"],
})
assert set(result) == {"HIGH", "I2V"}
def test_user_tags_lightning_turbo_manual(self):
result = extract_auto_tags({
"file_name": "sdxl_model_v1",
"base_model": "SDXL",
"civitai": {},
"tags": ["Lightning"],
})
assert set(result) == {"Lightning"}
def test_user_tags_case_insensitive_lowercase(self):
result = extract_auto_tags({
"file_name": "wan_masterpieces_v2",
"base_model": "Wan Video 14B t2v",
"civitai": {},
"tags": ["high"],
})
assert set(result) == {"HIGH", "T2V"}
def test_user_tags_case_insensitive_mixed(self):
result = extract_auto_tags({
"file_name": "model_v1",
"base_model": "SDXL",
"civitai": {},
"tags": ["lightning", "turbo", "i2v"],
})
assert set(result) == {"Lightning", "Turbo", "I2V"}
class TestAutoTagCategories:
def test_all_patterns_compile(self):