Skip to content

anvil.recorder

anvil.recorder

Records episodes of a running workcell into MCAP files, and compresses, annotates and labels them once finished.

An episode holds the loop's state, what it wrote, where each tick's time went, each controller's trace and command stream, the events, and every camera's JPEG with every stamp it got, in one MCAP file that opens in Foxglove as it is.

Episodes go into a Session, a directory <root>/<session>/ under a data root the caller names: each start is the session's next episode, 0001.mcap, 0002.mcap and on, so a session lists in the order it was recorded, and beside each is written <NNNN>.foxglove-layout.json, the layout that draws its robot, cameras and joints. Every file says which session and episode it is, and the session's task, in its anvil.episode metadata.

A Recorder records on threads of its own and reads the runtime and the cameras the way any other reader does, so the loop never waits on it and a camera window reading the same cameras still gets every frame. RecorderConfig says which samples of each source go in. A Compression turns a finished episode's JPEGs into H.264 in place, and can be canceled from another thread while it runs. annotate says things about a finished episode, in place, annotations reads them back, and label relabels how it went.

A LiveView serves the same channels as they happen, to Foxglove viewers over a WebSocket, beside a recording or without one.

A DatasetExport turns finished episodes into one LeRobot dataset, in its v3.0 layout, for a policy to train on. Each dataset frame is one frame of an anchor camera, carrying each actuator's position as the loop measured it as observation.state, as it commanded it as action, and each camera's image as video, encoded by ffmpeg. Encoding is the cost, several core-seconds per second of 480p video per camera at LeRobot's encoder settings: its ffmpeg runs at the lowest priority, and on the robot, cancel it the moment the robot needs the CPU. It needs ffmpeg on PATH, built with libsvtav1 for AV1 or libx264 for H.264.

An episode an older release recorded reads in this one. One a newer release recorded in a format this one may misread is refused wherever a finished episode is read, as one that is not, saying which version recorded it and which this one reads up to.

Classes:

Name Description
RecordingMode

Which samples of each source an episode keeps.

Outcome

How an episode went, as whoever stopped it judged; a failed episode is kept and labeled, not deleted.

RecorderConfig

How an episode is recorded; rated, free and synced build one of each mode.

RecorderStats

What an episode counted, as Recorder.stop returns it and the file's anvil.stats metadata keeps it.

Session

A directory of episodes, <root>/<name>/, created or reopened when this is constructed.

SessionSummary

One session under a data root, as sessions lists it.

Annotation

One thing said about an episode: a key and its value, on the whole episode or pinned to one moment of it.

CameraCompressed

One camera of a compressed episode, as CompressStats.cameras holds it.

CompressStats

What Compression.run did: the file's size before and after, and each camera's.

Compression

A compression of a finished episode in place, every camera's JPEGs to H.264, lossy and for good.

Recorder

Records episodes of a running workcell, one at a time, each into the next file of a Session.

LiveView

Serves a running workcell to Foxglove viewers as it runs, over a WebSocket, until stopped.

VideoCodec

Codec a dataset's videos are encoded with.

LeRobotExport

What DatasetExport.run wrote.

DatasetExport

An export of recorded episodes into a new LeRobot dataset.

Functions:

Name Description
sessions

Lists every session under the data root root, the most recently written first.

layout_path

Returns where the Foxglove layout that draws episode is written, beside it; whether either exists is not asked.

annotations

Reads what was said about the finished episode: its newest annotations, as annotate last wrote them.

annotate

Replaces the finished episode's annotations with annotations, in place, keeping the ones before as history.

label

Relabels how the finished episode went, in place, keeping the labels before as history.

mesh

Returns the bytes of a mesh a composed URDF names, from the copy shipped in the package.

RecordingMode

Bases: StrEnum

Which samples of each source an episode keeps.

Attributes:

Name Type Description
RATED

Each source at a rate of its own, keeping the newest sample of each period.

FREE

Every sample of every source: every frame and every tick, with the ticks the recorder missed counted.

SYNCED

One observation per frame of an anchor camera, the way a LeRobot dataset lays one out.

RATED class-attribute instance-attribute

RATED

Each source at a rate of its own, keeping the newest sample of each period.

By default the loop at 100 Hz, or its own rate where that is slower, and each camera at its fps.

FREE class-attribute instance-attribute

FREE

Every sample of every source: every frame and every tick, with the ticks the recorder missed counted.

SYNCED class-attribute instance-attribute

SYNCED

One observation per frame of an anchor camera, the way a LeRobot dataset lays one out.

Each other source contributes its latest sample at or before the frame's stamp.

Outcome

Bases: StrEnum

How an episode went, as whoever stopped it judged; a failed episode is kept and labeled, not deleted.

Attributes:

Name Type Description
UNLABELED

Nobody said: stopped without a judgment, or finished when its recorder was collected.

SUCCESS

