One pip install. No dependency hell. Runs models, executes workflows, and builds them.
Normally each of these models arrives with its own package, and each package brings its own dependencies — torch, numpy and OpenCV, every one pinned to something slightly different. Put a few in one environment and something breaks. The usual escape is a container per model, and paying for that forever.
Mozo ships none of them. Every model's inference path is vendored into mozo itself and verified bit-identical to the original implementation — so one environment runs all 81, and gives you the original's exact numbers rather than something close.
pip install mozo
mozo startNo Docker, no Kubernetes, no per-model build step, no conversion step, no git clone of
anything.
Serve from one install, in one process, behind one API.
A model on its own is rarely the job. Detect, then blur the faces, then save — that is a workflow, and mozo runs those too: as a graph you draw in the browser, a JSON file you keep in version control, or one call from Python. Same models, same process, no extra install.
A box, a class and a confidence per object.
| Family | Variants | Weights licence | Runtimes |
|---|---|---|---|
rfdetr |
nano small medium large |
Apache-2.0 | torch, onnx, coreml |
yolov8 |
nano small medium large xlarge |
AGPL-3.0 | torch, onnx, coreml |
yolov11 |
nano small medium large xlarge |
AGPL-3.0 | torch, onnx |
yolov12 |
nano small medium large xlarge |
AGPL-3.0 | torch, onnx, coreml |
yolov26 |
nano small medium large xlarge |
AGPL-3.0 | torch, onnx |
A box and a mask per object, from one pass. Same families and same call as detection above — the variant is what decides whether masks come back.
| Family | Variants | Weights licence | Runtimes |
|---|---|---|---|
rfdetr |
seg-nano seg-small seg-medium seg-large |
Apache-2.0 | torch, onnx, coreml |
yolov11 |
seg-nano seg-small seg-medium seg-large seg-xlarge |
AGPL-3.0 | torch, onnx |
yolov26 |
seg-nano seg-small seg-medium seg-large seg-xlarge |
AGPL-3.0 | torch, onnx |
COCO's 17 person joints per detection, as (x, y, confidence) in source-image pixels. RF-DETR
finds people and joints in one pass; ViTPose is top-down and takes detections as an argument —
details.
| Family | Variants | Weights licence | Runtimes |
|---|---|---|---|
rfdetr |
keypoint-preview |
Apache-2.0 | torch |
vitpose |
small base large huge |
Apache-2.0 | torch, onnx (not huge) |
Boxes for anything you can name, from a phrase rather than a fixed class list.
| Family | Variants | Weights licence | Prompt |
|---|---|---|---|
grounding_dino |
tiny base |
Apache-2.0 | descriptions, ≤256 tokens total |
owlv2 |
base base-ensemble large large-ensemble |
Apache-2.0 | phrases, ≤16 tokens |
sam3 |
sam3 |
SAM License | phrases, ≤32 tokens |
Name your classes in words and each is scored against the image — or take the vectors instead, and make a corpus embedded once searchable by words afterwards. Scores are similarities, not probabilities — details.
| Family | Variants | Weights licence | Prompt | Output |
|---|---|---|---|---|
clip |
base base-16 large large-336 |
MIT | phrases, ≤77 tokens each | a score per phrase, or 512/768-d vectors |
siglip2 |
base-224 base-256 so400m-384 so400m16-256 giant-384 |
Apache-2.0 | phrases, ≤64 tokens each | a probability per phrase, or 768/1152/1536-d vectors |
A mask from a click or a box. Returns detections, one row per mask.
| Family | Variants | Weights licence | Prompt |
|---|---|---|---|
sam2 |
tiny small base_plus large |
Apache-2.0 | points, box, or both |
edgetam |
edgetam |
Apache-2.0 | points, box, or both |
Text in the image, one detection per line. A variant is a script, not a language: latin alone
covers 41 languages.
| Family | Variants (scripts) | Weights licence |
|---|---|---|
easyocr |
english latin chinese-simplified japanese korean |
Apache-2.0 |
An HxW float32 map. Six of the nine variants predict metres and three are relative and unitless;
model.unit says which, and is None rather than a guess.
| Family | Variants | Weights licence | Unit |
|---|---|---|---|
depth_anything_v2 |
small |
Apache-2.0 | relative, unitless |
base large |
CC-BY-NC-4.0 | relative, unitless | |
indoor- and outdoor-, three sizes each |
Apache-2.0 | metres |
A per-pixel opacity matte, not a binary mask — which is the difference between keeping a head of hair and cutting it off. Details.
| Family | Variants | Weights licence | Output |
|---|---|---|---|
ben2 |
base |
MIT | an HxW uint8 alpha matte |
Give it a frame and a mask and it repaints the hole so the thing was never there — pair the mask with SAM 3 or EdgeTAM. It answers with a sample rather than an estimate — details.
| Family | Variants | Weights licence | Output |
|---|---|---|---|
moebius |
general places2 |
Apache-2.0 | an HxWx3 image |
How each family behaves and why — OWLv2 suppressing nothing, the encoder cache, EasyOCR's line ordering, prompt semantics — is in docs/models.md.
pip install mozoThat is the whole install — twelve ordinary dependencies, not one of them a model package.
# Optional runtimes, for the families that publish those artifacts
pip install 'mozo[onnx]' # onnxruntime
pip install 'mozo[coreml]' # coremltools, macOS onlyNo family needs an extra. The extras add ways to execute a model, not models: without them
runtime="auto" simply does not select those artifacts.
Three interfaces over the same models, plus two browser pages.
| HTTP server | serve models to other machines, or to a language that is not Python |
| Python API | build a pipeline, a script or a notebook, in-process |
| Workflows | wire models and image operations into a graph, and run it headless |
| Workflow editor | draw the graph, run it, watch each node finish |
mozo startcurl -X POST "http://localhost:8000/predict/rfdetr/medium" -F "file=@street.jpg"[
{"bbox": [0.0, 113.24, 763.19, 1269.33], "class_name": "person", "confidence": 0.917},
{"bbox": [709.0, 773.0, 1366.0, 1143.0], "class_name": "laptop", "confidence": 0.854}
]Trimmed for reading. The real response carries every PixelFlow field on every detection —
masks, segments, text, class_id, metadata and the rest — including the ones that are
null, because masks: null beside a filled segments is a difference worth being able to see.
The catalogue is answerable without loading anything:
curl http://localhost:8000/models # all 81, no torch import, no weights
curl http://localhost:8000/models/loaded # what is resident right nowFull parameter reference below, and the server documents itself at
http://localhost:8000/docs.
import mozo
model = mozo.get_model("rfdetr/medium")
for found in model.predict("street.jpg"):
print(found.class_name, found.confidence, found.bbox)
# person 0.92 [0.0, 113.24, 763.19, 1269.33]
# laptop 0.85 [709.0, 773.0, 1366.0, 1143.0]predict takes a path, encoded bytes, or an RGB array, and returns a unified PixelFlow Detections —
iterable, indexable, and the same shape from every family here.
# Ask for something in words
model = mozo.get_model("owlv2/base-ensemble")
found = model.predict("kitchen.jpg", ["kettle", "a mug", "the window"])
found[0].class_name # 'kettle' — the phrase you searched for
# Read the text on a sign
model = mozo.get_model("easyocr/english")
found = model.predict("sign.jpg")
found[0].text # 'EXIT 42' — what it says
found[0].class_name # None — OCR reads content, it does not pick a class
# Measure depth
model = mozo.get_model("depth_anything_v2/indoor-small")
depth = model.predict("room.jpg") # HxW float32, at the input's resolution
model.unit # 'metres' — or None, and then it is not a distanceA workflow is a graph of nodes in a JSON file. Draw it at /workflow, or write it by hand; run it
from Python, from the command line, or over HTTP.
from mozo.workflow import Workflow
results = Workflow.load("blur_faces.json").run(source="street.jpg")mozo run blur_faces.json --file street.jpg44 nodes: every model family, the image transforms and annotations PixelFlow provides, and the two ends that read and write files. A node is an ordinary Python function — its signature is its declaration, so what the editor offers you and what actually runs cannot disagree:
@node(category="Annotate")
def draw_boxes(image: Image, detections: Detections, thickness: int | None = None) -> Image:
"""Draw a box around each detection."""
return pf.annotate.box(image.copy(), detections, thickness=thickness)Connections are typed. A port carrying detections cannot be wired into one that takes an image, and the editor refuses it before anything runs. Adding a node is adding a function.
Vendoring a model normally means maintaining something that quietly drifts from the original. The whole claim above rests on that not happening, so it is checked rather than asserted — with no tolerance. Exact equality, because a tolerance hides precisely the drift a check exists to catch.
The gates in tools/verify/ compare every intermediate stage against the original implementation,
not just the final answer: 1,275 comparisons for EasyOCR, 226 for OWLv2, 138 for Grounding DINO,
every one identical. Fourteen of the seventeen families ship one; all seventeen have their parity measured
and recorded in
mozo/vendors/<family>_deploy/PROVENANCE.md, with the upstream commit it was built from.
Because the extraction is the implementation rather than a wrapper around one, none of this costs anything at run time: EasyOCR runs within 1% of the published package on the same weights.
POST /predict/{family}/{variant}
Content-Type: multipart/form-data| Parameter | Applies to | Meaning |
|---|---|---|
file |
all | The image. Required. |
threshold |
detection, text-prompted | Confidence floor. Omitted, the family's own published default applies — they differ, and the endpoint deliberately does not restate them. |
labels |
detection | Comma-separated class names overriding the model's own. |
text |
text-prompted | The concept to look for. Required by those families. Repeat it for several. |
point, label |
promptable | A click as x,y, and 1 to include or 0 to exclude. Repeat both together. |
box |
promptable | A box as x1,y1,x2,y2. May be combined with points. |
name |
promptable | What to call what you pointed at. |
multimask |
promptable | Three candidate masks rather than one. Default true. |
text is deliberately not comma-separated the way labels is: a prompt is free text, so
?text=a person, holding a mug stays one concept rather than becoming two wrong ones. label is
required with point and has no default — guessing between include and exclude returns a
confident mask of the wrong thing.
# Boxes for anything you can name, under a permissive licence
curl -X POST "http://localhost:8000/predict/owlv2/base-ensemble?text=kettle&text=a%20mug" \
-F "file=@kitchen.jpg"
# Click one thing: three candidate masks, best first
curl -X POST "http://localhost:8000/predict/edgetam/edgetam?point=820,640&label=1" \
-F "file=@street.jpg"
# A box, one mask, named by you
curl -X POST "http://localhost:8000/predict/sam2/tiny?box=40,60,300,480&multimask=false&name=cat" \
-F "file=@street.jpg"Depth answers with an image rather than JSON:
curl -X POST "http://localhost:8000/predict/depth_anything_v2/indoor-small" \
-F "file=@room.jpg" -D headers.txt --output depth.png
# depth.png is a 16-bit PNG; headers.txt carries what it means:
# X-Depth-Unit: metres | none X-Depth-Min / X-Depth-Max: the endpoints
# depth = min + png / 65535 * (max - min)16-bit rather than 8, because six of the nine variants predict metres and quantising those to 256 levels would discard the measurement.
CLIP is the one family with a second route. /predict classifies; /encode returns the vectors
instead of an answer, for images or for phrases but not both in one call:
# Score an image against phrases you make up
curl -X POST "http://localhost:8000/predict/clip/base?text=a%20forklift&text=a%20person" \
-F "file=@aisle.jpg"
# The vectors themselves, to put in a vector database of your own
curl -X POST "http://localhost:8000/encode/clip/base" -F "file=@aisle.jpg"
curl -X POST "http://localhost:8000/encode/clip/base?text=a%20forklift&text=a%20person"The response carries model and revision alongside the embeddings, and those are not decoration:
a vector is only comparable against others from the same weights, so a stored index is tied to them.
The other endpoints: GET / for health and residency, GET /models for the catalogue,
GET /models/loaded for what is in memory, GET /test-ui for the browser page, and GET /docs
for the generated OpenAPI reference.
mozo.get_model uses one process-wide cache. When you want a separate lifetime — a batch job
that should release its models at the end — build your own manager and drop it:
from mozo import ModelManager
models = ModelManager()
model = models.get_model("rfdetr", "medium", device="cpu")
scratch = ModelManager() # a separate lifetime; drop it and its models go with itdevice takes "cuda", "mps", "cpu", or None to take the best available.
Fine-tuned weights load through the same API, on architectures mozo supports:
model = models.get_model(
"rfdetr", "my-training",
checkpoint_path="runs/best.pth",
model_size="small", project_type="detection",
labels=["hardhat", "vest"],
)Everything that changes what gets built is part of the cache identity — a checkpoint, a pinned
revision, a runtime — so two people's my-training are two models rather than one.
Every detection family returns the same object, so filtering and annotation are written once:
import pixelflow as pf
found = model.predict(image)
filtered = found.filter_by_confidence(0.8).filter_by_class_id([0, 2])
annotated = pf.annotate.box(image, filtered)
annotated = pf.annotate.label(annotated, filtered)
json_output = filtered.to_json()More: PixelFlow
Nothing loads until it is asked for. The server starts instantly whatever is published, and the first request for a family downloads and loads it. That first request is slow — minutes, for a multi-gigabyte family on a cold cache — and every one after it is not.
Nothing is evicted. A model stays for the life of the process. This is deliberate: an earlier version bounded the cache by model count, and a count cannot tell 0.10 GB from 1.34 GB. The same number could not be right for a 6 GB laptop and an 80 GB accelerator, and measured, it broke the obvious deployment — detection, segmentation and depth from one instance, 0.60 GB between them — into a 762 ms eviction on every request. Memory is yours to manage, and the lever is which models you ask for.
A model is built once however many requests arrive for it at the same moment, and a request for a model already in memory never waits behind an unrelated load.
Weights resolve from a manifest that ships inside the package, so working out which bytes a model refers to needs no network and no configuration. Every artifact is verified by sha256 after download, and its licence and a NOTICE naming the exact upstream release are published beside it.
| Variable | Meaning |
|---|---|
MOZO_ENABLE |
Which models this server offers, comma-separated. A family (clip) or one variant (clip/base); mix freely. Unset offers everything. An allow-list, so an upgrade that adds a family does not start serving it unasked. A name that matches nothing is logged and ignored, never fatal — it can only subtract. |
MOZO_CACHE |
Where downloads live. Default ~/.cache/mozo. |
MOZO_BASE_URL |
Serve artifacts from a mirror instead of the manifest's. A file:// URL pointing at a weights/ tree works, which is how an air-gapped host can be fed from removable media. |
MOZO_OFFLINE |
Set to 1 to refuse downloads. A missing file raises an error naming the exact path, URL and hash, so it can be placed by hand. |
PYTORCH_ENABLE_MPS_FALLBACK |
Set to 1 on Apple silicon so unimplemented ops fall back to CPU rather than failing. |
mozo start # 0.0.0.0:8000
mozo start --port 8080
mozo start --workers 4 # read the note below first
mozo start --reload # development; forces one workerIt is a real server — FastAPI on uvicorn, thread-safe, with weights verified by hash — and three things about it are yours to arrange:
--workers Nmultiplies memory by N. Workers are separate processes and share nothing, so each loads its own copy of every model it serves. Four workers serving SAM 3 is four times 3.4 GB, not one. Prefer one worker unless you have measured otherwise.- There is no authentication and no rate limiting, and the default bind is
0.0.0.0. Put it behind something before it faces a network you do not control. - Nothing is evicted, so a process asked for every family will eventually hold every
family. Decide what an instance serves rather than letting callers decide for it —
MOZO_ENABLEis how, and it is also how the licence question below is answered.
- No training and no fine-tuning. Bring a checkpoint; mozo runs it.
- It is not a model hub. The catalogue is a curated 81, chosen because each one could be extracted and verified. Growth is deliberate and slow.
- Write an adapter in
mozo/adapters/your_model.py - Register it in
mozo/registry.py - It is available over HTTP, in Python, and on the test page
- Add a node in
mozo/workflow/nodes/model.py— one function — and it joins the editor's palette
Workflow nodes are the same idea one level up: a function with annotated arguments in
mozo/workflow/nodes/, and the catalogue, the editor's form and the type checking all follow from
it.
HTTP request → FastAPI server → ModelManager → Adapter → Vendor
↓
thread-safe cache
The rule that keeps this honest: a vendor imports no other vendor and never imports mozo.
Duplication between vendors is deliberate, so a family can be re-extracted from a newer upstream
release without touching anything else. tests/test_vendor_agreement.py enforces it, and each
vendor's PROVENANCE.md records the exact upstream commit it came from and what was changed.
pip install -e .
mozo start --reload
pytestThe workflow editor is a Svelte app in ui/. Its build output is committed, so npm is needed to
change it and never to install mozo:
cd ui && npm install && npm run build # writes mozo/workflow/static/- Repository
- Issues
- PixelFlow — the result format
Mozo's own code is Apache-2.0, and so is every vendored extraction under mozo/vendors/.
The weights are separate works travelling with it. Of the 81 published variants, 43 are
Apache-2.0, 30 are AGPL-3.0 (every YOLO variant), 5 are MIT (CLIP and BEN2), 2 are
CC-BY-NC-4.0 (Depth Anything base and large), and 1 carries Meta's SAM License
(SAM 3). The full licence and a NOTICE
naming the exact upstream release are published beside every checkpoint.
YOLO weights are AGPL-3.0, or covered by a commercial licence from Ultralytics. And serving predictions from them over a network places AGPL-3.0 section 13 obligations on you.
Complying with either is the operator's responsibility. MOZO_ENABLE is how an instance declines
to take it on — a server that never offers a model never fetches or serves its weights:
MOZO_ENABLE=ben2,clip,easyocr,edgetam,grounding_dino,moebius,owlv2,rfdetr,sam2,siglip2,vitpose,\
depth_anything_v2/small,depth_anything_v2/indoor-small,depth_anything_v2/indoor-base,\
depth_anything_v2/indoor-large,depth_anything_v2/outdoor-small,\
depth_anything_v2/outdoor-base,depth_anything_v2/outdoor-largeThat is the 48 Apache-2.0 and MIT variants, and it stays 48 through an upgrade that adds families — which a list of what to exclude would not. Depth Anything is named variant by variant because it is the one family whose licence is not uniform: seven of its nine are Apache-2.0 and two are CC-BY-NC-4.0.
Nothing here relicenses anything. It only decides what one deployment hands out.
