hyper-models 0.1.0__tar.gz → 0.2.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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyper-models
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A model zoo for non-Euclidean embedding models (hyperbolic, spherical, product manifolds)
5
5
  Project-URL: Homepage, https://github.com/Hyper3Labs/hyper-models
6
6
  Project-URL: Repository, https://github.com/Hyper3Labs/hyper-models
7
7
  Project-URL: Documentation, https://github.com/Hyper3Labs/hyper-models#readme
8
8
  Project-URL: Issues, https://github.com/Hyper3Labs/hyper-models/issues
9
- Author: Hyper3Labs
9
+ Author: hyper3labs
10
10
  License: MIT
11
11
  License-File: LICENSE
12
12
  Keywords: clip,embeddings,hyperbolic,model-zoo,non-euclidean,onnx
@@ -27,6 +27,9 @@ Requires-Dist: pillow>=10.0
27
27
  Provides-Extra: dev
28
28
  Requires-Dist: pytest>=7.0; extra == 'dev'
29
29
  Requires-Dist: ruff>=0.1; extra == 'dev'
30
+ Provides-Extra: ml
31
+ Requires-Dist: timm>=1.0.0; extra == 'ml'
32
+ Requires-Dist: torch>=2.9.1; extra == 'ml'
30
33
  Description-Content-Type: text/markdown
31
34
 
32
35
  # hyper-models
@@ -51,13 +54,22 @@ Description-Content-Type: text/markdown
51
54
  ## Why?
52
55
 
53
56
  - **Standardized access** to non-Euclidean embedding models
54
- - **Torch-free runtime** via ONNX (models published to Hugging Face Hub)
57
+ - **One catalog surface**: model names map to internal loaders such as ONNX or optional torch-backed runtimes
55
58
  - **Simple API** — `load()` and `encode_images()`
56
59
 
57
60
  ## Installation
58
61
 
59
62
  ```bash
60
- pip install hyper-models
63
+ uv pip install hyper-models
64
+ ```
65
+
66
+ This base install is the simple path: it stays **torch-free** and is enough for
67
+ ONNX-backed catalog entries such as HyCoCLIP and MERU.
68
+
69
+ For torch-backed checkpoints (for example UNCHA):
70
+
71
+ ```bash
72
+ uv pip install "hyper-models[ml]"
61
73
  ```
62
74
 
63
75
  ## Usage
@@ -68,7 +80,11 @@ from PIL import Image
68
80
 
69
81
  # List available models
70
82
  hyper_models.list_models()
71
- # ['hycoclip-vit-s', 'hycoclip-vit-b', 'meru-vit-s', 'meru-vit-b']
83
+ # ['hycoclip-vit-s', 'hycoclip-vit-b', 'meru-vit-s', 'meru-vit-b', 'uncha-vit-s', 'uncha-vit-b']
84
+
85
+ # Inspect supported internal loader kinds
86
+ hyper_models.list_loaders()
87
+ # ['onnx', 'uncha-image-torch']
72
88
 
73
89
  # Load model (auto-downloads from Hugging Face Hub)
74
90
  model = hyper_models.load("hycoclip-vit-s")
@@ -82,6 +98,7 @@ embeddings = model.encode_images(images) # (1, 513) ndarray
82
98
  # Get model info
83
99
  info = hyper_models.get_model_info("hycoclip-vit-s")
84
100
  info.hub_id # 'mnm-matin/hyperbolic-clip'
101
+ info.loader # 'onnx'
85
102
  info.license # 'CC-BY-NC'
86
103
 
87
104
  # Low-level: preprocess images yourself
@@ -89,6 +106,51 @@ batch = hyper_models.preprocess_images(images) # (B, 3, 224, 224)
89
106
  embeddings = model.encode(batch)