It did what its task asks.

FAILURE

It did not do what its task asks.

UNLABELED class-attribute instance-attribute

UNLABELED

Nobody said: stopped without a judgment, or finished when its recorder was collected.

SUCCESS class-attribute instance-attribute

SUCCESS

It did what its task asks.

FAILURE class-attribute instance-attribute

FAILURE

It did not do what its task asks.

RecorderConfig dataclass

RecorderConfig(
    mode: RecordingMode,
    rates_hz: Mapping[str, float] = {},
    anchor_camera: str | None = None,
    staleness_ns: int | None = None,
)

How an episode is recorded; rated, free and synced build one of each mode.

Nothing is checked here: Recorder.start refuses a config its sources cannot meet, with anvil.InvalidArgumentError, naming what is wrong. That covers a rate for a source the recording does not have, one that is not a positive number or is above what its source produces, an anchor_camera that is not one of the cameras recorded, a staleness_ns of 0, and a field the mode does not take, such as anchor_camera on a rated config.

Methods:

Name Description
rated

Builds a rated config: each source at rates_hz, or at its default where that leaves it out.

free

Builds a free config, which keeps every sample of every source.

synced

Builds a synced config: one observation per frame of the camera anchor_camera.

Attributes:

Name Type Description
mode RecordingMode

Which samples of each source the episode keeps.

rates_hz Mapping[str, float]

Rated only: the rate to record each source at, in hertz, by source.

anchor_camera str | None

Synced only: the name of the camera whose frames make the observations.

staleness_ns int | None

Synced only: how old another source's sample may be, in nanoseconds, before the observation is dropped.

mode instance-attribute

Which samples of each source the episode keeps.

rates_hz class-attribute instance-attribute

rates_hz: Mapping[str, float] = {}

Rated only: the rate to record each source at, in hertz, by source.

"robot" for the loop and otherwise a camera's name; a source left out runs at its default, as RecordingMode.RATED says.

anchor_camera class-attribute instance-attribute

anchor_camera: str | None = None

Synced only: the name of the camera whose frames make the observations.

staleness_ns class-attribute instance-attribute

staleness_ns: int | None = None

Synced only: how old another source's sample may be, in nanoseconds, before the observation is dropped.

A dropped observation is counted. None is one anchor frame period.

rated classmethod

rated(rates_hz: Mapping[str, float] | None = None) -> Self

Builds a rated config: each source at rates_hz, or at its default where that leaves it out.

Parameters:

Name Type Description Default
rates_hz Mapping[str, float] | None

The rate per source, in hertz, as rates_hz holds it; None records every source at its default.

None

Returns:

Type Description
Self

The config, checked when a recording starts with it.

free classmethod

free() -> Self

Builds a free config, which keeps every sample of every source.

synced classmethod

synced(
    anchor_camera: str, staleness_ns: int | None = None
) -> Self

Builds a synced config: one observation per frame of the camera anchor_camera.

Parameters:

Name Type Description Default
anchor_camera str

The name of the camera whose frames make the observations; one of the cameras recorded.

required
staleness_ns int | None

How old another source's sample may be before the observation is dropped and counted; None is one anchor frame period, and 0 is refused when the recording starts.

None

Returns:

Type Description
Self

The config, checked when a recording starts with it.

RecorderStats dataclass

RecorderStats(
    messages_written: int,
    ticks_missed: int,
    commands_missed: int,
    frames_missed: dict[str, int],
    frames_received: dict[str, int],
    link_sent: int,
    link_answered: int,
    observations_written: int,
    observations_dropped: int,
    stale_by_source: dict[str, int],
)

What an episode counted, as Recorder.stop returns it and the file's anvil.stats metadata keeps it.

Attributes:

Name Type Description
messages_written int

Messages in the file, on every channel; 0 until the file is finished.

ticks_missed int

Ticks the loop ran that the recorder never saw: gaps in the tick counter.

commands_missed int

Commands the loop picked up that the recorder never saw: gaps in their sequence numbers.

frames_missed dict[str, int]

Frames the recorder never got, by camera name.

frames_received dict[str, int]

Frames the recorder got, by camera name.

link_sent int

Frames the hw interface sent the drives over the episode, every actuator's added up.

link_answered int

Replies the drives sent back to link_sent's frames.

observations_written int

Observations written, when synced; 0 otherwise.

observations_dropped int

Observations dropped because some source had no sample recent enough, when synced; 0 otherwise.

stale_by_source dict[str, int]

The dropped observations each source was too stale for, by source.

messages_written instance-attribute

messages_written: int

Messages in the file, on every channel; 0 until the file is finished.

ticks_missed instance-attribute

ticks_missed: int

Ticks the loop ran that the recorder never saw: gaps in the tick counter.

commands_missed instance-attribute

commands_missed: int

Commands the loop picked up that the recorder never saw: gaps in their sequence numbers.

