You cannot find a model by typing its name unless you also reproduce its punctuation, and nobody remembers where the punctuation fell. `2xko` could not find `2X-KO_more_-ill_r1` at all — reported exactly that way. Matching now folds `-`, `_`, `.`, `/`, spaces and brackets out of BOTH sides before comparing, in three tiers: an exact prefix still outranks a folded one, which outranks a folded substring. So `2xko`, `2x-ko` and `2XKO` all reach `2X-KO_more_-ill_r1`, `spritepixel` reaches `8.0-sprite pixel art style by skormino`, and someone who typed a name exactly is never reordered beneath a fuzzier match. Free to do here: the model list is tens of entries, not the tag list's 286k. Tag search is deliberately left alone — it is a bisect over a sorted array of 286,337 names and prefix-only is what every booru autocomplete does. Sections were not delivering what I claimed for them. Tags lead, `lazy` matches twenty of them, and the embedding section existed but sat entirely below the fold — the user could not filter to their own models without scrolling a list they were not looking for. The leading section is now capped to 6 rows WHEN a model also matched, which is what actually makes the others reachable. A query matching nothing but tags still gets the full twenty. The LoRA and embedding settings move to the same hand-built switch as the tag one, for the reason that control exists: a count beside the toggle. "50 LoRAs found" answers the question a bare switch raises — is there anything for this to suggest? — and it is the question that otherwise sends people to the manager to check. And the pill now reads PrimeVue's own `--p-toggleswitch-*` custom properties for every dimension, not just its colours. Those are injected when their component first mounts, which has always happened by the time ours renders, because our row sits in their dialog among their switches — so at render time this IS their pill and follows their theme. Aura's defaults remain as fallbacks. Handle travel became a `calc()` over the same tokens rather than a fixed 16px, so a differently-sized track still lands the knob flush.
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
"""LoRA / embedding completion source.
|
|
|
|
Matching rules only — the HTTP layer is a thin wrapper over these, and the
|
|
folder enumeration belongs to ComfyUI.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from wp_api._model_index import build_hits, search
|
|
|
|
|
|
def hits(*paths: str):
|
|
return build_hits(list(paths))
|
|
|
|
|
|
class TestDisplayShape:
|
|
def test_strips_folder_and_extension_for_display(self):
|
|
[h] = hits("style/watercolour_v2.safetensors")
|
|
assert h.name == "watercolour_v2"
|
|
assert h.folder == "style"
|
|
# The insert must use the full path: two folders can hold the same
|
|
# filename and ComfyUI resolves by path, so inserting the short name
|
|
# would silently pick whichever it found first.
|
|
assert h.path == "style/watercolour_v2.safetensors"
|
|
|
|
def test_handles_windows_separators(self):
|
|
[h] = hits("character\\anime\\miku.safetensors")
|
|
assert h.name == "miku"
|
|
assert h.folder == "character/anime"
|
|
|
|
def test_a_file_at_the_root_has_no_folder(self):
|
|
[h] = hits("detail_tweaker.safetensors")
|
|
assert h.name == "detail_tweaker"
|
|
assert h.folder == ""
|
|
|
|
|
|
class TestSearch:
|
|
def test_empty_query_matches_nothing(self):
|
|
# Guards against a stray keystroke dumping the entire model folder into
|
|
# the popover.
|
|
assert search(hits("a.safetensors", "b.safetensors"), "", 10) == []
|
|
|
|
def test_prefix_matches_rank_above_substring_matches(self):
|
|
found = search(
|
|
hits("hand_fix.safetensors", "lazyhand.safetensors", "lazy_eyes.safetensors"),
|
|
"lazy",
|
|
10,
|
|
)
|
|
assert [h.name for h in found] == ["lazy_eyes", "lazyhand"]
|
|
|
|
def test_substring_matches_are_included(self):
|
|
found = search(hits("super_detail.safetensors"), "detail", 10)
|
|
assert [h.name for h in found] == ["super_detail"]
|
|
|
|
def test_matching_is_case_insensitive(self):
|
|
found = search(hits("Watercolour.safetensors"), "water", 10)
|
|
assert [h.name for h in found] == ["Watercolour"]
|
|
|
|
def test_the_folder_is_searchable_too(self):
|
|
# "which of my character loras was it" is a real way to look, and the
|
|
# folder is often the only place that word appears.
|
|
found = search(hits("character/miku.safetensors"), "character", 10)
|
|
assert [h.name for h in found] == ["miku"]
|
|
|
|
def test_limit_is_honoured(self):
|
|
many = hits(*[f"lazy{i}.safetensors" for i in range(30)])
|
|
assert len(search(many, "lazy", 5)) == 5
|
|
|
|
def test_ordering_is_stable_across_calls(self):
|
|
# An unstable list under a moving keyboard selection is how you press
|
|
# Enter on the wrong row.
|
|
pool = hits("lazyB.safetensors", "lazyA.safetensors", "lazyC.safetensors")
|
|
first = [h.name for h in search(pool, "lazy", 10)]
|
|
second = [h.name for h in search(pool, "lazy", 10)]
|
|
assert first == second == ["lazyA", "lazyB", "lazyC"]
|
|
|
|
|
|
class TestSeparatorInsensitiveSearch:
|
|
"""Real filenames scatter `-`, `_`, `.` and spaces unpredictably, and nobody
|
|
remembers where they fell. Matching the literal string meant reproducing
|
|
punctuation the user had no reason to recall — `2xko` could not find
|
|
`2X-KO_more_-ill_r1` at all, which is how this was reported."""
|
|
|
|
POOL = (
|
|
r"style\2X-KO_more_-ill_r1.safetensors",
|
|
r"style\8.0-sprite pixel art style by skormino.safetensors",
|
|
r"style\lazyhand-e12c.safetensors",
|
|
)
|
|
|
|
def found(self, query: str):
|
|
return [h.name for h in search(hits(*self.POOL), query, 5)]
|
|
|
|
def test_finds_a_name_whose_separators_the_user_omitted(self):
|
|
assert self.found("2xko") == ["2X-KO_more_-ill_r1"]
|
|
|
|
def test_finds_it_with_the_wrong_separator_too(self):
|
|
# Guessing "-" where the file has "_" is the same class of miss.
|
|
assert self.found("2x-ko") == ["2X-KO_more_-ill_r1"]
|
|
|
|
def test_ignores_case_as_well(self):
|
|
assert self.found("2XKO") == ["2X-KO_more_-ill_r1"]
|
|
|
|
def test_matches_across_a_space(self):
|
|
assert self.found("spritepixel") == ["8.0-sprite pixel art style by skormino"]
|
|
|
|
def test_matches_a_full_path_typed_inside_a_reference(self):
|
|
assert self.found("lazyhande12c") == ["lazyhand-e12c"]
|
|
|
|
def test_still_matches_mid_name(self):
|
|
assert self.found("komore") == ["2X-KO_more_-ill_r1"]
|
|
|
|
def test_an_exact_prefix_still_outranks_a_folded_one(self):
|
|
# Someone who typed the name exactly should not be reordered beneath a
|
|
# fuzzier match.
|
|
pool = hits(r"a\lazy-hand.safetensors", r"a\lazyhand.safetensors")
|
|
assert search(pool, "lazyhand", 5)[0].name == "lazyhand"
|