90
107
  ```
91
108
 
109
+ ### Architecture
110
+
111
+ `hyper-models` is intended to be a timm-like catalog for non-Euclidean models.
112
+
113
+ - The public abstraction is the catalog entry name, for example `hycoclip-vit-s`.
114
+ - Each entry declares metadata such as geometry, dimensionality, artifact path,
115
+ and an internal loader kind.
116
+ - Internal loaders may differ by model family:
117
+ - `onnx` for exported, torch-free runtimes
118
+ - `uncha-image-torch` for raw checkpoints that need a PyTorch image runtime
119
+
120
+ This keeps callers on one stable API:
121
+
122
+ ```python
123
+ model = hyper_models.load("hycoclip-vit-s")
124
+ model = hyper_models.load("uncha-vit-b")
125
+ ```
126
+
127
+ Callers do not need to know which internal loader is used, except for optional
128
+ dependency installation when choosing entries that need `hyper-models[ml]`.
129
+
130
+ ### HyperView integration
131
+
132
+ HyperView auto-detects `hyper-models` names and routes them to the `hyper-models` provider.
133
+
134
+ ```python
135
+ import hyperview as hv
136
+
137
+ dataset = hv.Dataset.from_huggingface(
138
+ name="demo",
139
+ hf_dataset="uoft-cs/cifar10",
140
+ split="train",
141
+ image_key="img",
142
+ )
143
+
144
+ # Uses provider='hyper-models' automatically.
145
+ space_key = dataset.compute_embeddings(model="uncha-vit-b")
146
+ layout_key = dataset.compute_visualization(space_key=space_key, layout="poincare")
147
+ ```
148
+
149
+ HyperView's simple path remains torch-free. If you use the default ONNX-backed
150
+ `hyper-models` entries or the default `embed-anything` provider, HyperView does
151
+ not need PyTorch. PyTorch is only needed when you explicitly select a
152
+ torch-backed catalog entry such as `uncha-vit-s` or `uncha-vit-b`.
153
+
92
154
  ## Models
93
155
 
94
156
  ### Hyperbolic
@@ -99,16 +161,20 @@ embeddings = model.encode(batch)
99
161
  | `hycoclip-vit-b` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/mnm-matin/hyperbolic-clip/tree/main/hycoclip-vit-b) | [ICLR 2025](https://arxiv.org/abs/2410.06912) | [PalAvik/hycoclip](https://github.com/PalAvik/hycoclip) |
100
162
  | `meru-vit-s` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/mnm-matin/hyperbolic-clip/tree/main/meru-vit-s) | [ICML 2023](https://arxiv.org/abs/2304.09172) | [facebookresearch/meru](https://github.com/facebookresearch/meru) |
101
163
  | `meru-vit-b` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/mnm-matin/hyperbolic-clip/tree/main/meru-vit-b) | [ICML 2023](https://arxiv.org/abs/2304.09172) | [facebookresearch/meru](https://github.com/facebookresearch/meru) |
164
+ | `uncha-vit-s` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/hayeonkim/uncha/blob/main/uncha_vit_s.pth) | [CVPR 2026](https://arxiv.org/abs/2603.22042) | [jeeit17/UNCHA](https://github.com/jeeit17/UNCHA) |
165
+ | `uncha-vit-b` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/hayeonkim/uncha/blob/main/uncha_vit_b.pth) | [CVPR 2026](https://arxiv.org/abs/2603.22042) | [jeeit17/UNCHA](https://github.com/jeeit17/UNCHA) |
102
166
  | `hyp-vit` | — | [CVPR 2022](https://arxiv.org/abs/2203.10833) | [htdt/hyp_metric](https://github.com/htdt/hyp_metric) |
103
167
  | `hie` | — | [CVPR 2020](https://arxiv.org/abs/1904.02239) | [leymir/hyperbolic-image-embeddings](https://github.com/leymir/hyperbolic-image-embeddings) |
104
168
  | `hcnn` | — | [ICLR 2024](https://openreview.net/forum?id=ekz1hN5QNh) | [kschwethelm/HyperbolicCV](https://github.com/kschwethelm/HyperbolicCV) |
105
169
 
106
- ### Spherical
170
+ ### Hyperspherical
171
+
172
+ | Model | Available | Paper | Code |
173
+ |------------------|:---------:|-------|------|
174
+ | `megadescriptor` (via timm) | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/BVRA/MegaDescriptor-L-384) | [WACV 2024](https://openaccess.thecvf.com/content/WACV2024/papers/Cermak_WildlifeDatasets_An_Open-Source_Toolkit_for_Animal_Re-Identification_WACV_2024_paper.pdf) | [WildlifeDatasets/wildlife-datasets](https://github.com/WildlifeDatasets/wildlife-datasets) |
175
+ | `sphereface` | — | [CVPR 2017](https://arxiv.org/abs/1704.08063) | [wy1iu/sphereface](https://github.com/wy1iu/sphereface) |
176
+ | `arcface` | — | [CVPR 2019](https://arxiv.org/abs/1801.07698) | [deepinsight/insightface](https://github.com/deepinsight/insightface) |
107
177
 
108
- | Model | Available | Paper | Code |
109
- |-------|:---------:|-------|------|
110
- | `sphereface` | — | [CVPR 2017](https://arxiv.org/abs/1704.08063) | [wy1iu/sphereface](https://github.com/wy1iu/sphereface) |
111
- | `arcface` | — | [CVPR 2019](https://arxiv.org/abs/1801.07698) | [deepinsight/insightface](https://github.com/deepinsight/insightface) |
112
178
 
113
179
  ### Product Manifolds
114
180
 
@@ -20,13 +20,22 @@
20
20
  ## Why?
21
21
 
22
22
  - **Standardized access** to non-Euclidean embedding models
23
- - **Torch-free runtime** via ONNX (models published to Hugging Face Hub)
23
+ - **One catalog surface**: model names map to internal loaders such as ONNX or optional torch-backed runtimes
24
24
  - **Simple API** — `load()` and `encode_images()`
25
25
 
26
26
  ## Installation
27
27
 
28
28
  ```bash