frames_missed instance-attribute

frames_missed: dict[str, int]

Frames the recorder never got, by camera name.

Gaps in the driver's sequence, whether the driver dropped them or the recorder read too slowly.

frames_received instance-attribute

frames_received: dict[str, int]

Frames the recorder got, by camera name.

link_sent: int

Frames the hw interface sent the drives over the episode, every actuator's added up.

link_answered: int

Replies the drives sent back to link_sent's frames.

observations_written instance-attribute

observations_written: int

Observations written, when synced; 0 otherwise.

observations_dropped instance-attribute

observations_dropped: int

Observations dropped because some source had no sample recent enough, when synced; 0 otherwise.

stale_by_source instance-attribute

stale_by_source: dict[str, int]

The dropped observations each source was too stale for, by source.

One dropped observation counts against every source that was.

Session

Session(
    root: Path,
    name: str | None = None,
    task: str | None = None,
    operator: str | None = None,
)

A directory of episodes, <root>/<name>/, created or reopened when this is constructed.

Hand one to Recorder.start to record its next episode. The directory and its episodes outlive this object.

Opens the session name under the data root root, creating either directory as needed.

A session that exists is reused, its episodes continuing from the next free index. task and operator are kept in the session's session.json and written into every episode recorded in it.

Parameters:

Name Type Description Default
root Path

The data root the session's directory sits under; nothing guesses it.

required
name str | None

The session's directory name: not empty, starting with no ., neither starting nor ending with a space, and with no /, backslash or control character. None creates a new session under a generated name of two words, like brisk-heron, that nothing under root has.

None
task str | None

What every episode in the session is of, e.g. "pick up the red cube"; None keeps what an existing session last said, or none for a new one.

None
operator str | None

Who records the episodes; None as for task.

None

Raises:

Type Description
TypeError

root is not a path, None included.

InvalidArgumentError

root is empty, name is not one directory name, or the session's session.json cannot be parsed, or a newer release saved it in a format this one may misread.

OperationError

root or the session's directory cannot be created, or its session.json cannot be read or written.

Methods:

Name Description
discard_last_episode

Deletes the newest episode and the layout beside it, so the next episode recorded takes its index.

remove

Deletes the session, which holds no episode: its session.json and its directory.

Attributes:

Name Type Description
name str

The session's name: its directory's under the data root.

path

The session's directory, <root>/<name>.

task str | None

What every episode in the session is of; None where it was never said.

operator str | None

Who records the session's episodes; None where it was never said.

name instance-attribute

name: str

The session's name: its directory's under the data root.

path instance-attribute

path

The session's directory, <root>/<name>.

task instance-attribute

task: str | None

What every episode in the session is of; None where it was never said.

operator instance-attribute

operator: str | None

Who records the session's episodes; None where it was never said.

discard_last_episode

discard_last_episode() -> str | None

Deletes the newest episode and the layout beside it, so the next episode recorded takes its index.

Returns:

Type Description
str | None

The deleted episode's name, e.g. "0003"; None for a session with no episodes.

Raises:

Type Description
BusyError

The newest episode is still being recorded, by this process or another; stop that recording first.

OperationError

The session's directory cannot be read, or the episode or its layout cannot be deleted.

remove

remove() -> None

Deletes the session, which holds no episode: its session.json and its directory.

Raises:

Type Description
OperationError

The session still holds an episode, or a file that is not its own, or its directory cannot be read or deleted.

SessionSummary dataclass

SessionSummary(
    name: str,
    path: Path,
    task: str | None,
    operator: str | None,
    episodes: int,
    modified_at: datetime,
)

One session under a data root, as sessions lists it.

Attributes:

Name Type Description
name str

The session's name: its directory's under the data root.

path Path

The session's directory, <root>/<name>.

task str | None

What every episode in the session is of; None where it was never said.

operator str | None

Who records the session's episodes; None where it was never said.

episodes int

How many episodes the session holds.

modified_at datetime

When the session's newest episode was last written, else when its directory was, in UTC.

name instance-attribute

name: str

The session's name: its directory's under the data root.

path instance-attribute

path: Path

The session's directory, <root>/<name>.

task instance-attribute

task: str | None

What every episode in the session is of; None where it was never said.

operator instance-attribute

operator: str | None

Who records the session's episodes; None where it was never said.

episodes instance-attribute

episodes: int

How many episodes the session holds.

modified_at instance-attribute

modified_at: datetime

When the session's newest episode was last written, else when its directory was, in UTC.

Annotation dataclass

Annotation(
    key: str, value: str, log_time_ns: int | None = None
)

One thing said about an episode: a key and its value, on the whole episode or pinned to one moment of it.

Attributes:

Name Type Description
key str

What is said, e.g. "grasp"; never empty, which annotate refuses.

value str

What it is, e.g. "slipped"; may be empty.

