Views
No views yet


jina-v5-omni-nano and jina-v5-omni-small define the open-weight frontier (Table 1 in the ArXiv report).jinaai/jina-embeddings-v5-omni-nano-retrieval for Apple
Silicon (M1/M2/M3/M4) inference. Accepts text, images, video, and audio
and produces 768-dim embeddings in the same vector space as the torch
reference and as
jinaai/jina-embeddings-v5-text-nano-retrieval
at the same task — index with text and query with any modality, no reindexing. For higher performance at a larger size, see jinaai/jina-embeddings-v5-omni-small-retrieval-mlx.jina-embeddings-v5-omni-nano MLX family.| Feature | Value |
|---|---|
| Parameters | ~0.95B (text + vision + audio towers) |
| Embedding Dimension | 768 |
| Supported Tasks | retrieval |
| Max Sequence Length | 8192 |
| Pooling Strategy | Last-token |
| Supported Inputs | text, image, video, audio |
| Supported File Types | images: .jpg, .jpeg, .png, .gif, .webp, .bmp, .tif, .tiff, .avif, .heic, .svg; video: .mp4, .avi, .mov, .mkv, .webm, .flv, .wmv; audio: .wav, .mp3, .flac, .ogg, .m4a, .opus; documents: .pdf |
| Matryoshka Dimensions | 32, 64, 128, 256, 512, 768 |
| Precision | bf16 (vision + audio), fp32 (language_model) |
1# Retrieve the configuration of the preconfigured omni-nano inference endpoint
2GET /_inference/embedding/.jina-embeddings-v5-omni-nano
3
4# Generate an embedding for a single piece of text using the predefined endpoint
5POST _inference/embedding/.jina-embeddings-v5-omni-nano
6{
7 "input": [
8 "This is a test"
9 ]
10}
11
12# Fuse a text description and an image into a single embedding via a multimodal content block
13POST _inference/embedding/.jina-embeddings-v5-omni-nano
14{
15 "input": [
16 {
17 "content": [
18 { "type": "text", "value": "A small blue square" },
19 { "type": "image", "format": "base64", "value": "<BASE64_IMAGE_DATA>" }
20 ]
21 }
22 ]
23}
24
25# Create a custom endpoint that truncates omni-nano embeddings to 32 dimensions
26PUT _inference/embedding/jina-omni-nano-32d
27{
28 "service": "elastic",
29 "service_settings": {
30 "model_id": "jina-embeddings-v5-omni-nano",
31 "dimensions": 32
32 }
33}pip install mlx tokenizers huggingface_hub transformers pillow requests librosa avmlx >= 0.23 recommended; transformers >= 4.57 for the
processors used in the image / video / audio quickstarts; av for video decoding.1import json
2from pathlib import Path
3
4import mlx.core as mx
5from huggingface_hub import snapshot_download
6from tokenizers import Tokenizer
7
8repo_dir = Path(snapshot_download("jinaai/jina-embeddings-v5-omni-nano-retrieval-mlx"))
9
10import sys
11sys.path.insert(0, str(repo_dir))
12from model import JinaOmniNanoEmbeddingModel, OmniNanoConfig # type: ignore
13
14cfg = OmniNanoConfig.from_dict(json.loads((repo_dir / "config.json").read_text()))
15model = JinaOmniNanoEmbeddingModel(cfg)
16model.load_weights(str(repo_dir / "model.safetensors"))
17mx.eval(model.parameters())
18
19tok = Tokenizer.from_file(str(repo_dir / "tokenizer.json"))
20
21
22def embed_text(text: str):
23 enc = tok.encode(text)
24 input_ids = mx.array([enc.ids])
25 attn = mx.array([enc.attention_mask])
26 return model.encode_text(input_ids, attn)
27
28
29q = embed_text("Query: Which planet is known as the Red Planet?")
30d = embed_text("Document: Mars is often referred to as the Red Planet.")
31cos = float((q[0] * d[0]).sum() / (mx.linalg.norm(q[0]) * mx.linalg.norm(d[0])))
32print(f"cos = {cos:.4f}")Query: to query-side text and Document: to document-side text — these prefixes are required to match the torch reference (they correspond to encode_query() / encode_document() in the HF transformers / sentence-transformers integrations).encode_* methods that internally apply last-token pooling and L2 normalization;
min_pixels / max_pixels / temporal_patch_size come from the bundled
processor and model.safetensors metadata.1from io import BytesIO
2import requests
3from PIL import Image
4from transformers import AutoProcessor
5
6proc = AutoProcessor.from_pretrained(str(repo_dir), trust_remote_code=True)
7
8
9def embed_image(image):
10 inputs = proc(images=[image], text="<image>", return_tensors="pt")
11 pixel_values = mx.array(inputs["pixel_values"].numpy())
12 grid_thw = mx.array(inputs["image_grid_thw"].numpy())
13 input_ids = mx.array(inputs["input_ids"].numpy())
14 attn = mx.array(inputs["attention_mask"].numpy())
15 return model.encode_image(pixel_values, grid_thw, input_ids, attn)
16
17
18url = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/OSIRIS_Mars_true_color.jpg/800px-OSIRIS_Mars_true_color.jpg"
19image = Image.open(BytesIO(requests.get(url).content)).convert("RGB")
20image_emb = embed_image(image)<image>) for video. The processor's video
path returns pixel_values_videos and video_grid_thw; rename them to the
image keys (the canonical custom_st flow) so the same encode_image path
handles temporal pairs (qwen3vl's temporal_patch_size=2 Conv3d):1import av # pip install av
2
3def decode_video(path):
4 container = av.open(path)
5 return [frame.to_image().convert("RGB") for frame in container.decode(video=0)]
6
7frames = decode_video("clip.mp4")
8inputs = proc(text="<image>", videos=frames, return_tensors="pt")
9pixel_values = mx.array(inputs["pixel_values_videos"].numpy())
10grid_thw = mx.array(inputs["video_grid_thw"].numpy()) # [T, H, W] with T = num_frames // 2
11input_ids = mx.array(inputs["input_ids"].numpy())
12attn = mx.array(inputs["attention_mask"].numpy())
13
14video_emb = model.encode_image(pixel_values, grid_thw, input_ids, attn)1import librosa
2from transformers import WhisperFeatureExtractor
3
4audio_path = "speech.wav"
5wav, _ = librosa.load(audio_path, sr=16000, mono=True)
6
7fx = WhisperFeatureExtractor(feature_size=128, sampling_rate=16000)
8feats = fx(wav, sampling_rate=16000, return_tensors="np")
9mel = feats["input_features"][0]
10feat_mask = feats.get("attention_mask")
11feat_len = int(feat_mask[0].sum()) if feat_mask is not None else mel.shape[-1]
12
13input_features = mx.array(mel)
14feature_lens = mx.array([feat_len])
15
16aftercnn, _ = model.audio_tower.feat_extract_output_lengths(feature_lens)
17# avg-pool downsamples 2x; one <|AUDIO|> token per output position
18n_audio_tokens = int(aftercnn.sum().item()) // 2
19AUDIO_TOKEN_ID = 128256
20input_ids = mx.array([[AUDIO_TOKEN_ID] * n_audio_tokens])
21attn = mx.ones_like(input_ids)
22
23audio_emb = model.encode_audio(input_features, feature_lens, input_ids, attn)WhisperFeatureExtractor) — only the audio tower forward is
ported to MLX.encode_text, encode_image,
encode_audio) on the same
model.safetensors. Call only the methods you need — there's no separate
modality= flag because the model is loaded once and the unused encoders simply
aren't invoked. This mirrors the HF modality= argument from a usage standpoint
without requiring a second weights file or a per-modality build.{32, 64, 128, 256, 512, 768}. Matryoshka is a property of the trained
projection head — verified end-to-end through the GGUF F16 path with 0.0000
prefix-vs-full drift on the same 7-input reference set; MLX uses the same
weights at bf16, so the structure is preserved by construction.1import numpy as np
2full = np.array(q[0].astype(mx.float32))
3truncated = full[:256]
4truncated /= np.linalg.norm(truncated)input_ids / attention_mask:1import mlx.core as mx
2
3texts = ["Query: query 1", "Query: query 2"]
4encs = [tok.encode(t) for t in texts]
5max_len = max(len(e.ids) for e in encs)
6pad = tok.token_to_id("<|endoftext|>") or 0
7input_ids = mx.array([e.ids + [pad] * (max_len - len(e.ids)) for e in encs])
8attn = mx.array([e.attention_mask + [0] * (max_len - len(e.attention_mask)) for e in encs])
9embs = model.encode_text(input_ids, attn) # (2, 768)model.embed() at fp32. MLX side: bf16
vision/audio + fp32 language_model (upcast at load time).| Modality | nano-retrieval |
|---|---|
| Text | 7/7 inputs ≥ 0.999 |
| Image (car) | 1.0000 |
| Image (cat) | 0.9999 |
| Audio (JFK 11s) | 1.0000 |
| PDF (2-page fused) | 0.9996 |
| Video (4-frame, 512²) | 0.9999 |
scripts/omni/mlx/test_mlx_full_parity.py --family nano --variant retrieval on Apple Silicon (script + torch reference JSON shipped from the v5-omni training repo).jinaai/jina-embeddings-v5-text-nano-retrieval — text-onlyjinaai/jina-embeddings-v5-omni-nano-retrieval — multimodal (transformers / ST / vLLM)jinaai/jina-embeddings-v5-omni-nano-retrieval-GGUF — multimodal (llama.cpp)WhisperFeatureExtractor) and
is not ported to MLX. Pass the extracted mel features in.language_model weights are upcast to fp32 at load time (the safetensors on disk stay bf16); vision and audio towers stay at bf16. This matches torch torch_dtype=torch.float32 and keeps short-multilingual text above the cos ≥ 0.99 floor (bf16 alone drifts ~0.97 on inputs like "Bonjour, comment ça va?" on some variants due to the 7-bit mantissa). Vision tokens still see kernel-level fp differences accumulating over long sequences (~0.998-0.9999 cos vs torch fp32).