This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.6.1 instead.
Reason given by maintainers: forks worker processes; use 0.6.1
labeltify
Upload frames, and optionally their annotations, into LabelTify from DeepStream, OpenCV, Ultralytics, or any Python pipeline. The only thing it needs is a pipeline token.
pip install labeltify
- On the dataset Upload page, create a pipeline token.
- Upload:
from labeltify import LabelTifyClient
client = LabelTifyClient("labeltify_…") # or set LABELTIFY_TOKEN and call LabelTifyClient()
img = client.upload_image(
"DATASET_ID",
jpeg_bytes,
filename="cam01.jpg",
captured_at="2026-06-01T12:00:00Z",
camera="cam-01",
pipeline="ds-prod",
trigger="low_confidence",
model_name="park-net",
model_version="1.2",
)
client.upload_file("DATASET_ID", "frames/cam02.jpg", captured_at="2026-06-01T12:00:05Z", camera="cam-02")
The token knows its org, so you don't pass one. Timeouts, 429, and 5xx responses are retried with exponential backoff (max_retries=4 by default). Anything else raises LabelTifyError with the API's message and .status.
captured_at is required. Pass an aware datetime or an ISO 8601 string with Z or an offset. A naive datetime is rejected before any request.
original_detection is what the device's model saw (COCO, LabelTify, or YOLO JSON bytes). LabelTify stores it and does not show it, and the frame stays untouched. final_annotation is a ready annotation file, the same import as before. With it, work_state must be done or in_progress. Images with no final annotation stay untouched.
model_name, model_version, gps ((lat, lon) or {"lat", "lon"}), and additional_data (a JSON object, at most 2 KiB) are optional and separate. model_version requires model_name.
Frames whose corrupt reason is smear, grey, green, blank or truncated, and whose score is at least 0.5, are not uploaded. They come back in report["files"] with status skipped_corrupt, the score, and the reason. A blocks, tear or ghost hit is uploaded with the score so LabelTify can show it as Suspected corrupt. Pass skip_corrupt=False to upload every frame (they are still scored). Scoring uses every CPU core, up to 8 processes, and falls back to threads. Pillow and numpy are installed with the package.
Upload a folder with its annotations
report = client.upload_directory(
"DATASET_ID",
"datasets/parking",
captured_at="exif_or_mtime",
work_state="done",
model_name="park-net",
model_version="1.2",
)
print(len(report["items"]), "images stored in", report["requests"], "requests;", report["imported"])
This sends the folder the way dropping it on the dataset's Upload page would: images, plus COCO (polygons and boxes), YOLO (labels/*.txt with data.yaml or classes.txt), Pascal VOC, LabelMe, CSV, metadata.jsonl, a folder per class, or a LabelTify export. The server reads the annotations; the client only splits the folder into requests (at most 100 images and 80 MB each). A COCO file is cut down to each request's images, so polygons stay polygons.
- The dataset must already exist. Create it on the site. Parking polygons need type Instance segmentation. Traffic boxes need Object detection.
captured_atis required.exifreads DateTimeOriginal and OffsetTimeOriginal (a missing offset names the file and stops).mtimeuses the file's mtime as UTC.exif_or_mtimeuses EXIF when the timestamp is there and mtime otherwise. You can also pass one aware datetime, a mapping of filename to datetime, or a function of the path.meta_forreturns a fullImageMetafor one file.work_state="done"marks every image that received a final annotation. Images with no annotation stay untouched. With any annotation file in the folder,work_statemust bedoneorin_progress.- A zip in the folder is expanded here. Each member keeps its path as the upload filename, and each image is scored and gets its own metadata.
- Smear, grey, green, blank and truncated frames are left out unless you pass
skip_corrupt=False. Other corrupt hits are uploaded and scored. Scoring runs on all cores. - Create the pipeline token on that dataset's Upload page.
- One credit covers 10,000 uploaded images. A free dataset holds 100 photos.
- Frame embeddings follow the dataset setting. Turn them off on the dataset before a large upload when you do not want that charge.
- Use the portable pixel COCO files. The raw Studio shards store boxes as fractions from 0 to 1, and the server divides by width and height again.
- Two images with the same file name in different folders stop the upload before anything is sent; rename one.
- Images over 20 MiB, empty files, and files that are neither images nor annotations are listed in
report["files"]and not sent. A zip is expanded when it holds at most 200 files and 20 MB. - If a request fails after its retries, the error says how many images were already stored; run the folder again and only what is missing is uploaded (see below).
compute_dhash=Truealso skips near-duplicate frames, like the Upload page. Pillow is already installed.
Migrating thousands of images
export LABELTIFY_TOKEN=labeltify_…
python -m labeltify upload ./photos --dataset DATASET_ID --captured-at exif_or_mtime --workers 6 --work-state done
or client.upload_directory(dataset_id, folder, workers=6, progress=print).
- Several requests at once.
workers(default 4, at most 8;1sends one after another) requests run in parallel over kept connections, so there is no new TLS handshake per request. Each request holds at mostmax_request_bytes(default 24 MiB) andmax_request_images(default 50). Files are read from disk only when their request is sent, so memory is aboutworkers× 24 MiB, not the size of the folder. - Resume for free. With
skip_existing=True(default) every image is hashed (SHA-256), the dataset is asked which hashes it already holds (client.images_exist(dataset_id, hashes)), and only the rest is sent. Running the same folder again after an interruption takes seconds, not a second upload. Label files of images that are already there are still sent.force=Trueorskip_existing=Falseturns this off, and an older server without the lookup is handled as before. - Progress.
progress=is called after each request withrequests_done,requests_total,images_storedandskipped_existing; the command line prints it. - Frame embeddings follow the dataset setting and are made in the background after the upload: thousands of images take a while to get their vectors. The dataset's Images tab shows how many are done.
- If a request fails, the requests not started yet are dropped, those already stored stay stored, and the error says how many; run the command again to continue.
upload_bundle(dataset_id, files, images_meta=…, work_state=…) sends one such request from bytes you already have (files is a list of {"filename", "data", "content_type"}, and images_meta is one ImageMeta per image).
Upgrade from 0.4
labels is gone. Capture time is required. Corrupt frames are skipped unless you opt in.
# 0.4
client.upload_image(dataset_id, data, "cam.jpg", labels=open("ann.json", "rb").read(), work_state="done")
# 0.5
client.upload_image(
dataset_id,
data,
filename="cam.jpg",
captured_at="2026-06-01T12:00:00Z",
final_annotation=open("ann.json", "rb").read(),
work_state="done",
)
# 0.4
client.upload_directory(dataset_id, "frames", work_state="done")
# 0.5
client.upload_directory(dataset_id, "frames", captured_at="exif_or_mtime", work_state="done")
A deployment behind an access proxy can take extra headers on every request: LabelTifyClient(token, base_url=…, headers={"CF-Access-Client-Id": …, "CF-Access-Client-Secret": …}).
Send model proposals to the Loop review queue:
client.propose(
"DATASET_ID",
img["id"],
[{"type": "bbox", "id": "p1", "classId": "car", "box": {"x": 0.1, "y": 0.1, "w": 0.2, "h": 0.2}, "source": "model", "confidence": 0.4}],
model_ref="deepstream@1",
uncertainty=0.8,
)
Boxes are normalized [0,1]. Camera, pipeline, and trigger are stored on the image so Loop can filter by them.
Local API: LabelTifyClient("labeltify_…", base_url="http://localhost:8787").
Download a dataset export
The export tool on the site (or the MCP export tool) hands out a file-list URL. One command pulls every image it lists, plus the annotation bundle:
export LABELTIFY_EXPORT_KEY="<key from the export>"
python -m labeltify download "https://labeltify.com/api/ds/EXPORT_ID/files" --out ./ofan --workers 16
or labeltify download … with the installed console script. From Python:
from labeltify import download_export
summary = download_export("https://labeltify.com/api/ds/…/files", "./ofan", key="<key>", progress=print)
# {"images": 77769, "downloaded": 77000, "skipped": 769, "failed": 0, "bytes": …, "annotations": True, "failures": []}
The export key can be passed via --key / LABELTIFY_EXPORT_KEY, as an explicit key= argument to download_export, or left in the URL as ?key=… (still supported for browsers and old scripts). For scripts and CI, prefer the header or explicit argument so the key does not land in request URLs and logs.
- Resume for free. Files already on disk with the right size (and SHA-256, when the list carries one) are skipped, so re-running after an interruption only fetches what is missing. Partial files land in
name.partand are replaced atomically. - Parallel.
--workers(default 16, at most 64) downloads at once. Network errors,429, and5xxare retried with exponential backoff (--retries, default 5), honouringRetry-After. An expired image link (403/404) is reported as failed, not retried forever. - Annotations are extracted next to the images, unless
--no-annotations. Zip entries that would land outside the output folder are refused. An images-only export has no annotations, so that step is skipped on its own. --no-verifyskips the size and hash checks. An expired file-list link (410) says so clearly. When every file made it, the exit code is 0 and the last stdout line is the JSON summary; otherwise it is 1.
Develop
pip install -e "clients/python[test]"
pytest clients/python/tests
Bump version in pyproject.toml to publish. Each new version that lands on main is published to PyPI by .github/workflows/publish-python.yml.
Metadata
Release files for labeltify 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| labeltify-0.6.0.tar.gz | 56.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| labeltify-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 94.9 kB
Release files / labeltify-0.6.0.tar.gz
| Download URL | labeltify-0.6.0.tar.gz |
|---|---|
| Size | 56.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
95ca76c62c9b345190d782f7f9a96adfc36a8bacdc503722c5f237ae7375a6eb
|
|
BLAKE2b-256 checksum How to use checksums |
6f2a5f73e3a8fdabb8c10859d0a1c1cb243d7fdf5b7e0bc6f772e001e90ffaf9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Release files / labeltify-0.6.0-py3-none-any.whl
| Download URL | labeltify-0.6.0-py3-none-any.whl |
|---|---|
| Size | 38.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d677ce15d63044a69f6ee669a7181e441c1a0be98bcabcf3882486a00b9d51b8
|
|
BLAKE2b-256 checksum How to use checksums |
50800523751656c10fac54119cd14819604a648c4e00a389def6d1bbbe1b8ca1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|