Studio HTTP API#

Everything the Studio does goes through a JSON API on the same port, so the pipeline can be scripted from CI, a notebook or another machine.

Note

The API serves the Studio’s own UI. It is stable enough for scripts but not versioned — endpoints may change between releases.

Conventions#

Base URL

http://<host>:8765 (--host / --port of wbc-mjlab-studio)

Auth

None by default. With --token SECRET, clients other than the machine itself send Cookie: wbc_studio_token=SECRET (or open /?token=SECRET once in a browser). Unauthorised requests get 401.

Requests

JSON bodies (content-type: application/json); query parameters on GET. Uploads are raw bytes (PUT).

Responses

JSON. Actions return {"ok": true, …}; slow actions run in the background and return {"ok": true, "task": "<name>"} — progress arrives as busy events on the websocket.

Errors

400 {"detail": "<message>"} for invalid input (unknown dataset, bad name …)

Example: the whole pipeline#

S=http://127.0.0.1:8765
J='content-type: application/json'

# data: import a folder of clips and convert them
curl -s -X POST $S/api/data/import -H "$J" \
  -d '{"source": "/path/to/csvs", "name": "my_clips", "link": true}'
curl -s -X POST $S/api/data/convert -H "$J" \
  -d '{"dataset": "my_clips", "which": "missing", "batch_size": 8}'

# preset: Wbc-G1 + the se module, twice the joint-velocity noise
curl -s -X PUT $S/api/presets/se_noisy -H "$J" -d '{
  "base": "Wbc-G1", "modules": ["se"],
  "overrides": {"observations": {"actor": {"terms": {"joint_vel": {"noise_scale": 2}}}}}}'

# train it and follow the job
curl -s -X POST $S/api/train/launch -H "$J" -d '{
  "task": "Wbc-G1--se_noisy", "dataset": "my_clips", "num_envs": 4096, "max_iterations": 30000}'
curl -s $S/api/jobs                         # status, progress, ETA
curl -s "$S/api/jobs/<job>?tail=40"         # one job + log tail

# play the run (watch it in the browser)
curl -s -X POST $S/api/play/configure -H "$J" -d '{
  "run": "wbc_g1--se_noisy/<run>", "checkpoint": "model_29999.pt",
  "motion": "<library path from /api/play/libraries>", "num_envs": 16}'

The same from Python:

import requests

S = "http://127.0.0.1:8765"
job = requests.post(f"{S}/api/train/launch", json={
  "task": "Wbc-G1", "dataset": "samples", "num_envs": 4096, "max_iterations": 30000,
}).json()["job"]
print(requests.get(f"{S}/api/jobs/{job}").json()["progress"])

Robots#

Endpoint

Description

GET /api/robots

Registered robots with model summary, datasets and runs

GET /api/robot

Selected robot: joints, wiring (anchor, keybodies, feet), tasks

POST /api/robot/select {name}

Select a robot (background task)

POST /api/robot/joint {name, value} · /reset {zero} · /show · /options {show_wiring}

Pose sliders, default / zero pose, show the robot, wiring markers

Adding a robot (Add a robot from model files):

Endpoint

Description

GET /api/robots/import

Robots folder, saved robots, unfinished uploads

PUT /api/robots/import/upload/{id}/{path}

Upload one model file at its relative path (meshes keep their layout)

POST /api/robots/import/path {id, source}

Copy a folder on the Studio machine

GET /api/robots/import/files?id=

Model files in the folder, the likely main model first

POST /api/robots/import/inspect {id, model}

Prepare and inspect the model: bodies, joints, suggested wiring and gains

POST /api/robots/import/preview {wiring}

Show the draft with wiring markers

POST /api/robots/import/check {spec}

Settle check: {ok, checks: [{name, ok, text}]}

POST /api/robots/import/save {spec} · /cancel

Save and register ({id, task}) · leave the import view

GET /api/robots/{id}/spec · DELETE /api/robots/{id}

A Studio robot’s robot.yaml (Robot spec (robot.yaml)) · delete it

Data#

Endpoint

Description

GET /api/data/datasets

Datasets of the robot with source / converted / missing counts

GET /api/data/clips?dataset=&q=&status=&page=&size=

Clip listing; q supports * wildcards, status ∈ all, converted, missing, raw

GET /api/data/joints · /preview

Joint list · preview state (qa.issues[].plot: the Plots channels and first frame of an issue)

POST /api/data/load {dataset, clip, source} · /grid · /close

Preview a clip (source: auto, npz, raw) · a grid of clips · close

POST /api/data/frame · /time · /playing · /options

Preview transport and display options

POST /api/data/scan {dataset}

Duration scan (hours, length histogram)

POST /api/data/convert {dataset, which, clips, batch_size, output_fps, mirror, debias_z, input_fps}

Conversion job (which ∈ missing, all, selected) → {job}

POST /api/data/new {name} · /import {source, name, link}

New empty dataset · import a folder (copy, or link without copying)