log_time_ns int | None

The moment it is pinned to, a log time on the episode's timeline, in nanoseconds since the Unix epoch.

key instance-attribute

key: str

What is said, e.g. "grasp"; never empty, which annotate refuses.

value instance-attribute

value: str

What it is, e.g. "slipped"; may be empty.

log_time_ns class-attribute instance-attribute

log_time_ns: int | None = None

The moment it is pinned to, a log time on the episode's timeline, in nanoseconds since the Unix epoch.

The timeline is CLOCK_MONOTONIC set to the wall clock as it read when recording started, so a step of the wall clock during the episode does not move it. None for the whole episode.

CameraCompressed dataclass

CameraCompressed(
    frames: int,
    gop_frames: int,
    jpeg_bytes: int,
    h264_bytes: int,
)

One camera of a compressed episode, as CompressStats.cameras holds it.

The file's anvil.compression metadata keeps the same.

Attributes:

Name Type Description
frames int

The camera's frames, each a JPEG before and one H.264 frame after.

gop_frames int

Frames from one keyframe to the next: about a second of them at the rate the episode recorded the camera at.

jpeg_bytes int

The camera's JPEGs' size before, added up, in bytes.

h264_bytes int

The camera's H.264 frames' size after, added up, in bytes.

frames instance-attribute

frames: int

The camera's frames, each a JPEG before and one H.264 frame after.

gop_frames instance-attribute

gop_frames: int

Frames from one keyframe to the next: about a second of them at the rate the episode recorded the camera at.

jpeg_bytes instance-attribute

jpeg_bytes: int

The camera's JPEGs' size before, added up, in bytes.

h264_bytes instance-attribute

h264_bytes: int

The camera's H.264 frames' size after, added up, in bytes.

CompressStats dataclass

CompressStats(
    source_bytes: int,
    compressed_bytes: int,
    cameras: dict[str, CameraCompressed],
)

What Compression.run did: the file's size before and after, and each camera's.

Attributes:

Name Type Description
source_bytes int

The episode's size before, in bytes.

compressed_bytes int

The episode's size after, in bytes.

cameras dict[str, CameraCompressed]

Each camera compressed, by name.

source_bytes instance-attribute

source_bytes: int

The episode's size before, in bytes.

compressed_bytes instance-attribute

compressed_bytes: int

The episode's size after, in bytes.

cameras instance-attribute

cameras: dict[str, CameraCompressed]

Each camera compressed, by name.

Compression

Compression(
    path: Path,
    crf: int | None = None,
    preset: str | None = None,
)

A compression of a finished episode in place, every camera's JPEGs to H.264, lossy and for good.

run does it, blocking, and cancel, from any other thread, stops it. Nothing happens to the file until run. Running it needs ffmpeg on PATH.

Prepares the compression of the finished episode at path; nothing is read until run.

Parameters:

Name Type Description Default
path Path

The episode's file.

required
crf int | None

x264's constant rate factor, 0 to 51, lower keeping more; None is 23, x264's own default.

None
preset str | None

x264's preset, "ultrafast" to "placebo", how much time it spends per frame for the size; None is "veryfast". A name x264 does not know fails run.

None

Raises:

Type Description
TypeError

path is not a path, None included.

InvalidArgumentError

crf is negative or above 51.

Methods:

Name Description
run

Compresses the episode, blocking until done; the compressed file replaces it once written and read back.

cancel

Stops a run under way on another thread promptly, killing its encoders; a run after it does nothing.

Attributes:

Name Type Description
path

The episode's file, as given.

path instance-attribute

path

The episode's file, as given.

run

run() -> CompressStats | None

Compresses the episode, blocking until done; the compressed file replaces it once written and read back.

It runs ffmpeg, one process per camera, and takes seconds to minutes. A failure or a cancel leaves the episode as it was. Never run it on an episode being recorded: one with no summary yet is refused, but only the caller knows whether a recording is about to finish into it.

Returns:

Type Description
CompressStats | None

What the compression did; None once canceled, at once for a run after cancel.

Raises:

Type Description
NotFoundError

There is no file at path.

OperationError

ffmpeg is not on PATH; the file is not a finished episode or has no JPEG camera, one already compressed included; <path>.compressing is already there; preset is not one x264 knows, or the encoder failed on a frame; or reading or writing the files failed.

cancel

cancel() -> None

Stops a run under way on another thread promptly, killing its encoders; a run after it does nothing.

A run that has already replaced the file by then finishes as if it was not canceled.

Recorder

Recorder(
    workcell: WorkcellConfig,
    runtime: Runtime,
    cameras: Mapping[str, Camera] | None = None,
    config: RecorderConfig | None = None,
    readiness: Report | None = None,
)

Records episodes of a running workcell, one at a time, each into the next file of a Session.

