python-lucide 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_lucide-0.3.0 → python_lucide-0.4.0}/PKG-INFO +1 -1
- {python_lucide-0.3.0 → python_lucide-0.4.0}/pyproject.toml +2 -1
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/build_clusters.py +88 -51
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/build_search.py +51 -28
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/cli.py +12 -2
- python_lucide-0.4.0/src/lucide/config.py +101 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/data/lucide-icon-clusters.json +2 -2
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/search.py +92 -29
- python_lucide-0.4.0/tests/build_clusters_test.py +107 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/tests/build_search_test.py +48 -10
- {python_lucide-0.3.0 → python_lucide-0.4.0}/tests/cli_test.py +27 -15
- python_lucide-0.4.0/tests/conftest.py +24 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/tests/search_test.py +93 -14
- python_lucide-0.3.0/src/lucide/config.py +0 -30
- {python_lucide-0.3.0 → python_lucide-0.4.0}/README.md +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/__init__.py +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/core.py +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/data/__init__.py +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/data/gemini-icon-descriptions.jsonl +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/data/lucide-icons.db +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/db.py +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/src/lucide/dev_utils.py +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/tests/__init__.py +0 -0
- {python_lucide-0.3.0 → python_lucide-0.4.0}/tests/core_test.py +0 -0
|
@@ -11,6 +11,7 @@ dev = [
|
|
|
11
11
|
"mypy>=1.0.0",
|
|
12
12
|
"plotly>=5.0.0",
|
|
13
13
|
"pre-commit>=3.0.0",
|
|
14
|
+
"pydantic-ai-slim[google]>=1.0.0",
|
|
14
15
|
"pytest-cov>=4.0.0",
|
|
15
16
|
"pytest>=7.0.0",
|
|
16
17
|
"ruff>=0.1.0",
|
|
@@ -40,7 +41,7 @@ license = {text = "MIT"}
|
|
|
40
41
|
name = "python-lucide"
|
|
41
42
|
readme = "README.md"
|
|
42
43
|
requires-python = ">=3.10"
|
|
43
|
-
version = "0.
|
|
44
|
+
version = "0.4.0"
|
|
44
45
|
|
|
45
46
|
[project.optional-dependencies]
|
|
46
47
|
search = ["fastembed>=0.4.0"]
|
|
@@ -13,51 +13,68 @@ import logging
|
|
|
13
13
|
import os
|
|
14
14
|
import pathlib
|
|
15
15
|
import sqlite3
|
|
16
|
-
import urllib.request
|
|
17
16
|
from datetime import datetime, timezone
|
|
18
17
|
|
|
18
|
+
from pydantic import BaseModel, field_validator
|
|
19
|
+
from pydantic_ai import Agent
|
|
20
|
+
from pydantic_ai.models import Model
|
|
21
|
+
|
|
22
|
+
from .config import DEFAULT_SEARCH_MODEL_ID
|
|
23
|
+
|
|
19
24
|
logger = logging.getLogger(__name__)
|
|
20
25
|
|
|
21
26
|
CLUSTER_NAMING_MODEL = "gemini-2.5-flash"
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
NAMING_INSTRUCTIONS = """\
|
|
29
|
+
You name clusters of icons. Given icon names that form a visual/semantic
|
|
30
|
+
cluster, respond with a short, evocative theme name (2-4 words).
|
|
31
|
+
Be specific about what unifies the icons.
|
|
32
|
+
Do not use generic labels like "UI elements" or "miscellaneous"."""
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
# Generous bound for a "2-4 word" name; anything longer means the model
|
|
35
|
+
# ignored the instruction (e.g. leaked its reasoning)
|
|
36
|
+
MAX_THEME_LENGTH = 40
|
|
29
37
|
|
|
30
|
-
Return ONLY the theme name, nothing else."""
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"{model}:generateContent?key={api_key}"
|
|
35
|
-
)
|
|
39
|
+
def _sanitize_theme(raw: str) -> str | None:
|
|
40
|
+
"""Validate a model-proposed theme name.
|
|
36
41
|
|
|
42
|
+
Models occasionally return their full chain-of-thought instead of just
|
|
43
|
+
the name — two shipped clusters once carried ~4k-char reasoning dumps
|
|
44
|
+
as their theme. Reject anything multi-line or implausibly long rather
|
|
45
|
+
than trying to salvage it.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
raw: The raw model response text.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The cleaned theme name, or None if the response is unusable.
|
|
52
|
+
"""
|
|
53
|
+
theme = raw.strip().strip("\"'").strip()
|
|
54
|
+
if not theme or "\n" in theme or len(theme) > MAX_THEME_LENGTH:
|
|
55
|
+
return None
|
|
56
|
+
return theme
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
except Exception:
|
|
59
|
-
logger.warning("Gemini API call failed", exc_info=True)
|
|
60
|
-
return None
|
|
58
|
+
|
|
59
|
+
class ClusterTheme(BaseModel):
|
|
60
|
+
"""Structured output for cluster naming.
|
|
61
|
+
|
|
62
|
+
A failed validation here becomes a retry request to the model, so a
|
|
63
|
+
leaked chain-of-thought gets re-asked instead of stored or discarded.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
theme: str
|
|
67
|
+
|
|
68
|
+
@field_validator("theme")
|
|
69
|
+
@classmethod
|
|
70
|
+
def _must_be_short_single_line(cls, value: str) -> str:
|
|
71
|
+
clean = _sanitize_theme(value)
|
|
72
|
+
if clean is None:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"theme must be a single line of at most {MAX_THEME_LENGTH} "
|
|
75
|
+
"characters — return only the 2-4 word name itself"
|
|
76
|
+
)
|
|
77
|
+
return clean
|
|
61
78
|
|
|
62
79
|
|
|
63
80
|
def discover_clusters(
|
|
@@ -83,7 +100,9 @@ def discover_clusters(
|
|
|
83
100
|
"SELECT e.name, e.embedding, d.description "
|
|
84
101
|
"FROM icon_embeddings e "
|
|
85
102
|
"JOIN icon_descriptions d ON e.name = d.name "
|
|
86
|
-
"
|
|
103
|
+
"WHERE e.model = ? "
|
|
104
|
+
"ORDER BY e.name",
|
|
105
|
+
(DEFAULT_SEARCH_MODEL_ID,),
|
|
87
106
|
).fetchall()
|
|
88
107
|
conn.close()
|
|
89
108
|
|
|
@@ -140,19 +159,40 @@ def name_clusters(
|
|
|
140
159
|
data: dict,
|
|
141
160
|
*,
|
|
142
161
|
api_key: str | None = None,
|
|
162
|
+
model: Model | None = None,
|
|
143
163
|
) -> dict:
|
|
144
|
-
"""Name each cluster using Gemini Flash.
|
|
164
|
+
"""Name each cluster using Gemini Flash via a Pydantic AI agent.
|
|
165
|
+
|
|
166
|
+
Output is validated by ``ClusterTheme``; an invalid response (e.g. a
|
|
167
|
+
leaked chain-of-thought) triggers an automatic retry instead of being
|
|
168
|
+
stored. Only after retries are exhausted does a cluster fall back to a
|
|
169
|
+
``Cluster {id}`` placeholder.
|
|
145
170
|
|
|
146
171
|
Args:
|
|
147
172
|
data: Output from ``discover_clusters()``.
|
|
148
173
|
api_key: Gemini API key. Falls back to ``GEMINI_API_KEY`` env var.
|
|
174
|
+
model: Model override, used by tests to avoid real API calls.
|
|
149
175
|
|
|
150
176
|
Returns:
|
|
151
177
|
The same data dict with ``theme`` populated for each cluster.
|
|
152
178
|
"""
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
if model is None:
|
|
180
|
+
from pydantic_ai.models.google import GoogleModel # noqa: PLC0415
|
|
181
|
+
from pydantic_ai.providers.google import GoogleProvider # noqa: PLC0415
|
|
182
|
+
|
|
183
|
+
api_key = api_key or os.environ.get("GEMINI_API_KEY")
|
|
184
|
+
if not api_key:
|
|
185
|
+
raise ValueError("Gemini API key required. Set GEMINI_API_KEY.")
|
|
186
|
+
model = GoogleModel(
|
|
187
|
+
CLUSTER_NAMING_MODEL, provider=GoogleProvider(api_key=api_key)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
agent = Agent(
|
|
191
|
+
model,
|
|
192
|
+
output_type=ClusterTheme,
|
|
193
|
+
instructions=NAMING_INSTRUCTIONS,
|
|
194
|
+
output_retries=3,
|
|
195
|
+
)
|
|
156
196
|
|
|
157
197
|
clusters = data["clusters"]
|
|
158
198
|
for lid in sorted(clusters, key=lambda k: -len(clusters[k]["icons"])):
|
|
@@ -163,17 +203,15 @@ def name_clusters(
|
|
|
163
203
|
icons = clusters[lid]["icons"]
|
|
164
204
|
# Cap at 40 names to keep prompt short
|
|
165
205
|
icon_names = ", ".join(icons[:40])
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
logger.info("Cluster %s (%d icons): %s", lid, len(icons), theme)
|
|
174
|
-
else:
|
|
206
|
+
try:
|
|
207
|
+
result = agent.run_sync(f"Icon cluster: {icon_names}")
|
|
208
|
+
clusters[lid]["theme"] = result.output.theme
|
|
209
|
+
logger.info(
|
|
210
|
+
"Cluster %s (%d icons): %s", lid, len(icons), result.output.theme
|
|
211
|
+
)
|
|
212
|
+
except Exception:
|
|
175
213
|
clusters[lid]["theme"] = f"Cluster {lid}"
|
|
176
|
-
logger.warning("Failed to name cluster %s", lid)
|
|
214
|
+
logger.warning("Failed to name cluster %s", lid, exc_info=True)
|
|
177
215
|
|
|
178
216
|
return data
|
|
179
217
|
|
|
@@ -260,8 +298,7 @@ def build_cluster_visualization(
|
|
|
260
298
|
fig.update_layout(
|
|
261
299
|
title={
|
|
262
300
|
"text": (
|
|
263
|
-
"Lucide Icon Embedding Clusters \u2014 "
|
|
264
|
-
"themes discovered via HDBSCAN in 768d space"
|
|
301
|
+
"Lucide Icon Embedding Clusters \u2014 themes discovered via HDBSCAN"
|
|
265
302
|
),
|
|
266
303
|
"font": {"size": 16},
|
|
267
304
|
},
|
|
@@ -30,10 +30,9 @@ from datetime import datetime, timezone
|
|
|
30
30
|
from typing import TypedDict
|
|
31
31
|
|
|
32
32
|
from .config import (
|
|
33
|
-
DEFAULT_EMBEDDING_DIM,
|
|
34
|
-
DEFAULT_EMBEDDING_MODEL,
|
|
35
33
|
DEFAULT_VLM_MODEL,
|
|
36
|
-
|
|
34
|
+
EMBEDDING_MODELS,
|
|
35
|
+
SEARCH_DB_SCHEMA_VERSION,
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
logger = logging.getLogger(__name__)
|
|
@@ -486,9 +485,10 @@ def _ensure_search_tables(conn: sqlite3.Connection) -> None:
|
|
|
486
485
|
)
|
|
487
486
|
conn.execute(
|
|
488
487
|
"CREATE TABLE IF NOT EXISTS icon_embeddings ("
|
|
489
|
-
" name TEXT
|
|
488
|
+
" name TEXT NOT NULL,"
|
|
490
489
|
" embedding BLOB NOT NULL,"
|
|
491
|
-
" model TEXT NOT NULL"
|
|
490
|
+
" model TEXT NOT NULL,"
|
|
491
|
+
" PRIMARY KEY (name, model)"
|
|
492
492
|
")"
|
|
493
493
|
)
|
|
494
494
|
conn.execute(
|
|
@@ -513,23 +513,35 @@ def _write_search_db( # noqa: PLR0913
|
|
|
513
513
|
search_db_path: pathlib.Path,
|
|
514
514
|
ordered_names: list[str],
|
|
515
515
|
records: dict[str, DescriptionRecord],
|
|
516
|
-
embeddings:
|
|
516
|
+
embeddings: dict[str, dict[str, object]],
|
|
517
517
|
clusters_path: pathlib.Path,
|
|
518
518
|
*,
|
|
519
519
|
version: str | None = None,
|
|
520
520
|
verbose: bool = False,
|
|
521
521
|
) -> None:
|
|
522
|
-
"""Write descriptions, embeddings, clusters, and metadata to SQLite.
|
|
522
|
+
"""Write descriptions, embeddings, clusters, and metadata to SQLite.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
search_db_path: Output SQLite path (recreated from scratch).
|
|
526
|
+
ordered_names: Icon names in insertion order.
|
|
527
|
+
records: Description records keyed by icon name.
|
|
528
|
+
embeddings: Embedding vectors as ``{model_id: {icon_name: vector}}``.
|
|
529
|
+
A model may cover only a subset of icons; absent names are
|
|
530
|
+
simply not inserted for that model.
|
|
531
|
+
clusters_path: JSON file with cluster assignments.
|
|
532
|
+
version: Lucide icon-set version for the metadata table.
|
|
533
|
+
verbose: Verbose logging.
|
|
534
|
+
"""
|
|
523
535
|
import numpy as np # noqa: PLC0415
|
|
524
536
|
|
|
525
537
|
search_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
538
|
+
# Recreate rather than DELETE-and-reuse: the schema itself may have
|
|
539
|
+
# changed since the last build (CREATE IF NOT EXISTS won't migrate it)
|
|
540
|
+
search_db_path.unlink(missing_ok=True)
|
|
526
541
|
conn = sqlite3.connect(search_db_path)
|
|
527
542
|
try:
|
|
528
543
|
_ensure_search_tables(conn)
|
|
529
544
|
|
|
530
|
-
conn.execute("DELETE FROM icon_descriptions")
|
|
531
|
-
conn.execute("DELETE FROM icon_embeddings")
|
|
532
|
-
|
|
533
545
|
for name in ordered_names:
|
|
534
546
|
rec = records[name]
|
|
535
547
|
conn.execute(
|
|
@@ -538,15 +550,18 @@ def _write_search_db( # noqa: PLR0913
|
|
|
538
550
|
(name, rec["description"], rec["model"]),
|
|
539
551
|
)
|
|
540
552
|
|
|
541
|
-
for
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
(name,
|
|
546
|
-
|
|
553
|
+
for model_id, vectors_by_name in embeddings.items():
|
|
554
|
+
for name in ordered_names:
|
|
555
|
+
if name not in vectors_by_name:
|
|
556
|
+
continue
|
|
557
|
+
blob = np.array(vectors_by_name[name], dtype=np.float32).tobytes()
|
|
558
|
+
conn.execute(
|
|
559
|
+
"INSERT INTO icon_embeddings (name, embedding, model)"
|
|
560
|
+
" VALUES (?, ?, ?)",
|
|
561
|
+
(name, blob, model_id),
|
|
562
|
+
)
|
|
547
563
|
|
|
548
564
|
# Load clusters
|
|
549
|
-
conn.execute("DELETE FROM icon_clusters")
|
|
550
565
|
cluster_data = json.loads(clusters_path.read_text())
|
|
551
566
|
for cid, cluster in cluster_data["clusters"].items():
|
|
552
567
|
theme = cluster.get("theme", f"Cluster {cid}")
|
|
@@ -584,10 +599,11 @@ def _write_search_db( # noqa: PLR0913
|
|
|
584
599
|
first = records[ordered_names[0]]
|
|
585
600
|
resolved_version = version or first.get("lucide_version", "unknown")
|
|
586
601
|
now = datetime.now(tz=timezone.utc).isoformat()
|
|
602
|
+
model_dims = json.dumps({mid: EMBEDDING_MODELS[mid].dim for mid in embeddings})
|
|
587
603
|
for key, value in [
|
|
588
604
|
("version", resolved_version),
|
|
589
|
-
("
|
|
590
|
-
("
|
|
605
|
+
("schema_version", str(SEARCH_DB_SCHEMA_VERSION)),
|
|
606
|
+
("embedding_models", model_dims),
|
|
591
607
|
("description_model", DEFAULT_VLM_MODEL),
|
|
592
608
|
("built_at", now),
|
|
593
609
|
]:
|
|
@@ -631,9 +647,10 @@ def build_search_db(
|
|
|
631
647
|
) -> None:
|
|
632
648
|
"""Build the SQLite search database from descriptions, embeddings, and clusters.
|
|
633
649
|
|
|
634
|
-
Reads descriptions from *jsonl_path*, computes
|
|
635
|
-
|
|
636
|
-
*search_db_path*. The DB
|
|
650
|
+
Reads descriptions from *jsonl_path*, computes one embedding set per
|
|
651
|
+
model in ``EMBEDDING_MODELS`` with fastembed, loads cluster assignments
|
|
652
|
+
from *clusters_path*, and writes everything to *search_db_path*. The DB
|
|
653
|
+
is rebuilt from scratch each time.
|
|
637
654
|
|
|
638
655
|
When *icons_db_path* is provided, only icons present in that database are
|
|
639
656
|
included and the version metadata is read from it.
|
|
@@ -687,12 +704,18 @@ def build_search_db(
|
|
|
687
704
|
if rec["categories"]:
|
|
688
705
|
parts.append(f"Categories: {', '.join(rec['categories'])}")
|
|
689
706
|
parts.append(rec["description"])
|
|
690
|
-
return
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
707
|
+
return ". ".join(parts)
|
|
708
|
+
|
|
709
|
+
base_texts = [_embedding_text(records[n]) for n in ordered_names]
|
|
710
|
+
embeddings: dict[str, dict[str, object]] = {}
|
|
711
|
+
for model_id, model_cfg in EMBEDDING_MODELS.items():
|
|
712
|
+
texts = [f"{model_cfg.document_prefix}{t}" for t in base_texts]
|
|
713
|
+
embedder = TextEmbedding(model_name=model_cfg.fastembed_model)
|
|
714
|
+
vecs = list(embedder.embed(texts))
|
|
715
|
+
embeddings[model_id] = dict(zip(ordered_names, vecs, strict=True))
|
|
716
|
+
logger.info(
|
|
717
|
+
"Computed %d embeddings with %s", len(texts), model_cfg.fastembed_model
|
|
718
|
+
)
|
|
696
719
|
|
|
697
720
|
_write_search_db(
|
|
698
721
|
search_db_path,
|
|
@@ -24,7 +24,11 @@ import sys
|
|
|
24
24
|
import tempfile
|
|
25
25
|
from datetime import datetime
|
|
26
26
|
|
|
27
|
-
from .config import
|
|
27
|
+
from .config import (
|
|
28
|
+
DEFAULT_LUCIDE_TAG,
|
|
29
|
+
DEFAULT_SEARCH_MODEL_ID,
|
|
30
|
+
EMBEDDING_MODELS,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
logger = logging.getLogger(__name__)
|
|
30
34
|
|
|
@@ -546,7 +550,7 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|
|
546
550
|
os.environ["LUCIDE_SEARCH_DB_PATH"] = str(candidate)
|
|
547
551
|
|
|
548
552
|
try:
|
|
549
|
-
results = search_icons(args.query, limit=args.limit)
|
|
553
|
+
results = search_icons(args.query, limit=args.limit, model=args.model)
|
|
550
554
|
except Exception as e:
|
|
551
555
|
print(f"Error: {e}")
|
|
552
556
|
return 1
|
|
@@ -856,6 +860,12 @@ def main() -> int:
|
|
|
856
860
|
help="Path to the search database",
|
|
857
861
|
default=None,
|
|
858
862
|
)
|
|
863
|
+
search_parser.add_argument(
|
|
864
|
+
"--model",
|
|
865
|
+
choices=sorted(EMBEDDING_MODELS),
|
|
866
|
+
default=DEFAULT_SEARCH_MODEL_ID,
|
|
867
|
+
help="Embedding model to search with",
|
|
868
|
+
)
|
|
859
869
|
search_parser.add_argument(
|
|
860
870
|
"-v",
|
|
861
871
|
"--verbose",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Configuration for python-lucide.
|
|
2
|
+
|
|
3
|
+
This module contains default configuration values used throughout the package.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
# Default Lucide tag to use when building the icon database
|
|
9
|
+
DEFAULT_LUCIDE_TAG = "1.17.0"
|
|
10
|
+
|
|
11
|
+
# Default size for the LRU cache used by lucide_icon function
|
|
12
|
+
DEFAULT_ICON_CACHE_SIZE = 128
|
|
13
|
+
|
|
14
|
+
# --- Semantic search ---
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class EmbeddingModelConfig:
|
|
19
|
+
"""One embedding model usable for icon search.
|
|
20
|
+
|
|
21
|
+
Each model must be available both as a fastembed ONNX model (build time
|
|
22
|
+
and Python query time) and as a transformers.js model (browser query
|
|
23
|
+
time), and the two runtimes must produce identical vectors. ``pooling``
|
|
24
|
+
is the transformers.js pooling mode that matches fastembed's output for
|
|
25
|
+
this model — verified empirically (cosine 1.0) before a model is added
|
|
26
|
+
here; mismatched pooling silently degrades search quality instead of
|
|
27
|
+
erroring.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
id: Short stable identifier stored in the search DB and web manifest.
|
|
31
|
+
fastembed_model: fastembed model name.
|
|
32
|
+
web_model: transformers.js (Hugging Face) model id. Empty means the
|
|
33
|
+
model is Python-only and excluded from the web manifest.
|
|
34
|
+
dim: Embedding dimensionality.
|
|
35
|
+
pooling: transformers.js pooling mode ("mean" or "cls").
|
|
36
|
+
web_dtype: transformers.js quantization. Document vectors always
|
|
37
|
+
come from fastembed (fp32), so a quantized browser model only
|
|
38
|
+
perturbs the query vector — q8 stays ~0.994 cosine to fp32,
|
|
39
|
+
which is ranking-equivalent, and roughly quarters the download.
|
|
40
|
+
query_prefix: Prefix prepended to queries (asymmetric retrieval).
|
|
41
|
+
document_prefix: Prefix prepended to documents at build time.
|
|
42
|
+
label: Human-facing label for the web UI model toggle.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
id: str
|
|
46
|
+
dim: int
|
|
47
|
+
fastembed_model: str = ""
|
|
48
|
+
web_model: str = ""
|
|
49
|
+
pooling: str = "mean"
|
|
50
|
+
web_dtype: str = "fp32"
|
|
51
|
+
query_prefix: str = ""
|
|
52
|
+
document_prefix: str = ""
|
|
53
|
+
label: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
EMBEDDING_MODELS: dict[str, EmbeddingModelConfig] = {
|
|
57
|
+
"minilm": EmbeddingModelConfig(
|
|
58
|
+
id="minilm",
|
|
59
|
+
fastembed_model="sentence-transformers/all-MiniLM-L6-v2",
|
|
60
|
+
web_model="Xenova/all-MiniLM-L6-v2",
|
|
61
|
+
dim=384,
|
|
62
|
+
pooling="mean",
|
|
63
|
+
label="Faster",
|
|
64
|
+
),
|
|
65
|
+
"bge-small": EmbeddingModelConfig(
|
|
66
|
+
id="bge-small",
|
|
67
|
+
fastembed_model="BAAI/bge-small-en-v1.5",
|
|
68
|
+
web_model="Xenova/bge-small-en-v1.5",
|
|
69
|
+
dim=384,
|
|
70
|
+
pooling="cls",
|
|
71
|
+
query_prefix="Represent this sentence for searching relevant passages: ",
|
|
72
|
+
label="Better",
|
|
73
|
+
),
|
|
74
|
+
"multilingual": EmbeddingModelConfig(
|
|
75
|
+
id="multilingual",
|
|
76
|
+
fastembed_model="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
|
77
|
+
web_model="Xenova/paraphrase-multilingual-MiniLM-L12-v2",
|
|
78
|
+
dim=384,
|
|
79
|
+
pooling="mean",
|
|
80
|
+
web_dtype="q8",
|
|
81
|
+
label="Multilingual",
|
|
82
|
+
),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
DEFAULT_SEARCH_MODEL_ID = "bge-small"
|
|
86
|
+
|
|
87
|
+
# VLM used to generate icon descriptions at build time
|
|
88
|
+
DEFAULT_VLM_MODEL = "gemini-2.5-flash-lite"
|
|
89
|
+
|
|
90
|
+
# Bumped when the search DB layout changes incompatibly (v2: multi-model
|
|
91
|
+
# embeddings keyed by (name, model)). Recorded in the DB metadata and baked
|
|
92
|
+
# into the local cache filename so a stale cache from an older package
|
|
93
|
+
# version is never misread after an upgrade.
|
|
94
|
+
SEARCH_DB_SCHEMA_VERSION = 2
|
|
95
|
+
|
|
96
|
+
# URL template for downloading pre-built search data.
|
|
97
|
+
# {version} is replaced with the Lucide icon-set version (e.g. "0.577.0").
|
|
98
|
+
SEARCH_DB_URL_TEMPLATE = (
|
|
99
|
+
"https://github.com/mmacpherson/python-lucide/releases/download/"
|
|
100
|
+
"search-v{version}/lucide-search.db"
|
|
101
|
+
)
|
|
@@ -1767,7 +1767,7 @@
|
|
|
1767
1767
|
"dice-6",
|
|
1768
1768
|
"dices"
|
|
1769
1769
|
],
|
|
1770
|
-
"theme": "
|
|
1770
|
+
"theme": "Gaming Dice"
|
|
1771
1771
|
},
|
|
1772
1772
|
"18": {
|
|
1773
1773
|
"icons": [
|
|
@@ -2098,7 +2098,7 @@
|
|
|
2098
2098
|
"phone-missed",
|
|
2099
2099
|
"phone-outgoing"
|
|
2100
2100
|
],
|
|
2101
|
-
"theme": "
|
|
2101
|
+
"theme": "Call Event Indicators"
|
|
2102
2102
|
},
|
|
2103
2103
|
"60": {
|
|
2104
2104
|
"icons": [
|