moondream 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.
- moondream/__init__.py +1 -0
- moondream/preprocess.py +62 -0
- moondream/vl.py +271 -0
- moondream-0.0.1.dist-info/METADATA +82 -0
- moondream-0.0.1.dist-info/RECORD +6 -0
- moondream-0.0.1.dist-info/WHEEL +4 -0
moondream/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .vl import VL
|
moondream/preprocess.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Tuple, Union
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def im_resize(
|
|
7
|
+
image: Image.Image,
|
|
8
|
+
size: Tuple[int, int],
|
|
9
|
+
resample: int = Image.Resampling.BICUBIC,
|
|
10
|
+
) -> Image.Image:
|
|
11
|
+
return image.resize(size, resample=resample)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize(
|
|
15
|
+
image: np.ndarray,
|
|
16
|
+
mean: List[float] = [0.5, 0.5, 0.5],
|
|
17
|
+
std: List[float] = [0.5, 0.5, 0.5],
|
|
18
|
+
) -> np.ndarray:
|
|
19
|
+
"""
|
|
20
|
+
Normalize an image array.
|
|
21
|
+
"""
|
|
22
|
+
return (image - np.array(mean)) / np.array(std)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_patches(image: Image.Image, image_patch_size=378) -> np.ndarray:
|
|
26
|
+
"""
|
|
27
|
+
Split the given image into a variable number of patches depending upon its
|
|
28
|
+
resolution.
|
|
29
|
+
"""
|
|
30
|
+
# Start off with the global patch.
|
|
31
|
+
patches = [im_resize(image, (image_patch_size, image_patch_size))]
|
|
32
|
+
|
|
33
|
+
# Find the closest resolution template.
|
|
34
|
+
res_templates = [(1, 2), (2, 1), (2, 2)]
|
|
35
|
+
im_width, im_height = image.size
|
|
36
|
+
max_dim = max(im_width, im_height)
|
|
37
|
+
if max_dim < image_patch_size * 1.4:
|
|
38
|
+
# If the image is already small, we just do a single patch that is a
|
|
39
|
+
# duplicate of the global patch. This creates a small amount of
|
|
40
|
+
# redundant computation now, but it is simpler and future-proofs us
|
|
41
|
+
# if/when we condition the vision encoder on the patch type.
|
|
42
|
+
patches.append(patches[0])
|
|
43
|
+
else:
|
|
44
|
+
aspect_ratio = im_width / im_height
|
|
45
|
+
res_template = min(
|
|
46
|
+
res_templates, key=lambda size: abs((size[1] / size[0]) - aspect_ratio)
|
|
47
|
+
)
|
|
48
|
+
# TODO: Actually implement patching... just going to put in the global
|
|
49
|
+
# patch for now to make progress on other aspects.
|
|
50
|
+
patches.append(patches[0])
|
|
51
|
+
|
|
52
|
+
return np.stack(
|
|
53
|
+
[
|
|
54
|
+
normalize(
|
|
55
|
+
(np.array(patch_img) / 255.0),
|
|
56
|
+
mean=[0.5, 0.5, 0.5],
|
|
57
|
+
std=[0.5, 0.5, 0.5],
|
|
58
|
+
).transpose(2, 0, 1)
|
|
59
|
+
for patch_img in patches
|
|
60
|
+
],
|
|
61
|
+
dtype=np.float16,
|
|
62
|
+
)
|
moondream/vl.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import onnx
|
|
2
|
+
import onnxruntime as ort
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
import tarfile
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from typing import Generator, List, Union, Optional, Dict, TypedDict, Any
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from tokenizers import Tokenizer
|
|
13
|
+
|
|
14
|
+
from .preprocess import create_patches
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class EncodedImage:
|
|
19
|
+
kv_caches: List[np.ndarray]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SamplingSettings = TypedDict(
|
|
23
|
+
"SamplingSettings",
|
|
24
|
+
{"max_tokens": int},
|
|
25
|
+
total=False,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
CaptionOutput = TypedDict(
|
|
29
|
+
"CaptionOutput", {"caption": Union[str, Generator[str, None, None]]}
|
|
30
|
+
)
|
|
31
|
+
QueryOutput = TypedDict(
|
|
32
|
+
"QueryOutput", {"answer": Union[str, Generator[str, None, None]]}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
DEFAULT_MAX_TOKENS = 1024
|
|
36
|
+
LATEST_SUPPORTED_VERSION = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Region:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VL:
|
|
44
|
+
def __init__(self, model_path: Optional[str], ort_settings: Dict[str, Any] = {}):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the Moondream VL (Vision Language) model.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
model_path (str): The path to the model file.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
None
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if model_path is None or not os.path.isfile(model_path):
|
|
56
|
+
raise ValueError("Model path is invalid or file does not exist.")
|
|
57
|
+
|
|
58
|
+
if not tarfile.is_tarfile(model_path):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"Model format not recognized. You may need to upgrade the moondream package."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.text_decoders = []
|
|
64
|
+
|
|
65
|
+
with tarfile.open(model_path, "r:*") as tar:
|
|
66
|
+
for member in tar.getmembers():
|
|
67
|
+
name = member.name.split("/")[-1]
|
|
68
|
+
|
|
69
|
+
f = tar.extractfile(member)
|
|
70
|
+
if f is not None:
|
|
71
|
+
contents = f.read()
|
|
72
|
+
else:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if name == "vision_encoder.onnx":
|
|
76
|
+
self.vision_encoder = ort.InferenceSession(contents, **ort_settings)
|
|
77
|
+
elif name == "vision_projection.onnx":
|
|
78
|
+
self.vision_projection = ort.InferenceSession(
|
|
79
|
+
contents, **ort_settings
|
|
80
|
+
)
|
|
81
|
+
elif name == "text_encoder.onnx":
|
|
82
|
+
self.text_encoder = ort.InferenceSession(contents, **ort_settings)
|
|
83
|
+
elif "text_decoder" in name and name.endswith(".onnx"):
|
|
84
|
+
self.text_decoders.append(
|
|
85
|
+
ort.InferenceSession(contents, **ort_settings)
|
|
86
|
+
)
|
|
87
|
+
elif name == "tokenizer.json":
|
|
88
|
+
self.tokenizer = Tokenizer.from_buffer(contents)
|
|
89
|
+
elif name == "initial_kv_caches.npy":
|
|
90
|
+
self.initial_kv_caches = [x for x in np.load(BytesIO(contents))]
|
|
91
|
+
elif name == "config.json":
|
|
92
|
+
self.config = json.loads(contents)
|
|
93
|
+
|
|
94
|
+
assert self.vision_encoder is not None
|
|
95
|
+
assert self.vision_projection is not None
|
|
96
|
+
assert self.text_encoder is not None
|
|
97
|
+
assert len(self.text_decoders) > 0
|
|
98
|
+
assert self.tokenizer is not None
|
|
99
|
+
assert self.initial_kv_caches is not None
|
|
100
|
+
assert self.config is not None
|
|
101
|
+
|
|
102
|
+
if type(self.config) != dict or "model_version" not in self.config:
|
|
103
|
+
raise ValueError("Model format not recognized.")
|
|
104
|
+
if self.config["model_version"] > LATEST_SUPPORTED_VERSION:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"Model version not supported. You may need to upgrade the moondream package."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.special_tokens = self.config["special_tokens"]
|
|
110
|
+
self.templates = self.config["templates"]
|
|
111
|
+
|
|
112
|
+
def encode_image(self, image: Union[Image.Image, EncodedImage]) -> EncodedImage:
|
|
113
|
+
"""
|
|
114
|
+
Preprocess the image by running it through the model.
|
|
115
|
+
|
|
116
|
+
This method is useful if the user wants to make multiple queries with the same image.
|
|
117
|
+
The output is not guaranteed to be backward-compatible across version updates,
|
|
118
|
+
and should not be persisted out of band.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
image (Image.Image): The input image to be encoded.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The encoded representation of the image.
|
|
125
|
+
"""
|
|
126
|
+
if type(image) == EncodedImage:
|
|
127
|
+
return image
|
|
128
|
+
|
|
129
|
+
image_patches = create_patches(image) # type: ignore
|
|
130
|
+
|
|
131
|
+
patch_emb = self.vision_encoder.run(None, {"input": image_patches})[0]
|
|
132
|
+
patch_emb = np.concatenate([patch_emb[0], patch_emb[1]], axis=-1)
|
|
133
|
+
patch_emb = np.expand_dims(patch_emb, axis=0)
|
|
134
|
+
(inputs_embeds,) = self.vision_projection.run(None, {"input": patch_emb})
|
|
135
|
+
|
|
136
|
+
kv_caches = self.initial_kv_caches
|
|
137
|
+
for i, decoder in enumerate(self.text_decoders):
|
|
138
|
+
inputs_embeds, kv_caches[i] = decoder.run(
|
|
139
|
+
None,
|
|
140
|
+
{
|
|
141
|
+
"inputs_embeds": inputs_embeds,
|
|
142
|
+
"kv_cache": kv_caches[i],
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
return EncodedImage(kv_caches=kv_caches)
|
|
146
|
+
|
|
147
|
+
def _generate(
|
|
148
|
+
self, hidden: np.ndarray, encoded_image: EncodedImage, max_tokens: int
|
|
149
|
+
) -> Generator[str, None, None]:
|
|
150
|
+
kv_caches = {
|
|
151
|
+
i: encoded_image.kv_caches[i] for i in range(len(self.text_decoders))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
generated_tokens = 0
|
|
155
|
+
while generated_tokens < max_tokens:
|
|
156
|
+
for i, decoder in enumerate(self.text_decoders):
|
|
157
|
+
hidden, kv_caches[i] = decoder.run(
|
|
158
|
+
None, {"inputs_embeds": hidden, "kv_cache": kv_caches[i]}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
next_token = np.argmax(hidden, axis=-1)[0]
|
|
162
|
+
if next_token == self.special_tokens["eos"]:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
yield self.tokenizer.decode([next_token])
|
|
166
|
+
generated_tokens += 1
|
|
167
|
+
(hidden,) = self.text_encoder.run(None, {"input_ids": [[next_token]]})
|
|
168
|
+
|
|
169
|
+
def caption(
|
|
170
|
+
self,
|
|
171
|
+
image: Union[Image.Image, EncodedImage],
|
|
172
|
+
length: str = "normal",
|
|
173
|
+
stream: bool = False,
|
|
174
|
+
settings: Optional[SamplingSettings] = None,
|
|
175
|
+
) -> CaptionOutput:
|
|
176
|
+
"""
|
|
177
|
+
Generate a caption for the input image.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
image (Union[Image.Image, EncodedImage]): The input image to be captioned.
|
|
181
|
+
settings (Optional[SamplingSettings]): Optional settings for the caption generation.
|
|
182
|
+
If not provided, default settings will be used.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
str: The caption for the input image.
|
|
186
|
+
"""
|
|
187
|
+
if "caption" not in self.templates:
|
|
188
|
+
raise ValueError("Model does not support captioning.")
|
|
189
|
+
if length not in self.templates["caption"]:
|
|
190
|
+
raise ValueError(f"Model does not support caption length '{length}'.")
|
|
191
|
+
|
|
192
|
+
(input_embeds,) = self.text_encoder.run(
|
|
193
|
+
None, {"input_ids": [self.templates["caption"][length]]}
|
|
194
|
+
)
|
|
195
|
+
if settings is None:
|
|
196
|
+
settings = {}
|
|
197
|
+
max_tokens = settings.get("max_tokens", DEFAULT_MAX_TOKENS)
|
|
198
|
+
|
|
199
|
+
encoded_image = self.encode_image(image)
|
|
200
|
+
|
|
201
|
+
def generator():
|
|
202
|
+
for t in self._generate(input_embeds, encoded_image, max_tokens):
|
|
203
|
+
yield t
|
|
204
|
+
|
|
205
|
+
if stream:
|
|
206
|
+
return {"caption": generator()}
|
|
207
|
+
else:
|
|
208
|
+
out = ""
|
|
209
|
+
for t in generator():
|
|
210
|
+
out += t
|
|
211
|
+
return {"caption": out}
|
|
212
|
+
|
|
213
|
+
def query(
|
|
214
|
+
self,
|
|
215
|
+
image: Union[Image.Image, EncodedImage],
|
|
216
|
+
question: str,
|
|
217
|
+
stream: bool = False,
|
|
218
|
+
settings: Optional[SamplingSettings] = None,
|
|
219
|
+
) -> QueryOutput:
|
|
220
|
+
"""
|
|
221
|
+
Generate an answer to the input question about the input image.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
image (Union[Image.Image, EncodedImage]): The input image to be queried.
|
|
225
|
+
question (str): The question to be answered.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
str: The answer to the input question about the input image.
|
|
229
|
+
"""
|
|
230
|
+
if "query" not in self.templates:
|
|
231
|
+
raise ValueError("Model does not support querying.")
|
|
232
|
+
|
|
233
|
+
question_toks = (
|
|
234
|
+
self.templates["query"]["prefix"]
|
|
235
|
+
+ self.tokenizer.encode(question).ids
|
|
236
|
+
+ self.templates["query"]["suffix"]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
(input_embeds,) = self.text_encoder.run(None, {"input_ids": [question_toks]})
|
|
240
|
+
if settings is None:
|
|
241
|
+
settings = {}
|
|
242
|
+
max_tokens = settings.get("max_tokens", DEFAULT_MAX_TOKENS)
|
|
243
|
+
|
|
244
|
+
encoded_image = self.encode_image(image)
|
|
245
|
+
|
|
246
|
+
def generator():
|
|
247
|
+
for t in self._generate(input_embeds, encoded_image, max_tokens):
|
|
248
|
+
yield t
|
|
249
|
+
|
|
250
|
+
if stream:
|
|
251
|
+
return {"answer": generator()}
|
|
252
|
+
else:
|
|
253
|
+
out = ""
|
|
254
|
+
for t in generator():
|
|
255
|
+
out += t
|
|
256
|
+
return {"answer": out}
|
|
257
|
+
|
|
258
|
+
def detect(
|
|
259
|
+
self, image: Union[Image.Image, EncodedImage], object: str
|
|
260
|
+
) -> List[Region]:
|
|
261
|
+
"""
|
|
262
|
+
Detect and localize the specified object in the input image.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
image (Union[Image.Image, EncodedImage]): The input image to be analyzed.
|
|
266
|
+
object (str): The object to be detected in the image.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List[Region]: A list of Region objects representing the detected instances of the specified object.
|
|
270
|
+
"""
|
|
271
|
+
return []
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: moondream
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python client library for moondream
|
|
5
|
+
Author: vik
|
|
6
|
+
Author-email: vik@moondream.ai
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: numpy (>=2.1.2,<3.0.0)
|
|
14
|
+
Requires-Dist: onnx (>=1.17.0,<2.0.0)
|
|
15
|
+
Requires-Dist: onnxruntime (>=1.19.2,<2.0.0)
|
|
16
|
+
Requires-Dist: pillow (>=10.4.0,<11.0.0)
|
|
17
|
+
Requires-Dist: tokenizers (>=0.20.1,<0.21.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Moondream Python Client Library
|
|
21
|
+
|
|
22
|
+
Python client library for moondream. This library is an alpha preview -- it is
|
|
23
|
+
in an early stage of development, and backward compatibility is not yet
|
|
24
|
+
guaranteed. If you are using this in production, please pin the revision you
|
|
25
|
+
are using.
|
|
26
|
+
|
|
27
|
+
This library currently offers optimized CPU inference, but will be slower than
|
|
28
|
+
the PyTorch implementation for CUDA and MPS backends. If you are running on a
|
|
29
|
+
Mac with M1/M2/M3 etc. chips, or if you have a GPU available, this library is
|
|
30
|
+
not recommended yet.
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
Install the library using pip:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install moondream==0.0.1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then download the model weights:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
# int8 weights (recommended):
|
|
44
|
+
wget "https://huggingface.co/vikhyatk/moondream2/resolve/client/moondream-latest-int8.bin.gz?download=true" -O - | gunzip > moondream-latest-int8.bin
|
|
45
|
+
# ...or, for FP16 weights:
|
|
46
|
+
wget "https://huggingface.co/vikhyatk/moondream2/resolve/client/moondream-latest-f16.bin.gz?download=true" -O - | gunzip > moondream-latest-f16.bin
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import moondream as md
|
|
53
|
+
from PIL import Image
|
|
54
|
+
|
|
55
|
+
model = md.VL("moondream-latest-int8.bin")
|
|
56
|
+
image = Image.open("path/to/image.jpg")
|
|
57
|
+
|
|
58
|
+
# Optional -- encode the image to efficiently run multiple queries on the same
|
|
59
|
+
# image. This is not mandatory, since the model will automatically encode the
|
|
60
|
+
# image if it is not already encoded.
|
|
61
|
+
encoded_image = model.encode_image(image)
|
|
62
|
+
|
|
63
|
+
# Caption the image.
|
|
64
|
+
caption = model.caption(encoded_image)
|
|
65
|
+
|
|
66
|
+
# ...or, if you want to stream the output:
|
|
67
|
+
for t in model.caption(encoded_image, stream=True)["caption"]:
|
|
68
|
+
print(t, end="", flush=True)
|
|
69
|
+
|
|
70
|
+
# Ask a question about the image.
|
|
71
|
+
question = "How many people are in this image?"
|
|
72
|
+
answer = model.answer_question(encoded_image, question)["answer"]
|
|
73
|
+
|
|
74
|
+
# ...or again, if you want to stream the output:
|
|
75
|
+
for t in model.answer_question(encoded_image, question, stream=True)["answer"]:
|
|
76
|
+
print(t, end="", flush=True)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Accelerators
|
|
80
|
+
|
|
81
|
+
(TK -- document how ONNX execution providers work.)
|
|
82
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
moondream/__init__.py,sha256=gXJJSmxgNDWER5FA9OYQa8y87NtPxpJxnzCTXtFsMAE,19
|
|
2
|
+
moondream/preprocess.py,sha256=19hE-Lzf_SbRLDJCS_LkX7Y7qoeHxivk_hF8fKbiufU,1970
|
|
3
|
+
moondream/vl.py,sha256=ns1Rv18yzsOHsIkCvpSHvZv3dNqmKGB58DZHhHMiux0,9162
|
|
4
|
+
moondream-0.0.1.dist-info/METADATA,sha256=8RJG1zGQInR1rd328z_WGBerPe1CEe7-oBDfEPyELR8,2656
|
|
5
|
+
moondream-0.0.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
6
|
+
moondream-0.0.1.dist-info/RECORD,,
|