jevimage ships as one package with three runtime extras, plus [dev] and [docs] for working on the package itself. The split exists because the encoder is the expensive part and not everybody runs it. Someone who only calls a server should not download a multi-gigabyte deep-learning stack to send an HTTP request, so the base install is the HTTP client and nothing else. Both paths are first class. jevimage.connect(url) and jevimage.load() answer the same calls and return the same objects. Two methods differ: ask_embedding is local only because its point is skipping the network, and health is remote only because there is no server to report on locally. One argument differs too: template= on train() and compare() has no remote equivalent, because the server picks the template. The full list is the difference table. Which one you install is a deployment decision, not an API decision. The server can be yours: the axis is how many processes need the model, not who owns the machine. One process (a script, a batch job, a single worker): use load(). Several worker processes on one box: run jev serve there and connect() to it from each, because uvicorn --workers N or N app processes each load their own copy of the weights, and you pay N times 1.5 GB and N cold starts for a model that is idle most of the time.

Which one do you want

[serve] and [openclip] both depend on [local], so you never need to list it twice. pip install 'jevimage[serve,openclip]' is valid and gives you all of it. Sizes measured on the machine this page was written on (Linux, CPython 3.12, x86-64; yours will differ, especially for torch): Model weights are a separate download on top of [local], and they are the big number: see Model weights and offline use.

The API-only install

This is the one most people should start with. Asking, embedding, listing heads, deleting heads and training all work through it, because the encoder that fits the head lives on the server:
Training from a machine with no torch on it at all:
That server was running a deterministic stand-in encoder (encoder toy in the output); against a real encoder the accuracies are real accuracies and everything else about the exchange is identical. Two things behave differently without torch, both deliberately:
  • Remote.embed() returns a plain list of lists instead of a (n, dim) tensor. With torch installed it returns the tensor, so the local and remote calls stay interchangeable.
  • jev heads without --url still needs torch. Not because of the files: the local branch imports jevimage.training for its one formatting helper, and that module imports torch at module scope, so the command fails even when the head directory is empty or absent. The CLI’s error says “this command runs the encoder locally”, which overstates it slightly (no encoder is loaded), but the fix it names is the right one. jev rm is the exception: it is an unlink and never takes that branch, so it works on the base install.
jev encoders works in the API-only install — listing which encoders exist should not require the stack that runs them:

What load() does without [local]

It raises ImportError naming both fixes, rather than letting a ModuleNotFoundError from three frames down be your first hint:
The same applies to jevimage.Jev. jevimage.Encoder, register, available, DEFAULT and JevError resolve without torch, so you can inspect the registry and catch errors in an API-only process.

Verifying an install landed

Run the check for the level you installed. Each of these was run as printed. Base. Imports, version, and the encoder registry:
Against a server you can reach, health() is the end-to-end check — it tells you which encoder your answers and heads will come from:
[local]. The real check is loading an encoder, because that is what exercises torch, transformers and the weight cache at once:
That was a warm cache on a 16-thread laptop CPU (12th Gen Core i7-12650H): repeated runs on a quiet box land between 8 and 10 s, and one while the machine was otherwise busy took 32.8 s, because this is dominated by pulling 1.5 GB off disk. The first ever call also downloads that 1.5 GB. [serve]. Start it and ask it how it is:
auth: open means no API key is set and anyone who can reach the port can use it. See Serving before you bind it to anything but localhost. [openclip]. python -c "import open_clip" and then jevimage.load("dfn5b-h-14-384"). Without the extra, the failure is the plain import error from the encoder’s constructor. It is not wrapped, so read the last line:
Those DFN5B weights are Apple’s and carry Apple’s own licence. That is why open_clip is an extra and the weights are never a dependency: whether to accept that licence is your call, not the package’s.

Python versions

requires-python = ">=3.10". The source is written in PEP 604 union syntax (str | None) throughout, and while from __future__ import annotations keeps most of that from being evaluated, pydantic resolves the server’s request-model annotations at runtime, so 3.10 is a real floor. Everything on this page was run on CPython 3.12.3; the declared floor is what pip enforces, and 3.10 and 3.11 are not exercised here. There is no upper bound in the metadata, so on a very new Python the thing likely to stop you is torch, not jevimage.

From source