start begins an episode and stop finishes it and returns what it counted; a recorder then records the next one the same way. Each episode reads the runtime and the cameras through readers of its own, on threads of its own, so the loop never waits on it. An episode's file is never another's: two recorders starting in one session at once get the next index each. An episode still recording when the recorder is garbage collected is finished then, labeled Outcome.UNLABELED.

event, log, failed and stats may be called from any thread, a stop on another one included.

Holds what to record; nothing is checked or opened until start.

Parameters:

Name Type Description Default
workcell WorkcellConfig

The workcell the runtime runs, written into every episode.

required
runtime Runtime

The anvil.control.Runtime to record.

required
cameras Mapping[str, Camera] | None

The cameras to record, by the names workcell gives them: an anvil.camera.Cameras, or any mapping of some of them; None records none.

None
config RecorderConfig | None

How to record; None is RecorderConfig.rated(), every source at its default rate.

None
readiness Report | None

The anvil.hw.Report the robot came up on, whose hardware, the physical robot it found, each episode records; None for a run with no readiness check, such as the sim's.

None

Methods:

Name Description
recording

Returns whether an episode is being recorded: from a start that returned until its stop is called.

start

Creates session's next episode and its layout, and records into it until stop; path says where.

event

Writes an event of the caller's into the episode being recorded, stamped now.

annotate

Pins key and its value to now in the episode being recorded, as one of its annotations.

log

Writes message on topic, a channel of the caller's own, into the episode being recorded, stamped now.

failed

Returns why the episode being recorded stopped being written before stop asked it to, once it has.

stats

Returns what the episode being recorded has counted so far, as of a moment ago.

stop

Stops reading, writes what was read and finishes the file, blocking until it is written.

Attributes:

Name Type Description
workcell

The workcell recorded, as given.

runtime

The runtime recorded, as given.

cameras

The cameras recorded, by name.

config

How each episode is recorded.

readiness

The readiness report each episode records the hardware of; None for none.

path Path | None

The file of the episode being recorded, <session>/<NNNN>.mcap; None while none is.

workcell instance-attribute

workcell

The workcell recorded, as given.

runtime instance-attribute

runtime

The runtime recorded, as given.

cameras instance-attribute

cameras

The cameras recorded, by name.

config instance-attribute

config

How each episode is recorded.

readiness instance-attribute

readiness

The readiness report each episode records the hardware of; None for none.

path instance-attribute

path: Path | None

The file of the episode being recorded, <session>/<NNNN>.mcap; None while none is.

recording

recording() -> bool

Returns whether an episode is being recorded: from a start that returned until its stop is called.

start

start(session: Session) -> None

Creates session's next episode and its layout, and records into it until stop; path says where.

Recording runs on threads of its own; this returns once it has started. A start that fails leaves the recorder idle, to start again; one that fails writing the episode's file removes it and its layout.

Parameters:

Name Type Description Default
session Session

The session to record the episode into; it stays the caller's.

required

Raises:

Type Description
TypeError

workcell is not an anvil.workcell.WorkcellConfig.

InvalidArgumentError

config is one the sources cannot meet, as RecorderConfig says; a camera is not one workcell names; or the robot's URDF does not compose. Each is refused before a file is made.

NotFoundError

The session's directory is not there.

OperationError

An episode is being recorded already; the runtime or a camera is closed; or the episode's file or layout cannot be created or written.

event

event(
    source: str,
    kind: str,
    fields: Mapping[str, str] | None = None,
) -> None

Writes an event of the caller's into the episode being recorded, stamped now.

An event is something the loop cannot know, such as who drives the arms, written to the episode's /events channel with its fields in key order.

Parameters:

Name Type Description Default
source str

Who says it, e.g. "web".

required
kind str

What happened, dotted, e.g. "driver.attached".

required
fields Mapping[str, str] | None

What it says, strings by name, e.g. {"driver": "quest"}; None for none.

None

Raises:

Type Description
InvalidArgumentError

A field's value is not a string.

OperationError

Nothing is being recorded, a stop on another thread included; or the episode has stopped being written, as failed says.

annotate

annotate(key: str, value: str) -> None

Pins key and its value to now in the episode being recorded, as one of its annotations.

The episode holds them once stop finishes it, in the order they were pinned, each at the log time of the call, and annotations reads them back as it does the ones annotate writes into a finished episode, which replaces them. Each is also written into /events as it is pinned, an annotation event from recorder with key and value fields, so it shows on the episode's timeline and is kept by an episode that never stops.

Parameters:

Name Type Description Default
key str

What is said, e.g. "phase"; never empty.

required
value str

What it is, e.g. "grasp"; may be empty.

required

Raises:

Type Description
InvalidArgumentError

key is empty.

OperationError

Nothing is being recorded, a stop on another thread included; or the episode has stopped being written, as failed says.

log

log(topic: str, message: object) -> None

