Files
Will Miao 0b46852f91 feat(search): search LoRAs by trigger word
Users see a trigger word in someone else's prompt and have no way to tell
whether the LoRA is already in their library, so they fall back to searching
the model sites again.

- Read trigger words from civitai.trainedWords through a small helper that
  tolerates the shapes older sidecars contain (None, a bare string, mixed
  lists). Provenance does not matter: CivitAI/CivArchive sync, HuggingFace
  or ModelScope model cards, the AI enrichment skill and manual edits all
  write the same field.
- Add an opt-in trigger_words search option to SearchStrategy, evaluated
  last before the always-on hash fallback. Matching is case-insensitive
  substring only: fuzzy would run the sequence matcher over every declared
  word of every entry and cost seconds on large libraries.
- Expose it as a "Trigger Words" chip on the LoRAs page only, plumb the
  option through page state and the list request, and parse the new
  search_trigger_words query param.
- Cover the strategy (including AI-enriched HuggingFace entries and
  malformed trainedWords), the param parsing, the chip-to-state wiring and
  the request query string. The English key is synced with a
  [TODO: Translate] placeholder in the other locales.
2026-11-05 20:24:18 +08:00

41 lines
1.4 KiB
Python

"""Helpers for reading trigger words out of a model's ``civitai`` metadata.
Trigger words live in ``civitai.trainedWords`` no matter where the model came
from: CivitAI/CivArchive sync, a HuggingFace/ModelScope model card, the AI
metadata-enrichment skill, or a manual edit in the model modal. Readers must
accept the loose shapes older sidecars can contain (``None``, a bare string, a
list holding non-string entries) instead of assuming a clean list, which is
also why this lives here rather than repeated at every call site.
"""
from __future__ import annotations
from typing import Any, Iterator, Mapping
def iter_trigger_words(civitai: Any) -> Iterator[str]:
"""Yield the non-empty trigger words declared in a ``civitai`` block.
Internal whitespace is collapsed so a sidecar holding ``"anime\nstyle"``
still matches an ``"anime style"`` query. Declaration order and duplicates
are preserved; callers that need a set can build one themselves.
"""
if not isinstance(civitai, Mapping):
return
words = civitai.get("trainedWords")
if isinstance(words, str):
words = [words]
if not isinstance(words, (list, tuple)):
return
for word in words:
if not isinstance(word, str):
continue
normalized = " ".join(word.split())
if normalized:
yield normalized
__all__ = ["iter_trigger_words"]