29
- pip install hyper-models
29
+ uv pip install hyper-models
30
+ ```
31
+
32
+ This base install is the simple path: it stays **torch-free** and is enough for
33
+ ONNX-backed catalog entries such as HyCoCLIP and MERU.
34
+
35
+ For torch-backed checkpoints (for example UNCHA):
36
+
37
+ ```bash
38
+ uv pip install "hyper-models[ml]"
30
39
  ```
31
40
 
32
41
  ## Usage
@@ -37,7 +46,11 @@ from PIL import Image
37
46
 
38
47
  # List available models
39
48
  hyper_models.list_models()
40
- # ['hycoclip-vit-s', 'hycoclip-vit-b', 'meru-vit-s', 'meru-vit-b']
49
+ # ['hycoclip-vit-s', 'hycoclip-vit-b', 'meru-vit-s', 'meru-vit-b', 'uncha-vit-s', 'uncha-vit-b']
50
+
51
+ # Inspect supported internal loader kinds
52
+ hyper_models.list_loaders()
53
+ # ['onnx', 'uncha-image-torch']
41
54
 
42
55
  # Load model (auto-downloads from Hugging Face Hub)
43
56
  model = hyper_models.load("hycoclip-vit-s")
@@ -51,6 +64,7 @@ embeddings = model.encode_images(images) # (1, 513) ndarray
51
64
  # Get model info
52
65
  info = hyper_models.get_model_info("hycoclip-vit-s")
53
66
  info.hub_id # 'mnm-matin/hyperbolic-clip'
67
+ info.loader # 'onnx'
54
68
  info.license # 'CC-BY-NC'
55
69
 
56
70
  # Low-level: preprocess images yourself
@@ -58,6 +72,51 @@ batch = hyper_models.preprocess_images(images) # (B, 3, 224, 224)
58
72
  embeddings = model.encode(batch)
59
73
  ```
60
74
 
75
+ ### Architecture
76
+
77
+ `hyper-models` is intended to be a timm-like catalog for non-Euclidean models.
78
+
79
+ - The public abstraction is the catalog entry name, for example `hycoclip-vit-s`.
80
+ - Each entry declares metadata such as geometry, dimensionality, artifact path,
81
+ and an internal loader kind.
82
+ - Internal loaders may differ by model family:
83
+ - `onnx` for exported, torch-free runtimes
84
+ - `uncha-image-torch` for raw checkpoints that need a PyTorch image runtime
85
+
86
+ This keeps callers on one stable API:
87
+
88
+ ```python
89
+ model = hyper_models.load("hycoclip-vit-s")
90
+ model = hyper_models.load("uncha-vit-b")
91
+ ```
92
+
93
+ Callers do not need to know which internal loader is used, except for optional
94
+ dependency installation when choosing entries that need `hyper-models[ml]`.
95
+
96
+ ### HyperView integration
97
+
98
+ HyperView auto-detects `hyper-models` names and routes them to the `hyper-models` provider.
99
+
100
+ ```python
101
+ import hyperview as hv
102
+
103
+ dataset = hv.Dataset.from_huggingface(
104
+ name="demo",
105
+ hf_dataset="uoft-cs/cifar10",
106
+ split="train",
107
+ image_key="img",
108
+ )
109
+
110
+ # Uses provider='hyper-models' automatically.
111
+ space_key = dataset.compute_embeddings(model="uncha-vit-b")
112
+ layout_key = dataset.compute_visualization(space_key=space_key, layout="poincare")
113
+ ```
114
+
115
+ HyperView's simple path remains torch-free. If you use the default ONNX-backed
116
+ `hyper-models` entries or the default `embed-anything` provider, HyperView does
117
+ not need PyTorch. PyTorch is only needed when you explicitly select a
118
+ torch-backed catalog entry such as `uncha-vit-s` or `uncha-vit-b`.
119
+
61
120
  ## Models
62
121
 
63
122
  ### Hyperbolic
