Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

qualia-sdk — Python client for Qualia robot data: upload, curate, version, export

Status: beta pre-release (0.8.0rc16) · Published: qualia-sdk wheels for Python 3.10+

Qualia keeps robot recordings (MCAP or LeRobot) as datasets of episodes. From Python you upload a folder of takes and read what preparation found in it. You link the episodes to a Task (what the robot was asked to do), run checks that flag failed or broken takes, and keep or reject each take. The kept takes become an immutable version, which downloads as LeRobot v3 or v2.1, ROS 2 MCAP or a partner layout. The upload engine is compiled into the wheel, so there is nothing else to install. The qualia command line is the separate qualia-cli package.

Install

pip install --pre qualia-sdk

The client and the deployment it talks to negotiate the upload protocol exactly, so pin the version your deployment's docs name, for example pip install 'qualia-sdk==0.8.0rc16'. client.preparation first shipped in 0.8.0rc12; 0.8.0rc11 raises AttributeError. client.preparation.wait first shipped in 0.8.0rc14.

Usage

Set QUALIA_API_KEY (and QUALIA_BASE_URL for a deployment other than the default), or log in once with qualia auth login. An API key names its account, so no account setting is needed.

from qualia import Qualia

client = Qualia()

dataset = client.data.upload("./takes", "box-pick")["dataset_id"]
# Preparation starts by itself. Wait for it to end.
prep = client.preparation.wait(dataset)
print(prep.state, prep.warnings)  # what preparation found

# The VLM check reads each episode against the published Task it is linked to.
task = client.tasks.create(
    "Pick a box", draft={"description": "Move the box from the crate to the table"}
)
task = client.tasks.update(task.id, success="The box stands on the table")
client.tasks.publish(task.id, etag=task.etag)
client.datasets.update(dataset, linked_task=task.id)

estimate = client.checks.estimate(dataset)  # free: nothing is reserved
started = client.checks.run(dataset, max_cost=estimate.credits)
client.checks.wait(started.check_runs[0])  # a VLM check starts one run
for issue in client.checks.findings(dataset).items:
    print(issue.text)

client.episodes.reject(dataset, ["<episode-id>"], reason="the box slipped")
client.episodes.keep(dataset, filter="state = UNREVIEWED")
client.versions.create(dataset, "1.0")  # the takes a person kept
client.data.download(f"{dataset}:1.0", "./box-pick-1.0", format="lerobot_v2_1")

An API key names its account automatically. Set an account override only when needed; the API guide describes the server requirement and release history. An account= (or QUALIA_ACCOUNT) that is not an account UUID is refused with a ValueError; leave it unset to use the key's own account. Publishing a preview (client.versions.create(dataset, tag, preview=...), or qualia dataset pin <dataset> <tag> --preview <preview>) needs no override either: the tag the server answers under the key's account UUID is the one asked for.

client.data.download("<dataset>") with no version takes stable, the newest tag. A fresh upload has no tag yet, so it takes the newest version, v0 (the upload as it arrived), and reports {"step": "newest", "version": "<slug>:v0"} to on_progress. Only a dataset with no version at all is refused. While the format renders, a status read or start that times out, drops its connection or gets a 429, 503 or 504 is tried again, up to 4 tries in a row, and none starts past timeout. The listing that signs the links again during a long transfer gets the same 4 tries when it has no answer or a 503 or 504. The API guide's Data section has the examples and the waits.

Any upload can name the rig it was recorded on with rig=: any rig of the workspace, such as an arm rig for teleop or joint-recorded data (client.data.upload("./session", "kitchen", rig="my-franka-rig")), a UMI rig or another capture rig (the Rigs page's "Other rigs"). Only a UMI rig converts a handheld recording; the others are just recorded on the upload, and the dataset shows the rig once the upload publishes. A name that is no rig of the workspace (a robot template, a gripper template, a robot's own mounted gripper) is refused before anything is stored, and so is an arm rig named for a tree whose meta/ingest.toml declares a [rig], or another capture rig named for a tree that declares the UMI kind. This needs 0.8.0rc13 or newer; earlier wheels send no transfer session, so nothing is recorded and the server asks you to upgrade.

