jevimage.load(), which runs the encoder in
your process, and once with jevimage.connect(url), which calls a jev serve instance
over HTTP. The two have the same method names and return the same objects, so the
choice is one line in your code.
About the numbers on this page. The outputs below were produced with a deterministic
toy encoder substituted for SigLIP, so that every block is reproducible in a second and
costs no GPU. The shapes, keys, types and error messages are real; the probabilities are
the toy’s, not SigLIP’s, and mean nothing about red squares. Where a line would differ
under a real encoder, it says so.
Install
git clone <repo> && cd jevimage-oss, then pip install ., pip install '.[local]',
pip install '.[serve]'. See Install.
The base install is the whole client. jevimage.connect(url) works fully under it,
including train(), and so does jev ask/train/heads/rm --url. What it cannot do is
run a model: jevimage.load() raises an ImportError that names the fix. See
When it goes wrong.
There is a fourth extra, 'jevimage[openclip]', needed only for the dfn5b-h-14-384
registry entry; those weights are Apple-licensed, which is why they are opt-in.
Ask one image a question
A question is a dict: atype and some criteria. criteria for a choice maps your
label to a caption — dual encoders were trained on captions, not questions, so
“an indoor scene” reads better than “is it indoors?”.
The images below are a 12-square fixture, ds/red/0..5.png and ds/blue/0..5.png,
written by examples/make_fixtures.py - six lines of
Pillow, no download:
Running the encoder yourself
jevimage.load(Toy(), heads_dir="qs_heads") — the toy
encoder standing in for SigLIP, at its default temperature. A real encoder will not
report 1.0 on a question like this; it would be somewhere short of certain.)
ask takes a path, a Path, raw JPEG/PNG/WebP bytes, a PIL.Image, or a
data:image/...;base64,... string. A string that is neither an existing path nor a data
URL raises FileNotFoundError rather than being guessed at as base64.
Against a server
Same script, one line different, no torch on this machine at all:heads counts the heads that server holds, so yours will report its
own; this one was empty.) health() is the one method that exists only on the remote
side: it is how you find out which encoder your answers and heads are coming from, since
there is no encoder object on this end of the wire.
One caveat before you write assert local == remote in a test. The answers here matched
digit for digit, and probabilities are rounded to 6 decimals before you see them, but the
two sides are not promised to be bit-identical: captions are encoded in whatever batch a
request happens to need, float reductions are not associative, and a GPU encoder runs
bf16 while a CPU one runs fp32. Compare with a tolerance rather than ==, and pin
JEV_DEVICE=cpu on both sides if you need the same digits every time.
To start the server yourself: pip install 'jevimage[serve]' then jev serve
(--host, --port, --encoder, --api-key). The same thing from the CLI, against
the same server:
--url is also $JEV_URL, with $JEV_API_KEY for the key. Without either, jev ask
loads a local encoder.
What an answer is
ask returns {question id: Answer}. Answer is a dict subclass with attribute
access, so a.choice and a["choice"] are the same thing and json.dumps(a) works
unchanged. print() gives the short repr, which is not the whole answer:
confidence is not max(probabilities). It is rescaled so that a uniform distribution
is 0 and certainty is 1, because 0.5 over two options is lukewarm and 0.5 over fifty is
emphatic. That rescaling is what lets you use one threshold across differently shaped
questions. Asking an Answer for a key it does not carry raises AttributeError
listing the keys it does have.
Many questions, one encode
The image is encoded once; every question is a matmul against that one vector. So ask them together rather than in a loop. If you will come back to the same image later, keep the embedding and useask_embedding (local only: its entire purpose is skipping
the encode, and over HTTP you would be uploading the embedding anyway).
jev here is the local one from the first example, not the connect() one. The
trained question reads the head trained in the next
section; run this before that one and the whole call
raises ValueError: unknown head 'colours'; trained heads: none trained yet, because
every question is planned before any is answered.
choice (2..255 named options), score (an
ordered 2..10 level rubric, answered as an expected level index), noul (one yes/no
probability), and head (a classifier you trained). A score’s probabilities are
keyed by level number, and legend maps those numbers back to your rubric text.
The noul form above gives criteria a true/false caption pair, which works on
every encoder. The single-caption form (instructions instead of criteria) needs an
encoder that publishes both logit_scale and logit_bias, and raises rather than
inventing a probability. SigLIP publishes both; CLIP publishes neither, and an ensemble
has no bias. Prefer the pair.
Train a head on your own images
train() fits a linear head on the frozen encoder: seconds of work, tens of
kilobytes on disk, one extra matmul at query time. The layout is one sub-folder per
label (ds/red/*.png, ds/blue/*.png), or a {label: [images]} mapping, or a list of
(image, label) pairs.
accuracy is stratified k-fold held-out accuracy, with k = min(5, thinnest class) —
never the accuracy on the images it was fitted to. With 6 examples per class that number
is worth very little, and the toy encoder makes it worth nothing at all; read it next to
n and counts before believing it. A class with fewer than two examples is refused
outright, so k is always at least 2 and a head train() returns always carries a number
here.
comparison is the part to look at: your trained head against plain zero-shot prompts,
on the same images, per class. Training does not always win. That is why this is
computed every time instead of being left to you to discover in production.
Now use it; the question type is head:
trained_on and accuracy, so a downstream consumer can see how
much evidence is behind the number it is reading.
Locally, heads are files under $JEV_HEADS_DIR, else $JEV_HOME/heads, else
~/.jev/heads (one file per head, so a head can be copied or committed on its own);
jevimage.load(..., heads_dir=...) overrides it. jev heads lists them, jev rm NAME
deletes one.
The same training, through a server
Uploads the images, fits on the server’s encoder, leaves the weights there:Head, same numbers, no torch installed on the client. What crosses the wire is
metadata, never the weights: list_heads() gives you name, encoder, classes,
n, counts, cv_accuracy, per_class, folds, created, dim, bytes.
When it goes wrong
These are the four you are most likely to hit first. All four messages below are verbatim.load() on an API-only install. Nothing is broken; you asked to run a model in a
process that has no model runtime:
jev encoders still works there (it lists the registry without loading anything), and
so does every --url command.
Single-caption noul on an encoder with no calibration.
connect() does not connect (it builds a URL), so the first
actual call is where you find out:
JevError subclasses ValueError, deliberately: a local Jev raises ValueError for
the same refusals, so an existing except ValueError keeps working when your load()
line becomes a connect() line. It carries .status (the HTTP code) and .detail
(the server’s own sentence).
Where to go next
- The four question types in full, including the shape of every answer and both
noulforms: Question types. - Choosing an encoder, joining two with
+, or registering your own object with.name/.dim/.img()/.txt(): Choosing an encoder. - Serving:
jev serve, API keys and deployment shapes in Serving; the wire contract route by route in the HTTP reference. - Runnable versions of everything above:
examples/quickstart.pyandexamples/train_your_own.py.
← Install · Docs index · Question types →