@@ -68,16 +127,20 @@ embeddings = model.encode(batch)
68
127
  | `hycoclip-vit-b` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/mnm-matin/hyperbolic-clip/tree/main/hycoclip-vit-b) | [ICLR 2025](https://arxiv.org/abs/2410.06912) | [PalAvik/hycoclip](https://github.com/PalAvik/hycoclip) |
69
128
  | `meru-vit-s` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/mnm-matin/hyperbolic-clip/tree/main/meru-vit-s) | [ICML 2023](https://arxiv.org/abs/2304.09172) | [facebookresearch/meru](https://github.com/facebookresearch/meru) |
70
129
  | `meru-vit-b` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/mnm-matin/hyperbolic-clip/tree/main/meru-vit-b) | [ICML 2023](https://arxiv.org/abs/2304.09172) | [facebookresearch/meru](https://github.com/facebookresearch/meru) |
130
+ | `uncha-vit-s` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/hayeonkim/uncha/blob/main/uncha_vit_s.pth) | [CVPR 2026](https://arxiv.org/abs/2603.22042) | [jeeit17/UNCHA](https://github.com/jeeit17/UNCHA) |
131
+ | `uncha-vit-b` | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/hayeonkim/uncha/blob/main/uncha_vit_b.pth) | [CVPR 2026](https://arxiv.org/abs/2603.22042) | [jeeit17/UNCHA](https://github.com/jeeit17/UNCHA) |
71
132
  | `hyp-vit` | — | [CVPR 2022](https://arxiv.org/abs/2203.10833) | [htdt/hyp_metric](https://github.com/htdt/hyp_metric) |
72
133
  | `hie` | — | [CVPR 2020](https://arxiv.org/abs/1904.02239) | [leymir/hyperbolic-image-embeddings](https://github.com/leymir/hyperbolic-image-embeddings) |
73
134
  | `hcnn` | — | [ICLR 2024](https://openreview.net/forum?id=ekz1hN5QNh) | [kschwethelm/HyperbolicCV](https://github.com/kschwethelm/HyperbolicCV) |
74
135
 
75
- ### Spherical
136
+ ### Hyperspherical
137
+
138
+ | Model | Available | Paper | Code |
139
+ |------------------|:---------:|-------|------|
140
+ | `megadescriptor` (via timm) | [![HF](https://img.shields.io/badge/🤗-HuggingFace-yellow)](https://huggingface.co/BVRA/MegaDescriptor-L-384) | [WACV 2024](https://openaccess.thecvf.com/content/WACV2024/papers/Cermak_WildlifeDatasets_An_Open-Source_Toolkit_for_Animal_Re-Identification_WACV_2024_paper.pdf) | [WildlifeDatasets/wildlife-datasets](https://github.com/WildlifeDatasets/wildlife-datasets) |
141
+ | `sphereface` | — | [CVPR 2017](https://arxiv.org/abs/1704.08063) | [wy1iu/sphereface](https://github.com/wy1iu/sphereface) |
142
+ | `arcface` | — | [CVPR 2019](https://arxiv.org/abs/1801.07698) | [deepinsight/insightface](https://github.com/deepinsight/insightface) |
76
143
 
77
- | Model | Available | Paper | Code |
78
- |-------|:---------:|-------|------|
79
- | `sphereface` | — | [CVPR 2017](https://arxiv.org/abs/1704.08063) | [wy1iu/sphereface](https://github.com/wy1iu/sphereface) |
80
- | `arcface` | — | [CVPR 2019](https://arxiv.org/abs/1801.07698) | [deepinsight/insightface](https://github.com/deepinsight/insightface) |
81
144
 
82
145
  ### Product Manifolds
83
146
 
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "hyper-models"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "A model zoo for non-Euclidean embedding models (hyperbolic, spherical, product manifolds)"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
7
7
  requires-python = ">=3.10"
8
- authors = [{ name = "Hyper3Labs" }]
8
+ authors = [{ name = "hyper3labs" }]
9
9
  keywords = ["embeddings", "hyperbolic", "onnx", "clip", "model-zoo", "non-euclidean"]
10
10
  classifiers = [
11
11
  "Development Status :: 3 - Alpha",
@@ -30,6 +30,10 @@ dev = [
30
30
  "pytest>=7.0",
31
31
  "ruff>=0.1",
32
32
  ]
33
+ ml = [
34
+ "torch>=2.9.1",
35
+ "timm>=1.0.0",
36
+ ]
33
37
 
34
38
  [project.urls]
35
39
  Homepage = "https://github.com/Hyper3Labs/hyper-models"
@@ -1,6 +1,7 @@
1
1
  """hyper-models: A model zoo for non-Euclidean embedding models.
2
2
 
3
- Hyperbolic, spherical, and product manifold models with torch-free ONNX inference.
3
+ Hyperbolic, spherical, and product manifold models exposed through one catalog
4
+ surface, with internal loaders such as ONNX and optional torch-backed runtimes.
4
5
 
5
6
  Example:
6
7
  >>> import hyper_models
@@ -11,12 +12,14 @@ Example:
11
12
  """
12
13
 
13
14
  from hyper_models.loader import load
15
+ from hyper_models.loaders import list_loaders
14
16
  from hyper_models.registry import ModelInfo, get_model_info, list_models
15
17
  from hyper_models.models import ONNXModel
16
18
  from hyper_models.preprocessing import ImageConfig, preprocess_images
17
19
 
18
20
  __all__ = [
19
21
  "load",
22
+ "list_loaders",
20
23
  "list_models",
21
24
  "get_model_info",
22
25
  "ModelInfo",
@@ -24,5 +27,5 @@ __all__ = [
24
27
  "ImageConfig",
25
28
  "preprocess_images",
26
29
  ]
27
- __version__ = "0.1.0"
30
+ __version__ = "0.2.0"
28
31
 
@@ -1,23 +1,24 @@
1
- """Model loading - download from Hub and instantiate."""
1
+ """Public catalog loading entrypoint for hyper-models."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
+ from typing import Any
6
7
 
7
8
  from huggingface_hub import snapshot_download
8
9
 
9
- from hyper_models.models import ONNXModel
10
+ from hyper_models.loaders import load_model
10
11
  from hyper_models.registry import get_model_info
11
12
 
12
13
  __all__ = ["load"]
13
14
 
14
15
 
15
- def load(name: str, *, local_path: str | Path | None = None) -> ONNXModel:
16
+ def load(name: str, *, local_path: str | Path | None = None) -> Any:
16
17
  """Load a model by name.
17
18
 
18
19
  Args:
19
- name: Model name (e.g., 'hycoclip-vit-s').
20
- local_path: Optional local ONNX path (skips Hub download).
20
+ name: Catalog model name (e.g., 'hycoclip-vit-s').
21
+ local_path: Optional local artifact path (skips Hub download).
21
22
 
22
23
  Returns:
23
24
  Model instance ready for inference.
@@ -28,18 +29,14 @@ def load(name: str, *, local_path: str | Path | None = None) -> ONNXModel:
28
29
  """
29
30
  info = get_model_info(name)
30
31
 
32
+ # Route-only entries (e.g. timm-image) raise immediately without downloading.
33
+ if info.loader == "timm-image":
34
+ return load_model(info, Path())
35
+
31
36
  if local_path is None:
32
- hub_path = f"{info.name}/model.onnx"
33
- local_dir = snapshot_download(info.hub_id, allow_patterns=[f"{hub_path}*"])
34
- local_path = Path(local_dir) / hub_path
37
+ local_dir = snapshot_download(info.hub_id, allow_patterns=[f"{info.hub_path}*"])
38
+ artifact_path = Path(local_dir) / info.hub_path
35
39
  else:
36
- local_path = Path(local_path)
37
-
38
- return ONNXModel(
39
- path=Path(local_path),
40
- geometry=info.geometry,
41
- dim=info.dim,
42
- input_name=info.input_name,
43
- output_name=info.output_name,
44
- image_config=info.image_config,
45
- )
40
+ artifact_path = Path(local_path)
41
+
42
+ return load_model(info, artifact_path)
@@ -0,0 +1,72 @@
1
+ """Internal loader implementations for hyper-models catalog entries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Callable
7
+
8
+ from hyper_models.models import ONNXModel
9
+ from hyper_models.registry import ModelInfo
10
+
11
+ __all__ = ["list_loaders", "load_model"]
12
+
13
+ LoaderFn = Callable[[ModelInfo, Path], Any]
14
+
15
+
16
+ def _load_onnx_model(info: ModelInfo, artifact_path: Path) -> ONNXModel:
17
+ return ONNXModel(
18
+ path=artifact_path,
19
+ geometry=info.geometry,
20
+ dim=info.dim,
21
+ input_name=info.input_name,
22
+ output_name=info.output_name,
23
+ image_config=info.image_config,
24
+ )
25
+
26
+
27
+ def _load_uncha_image_torch_model(info: ModelInfo, artifact_path: Path) -> Any:
28
+ if info.variant is None:
29
+ raise ValueError(f"UNCHA model '{info.name}' is missing a registry variant")
30
+
31
+ from hyper_models.torch_models import UNCHATorchModel
32
+
33
+ return UNCHATorchModel(
34
+ checkpoint_path=artifact_path,
35
+ geometry=info.geometry,
36
+ dim=info.dim,
37
+ variant=info.variant,
38
+ image_config=info.image_config,
39
+ )
40
+
41
+
42
+ def _load_timm_image_route(info: ModelInfo, artifact_path: Path) -> Any:
43
+ raise ValueError(
44
+ f"Model '{info.name}' is a timm model. "
45
+ f"Use HyperView provider='timm-image' with model='hf-hub:{info.hub_id}' instead.\n"
46
+ f" dataset.compute_embeddings(model='hf-hub:{info.hub_id}', provider='timm-image')"
47
+ )
48
+
49
+
50
+ _LOADERS: dict[str, LoaderFn] = {
51
+ "onnx": _load_onnx_model,
52
+ "uncha-image-torch": _load_uncha_image_torch_model,
53
+ "timm-image": _load_timm_image_route,
54
+ }
55
+
56
+
57
+ def list_loaders() -> list[str]:
58
+ """List supported internal loader kinds for catalog entries."""
59
+ return sorted(_LOADERS)
60
+
61
+
62
+ def load_model(info: ModelInfo, artifact_path: Path) -> Any:
63
+ """Instantiate a catalog entry using the loader declared in ``ModelInfo``."""
64
+ try:
65
+ loader = _LOADERS[info.loader]
66
+ except KeyError:
67
+ available = ", ".join(sorted(_LOADERS))
68
+ raise ValueError(
69
+ f"Unsupported loader '{info.loader}' for model '{info.name}'. Available: {available}"
70
+ ) from None
71
+
72
+ return loader(info, artifact_path)
@@ -1,4 +1,4 @@
1
- """Model registry - maps model names to hub locations and metadata."""
1
+ """Catalog registry for hyper-models entries and their loader metadata."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -11,13 +11,22 @@ __all__ = ["ModelInfo", "list_models", "get_model_info"]
11
11
 
12
12
  @dataclass
13
13
  class ModelInfo:
14
- """Metadata for a registered model."""
14
+ """Metadata for a registered catalog model.
15
+
16
+ `loader` describes how the catalog entry is instantiated internally.
17
+ This is intentionally separate from the public catalog surface so callers
18
+ can always use ``hyper_models.load(name)`` regardless of runtime backend.
19
+ """
15
20
 
16
21
  name: str
17
22
  geometry: str # 'hyperboloid', 'poincare', 'sphere', 'euclidean'
18
23
  dim: int
19
24
  hub_id: str
25
+ hub_path: str
20
26
  license: str
27
+ loader: str = "onnx" # e.g. 'onnx', 'uncha-image-torch'
28
+ variant: str | None = None # optional loader hint (e.g., 'vit_s', 'vit_b')
29
+ optional_dependencies: tuple[str, ...] = ()
21
30
  description: str = ""
22
31
  input_name: str = "image"
23
32
  output_name: str | None = None
@@ -30,6 +39,7 @@ _MODELS: dict[str, ModelInfo] = {
30
39
  geometry="hyperboloid",
31
40
  dim=513,
32
41
  hub_id="mnm-matin/hyperbolic-clip",
42
+ hub_path="hycoclip-vit-s/model.onnx",
33
43
  license="CC-BY-NC",
34
44
  description="HyCoCLIP ViT-Small (512D hyperboloid)",
35
45
  output_name="embedding_hyperboloid",
@@ -39,6 +49,7 @@ _MODELS: dict[str, ModelInfo] = {
39
49
  geometry="hyperboloid",
40
50
  dim=513,
41
51
  hub_id="mnm-matin/hyperbolic-clip",
52
+ hub_path="hycoclip-vit-b/model.onnx",
42
53
  license="CC-BY-NC",
43
54
  description="HyCoCLIP ViT-Base (512D hyperboloid)",
44
55
  output_name="embedding_hyperboloid",
@@ -48,6 +59,7 @@ _MODELS: dict[str, ModelInfo] = {
48
59
  geometry="hyperboloid",
49
60
  dim=513,
50
61
  hub_id="mnm-matin/hyperbolic-clip",
62
+ hub_path="meru-vit-s/model.onnx",
51
63
  license="CC-BY-NC",
52
64
  description="MERU ViT-Small (512D hyperboloid)",
53
65
  output_name="embedding_hyperboloid",
@@ -57,10 +69,46 @@ _MODELS: dict[str, ModelInfo] = {
57
69
  geometry="hyperboloid",
58
70
  dim=513,
59
71
  hub_id="mnm-matin/hyperbolic-clip",
72
+ hub_path="meru-vit-b/model.onnx",
60
73
  license="CC-BY-NC",
61
74
  description="MERU ViT-Base (512D hyperboloid)",
62
75
  output_name="embedding_hyperboloid",
63
76
  ),
77
+ "uncha-vit-s": ModelInfo(
78
+ name="uncha-vit-s",
79
+ geometry="hyperboloid",
80
+ dim=513,
81
+ hub_id="hayeonkim/uncha",
82
+ hub_path="uncha_vit_s.pth",
83
+ license="Unknown",
84
+ loader="uncha-image-torch",
85
+ variant="vit_s",
86
+ optional_dependencies=("ml",),
87
+ description="UNCHA ViT-S/16 checkpoint (HF .pth, torch inference)",
88
+ ),
89
+ "uncha-vit-b": ModelInfo(
90
+ name="uncha-vit-b",
91
+ geometry="hyperboloid",
92
+ dim=513,
93
+ hub_id="hayeonkim/uncha",
94
+ hub_path="uncha_vit_b.pth",
95
+ license="Unknown",
96
+ loader="uncha-image-torch",
97
+ variant="vit_b",
98
+ optional_dependencies=("ml",),
99
+ description="UNCHA ViT-B/16 checkpoint (HF .pth, torch inference)",
100
+ ),
101
+ "megadescriptor": ModelInfo(
102
+ name="megadescriptor",
103
+ geometry="sphere",
104
+ dim=1024,
105
+ hub_id="BVRA/MegaDescriptor-L-384",
106
+ hub_path="",
107
+ license="MIT",
108
+ loader="timm-image",
109
+ description="MegaDescriptor-L-384 (via timm, use provider='timm-image')",
110
+ image_config=ImageConfig(size=384),
111
+ ),
64
112
  }
65
113
 
66
114
 
@@ -0,0 +1,230 @@
1
+ """Torch-backed catalog model implementations.
2
+
3
+ These are internal loader targets used by selected hyper-models catalog entries.
4
+ They do not create a separate public provider surface.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+ from PIL import Image
14
+
15
+ from hyper_models.preprocessing import ImageConfig, preprocess_images
16
+
17
+ __all__ = ["UNCHATorchModel"]
18
+
19
+
20
+ class _UNCHAImageEncoder:
21
+ """Minimal UNCHA image encoder graph for inference-only embeddings."""
22
+
23
+ def __init__(self, torch: Any, timm: Any, nn: Any, *, variant: str) -> None:
24
+ self._torch = torch
25
+ self._timm = timm
26
+ self._nn = nn
27
+ self._variant = variant
28
+
29
+ self._register_custom_variants()
30
+ self.visual = self._build_visual()
31
+ self.visual_proj = nn.Linear(self.visual.width, 512, bias=False)
32
+
33
+ self.visual_alpha = nn.Parameter(torch.tensor(512**-0.5).log())
34
+ self.curv = nn.Parameter(torch.tensor(1.0).log())
35
+
36
+ self.pixel_mean = torch.tensor((0.485, 0.456, 0.406)).view(1, 3, 1, 1)
37
+ self.pixel_std = torch.tensor((0.229, 0.224, 0.225)).view(1, 3, 1, 1)
38
+
39
+ def _register_custom_variants(self) -> None:
40
+ if self._timm.models.is_model("vit_small_mocov3_patch16_224"):
41
+ return
42
+
43
+ @self._timm.models.register_model
44
+ def vit_small_mocov3_patch16_224(**kwargs: Any):
45
+ return self._timm.models.vision_transformer._create_vision_transformer(
46
+ "vit_small_patch16_224",
47
+ patch_size=16,
48
+ embed_dim=384,
49
+ depth=12,
50
+ num_heads=12,
51
+ **kwargs,
52
+ )
53
+
54
+ def _build_visual(self) -> Any:
55
+ arch = {
56
+ "vit_s": "vit_small_mocov3_patch16_224",
57
+ "vit_b": "vit_base_patch16_224",
58
+ }[self._variant]
59
+
60
+ visual = self._timm.create_model(
61
+ arch,
62
+ num_classes=0,
63
+ global_pool="token",
64
+ class_token=True,
65
+ norm_layer=self._nn.LayerNorm,
66
+ pretrained=False,
67
+ )
68
+
69
+ # Match upstream UNCHA image encoder API expected by projection layers.
70
+ visual.width = int(getattr(visual, "embed_dim", getattr(visual, "num_features", 0)))
71
+ if visual.width <= 0:
72
+ raise RuntimeError("Unable to infer visual width for UNCHA image encoder")
73
+ return visual
74
+
75
+ def _exp_map0(self, tangent: Any, curvature: Any) -> Any:
76
+ torch = self._torch
77
+ sqrt_curvature = torch.sqrt(curvature)
78
+ norm = torch.linalg.norm(tangent, dim=-1, keepdim=True).clamp_min(1e-12)
79
+ scale = torch.sinh(sqrt_curvature * norm) / (sqrt_curvature * norm)
80
+ return tangent * scale
81
+
82
+ def _lift_hyperboloid(self, space: Any, curvature: Any) -> Any:
83
+ torch = self._torch
84
+ time = torch.sqrt((1.0 / curvature) + torch.sum(space * space, dim=-1, keepdim=True))
85
+ return torch.cat([time, space], dim=-1)
86
+
87
+ def encode(self, images_bchw: Any) -> Any:
88
+ torch = self._torch
89
+
90
+ images = (images_bchw - self.pixel_mean) / self.pixel_std
91
+ image_feats = self.visual(images)
92
+ image_feats = self.visual_proj(image_feats)
93
+
94
+ visual_scale = torch.exp(torch.clamp(self.visual_alpha, max=0.0))
95
+ tangent = image_feats * visual_scale
96
+
97
+ curvature = torch.exp(self.curv).clamp(min=1e-8)
98
+ space = self._exp_map0(tangent, curvature)
99
+ return self._lift_hyperboloid(space, curvature)
100
+
101
+
102
+ class UNCHATorchModel:
103
+ """UNCHA catalog entry runtime using torch+timm for image inference."""
104
+
105
+ def __init__(
106
+ self,
107
+ checkpoint_path: Path,
108
+ *,
109
+ geometry: str,
110
+ dim: int,
111
+ variant: str,
112
+ image_config: ImageConfig | None = None,
113
+ device: str | None = None,
114
+ ) -> None:
115
+ self._checkpoint_path = checkpoint_path
116
+ self.geometry = geometry
117
+ self.dim = dim
118
+ self._variant = variant
119
+ self._image_config = image_config or ImageConfig()
120
+ self._device_override = device
121
+
122
+ self._torch = None
123
+ self._device = None
124
+ self._encoder = None
125
+
126
+ def _import_ml_stack(self) -> tuple[Any, Any, Any]:
127
+ try:
128
+ import timm
129
+ import torch
130
+ from torch import nn
131
+ except ImportError as e:
132
+ raise ImportError(
133
+ "This catalog entry requires the optional 'ml' dependencies. "
134
+ "Install with: uv sync --extra ml or uv pip install 'hyper-models[ml]'"
135
+ ) from e
136
+
137
+ return torch, timm, nn
138
+
139
+ def _resolve_device(self, torch: Any) -> Any:
140
+ if self._device_override:
141
+ return torch.device(self._device_override)
142
+ if torch.cuda.is_available():
143
+ return torch.device("cuda")
144
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
145
+ return torch.device("mps")
146
+ return torch.device("cpu")
147
+
148
+ def _load_state_dict(self) -> dict[str, Any]:
149
+ assert self._torch is not None
150
+
151
+ checkpoint_obj = self._torch.load(
152
+ self._checkpoint_path,
153
+ map_location="cpu",
154
+ weights_only=False,
155
+ )
156
+
157
+ if isinstance(checkpoint_obj, dict) and "model" in checkpoint_obj:
158
+ model_state = checkpoint_obj["model"]
159
+ if isinstance(model_state, dict):
160
+ return model_state
161
+
162
+ if isinstance(checkpoint_obj, dict):
163
+ return checkpoint_obj
164
+
165
+ raise TypeError("Unexpected UNCHA checkpoint format; expected dict or {'model': state_dict}")
166
+
167
+ def _ensure_encoder(self) -> None:
168
+ if self._encoder is not None:
169
+ return
170
+
171
+ torch, timm, nn = self._import_ml_stack()
172
+ self._torch = torch
173
+ self._device = self._resolve_device(torch)
174
+
175
+ encoder = _UNCHAImageEncoder(torch, timm, nn, variant=self._variant)
176
+ state_dict = self._load_state_dict()
177
+
178
+ own_state: dict[str, Any] = {}
179
+ for key, value in state_dict.items():
180
+ if key.startswith("module."):
181
+ key = key[len("module.") :]
182
+ if key.startswith("model."):
183
+ key = key[len("model.") :]
184
+
185
+ if key.startswith("visual.") or key.startswith("visual_proj."):
186
+ own_state[key] = value
187
+ elif key in {"visual_alpha", "curv"}:
188
+ own_state[key] = value
189
+
190
+ module = torch.nn.Module()
191
+ module.visual = encoder.visual
192
+ module.visual_proj = encoder.visual_proj
193
+ module.visual_alpha = encoder.visual_alpha
194
+ module.curv = encoder.curv
195
+
196
+ load_result = module.load_state_dict(own_state, strict=False)
197
+ if len(own_state) == 0:
198
+ raise RuntimeError("UNCHA checkpoint did not contain visual encoder weights")
199
+ if "visual_proj.weight" in load_result.missing_keys:
200
+ raise RuntimeError("UNCHA checkpoint missing required key: visual_proj.weight")
201
+
202
+ encoder.visual = module.visual.to(self._device)
203
+ encoder.visual_proj = module.visual_proj.to(self._device)
204
+ encoder.visual_alpha = torch.nn.Parameter(module.visual_alpha.to(self._device))
205
+ encoder.curv = torch.nn.Parameter(module.curv.to(self._device))
206
+ encoder.pixel_mean = encoder.pixel_mean.to(self._device)
207
+ encoder.pixel_std = encoder.pixel_std.to(self._device)
208
+
209
+ encoder.visual.eval()
210
+ encoder.visual_proj.eval()
211
+
212
+ self._encoder = encoder
213
+
214
+ def encode(self, inputs: np.ndarray) -> np.ndarray:
215
+ """Encode preprocessed inputs (B, C, H, W) to embeddings (B, D)."""
216
+ self._ensure_encoder()
217
+
218
+ assert self._encoder is not None
219
+ assert self._torch is not None
220
+ assert self._device is not None
221
+
222
+ images = self._torch.from_numpy(inputs).to(device=self._device, dtype=self._torch.float32)
223
+ with self._torch.inference_mode():
224
+ emb = self._encoder.encode(images)
225
+
226
+ return np.asarray(emb.detach().cpu().numpy(), dtype=np.float32)
227
+
228
+ def encode_images(self, images: list[Image.Image]) -> np.ndarray:
229
+ """Encode PIL images to embeddings (B, D)."""
230
+ return self.encode(preprocess_images(images, self._image_config))
File without changes
File without changes