jevimage, with its exact signature, what it returns, what it
raises, and a one-line example. Nothing here is aspirational: every snippet below was
run, and its output pasted back, shortened only where a long absolute path or a
repeated float would obscure the point, never altered.
For the ideas behind the API (why answers are probabilities, why a head is linear),
start at index.md. For the same surface over HTTP see
reference-http.md, and for the shell see
reference-cli.md.
About the numbers on this page. Real encoder weights are a multi-gigabyte download,
so every example runs against a deterministic 16-dimensional toy encoder
(name = "toy", dim = 16, no logit_scale/logit_bias) and the 12-image red/blue
ds/ dataset that examples/make_fixtures.py writes. The shapes, types, keys and error messages are exactly what you get from
SigLIP2; the probability values are the toy’s and mean nothing about images. Where a
number would be misleading if you read it as a model result, it is called out.
The public surface
load, Jev, Encoder, register, available, DEFAULT and JevError are resolved
lazily by a module __getattr__, so import jevimage never imports torch. Only load
and Jev need it; the rest are lazy purely to keep import time down, and they work on
an API-only install.
The object connect() returns is jevimage.client.Remote. It is deliberately not
exported; you get one from connect(), you do not construct one by name.
On an API-only install
pip install jevimage brings pillow and the standard library, nothing else. Everything
above works except load/Jev, which say so:
Entry points
load
Jev.
Raises
ImportError if torch/transformers are missing, KeyError for an unknown
registry name, and ValueError for a non-positive or non-finite temperature.
Instances are cached per registry name, so calling load("siglip2-base-224") twice
loads the weights once.
KeyError, not ValueError:
jevimage.Jev(...) takes the identical arguments; load() is the name to use.
connect
Remote pointed at a jev serve instance. Downloads nothing,
imports no torch.
Raises
ValueError immediately if url has no scheme. That is the only validation done
at construction time, because nothing else can be checked without a round trip:
<Remote 'http://…' keyed>. The key itself is never printed.
Remote keeps one connection open and reuses it. A TCP handshake costs a full round
trip, which against a server on another continent is around 200ms, an order of magnitude
more than the model spends answering, so paying it once per call would dominate every
measurement. Calls are serialised on a lock, so one instance is safe to share between
threads; for genuine upload concurrency, give each thread its own. A connection the
server has since closed is reopened once, silently, for a GET or a DELETE. A
POST (ask, embed, train) is not replayed - the failure can surface after the
server has already done the work - so it raises JevError and you decide whether
repeating it is safe. Release it with close(), or use
the client as a context manager:
Jev
The local object: a loaded encoder plus its head directory. Thread-safe for reads;
train and delete_head write files.
Attributes: .encoder (the encoder object), .heads (a HeadStore, a real dict of
name → raw record including the weights), .engine (the readout engine).
Methods: ask, ask_embedding, embed,
train, compare, head,
list_heads, delete_head.
Jev.ask
image once, then answer every question against that one embedding.
Returns
{question id: Answer} in the order the questions were given. Raises
ValueError for a malformed question, an unknown head, a bad temperature, duplicate
ids, or a non-finite result; KeyError if a head question names a head
file that is present but will not load; TypeError if questions is neither a dict nor
a list, or if image is a list rather than one image. Jev and Remote refuse it with
the same first sentence; the advice differs, because ask_embedding is local-only:
type
values are choice, score, noul and head; every type also accepts an optional
"prompt" template that must contain {}.
Jev.ask_embedding
Remote is.
Use it to ask more questions later about an image you embedded earlier.
embedding is a (dim,) or (n, dim) fp32 tensor; only the first row is read.
It must be L2-normalised; the engine checks and refuses otherwise.
Jev.embed
(n, dim) fp32 L2-normalised tensor. This is the
whole cost of a request; everything downstream is a matmul, so cache what this returns
if you will ask about the same image again.
(1, dim), not (dim,). There is no cap on the
list length locally.
Jev.train
heads_dir.
Returns a
Head whose .comparison is already filled in. Raises ValueError
for fewer than 2 distinct labels, more than 100 classes, any class with fewer than 2
examples, labels differing only by surrounding whitespace, more than 4000 examples, or
an invalid name.
accuracy is held-out, from stratified k-fold with k = min(5, thinnest class),
never the fit. It is None only when no fold was possible, which means a class with
fewer than 2 examples, something train() refuses outright, so a head trained here
always carries a number. (50.0% here is the toy encoder hashing pixel bytes on twelve
squares; it is not a claim about SigLIP.)
Failure modes worth knowing:
Jev.compare
None for a head saved without its embeddings. Raises KeyError if no such
head, ValueError if the head was fitted under a different encoder.
basis says so in every response. That
asymmetry favours zero-shot, which is the right bias for a “should I have bothered”
question.
template changes the prompts the baseline is built from, which moves the zero-shot
side. On the toy data, "a close-up photo of {}" moves it from 0.75 to 0.5 and
turns the verdict from delta: -0.25 into delta: 0.0. A zero-shot baseline is only
as good as the prompt you compare against.
Jev.head
.comparison is always None here. Computing it means
re-scoring the stored embeddings, so it is done by train() and compare(), not by
this lookup.
list_heads() omits such a file entirely; jev.heads.broken is the {name: why} map
if you need to see them.
Jev.list_heads
created is a Unix timestamp; bytes is the size of the weight matrices in fp32.
Jev.delete_head
True if a file was removed, False if there was nothing to remove. Raises
ValueError for an invalid name. Deleting twice is not an error:
Remote
What connect() returns. Same method names, arguments and return types as
Jev, so moving a script from a local encoder to a server is a one-line change.
It keeps one connection open. Calls are serialised on a lock, so one instance is
safe to share between threads; for genuine upload concurrency, give each thread its
own, and release the connection with close() or the context manager.
Attributes: .url (normalised, no trailing slash), .api_key, .timeout.
Methods: health, ask, embed, train, compare, head,
list_heads, delete_head.
What differs from Jev
Everything else matches, including
Answer objects out of ask, a Head out of
train, None out of compare for a head with no stored embeddings, and False out
of delete_head for a head that was not there.
Remote.health
noul will work; that needs the server’s
logit_scale/logit_bias, which /v1/health does not report. Ask a single-caption
noul and read the error, or use the {"true": …, "false": …} pair form, which works
on every encoder.
The rest, verified against a running server
train() uploads every image once; the head and its weights stay on the server, and
what comes back is metadata. A head name is validated on this side before a URL is
built out of it:
Answer
A dict subclass, so it serialises to JSON unchanged, with attribute access because
a.choice reads better than a["choice"]. repr() is a one-line summary; the full
shape is json.dumps(answer).
AttributeError naming what the answer does have:
Shapes per question type
Probabilities are rounded to 6 decimals,
score to 4. Real examples of each:
basis tells you which noul form ran. The {"true": …, "false": …} pair gives
"contrast pair" and works on every encoder. A single caption gives "sigmoid readout"
and needs an encoder publishing both logit_scale and logit_bias. SigLIP does;
CLIP-family models and every Ensemble do not, and say so rather than returning a
number that looks like a probability and is not one:
Head
The metadata of a trained head. The same object comes back from a local train() and
a remote one, because a head is metadata plus numbers; the weights stay wherever the
encoder is, and never cross the wire.
to_dict() carries four fields with no property of their own: folds, created,
dim, bytes.
cv_accuracy in the dict and .accuracy on the object. to_dict() is
also exactly what list_heads() returns per head and what the HTTP API serves.
A head records its encoder and is checked, not trusted:
compare(). It compares the encoder name, not just the width,
because two encoders of equal dimension would otherwise produce confident nonsense.
Encoders
Encoder
The interface. Subclass it, or duck-type the four required members and pass the object
straight to load(); registration is optional.
The normalisation requirement is enforced, not assumed. A rescaled embedding would
saturate the softmax to exactly 1.0 and 0.0:
jevimage.encoders.unit() does that for you: it pulls the tensor out of whatever a
model returned, mean-pools token-level output, and normalises in fp32.
register
factory is called with no arguments the first time
load(name) asks for it, so the weights load lazily. Re-registering a name replaces
any cached instance.
Raises ValueError if name contains '+' (reserved for ensembles) and TypeError
if factory is not callable.
jev CLI never imports your code, so a
registered encoder is invisible to it; see encoders.md.
available
'a+b' and are not listed.
Needs no torch, so it works on an API-only install.
DEFAULT
load() uses when given neither an argument nor $JEV_ENCODER.
Image and data helpers
All four are pure pillow/stdlib and available on every install.to_image
IMAGE_SUFFIXES governs folder scanning, and
the HTTP server enforces its own format allowlist.
read_folder
IMAGE_SUFFIXES, sorts both labels and
files. Loose files directly in folder are ignored, because they have no label.
as_examples
train() accepts into parallel (images, labels) lists.
Labels are always stringified.
str/Path is passed to read_folder first. max_per_class
applies to all three shapes: for a list of (image, label) pairs it counts per label and
keeps the first N of each. Images are not opened here; that happens at embed time.
confidence
0.0 and certain is 1.0:
(max(p) - 1/k) / (1 - 1/k). p is any sequence of probabilities; k = 1 returns
1.0.
choice, score and head answers carry
it already; this function is for distributions you built yourself.
IMAGE_SUFFIXES
read_folder picks up (sorted here because it is a set and its repr
order changes between runs). Deliberately short: an animated GIF, an SVG or a RAW file is
not a thing to silently take the first frame of. It is a plain set, so you can add to
it, at your own risk, since the encoders and the HTTP server have their own format
expectations.
__version__
Remote.health()["version"], which is how you check that a server and
a client agree.
JevError
Remote method for a request the server refused or could not reach.
It subclasses ValueError on purpose: a local Jev raises ValueError for these same
refusals, so an existing except ValueError keeps working when load() becomes
connect().
str(e) adds the status, method and URL for a log line; .detail is the part a human
needs. Typical statuses:
The
None case is the connection failure, and it names the port and what to run:
JevError too, suggesting timeout=. Two statuses are deliberately
swallowed so Remote matches Jev: a 404 from delete_head() becomes False, and a
409 from compare() becomes None.
Limits and errors
These ceilings are enforced by the engine. They are module constants, not part of the public API, but hitting one produces a message you may need to recognise.
Every message below is verbatim. All of them are
ValueError unless marked otherwise.
Answer attribute misses raise AttributeError; to_image raises FileNotFoundError,
IsADirectoryError, ValueError (a malformed data: URL) or TypeError; read_folder
raises NotADirectoryError.
Temperature
The divisor applied to the logits before the softmax. It affectschoice, score, the
noul pair form and head; it does not affect the noul sigmoid readout, which is
calibrated by the encoder’s own scale and bias.
Resolution differs between the two cases, because they are not the same kind of logit:
head question reacts to the same argument, from a base of 1.0:
0.01,
which is the CLIP-family convention (a raw cosine lives in roughly [0, 0.3] and
softmaxes to nearly uniform without it). A calibrated encoder’s own logit_scale is the
temperature it was trained to be read at, so overriding it costs you the calibration
that makes the number mean something. Change it only if you have measured the result.
← CLI reference · Docs index · HTTP reference →