Writes message on topic, a channel of the caller's own, into the episode being recorded, stamped now.

For what a part above the loop saw, such as a headset's raw input; it is written in every mode, as it comes.

Parameters:

Name Type Description Default
topic str

The channel, / and a name, e.g. "/quest/input"; never under one the recorder writes itself, /robot/, /controller/ or /camera/, nor /events, /observation or /robot_description.

required
message object

The message, anything json.dumps takes, written as that JSON.

required

Raises:

Type Description
TypeError

json.dumps cannot encode message.

InvalidArgumentError

topic is not / and a name, or is one the recorder writes itself.

OperationError

Nothing is being recorded, a stop on another thread included; or the episode has stopped being written, as failed says.

failed

failed() -> str | None

Returns why the episode being recorded stopped being written before stop asked it to, once it has.

The disk filling is one reason. Nothing more goes into the episode from then, what was written before stays, and stop raises saying the same.

Returns:

Type Description
str | None

Why it stopped being written; None while it is being written.

Raises:

Type Description
OperationError

Nothing is being recorded.

stats

stats() -> RecorderStats

Returns what the episode being recorded has counted so far, as of a moment ago.

messages_written stays 0 until stop finishes the file.

Raises:

Type Description
OperationError

Nothing is being recorded.

stop

stop(
    outcome: Outcome = UNLABELED, note: str | None = None
) -> RecorderStats

Stops reading, writes what was read and finishes the file, blocking until it is written.

How the episode went goes in the file's anvil.outcome metadata, and what it counted in its anvil.stats. The recorder is idle afterwards, whatever this raises, ready to start the next episode.

Parameters:

Name Type Description Default
outcome Outcome

How the episode went; label changes it later.

UNLABELED
note str | None

Why, in the operator's words; None for no note.

None

Returns:

Type Description
RecorderStats

What the episode counted.

Raises:

Type Description
OperationError

Nothing is being recorded, or writing the file failed, saying why; what was written before stays in it.

LiveView

LiveView(
    workcell: WorkcellConfig,
    runtime: Runtime,
    cameras: Mapping[str, Camera] | None = None,
    address: str | None = None,
)

Serves a running workcell to Foxglove viewers as it runs, over a WebSocket, until stopped.

A viewer sees what an episode would hold, as it happens: in the Foxglove desktop app, Open connection, Foxglove WebSocket, then ws://<host>:<port>, and import layout to draw the robot, each camera and every joint. Each source is kept at a RecorderConfig.rated() recording's default rate. It reads the runtime and the cameras through readers of its own, on threads of its own, so the loop never waits on it and a Recorder records beside it. Each viewer is sent only what its panels subscribe to, and one that falls behind loses messages rather than slowing the others. Nothing it serves is encrypted or authenticated, so it listens on this machine alone unless given an address.

start begins serving and stop ends it; a live view then starts again the same way. One still serving when it is garbage collected stops then.

Holds what to serve; nothing is checked or opened until start.

Parameters:

Name Type Description Default
workcell WorkcellConfig

The workcell the runtime runs, whose robot the viewers draw.

required
runtime Runtime

The anvil.control.Runtime to serve.

required
cameras Mapping[str, Camera] | None

The cameras to serve, by the names workcell gives them: an anvil.camera.Cameras, or any mapping of some of them; None serves none.

None
address str | None

Where to listen, "<host>:<port>", the host an address or a name this machine resolves, such as "0.0.0.0:8765" for every interface, and port 0 for a free one; None is this machine alone, on port 8765, the one Foxglove offers first. Refused at start.

None

Methods:

Name Description
serving

Returns whether it is serving: from a start that returned until stop.

start

Listens on address and serves the runtime and the cameras to every viewer that connects, until stop.

stop

Stops serving and lets every viewer go, blocking until its threads have stopped; stopped, it does nothing.

Attributes:

Name Type Description
workcell

The workcell served, as given.

runtime

The runtime served, as given.

cameras

The cameras served, by name.

address

Where it listens, as given; None for this machine alone, on port 8765.

port int | None

The port it serves on, the one chosen for port 0; None while it is not serving.

layout dict[str, Any] | None

The Foxglove layout that draws it, as an episode's .foxglove-layout.json holds one.

workcell instance-attribute

workcell

The workcell served, as given.

runtime instance-attribute

runtime

The runtime served, as given.

cameras instance-attribute

cameras

The cameras served, by name.

address instance-attribute

address

Where it listens, as given; None for this machine alone, on port 8765.

port instance-attribute

port: int | None

The port it serves on, the one chosen for port 0; None while it is not serving.

layout instance-attribute

layout: dict[str, Any] | None

The Foxglove layout that draws it, as an episode's .foxglove-layout.json holds one.

It draws the robot in a 3D panel, each camera in an Image panel and every joint's position in a Plot; import it in Foxglove with Layouts, Import from file, once written to a file. None while it is not serving.