A handheld (UMI) recording converts at a UMI rig of its workspace. Make one from a gripper template with client.robots.make_umi_rig(template, name) (or qualia gripper umi-rig), then give it its calibration with client.robots.import_gripper_calibration(rig.name, "tcp_calibration.json"); a workspace may have several. Name the rig with rig= on upload. Without it, an upload into a merged dataset whose uploads agree on one rig takes it, and a tree that declares [rig] kind = "umi-handheld" takes the workspace's one UMI rig: none is refused (404), several are refused asking which (409). client.datasets.resolve_poses(dataset, gripper="<name>") names the rig a resolve uses, and asks which when there are several and none is named. The API guide's Data section has the example.

A merged dataset moves its uploads with precheck_merged_poses and move_merged_poses. With several UMI rigs of one kind, name the rig of each upload that asks. The rig of a move that lands is stored for the upload:

check = client.datasets.precheck_merged_poses(merged.name)
moved = client.datasets.move_merged_poses(
    merged.name,
    membership_revision=check.membership_revision,
    rigs={upload.name: rig.name},  # upload to the UMI rig it was recorded on
)

The API guide has the full example. An upload sends its transfer session id when it begins, so the UMI rig composed for it stays its own. In a workspace with several UMI rigs, an upload from an SDK too old to send it reads as not composed and is refused, so upgrade. Against a gateway that predates the field, the SDK drops it and begins again (QDS service: UMI rigs).

A LeRobot dataset can say who recorded each episode, on which station and in which shift: put meta/qualia/episodes.jsonl in the folder before you upload it, one line per episode with its episode_index (a number, from 0) and operator, scene (the station) and session_id (the shift) as quoted strings:

{"episode_index": 0, "operator": "ana", "scene": "station-2", "session_id": "2026-11-09-am"}
{"episode_index": 1, "operator": "ben", "scene": "station-1", "session_id": "2026-11-09-am"}
{"episode_index": 2, "operator": "ana", "scene": "station-2", "session_id": "2026-11-09-pm"}

An episode without a line keeps them empty, and a line that cannot be applied is left out with a finding, never a failed upload. rig_id there means the robot, as in episode.json, so leave stations out of it. Filter and count with client.episodes.list(dataset, filter='operator = "ana" AND session = "2026-11-09-am"') and client.episodes.summarize(dataset, facets=["operator", "scene", "session"]), and correct a station with client.episodes.update_many(dataset, [{"episode": "<EPISODE>", "scene": "station-1"}]), which wins over the file (data collection guide).

client.datasets.setup() takes no solver: a dataset follows its robot's solver, set on the robot's Solver page (setting a dataset up). client.checks.reach_status(dataset).calibration_mix is set when the dataset's uploads hold more than one capture calibration (or the solver is refused for a mix), and says which calibrations and rigs, whether each is current and whether the solver will not run on the mix or runs (API guide).

Upgrading from 0.3.6: 0.8.0rc11 removed the fine-tuning calls, client.finetune and client.jobs. Training isn't available on your account's plan yet. We're working on it. To request access, write to support@qualiastudios.dev.

Labels with a LeRobot download

A LeRobot download names each exported episode (qds_episode_id) and carries nothing else Qualia knows about it. labels=True (qualia dataset download <dataset>:<tag> --labels) writes two more files into a lerobot_v3 or lerobot_v2_1 folder, once its files are checked and before it appears. Any other format is refused. Not in a released wheel yet (after 0.8.0rc15).

result = client.data.download("box-pick:1.0", "./box-pick-1.0", labels=True)
print(result.labels.counts)  # episodes, kept, checked, not_checked, failed
rows = client.versions.labels("box-pick:1.0").rows  # the same rows, no download

The labels reference has the fields of each row and of export.json, how rows are matched in each format, the time base, and when the labels are refused or a source is skipped.

