QuadEmbed
Multimodal embeddings for text, image, audio, and video in one shared 768-dimensional vector space. Compare any modality against any other with a dot product.
Trained end-to-end on a single RTX 4060 laptop GPU (8GB VRAM) by freezing three pretrained encoders and training only two small projection heads, reproducing the GELATO architecture behind Jina AI's jina-embeddings-v5-omni.
⚠️ Non-commercial license. QuadEmbed builds on
jina-embeddings-v5-text-nano, which is CC-BY-NC-4.0. That restriction carries through to this package and its weights. Research and educational use is fine; commercial use is not, without a separate license from Jina AI.
Install
pip install quadembed # text + image + audio
pip install quadembed[video] # adds video support (OpenCV)
pip install quadembed[all] # everything, plus audio file loading helpers
Quickstart
from quadembed import QuadEmbed
from PIL import Image
model = QuadEmbed.from_pretrained() # downloads weights from the Hub on first run
texts = ["a dog running on the beach", "a bowl of ramen noodles"]
text_embeds = model.embed_text(texts)
image_embeds = model.embed_image([Image.open("photo.jpg").convert("RGB")])
# embeddings are L2-normalized, so this is cosine similarity
similarity = text_embeds @ image_embeds.T
for text, score in zip(texts, similarity[:, 0].tolist()):
print(f"{score:.3f} {text}")
Audio
Pass mono float32 arrays at 16 kHz (what the frozen Whisper encoder expects):
import soundfile as sf
audio, sr = sf.read("clip.wav")
audio_embeds = model.embed_audio([audio.astype("float32")], sampling_rate=sr)
similarity = model.embed_text(["a dog barking"]) @ audio_embeds.T
Video
model = QuadEmbed.from_pretrained(modalities=("text", "video"))
video_embeds = model.embed_video_file("clip.mp4", num_frames=4)
similarity = model.embed_text(["a person skateboarding"]) @ video_embeds.T
Loading only what you need
Each encoder costs memory and download time. Load a subset:
model = QuadEmbed.from_pretrained(modalities=("text", "vision")) # skip audio
model = QuadEmbed.from_pretrained(device="cpu") # force CPU
How it works
Three frozen encoders, two trained projectors:
| Role | Model (frozen) | Trainable on top |
|---|---|---|
| Text (anchor) | jinaai/jina-embeddings-v5-text-nano (239M) |
nothing, it defines the space |
| Vision | google/siglip2-base-patch16-naflex |
vision projector (2.36M params) |
| Audio | openai/whisper-large-v3 encoder |
audio projector (0.98M params) |
Only ~3.3M parameters were ever trained, against nearly a billion frozen ones. Video needs no encoder or projector of its own: frames are sampled, run through the vision path, and mean-pooled over time.
Training used bidirectional in-batch InfoNCE (temperature 0.02) plus Matryoshka representation learning over prefix dims {32, 64, 128, 256, 768}, so truncated embeddings remain usable if you need a smaller index.
Measured performance
Cross-modal retrieval recall@k on held-out splits, text-query direction:
| Modality | R@1 | R@5 | R@10 | n |
|---|---|---|---|---|
| Image | 13.7% | 68.6% | 81.1% | 1024 |
| Audio | 67% | 97% | 100% | 33 |
| Video | 40% | 86% | 94% | 50 |
Random-chance R@1 on the 1024-candidate image eval is ~0.1%.
These are honest small-scale numbers. Vision was trained on ~172k image-caption pairs, orders of magnitude less than production embedding models, and image R@1 is the weakest metric as a result. R@5 and R@10 are considerably stronger, so this is more useful for candidate retrieval and reranking than for exact top-1 matching.
Links
- Source and training code: github.com/mithilai/QuadEmbed
- Model weights: huggingface.co/Mithil-AI/quadembed-nano
- Full write-up: I Built a Multimodal Embedding Model From Scratch on an RTX 4060
Credit
QuadEmbed reproduces the architecture described in Jina AI's GELATO paper (arXiv:2605.08384). All credit for the original architecture and training recipe belongs there. This is an independent reproduction, not affiliated with or endorsed by Jina AI.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quadembed-0.1.2.tar.gz.
File metadata
- Download URL: quadembed-0.1.2.tar.gz
- Upload date:
- Size: 10.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59fa2e1f206533c5dc42820709bcb5aa524b593b65cbda3faff577ba373ffb8e
|
|
| MD5 |
ac749fa7a4619dfd5a3086d1afdac1c6
|
|
| BLAKE2b-256 |
229869af35a6395104c593ea1b34360e45b92e71dd1d86dd3c5843197f40b729
|
File details
Details for the file quadembed-0.1.2-py3-none-any.whl.
File metadata
- Download URL: quadembed-0.1.2-py3-none-any.whl
- Upload date:
- Size: 12.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab2b0614f99550c69bdfff557e74fe5cac56e8f8e7ff237993a4e3a73163b641
|
|
| MD5 |
6ab3ff57502cd3447c757018864ddd6e
|
|
| BLAKE2b-256 |
f5fc17dc5f384adf4db3a159583b62bece50f80cd24c23d10ed59d09a9d0c591
|