serving

serving() -> bool

Returns whether it is serving: from a start that returned until stop.

start

start() -> None

Listens on address and serves the runtime and the cameras to every viewer that connects, until stop.

Serving runs on threads of its own; this returns once it listens, and sets port and layout. A start that fails leaves it stopped, to start again.

Raises:

Type Description
TypeError

workcell is not an anvil.workcell.WorkcellConfig.

InvalidArgumentError

address is not "<host>:<port>" with a host this machine resolves; a camera is not one workcell names, or is given twice; or the robot's URDF does not compose.

BusyError

Something else listens on address already.

OperationError

It is serving already; the runtime or a camera is closed; or address cannot be listened on, such as a port under 1024 without the right to it.

stop

stop() -> None

Stops serving and lets every viewer go, blocking until its threads have stopped; stopped, it does nothing.

VideoCodec

Bases: StrEnum

Codec a dataset's videos are encoded with.

Attributes:

Name Type Description
AV1

AV1 through SVT-AV1, LeRobot's default: small files, slow to encode.

H264

H.264 through x264: larger files, several times faster to encode.

AV1 class-attribute instance-attribute

AV1

AV1 through SVT-AV1, LeRobot's default: small files, slow to encode.

H264 class-attribute instance-attribute

H264

H.264 through x264: larger files, several times faster to encode.

LeRobotExport dataclass

LeRobotExport(
    path: Path,
    num_episodes: int,
    num_frames: int,
    fps: int,
    cameras: list[str],
    tasks: list[str],
)

What DatasetExport.run wrote.

Attributes:

Name Type Description
path Path

Dataset's root directory, what LeRobot's LeRobotDataset(root=...) is given.

num_episodes int

Number of episodes written, one per episode given.

num_frames int

Number of frames written across every episode.

fps int

Dataset's frame rate, in frames per second: the anchor camera's, as its episodes recorded it.

cameras list[str]

Each camera's name, in feature order; its video is the feature observation.images.<camera>.

tasks list[str]

Each distinct task, at its task index.

path instance-attribute

path: Path

Dataset's root directory, what LeRobot's LeRobotDataset(root=...) is given.

num_episodes instance-attribute

num_episodes: int

Number of episodes written, one per episode given.

num_frames instance-attribute

num_frames: int

Number of frames written across every episode.

fps instance-attribute

fps: int

Dataset's frame rate, in frames per second: the anchor camera's, as its episodes recorded it.

cameras instance-attribute

cameras: list[str]

Each camera's name, in feature order; its video is the feature observation.images.<camera>.

tasks instance-attribute

tasks: list[str]

Each distinct task, at its task index.

DatasetExport

DatasetExport(
    episodes: Sequence[Path],
    out_dir: Path,
    anchor_camera: str | None = None,
    codec: VideoCodec | None = None,
    fallback_task: str | None = None,
)

An export of recorded episodes into a new LeRobot dataset.

run does it, blocking for minutes while it encodes video, and cancel, from any other thread, stops it. Nothing is read or written until run. Running it needs ffmpeg on PATH.

Prepares the export of episodes into out_dir; nothing is read until run.

Parameters:

Name Type Description Default
episodes Sequence[Path]

Paths of the episodes, in the order the dataset numbers them.

required
out_dir Path

Directory to write the dataset into; it must not exist. Its missing parent directories are created, and stay on a failure.

required
anchor_camera str | None

Name of the camera whose frames the dataset's are, and whose recorded rate is the dataset's; it must have been recorded at a whole number of frames per second. None is the first camera by name.

None
codec VideoCodec | None

Codec to encode every video with; None is VideoCodec.AV1.

None
fallback_task str | None