The package is not on PyPI yet. Until it is, every pip install jevimage... line in these docs means the equivalent against a clone: pip install ., pip install '.[local]', and so on.
The build needs setuptools>=77. That is the release where license = "MIT" became the correct spelling in pyproject.toml, and an older setuptools will reject the metadata. Normally pip fetches it into an isolated build environment for you. On a machine with no index reachable, supply it yourself:
The built wheel is 51 KB. --no-deps there means you still have to have pillow (and, for [local], torch and transformers) present by some other route.

CPU, GPU, and how the device is chosen

jevimage.encoders.resolve_device() decides, in this order:
  1. an explicit device= argument to the encoder constructor,
  2. $JEV_DEVICE,
  3. "cuda" if torch.cuda.is_available(),
  4. "cpu".
The dtype follows from the device and is not separately configurable: bfloat16 on CUDA, float32 on CPU. bf16 is faster on a GPU at no measured accuracy cost, and slower than fp32 on a CPU, so CPU stays fp32. Embeddings are normalised back to fp32 either way, because bf16 has too few mantissa bits for a stable softmax, and a probability that changes with the dtype is not a probability.
$JEV_DEVICE takes any string torch accepts, so cuda:1 and mps work as far as torch does. A value torch does not recognise, or a CUDA ordinal this machine does not have, is refused before any weights load - one line, exit 1, no model download for a typo. Two caveats:
  • Only the built-in HFCLIP and OpenCLIP encoders consult resolve_device. A custom encoder you pass to load() does whatever you wrote.
  • /v1/health reports getattr(jev.encoder, "device", "cpu"). An encoder without a .device attribute is reported as cpu whether or not that is true. The built-ins always set it.
CPU is a supported way to run this. A base-224 encode on CPU is slow by GPU standards and fine for a batch job. If you have no GPU, install torch from the CPU-only index first and then jevimage; that is what avoids the several gigabytes of nvidia-* wheels listed above.

Model weights and offline use

jevimage downloads nothing itself. HFCLIP hands the model id to transformers and OpenCLIP hands it to open_clip; both cache through huggingface_hub, and all the offline knobs are huggingface’s, not jevimage’s. Default cache, as reported by the installed huggingface_hub:
Setting HF_HOME moves everything; HF_HUB_CACHE moves just the model blobs. Point them at a shared volume if several processes or containers should share one copy. The cache is content-addressed and safe to share read-only once it is populated. What the default encoder actually occupies, from the cache on this machine:
1.5 GB for siglip2-base-224. siglip2-so400m-384 came to 4.3 GB in the same cache. Budget accordingly before picking a bigger encoder in a container image. Air-gapped. Pre-download on a connected machine with the same encoder name, copy the cache directory over, and set HF_HUB_OFFLINE=1 on the air-gapped one. Loading then never touches the network:
The pre-download itself is a plain hf download google/siglip2-base-patch16-224. That is huggingface_hub’s own CLI, and on hub 1.x it is the only spelling that works; the older huggingface-cli download now exits with “huggingface-cli is deprecated and no longer works”. Calling jevimage.load("siglip2-base-224") once while the network is up does the same job. The registry name to model id mapping is in jevimage/encoders.py; see Choosing an encoder. A cache miss under HF_HUB_OFFLINE=1 fails from transformers, not from jevimage, and the message is about the hub rather than about the encoder name. Do not read it as “no such encoder”:
A wrong name looks completely different, and lists what it has:
The other fully offline option is to skip the hub entirely: load() accepts any object with .name, .dim, .img() and .txt(), so an encoder you load from a local directory needs no registry entry and no network. That is Choosing an encoder. Nothing about the API-only install touches the hub at all — no weights, no cache, no HF_* variables. That is often the simplest answer to an air-gap: one machine holds the weights, everything else connects to it.

Environment variables

Every variable the package itself reads. These are the complete set; anything else is read by torch, transformers or huggingface_hub, not here.
jev serve --encoder, --heads-dir and --api-key work by setting JEV_ENCODER, JEV_HEADS_DIR and JEV_API_KEYS in the process before the app is built, so the flags and the variables are the same knob, and a unit file or container that sets the variables directly behaves identically. heads_dir= passed to jevimage.load() wins over both directory variables. JEV_URL, JEV_API_KEY and JEV_TIMEOUT are read by the CLI only: from Python you pass the URL, key and timeout= to jevimage.connect() yourself.

Failure modes

Next

Quickstart runs the same walkthrough twice, once against a server and once in-process. Serving covers jev serve for real traffic. Choosing an encoder covers the registry, ensembles and bringing your own model.
← Overview · Quickstart →