siglip2-toolkit 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,71 @@
1
+ """siglip2-toolkit — local-first toolkit around Google's SigLIP 2 models.
2
+
3
+ The package is deliberately lazy: importing :mod:`siglip2_toolkit` does not
4
+ import torch, transformers or FastAPI. Heavy modules are loaded on first
5
+ attribute access, so tooling and metadata inspection stay fast::
6
+
7
+ from siglip2_toolkit import Embedder
8
+
9
+ embedder = Embedder(model="google/siglip2-base-patch16-naflex", device="auto")
10
+ vectors = embedder.embed_texts(["a wedding at sunset"])
11
+ """
12
+
13
+ from importlib import import_module
14
+
15
+ try:
16
+ from importlib.metadata import version as _dist_version
17
+
18
+ __version__ = _dist_version("siglip2-toolkit")
19
+ except Exception: # pragma: no cover - source checkout without installed metadata
20
+ __version__ = "0.0.1"
21
+
22
+ #: Lazy attributes: name -> (relative module, attribute or None for the module itself)
23
+ _LAZY_ATTRS: dict[str, tuple[str, str | None]] = {
24
+ # Public API
25
+ "Embedder": (".embedder", "Embedder"),
26
+ "Exporter": (".exporter", "Exporter"),
27
+ "ExportConfig": (".exporter", "ExportConfig"),
28
+ "ExportResult": (".exporter", "ExportResult"),
29
+ "EmbeddingSpace": (".formats", "EmbeddingSpace"),
30
+ "ImageClassifier": (".classification", "ImageClassifier"),
31
+ "Filter": (".filters", "Filter"),
32
+ "field": (".filters", "field"),
33
+ # Submodules
34
+ "formats": (".formats", None),
35
+ "retrieval": (".retrieval", None),
36
+ "filters": (".filters", None),
37
+ "models": (".models", None),
38
+ "utils": (".utils", None),
39
+ "embedder": (".embedder", None),
40
+ "exporter": (".exporter", None),
41
+ "classification": (".classification", None),
42
+ "cli": (".cli", None),
43
+ "server": (".server", None),
44
+ }
45
+
46
+ __all__ = [
47
+ "Embedder",
48
+ "Exporter",
49
+ "ExportConfig",
50
+ "ExportResult",
51
+ "EmbeddingSpace",
52
+ "ImageClassifier",
53
+ "Filter",
54
+ "field",
55
+ "__version__",
56
+ ]
57
+
58
+
59
+ def __getattr__(name: str):
60
+ try:
61
+ module_name, attr = _LAZY_ATTRS[name]
62
+ except KeyError:
63
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
64
+ module = import_module(module_name, __name__)
65
+ value = module if attr is None else getattr(module, attr)
66
+ globals()[name] = value # cache for subsequent lookups
67
+ return value
68
+
69
+
70
+ def __dir__() -> list[str]:
71
+ return sorted(set(globals()) | set(_LAZY_ATTRS))
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m siglip2_toolkit`` as an alias for the ``siglip2`` CLI."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,161 @@
1
+ """Fine-tuned image classification with SigLIP-family checkpoints.
2
+
3
+ Zero-shot classification (prompt-based) lives on the embedder:
4
+ :meth:`siglip2_toolkit.embedder.Embedder.classify`. This module covers
5
+ checkpoints that ship a trained classification head, consumed through
6
+ ``AutoModelForImageClassification`` (e.g. ``SiglipForImageClassification``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import threading
13
+ from pathlib import Path
14
+ from typing import Any, Optional, Sequence
15
+
16
+ import numpy as np
17
+
18
+ from . import utils
19
+ from .embedder import ImageInput, softmax_rows
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ImageClassifier:
25
+ """Classifies images with a fine-tuned image-classification model.
26
+
27
+ The model and its image processor are loaded lazily on first use. Labels
28
+ come from the checkpoint's ``config.id2label``; ``num_labels`` overrides
29
+ the head size when the checkpoint requires it.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ model: str | Path,
35
+ device: str = "auto",
36
+ dtype: Optional[str] = None,
37
+ num_labels: Optional[int] = None,
38
+ trust_remote_code: bool = False,
39
+ ) -> None:
40
+ self.model_path = str(Path(model).expanduser())
41
+ self.model_name = self.model_path
42
+ self.requested_device = device
43
+ self.requested_dtype = dtype
44
+ self.num_labels = num_labels
45
+ self.trust_remote_code = trust_remote_code
46
+
47
+ self._model: Any = None
48
+ self._processor: Any = None
49
+ self._torch: Any = None
50
+ self._device: Any = None
51
+ self._id2label: dict[int, str] = {}
52
+ self._lock = threading.Lock()
53
+
54
+ # ------------------------------------------------------------------ #
55
+
56
+ @property
57
+ def is_loaded(self) -> bool:
58
+ return self._model is not None
59
+
60
+ @property
61
+ def labels(self) -> dict[int, str]:
62
+ """``id -> label`` mapping (empty until the model is loaded)."""
63
+ return dict(self._id2label)
64
+
65
+ @property
66
+ def device(self) -> Optional[str]:
67
+ return str(self._device) if self._device is not None else None
68
+
69
+ def load(self) -> "ImageClassifier":
70
+ """Load the model and its image processor from a local directory (idempotent)."""
71
+ if self._model is not None:
72
+ return self
73
+
74
+ path = Path(self.model_path)
75
+ if not path.is_dir():
76
+ raise FileNotFoundError(
77
+ f"ImageClassifier expects a local model directory, got: "
78
+ f"'{self.model_path}'. Get one with `siglip2 models download` or "
79
+ "`siglip2 models import`, or pass the path to one."
80
+ )
81
+
82
+ import torch
83
+ from transformers import AutoImageProcessor, AutoModelForImageClassification
84
+
85
+ device, dtype = utils.resolve_torch_device(
86
+ torch, self.requested_device, self.requested_dtype
87
+ )
88
+ logger.info("Loading classifier '%s' on %s (%s)...", self.model_name, device, dtype)
89
+
90
+ extra: dict[str, Any] = {}
91
+ if self.num_labels is not None:
92
+ extra["num_labels"] = self.num_labels
93
+
94
+ try:
95
+ model = AutoModelForImageClassification.from_pretrained(
96
+ self.model_path,
97
+ trust_remote_code=self.trust_remote_code,
98
+ torch_dtype=dtype,
99
+ **extra,
100
+ )
101
+ processor = AutoImageProcessor.from_pretrained(
102
+ self.model_path,
103
+ trust_remote_code=self.trust_remote_code,
104
+ )
105
+ except Exception as exc:
106
+ raise OSError(
107
+ f"Could not load a classifier from '{self.model_path}': {exc}"
108
+ ) from exc
109
+ model = model.to(device).eval()
110
+
111
+ self._torch = torch
112
+ self._model = model
113
+ self._processor = processor
114
+ self._device = device
115
+ self._id2label = {
116
+ int(key): str(value)
117
+ for key, value in (getattr(model.config, "id2label", None) or {}).items()
118
+ }
119
+ logger.info("Classifier ready: %d label(s) ✅", len(self._id2label))
120
+ return self
121
+
122
+ # ------------------------------------------------------------------ #
123
+
124
+ def classify(
125
+ self,
126
+ images: Sequence[ImageInput],
127
+ top_k: Optional[int] = None,
128
+ batch_size: int = 16,
129
+ ) -> list[list[tuple[str, float]]]:
130
+ """Classify images; returns (label, probability) pairs sorted by score."""
131
+ self.load()
132
+ images = list(images)
133
+ results: list[list[tuple[str, float]]] = []
134
+
135
+ for start in range(0, len(images), batch_size):
136
+ batch = [utils.load_image(item) for item in images[start : start + batch_size]]
137
+ if not batch:
138
+ continue
139
+ inputs = self._processor(images=batch, return_tensors="pt").to(self._device)
140
+ with self._lock, self._torch.no_grad():
141
+ logits = self._model(**inputs).logits
142
+ probabilities = softmax_rows(
143
+ logits.detach().cpu().to(self._torch.float32).numpy()
144
+ )
145
+ for row in probabilities:
146
+ order = np.argsort(row)[::-1]
147
+ if top_k is not None:
148
+ order = order[: max(0, int(top_k))]
149
+ results.append(
150
+ [
151
+ (self._id2label.get(int(index), f"LABEL_{int(index)}"), float(row[index]))
152
+ for index in order
153
+ ]
154
+ )
155
+ return results
156
+
157
+ def __repr__(self) -> str:
158
+ return (
159
+ f"ImageClassifier(model={self.model_name!r}, "
160
+ f"device={self.requested_device!r}, loaded={self.is_loaded})"
161
+ )