mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-07 22:40:14 -03:00
feat(autocomplete): search loras within active filters of LoRA Manager page
Add /af and /noaf toggle commands (plus /activefilters aliases) to the
loras autocomplete widget. When enabled (default off), suggestions are
matched within the active filters (folder, base model, tags, auto-tags,
license, tag logic) persisted by the LoRA Manager page in localStorage,
keeping the match pool consistent with the list endpoint, including the
global show_only_sfw setting.
Backend: /lm/{prefix}/relative-paths accepts the filter query params and
pre-filters the scanner cache with ModelFilterSet. The presence of the
recursive param signals the filter pipeline to run even without concrete
filters so global settings stay in parity with the list endpoint.
This commit is contained in:
@@ -27,6 +27,13 @@ class FakeScanner:
|
||||
return list(self._roots)
|
||||
|
||||
|
||||
class StubSettings:
|
||||
"""Settings stub that returns defaults, avoiding the real settings singleton."""
|
||||
|
||||
def get(self, key, default=None):
|
||||
return default
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_supports_multiple_tokens():
|
||||
scanner = FakeScanner(
|
||||
@@ -101,3 +108,274 @@ async def test_search_safe_does_not_match_all_files():
|
||||
matching = await service.search_relative_paths("safe")
|
||||
|
||||
assert len(matching) == 0
|
||||
|
||||
|
||||
class SfwStubSettings(StubSettings):
|
||||
"""Settings stub with the global SFW filter enabled."""
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key == "show_only_sfw":
|
||||
return True
|
||||
return default
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_respects_global_sfw_setting():
|
||||
"""Filtered search applies show_only_sfw like the list endpoint (parity)."""
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/sfw-model.safetensors", "preview_nsfw_level": 0},
|
||||
{"file_path": "/models/nsfw-model.safetensors", "preview_nsfw_level": 4},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=SfwStubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("model", apply_filters=True)
|
||||
|
||||
assert matching == ["sfw-model.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_sfw_only_applied_when_filter_mode_is_on():
|
||||
"""Global settings (show_only_sfw) apply only when the filter pipeline runs."""
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/sfw-model.safetensors", "preview_nsfw_level": 0},
|
||||
{"file_path": "/models/nsfw-model.safetensors", "preview_nsfw_level": 4},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=SfwStubSettings()
|
||||
)
|
||||
|
||||
default_matching = await service.search_relative_paths("model")
|
||||
|
||||
assert default_matching == [
|
||||
"sfw-model.safetensors",
|
||||
"nsfw-model.safetensors",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_folder_filter_recursive():
|
||||
"""folder filter with recursive=True (default) matches subfolders."""
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/anime/model-a.safetensors", "folder": "anime"},
|
||||
{
|
||||
"file_path": "/models/anime/nsfw/model-b.safetensors",
|
||||
"folder": "anime/nsfw",
|
||||
},
|
||||
{"file_path": "/models/realistic/model-c.safetensors", "folder": "realistic"},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("model", folder="anime")
|
||||
|
||||
assert matching == [
|
||||
f"anime{os.sep}model-a.safetensors",
|
||||
f"anime{os.sep}nsfw{os.sep}model-b.safetensors",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_folder_filter_exact():
|
||||
"""folder filter with recursive=False matches only the exact folder."""
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/anime/model-a.safetensors", "folder": "anime"},
|
||||
{
|
||||
"file_path": "/models/anime/nsfw/model-b.safetensors",
|
||||
"folder": "anime/nsfw",
|
||||
},
|
||||
{"file_path": "/models/realistic/model-c.safetensors", "folder": "realistic"},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths(
|
||||
"model", folder="anime", recursive=False
|
||||
)
|
||||
|
||||
assert matching == [f"anime{os.sep}model-a.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_base_model_filter():
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/model-a.safetensors", "base_model": "SD 1.5"},
|
||||
{"file_path": "/models/model-b.safetensors", "base_model": "SDXL"},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("model", base_models=["SD 1.5"])
|
||||
|
||||
assert matching == ["model-a.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_tag_include():
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/model-a.safetensors", "tags": ["anime"]},
|
||||
{"file_path": "/models/model-b.safetensors", "tags": ["realistic"]},
|
||||
{"file_path": "/models/model-c.safetensors", "tags": ["anime", "realistic"]},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("model", tags={"anime": "include"})
|
||||
|
||||
assert set(matching) == {"model-a.safetensors", "model-c.safetensors"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_tag_exclude():
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/model-a.safetensors", "tags": ["anime"]},
|
||||
{"file_path": "/models/model-b.safetensors", "tags": ["realistic"]},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("model", tags={"anime": "exclude"})
|
||||
|
||||
assert matching == ["model-b.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_auto_tag_include():
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{
|
||||
"file_path": "/models/model-i2v.safetensors",
|
||||
"file_name": "model-i2v.safetensors",
|
||||
},
|
||||
{
|
||||
"file_path": "/models/model-t2v.safetensors",
|
||||
"file_name": "model-t2v.safetensors",
|
||||
},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths(
|
||||
"model", auto_tags={"I2V": "include"}
|
||||
)
|
||||
|
||||
assert matching == ["model-i2v.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_tag_logic_all():
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/model-a.safetensors", "tags": ["anime", "style"]},
|
||||
{"file_path": "/models/model-b.safetensors", "tags": ["anime"]},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths(
|
||||
"model", tags={"anime": "include", "style": "include"}, tag_logic="all"
|
||||
)
|
||||
|
||||
assert matching == ["model-a.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_credit_required_filter():
|
||||
# license_flags bit0: 1 = no credit required, 0 = credit required
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/model-a.safetensors", "license_flags": 127},
|
||||
{"file_path": "/models/model-b.safetensors", "license_flags": 0},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("model", credit_required=True)
|
||||
assert matching == ["model-b.safetensors"]
|
||||
|
||||
matching = await service.search_relative_paths("model", credit_required=False)
|
||||
assert matching == ["model-a.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_allow_selling_filter():
|
||||
# license_flags bit1: 1 = commercial image use allowed, 0 = not allowed
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/model-a.safetensors", "license_flags": 2},
|
||||
{"file_path": "/models/model-b.safetensors", "license_flags": 1},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths(
|
||||
"model", allow_selling_generated_content=True
|
||||
)
|
||||
assert matching == ["model-a.safetensors"]
|
||||
|
||||
matching = await service.search_relative_paths(
|
||||
"model", allow_selling_generated_content=False
|
||||
)
|
||||
assert matching == ["model-b.safetensors"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_relative_paths_no_filters_regression():
|
||||
"""No filter kwargs -> behavior is byte-identical to plain token matching."""
|
||||
scanner = FakeScanner(
|
||||
[
|
||||
{"file_path": "/models/flux/detail-model.safetensors"},
|
||||
{"file_path": "/models/flux/only-flux.safetensors"},
|
||||
],
|
||||
["/models"],
|
||||
)
|
||||
service = DummyService(
|
||||
"stub", scanner, BaseModelMetadata, settings_provider=StubSettings()
|
||||
)
|
||||
|
||||
matching = await service.search_relative_paths("flux")
|
||||
|
||||
assert matching == [
|
||||
f"flux{os.sep}only-flux.safetensors",
|
||||
f"flux{os.sep}detail-model.safetensors",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user