Task of an episode whose session named none (anvil.recorder.Session's task); None is "unspecified".

None

Raises:

Type Description
TypeError

episodes is one path rather than a list of them, or out_dir or an episode is not a path, None included.

InvalidArgumentError

codec is not one of VideoCodec.

Methods:

Name Description
run

Writes the dataset, blocking until done; a failure or a cancel removes out_dir again.

cancel

Stops a run under way on another thread at the next record it reads, killing its ffmpeg.

Attributes:

Name Type Description
out_dir

The directory the dataset is written into, as given.

out_dir instance-attribute

out_dir

The directory the dataset is written into, as given.

run

run() -> LeRobotExport | None

Writes the dataset, blocking until done; a failure or a cancel removes out_dir again.

The episodes are MCAP files as the recorder writes them, compressed or not, in any mix, and must be one workcell's: episodes whose workcell, actuators, cameras or frame rate differ are refused, naming the first difference. There is one dataset frame per period of the anchor camera's recorded rate, each the anchor's frame nearest it and every other source's latest sample at or before that frame, so a camera that delivers fewer frames than its rate repeats one rather than playing back fast. Frames before every source has a sample are left out, and so is any frame where the loop commanded no position, as when it holds the arms limp while idle, so LeRobotExport.num_frames can be far fewer than the frames recorded. Each camera is encoded on a thread of its own, by an ffmpeg at the lowest priority. out_dir is created before any episode is read, so one already there is refused at once.

Returns:

Type Description
LeRobotExport | None

What was written, with LeRobotExport.path ready to open in LeRobot; None once canceled, at once for a run after cancel.

Raises:

Type Description
InvalidArgumentError

episodes is empty, or out_dir is an empty path.

NotFoundError

An episode is not there, or ffmpeg is not on PATH.

OperationError

An episode cannot be read, holds nothing yet, as one still being recorded does, or differs from the first in its arms' variant, actuators, cameras or frame rate; anchor_camera names a camera an episode lacks, or one recorded at a rate that is not a whole number; out_dir exists or cannot be created; or ffmpeg fails.

cancel

cancel() -> None

Stops a run under way on another thread at the next record it reads, killing its ffmpeg.

A run after it does nothing.

sessions

sessions(root: Path) -> list[SessionSummary]

Lists every session under the data root root, the most recently written first.

Every directory under root whose name a session could have is a session; anything else there, a hidden directory among them, is passed over.

Parameters:

Name Type Description Default
root Path

The data root.

required

Returns:

Type Description
list[SessionSummary]

A summary of each session, the most recently written first; empty for a root that does not exist yet.

Raises:

Type Description
TypeError

root is not a path, None included.

InvalidArgumentError

A session's session.json cannot be parsed.

OperationError

root, a session's directory or its session.json cannot be read.

layout_path

layout_path(episode: Path) -> Path

Returns where the Foxglove layout that draws episode is written, beside it; whether either exists is not asked.

Foxglove opens the episode with this layout to draw its robot, cameras and joints.

Parameters:

Name Type Description Default
episode Path

The episode's file, <session>/<NNNN>.mcap.

required

Returns:

Type Description
Path

The layout's path, <session>/<NNNN>.foxglove-layout.json.

Raises:

Type Description
TypeError

episode is not a path, None included.

InvalidArgumentError

episode names no file, as Path(""), which is ., does not.

annotations

annotations(episode: Path) -> list[Annotation]

Reads what was said about the finished episode: its newest annotations, as annotate last wrote them.

Parameters:

Name Type Description Default
episode Path

The episode's file.

required

Returns:

Type Description
list[Annotation]

The annotations; empty where nothing was said.

Raises:

Type Description
TypeError

episode is not a path, None included.

InvalidArgumentError

The file is not a finished episode, or its annotations cannot be read.

NotFoundError

There is no file at episode.

OperationError

The file cannot be opened.

annotate

annotate(
    episode: Path, annotations: Sequence[Annotation]
) -> None

Replaces the finished episode's annotations with annotations, in place, keeping the ones before as history.

It appends to the file, so it takes as long on an episode of any size. Never call it on an episode being recorded or compressed: one with no summary yet is refused, but only the caller knows whether a recording or a compression is about to finish into it. A failure leaves the file as it was, except one partway through writing, which leaves its messages whole and its summary cut; mcap recover rebuilds it.

Parameters:

Name Type Description Default
episode Path

The episode's file.

required
annotations Sequence[Annotation]

The annotations it holds from now on; empty clears them.

required

Raises:

Type Description
TypeError

episode is not a path, None included.

InvalidArgumentError

An annotation has an empty key, or the file is not a finished episode, one still being recorded included.

NotFoundError

There is no file at episode.

OperationError

The file cannot be read or written.

label

label(
    episode: Path, outcome: Outcome, note: str | None = None
) -> None

Relabels how the finished episode went, in place, keeping the labels before as history.

The new label is a new anvil.outcome record, the one Recorder.stop wrote and any since kept behind it. As with annotate, it takes as long on an episode of any size, and is never for an episode being recorded or compressed.

Parameters:

Name Type Description Default
episode Path

The episode's file.

required
outcome Outcome

How it went.

required
note str | None

Why, in the operator's words; None for no note.

None

Raises:

Type Description
TypeError

episode is not a path, None included.

InvalidArgumentError

The file is not a finished episode, one still being recorded included.

NotFoundError

There is no file at episode.

OperationError

The file cannot be read or written.

mesh

mesh(filename: str) -> bytes

Returns the bytes of a mesh a composed URDF names, from the copy shipped in the package.

So drawing the robot, or carrying a copy of it off the box, needs no description tree.

Parameters:

Name Type Description Default
filename str

The mesh's filename as a composed URDF gives it, its path in robot-descriptions/, e.g. "arms/openarm/v2/meshes/link1.stl".

required

Returns:

Type Description
bytes

The mesh file's contents.

Raises:

Type Description
InvalidArgumentError

No shipped URDF names a mesh filename.