PUT /api/data/upload/{dataset}/{file} · POST /api/data/uploaded

Upload one clip · refresh after an upload batch

Presets and modules#

Endpoint

Description

GET /api/presets

Tasks, your presets, modules, the config directory

GET /api/presets/schema?base=&modules=

Every editable knob of a task (+ modules) with defaults, and the observation term library

GET | PUT | DELETE /api/presets/{name}

Read · write {base, overrides, modules, description} · delete a preset (Studio preset file)

GET | PUT | DELETE /api/modules/{name}

Read · write {overrides, description} · delete a module

Training and jobs#

Endpoint

Description

GET /api/tasks · GET /api/runs

Tasks of the robot (built-in and presets) · runs on disk

POST /api/train/launch {task, dataset, num_envs, max_iterations, run_name, resume_run, resume_checkpoint, logger, extra}

Launch a detached training job → {job}

GET /api/jobs · GET /api/jobs/{name}?tail=

Jobs with status, progress and ETA · one job with its log tail

POST /api/jobs/{name}/stop · DELETE /api/jobs/{name}

Stop · forget a job (its run stays)

GET /api/curves?job=&run=&group=

Training curves (group: train, errors, rewards, sampling, loss, all)

GET /tensorboard?run=

Start TensorBoard if needed and redirect to it

Play#

Endpoint

Description

GET /api/play · /api/play/runs · /api/play/libraries?run= · /api/play/clips

Playback state · runs · motion libraries for a run · clips of the session

POST /api/play/configure {run, checkpoint, motion, num_envs, terminations}

Load a policy (checkpoint: "" = zero action), rebuilt in the background; on a running session another checkpoint of its run, the env count up to its capacity and terminations change in place

POST /api/play/stop · /playing · /step · /restart

Stop · pause / resume · single step · reset every env

POST /api/play/clip {clip, env} · /api/focus {index}

Pin a clip to an env (it loops) · focus an env

POST /api/play/options · /push {speed} · /rewards {rewards}

Ghost mode and targets · push the robot · live reward weights

GET /api/scene/terrain.bin

Terrain tiles under the robots (binary mesh, see below)

Plots#

The data behind the Plots window (Plots). Each channel is {id, name, group, unit, lower, upper} (unit: rad, m, Nm, reward, raw or "" for actions; joint torques with effort limits add limit_note, where the limits come from); ref is NaN where a channel has no reference.

Endpoint

Description

GET /api/plots/clip

The clip open in Data, JSON: {title, fps, frames, channels, data, ref, ref_label} with data / ref rows × channels float32 arrays (little-endian, base64) — ref is the raw source of a converted clip, resampled to its frames (or null). 409 when no single clip is open

GET /api/plots/play?env=&since=&epoch=

A Play env’s record (default env: the focused one), binary: [u32 header length][header JSON][pad to 4] then float32 data, ref and meta rows (little-endian; the header’s cols gives their column counts). The header: {epoch, env, fps, start, end, fresh}; rows since… of the record. ref holds the ref_cols channels only (root and joints); meta the clip index, frame in the clip and episode step per row. Without epoch or for another one (fresh: true: another env, a restart) it returns every kept row (last 60 s) and the header adds channels, ref_cols, clips and history_s. Every env of a session of up to 32 envs is recorded from the start; in a larger one an env records from when it is first focused or fetched. 409 without a session

import json, struct, numpy as np, requests

S = "http://127.0.0.1:8765"
blob = requests.get(f"{S}/api/plots/play", params={"env": 0}).content
(hlen,) = struct.unpack_from("<I", blob)
head = json.loads(blob[4 : 4 + hlen])
rows = head["end"] - head["start"]
flat = np.frombuffer(blob, "<f4", offset=4 + hlen)
robot, ref, meta = np.split(flat, np.cumsum([rows * c for c in head["cols"]])[:2])
robot, ref = robot.reshape(rows, -1), ref.reshape(rows, -1)
knee = [c["id"] for c in head["channels"]].index("left_knee_joint")
ref_knee = ref[:, head["ref_cols"].index(knee)]
rmse = np.sqrt(np.nanmean((robot[:, knee] - ref_knee) ** 2))

Live updates (websocket)#

/ws streams binary frames and JSON events.

A frame is [u32 header length][header JSON][padding to 4 bytes][float32 blob]. The header carries the instance count, the owner of the scene (robots, data, play, import), the HUD, model_version and terrain_version, and the offsets of the overlays in the blob. The blob holds body poses (n, nbody, 7) (position + wxyz), then the reference ghost poses, then overlay data. When model_version changes, clients fetch /api/robot/model + /api/robot/model.bin; when terrain_version changes, /api/scene/terrain.bin ([u32 nv][u32 nf][f32 verts][u8 colours, padded][u32 faces]).

Events are JSON {"type": "event", "topic": …, "payload": …} with topics log, busy, robot, robots, data, jobs, play, presets, owner and focus.