Catalog checks

Use client.checks.estimate(dataset, kind="heuristic", categories=["heuristic"]) to see which free checks would run, then client.checks.run(dataset, kind="heuristic", all=True) to start them without a credit ceiling. kind can also be "vlm"; without it the catalog selects runnable heuristics (VLM catalog checks are unavailable here). A run contains only one kind. VLM runs need max_cost covering the estimated bound.

Automatic checks

Free quality checks default on, paid video checks default off. An organization Admin/Owner can set the org default, a workspace Admin/Owner its default, and a dataset writer an override. Each half inherits independently from dataset → workspace → org → platform. Reading returns the effective on/off value and its source. Switching on starts nothing; existing episodes need a manual run.

client.checks.automatic_policy("<ORG_UUID>", org=True)
client.checks.set_automatic_policy("<ORG_UUID>", org=True, graph=False)
client.checks.set_automatic_policy("<WORKSPACE_UUID>", workspace=True, heuristic=False)
client.checks.set_automatic_policy("<DATASET_UUID>", reset=("graph",))

Automatic policy contract and permissions.

Pipeline templates

A workspace keeps its own pipeline templates as TOML files that pin public templates. client.pipelines.catalog() lists what may be pinned; validate and apply take the file's text, and apply writes the next version only when the graph changed. estimate gives the most a launch may cost; launch takes that bound as max_cost and is refused if the run no longer prices as quoted.

from pathlib import Path

applied = client.pipelines.apply(Path("our-act.toml").read_text(encoding="utf-8"))
values = {"dataset": "<VERSION_ID>"}
bound = client.pipelines.estimate(
    applied.pipeline_template_id, project_id="<PROJECT_ID>", values=values
)
client.pipelines.launch(
    applied.pipeline_template_id,
    project_id="<PROJECT_ID>",
    values=values,
    max_cost=bound.credits,
    quote_digest=bound.quote_digest,
)

Your workspace's templates are billed per run. On a deployment that does not bill per run, apply still writes the template but answers launchable=False with a launch_refusal (code, message) saying why, and launch is refused with run_billing_required. validate, catalog() and templates() carry the same two fields.

versions, version and retire read and end a template's history. The file format and every rule.

API

In the repository (these links do not resolve on PyPI):

License

MIT

Metadata

Release files for qualia-sdk 0.8.0rc16

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for qualia-sdk 0.8.0rc16
File Size Uploaded Provenance Actions
qualia_sdk-0.8.0rc16.tar.gz 578.9 kB Without provenance Details for qualia_sdk-0.8.0rc16.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for qualia-sdk 0.8.0rc16
File Interpreter ABI Platform Actions
qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl

Total release size: 4.0 MB

Release files / qualia_sdk-0.8.0rc16.tar.gz

Download URL qualia_sdk-0.8.0rc16.tar.gz
Size 578.9 kB
Tags Source
SHA-256 checksum
How to use checksums
3725c691923f85f65023bd7a427090f996b50b2d6e79d3329c9d4cf54b0d2655
BLAKE2b-256 checksum
How to use checksums
b294e829ca4c8b8265ade15c73fe9c00e806ed103a83927d5ed452c382ea2a78
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.16.0

Release files / qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

Download URL qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Size 1.9 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
2c18fa29e94e62f7631f20b28383a7b7c15ba0506400fa7c789a145d30dddce4
BLAKE2b-256 checksum
How to use checksums
d16c6d96c41b127ed9d1fc2d7bc641da57c75826a30cfba883d1c3f5d523eddc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.16.0

Release files / qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl

Download URL qualia_sdk-0.8.0rc16-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Size 1.5 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
423d8ff3cd85ec43dc89a8279400f3a236dcc8d0d3cdf55e9a81341301ba4077
BLAKE2b-256 checksum
How to use checksums
f07539c20a9eb3fb3a9f8defbd292415ef1eee0d46c9f3c6ea0fdb144d19a1de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.16.0

Security for qualia-sdk 0.8.0rc16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page