Mozo
81 computer vision models, and the workflows that run them.
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 start
No 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.
Model Catalog
Object detection
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 |
Instance segmentation
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 |
Keypoints
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) |
Text-prompted
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 |
Zero-shot classification and embeddings
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 |
Promptable segmentation
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 recognition
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 |
Depth
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 |
Background removal
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 |
Object removal
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.
Install
pip install mozo
That 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 only
No family needs an extra. The extras add ways to execute a model, not models: without them
runtime="auto" simply does not select those artifacts.
Using it
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 |
1. HTTP server
mozo start
curl -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 now
Full parameter reference below, and the server documents itself at
http://localhost:8000/docs.
2. Python API
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 distance
3. Workflows
A 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.jpg
44 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.
Verification
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.
HTTP API
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.
Python API
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 it
device takes "cuda", "mps", "cpu", or None to take the best available.
Your own checkpoints
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.
PixelFlow
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
How it works
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.
Configuration
| 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. |
Deploying
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 worker
It 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.
What mozo does not do
- 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.
Extending
- 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.
Development
pip install -e .
mozo start --reload
pytest
The 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/
Links
- Repository
- Issues
- PixelFlow — the result format
License
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-large
That 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.
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 mozo-1.1.2.tar.gz.
File metadata
- Download URL: mozo-1.1.2.tar.gz
- Upload date:
- Size: 9.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bfaa5c83040a0dc1e3794e8e1dac540aa8437f10ffc017c1bb8b40a353483c0
|
|
| MD5 |
866e7b3139f02c8365112b5da7eb5cb4
|
|
| BLAKE2b-256 |
a3c28eb73e83c62efb9d214f736fdbb40218d198a46194f97f5ee49e4b960dcd
|
Provenance
The following attestation bundles were made for mozo-1.1.2.tar.gz:
Publisher:
publish.yml on datamarkin/mozo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mozo-1.1.2.tar.gz -
Subject digest:
7bfaa5c83040a0dc1e3794e8e1dac540aa8437f10ffc017c1bb8b40a353483c0 - Sigstore transparency entry: 2685432769
- Sigstore integration time:
-
Permalink:
datamarkin/mozo@75b54c358bcf9a0fbfbcc71a82b26bf03e405d62 -
Branch / Tag:
refs/tags/v1.1.2 - Owner: https://github.com/datamarkin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@75b54c358bcf9a0fbfbcc71a82b26bf03e405d62 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mozo-1.1.2-py3-none-any.whl.
File metadata
- Download URL: mozo-1.1.2-py3-none-any.whl
- Upload date:
- Size: 9.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dcdef4e4761b623ab0bebe0fa4536095d1b03e612a811394a068237ac15801c2
|
|
| MD5 |
c9f93f5babcc15a01c32bf19c9145c62
|
|
| BLAKE2b-256 |
c536f26580e437ed940ea9d9c4194b2dc7ab1a08e4af69140f7de50fc5d20f67
|
Provenance
The following attestation bundles were made for mozo-1.1.2-py3-none-any.whl:
Publisher:
publish.yml on datamarkin/mozo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mozo-1.1.2-py3-none-any.whl -
Subject digest:
dcdef4e4761b623ab0bebe0fa4536095d1b03e612a811394a068237ac15801c2 - Sigstore transparency entry: 2685432785
- Sigstore integration time:
-
Permalink:
datamarkin/mozo@75b54c358bcf9a0fbfbcc71a82b26bf03e405d62 -
Branch / Tag:
refs/tags/v1.1.2 - Owner: https://github.com/datamarkin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@75b54c358bcf9a0fbfbcc71a82b26bf03e405d62 -
Trigger Event:
push
-
Statement type: