dynamic-mani SESSION RECORDING

Guides

On this page

Session recording

Robot recordings use an episode and review workflow: engage teleoperation with a to begin a take, then pause or park to seal it. Only TELEOP events are saved. Startup, idle, pause, fault, and shutdown samples are discarded. Each episode has separate event, camera, and tactile files. Review changes metadata and retains the local samples. Camera-only recordings use the same startup confirmation and take workflow.

#Recording types

--robot selects the control rig; --record TYPE independently selects the saved signals and cameras. UR5e and UR5e + Sharpa use these default BC profiles:

Type Signals RGB views
bc-ur5e Arm commands, targets, measured joints, wrist/EE poses, control events ZED left, D405 color, wrist
bc-ur5e-sharpa Arm + hand joints, glove, landmarks, physical five-fingertip tactile, control events ZED left, wrist
hand-eye-calib Arm commands, targets, measured joints, wrist/EE poses, control events Two ZED devices × left/right = four views
zed-only Camera frames and calibration metadata; requires --robot none Two ZED devices × left/right = four views
bash
uv run dmani --robot ur5e --mode hw --record bc-ur5e
uv run dmani --robot ur5e-sharpa --mode hw --record bc-ur5e-sharpa
uv run dmani --robot ur5e --mode hw --record hand-eye-calib
uv run dmani --robot none --record zed-only
# Choose the archive directory separately:
uv run dmani --robot ur5e --mode hw --record hand-eye-calib --record-output out/hand-eye-take

Profiles live in configs/recording/. Selecting a type leaves the robot input, retargeting, IK, state machine, and actuator sinks unchanged. hand-eye-calib requires the arm-only --robot ur5e; hand rigs are rejected before launch. No hand driver, glove, or hand retargeting nodes are started. Arm feedback, command safety, and y startup confirmation stay active. An explicitly named type enables its cameras in sim, dummy, or hardware mode. Bare --record uses the default profile and retains mock-input camera opt-out. Legacy directory paths such as --record out/take still work.

--robot none defaults to zed-only and uses the shared state manager and keyboard. Press y to confirm startup, wait for IDLE, then a to start recording. a pauses/resumes, b returns to idle, and c shuts down. Only TELEOP takes are saved; y/x keeps/excludes a paused or idle take. No robot, hand, glove, Vive, IK, or simulator is started. Automatic/headless launches are rejected. Sessions open in dmani-view. Only complete stereo pairs are saved; unpaired_camera_frames counts any unmatched frames omitted at shutdown.

hand-eye-calib and zed-only store zed_1_left, zed_1_right, zed_2_left, and zed_2_right as 1280 × 720 RGB images from 60 FPS stereo capture. hand-eye-calib saves each view at 30 FPS; zed-only saves each view at 60 FPS. Each ZED is opened once; its two eye streams share capture timestamps and frame indices. Capture timing between the two devices is independent. Their USB serials match, so the profile pins distinct /dev/v4l/by-path/ ports. Update these sources if moving USB ports. Missing or duplicate devices stop capture startup; --no-cameras is incompatible with these stereo profiles. Depth is not saved.

Calibration metadata in session.json includes each eye's pixel-space K, fx/fy/cx/cy, distortion model and coefficients, native resolution, and unrectified image status. camera_calibrations preserves the complete factory files and SHA-256 hashes. The current files belong to serials 36155166 and 31793552; startup reads each ZED identity and rejects a mismatch before any nodes start. Values use the raw factory calibration. The native calibration views cannot use --image-size. --print-dataflow shows configured intrinsics without querying devices; synthetic sources mark intrinsics unavailable.

#Calibrate the two left eyes

See 2. Camera extrinsic calibration for board setup, offline fitting, validation, and the camera-to-base preview.

#Automatic hardware recording

Hardware launches through dmani, dmani-ur5e, tianji, dmani-hand, dmani-replay, or dmani-policy deploy record automatically by default. This includes hardware replay. The launcher prints a new out/recordings/<local timestamp>/ directory; no --record flag is needed. The run archive's run.json also names that directory as recording_dir.

bash
# Active hardware control with automatic telemetry capture:
uv run dmani --robot ur5e --mode hw

# Choose a new telemetry directory:
uv run dmani --robot ur5e --mode hw --record-output out/my-hardware-run --no-cameras

During TELEOP, the archive retains each received arm/hand joint command, state-manager target, and measured joint position, plus available wrist/glove inputs and control/safety events. No rows or image/tactile payloads are written outside TELEOP. These are the joint-position streams published by the current drivers; velocity, torque, and motor current are not added by this change.

UR5e + Sharpa hardware automatically records all five tactile fingertips and adds the wrist and ZED cameras from its default bc-ur5e-sharpa profile. No --record flag is needed:

bash
uv run dmani --robot ur5e-sharpa --mode hw

The session manifest is created when the dataflow starts; data saving begins only at the TELEOP state edge after a and ends when TELEOP exits. The launcher checks camera device paths, Sharpa SDK files/network routing, and local UDP joint-feedback port 50000 before starting any nodes. An occupied port is reported with its socket owner; close that application normally before restarting. A missing configured camera blocks startup; it is never silently omitted. Enabled recording cameras must be connected. Live operation also requires actual SDK binaries in hand_hw.sdk_root and keyboard y confirmation after measured-pose enable.

Use --no-record on the teleop, hand-only, or replay launchers to disable saving:

bash
uv run dmani --robot ur5e-sharpa --mode hw --no-record
uv run dmani-hand --mode hw --no-record

No out/recordings session is created. Recording-camera nodes and Sharpa tactile acquisition are skipped, including tactile startup and missing/expired-frame checks. Run diagnostics still go to out/runs; measured-pose startup, keyboard y, and all joint-command safety checks remain active. --record and explicit --camera options conflict with --no-record. Omitting --no-record restores the default capture behavior.

Option on UR5e + Sharpa hardware Session saved Recording cameras Tactile acquisition/checks
Default Telemetry, images, tactile Enabled Enabled, all five fingertips
--no-cameras Telemetry and tactile Disabled Enabled, all five fingertips
--no-record None Disabled Disabled

Other hardware rigs use --record for the live-input camera presets below; --no-cameras explicitly suppresses cameras. Cameras already used by a policy are included in its hardware recording. Simulation and dummy CLI runs require --record or dmani-record run.

The direct uv run dora run dataflows/teleop_hw.yml graph also records, including when its driver dry-run environment flags are set. Its recorder logs the chosen path and reads an optional DMANI_RECORD_OUTPUT override. It saves telemetry without cameras or interactive take-review keys; completed takes can be reviewed offline. Recorder failures follow the existing safety-stop path.

#Start a recording

UR5e with the hand selected in configs/ur5e_wuji.yml:

bash
# Device-free MuJoCo check; exits automatically after eight seconds of TELEOP:
uv run dmani-record run --headless --duration 8

# Live Vive + Wuji glove, controlling MuJoCo:
uv run dmani-record run --input vive --operator weison --task "pick and place"

# Live physical UR5e + a supported Wuji hand:
uv run dmani-record run --input vive --mode hw

dmani-record run defaults to simulation and mock inputs. The dmani teleop launcher defaults to Vive for UR5e, plus the Wuji glove for hand rigs. Live hardware still requires y during BRINGUP, after enable holds the measured pose. The Tianji + Sharpa hardware block, selected-arm isolation, and all command gates remain in effect.

Tianji uses the same recorder:

bash
uv run tianji --arm-mode bimanual --record
uv run tianji --arm-mode right --input vive --record
uv run tianji --arm-mode right --mode dummy --headless --duration 8 --record

Omit --output on dmani-record run, or supply bare --record on tianji, to automatically name the directory from the local session start time: out/recordings/YYYY-MM-DD_HH-MM-SS.ffffff±HHMM/, for example out/recordings/2026-09-16_19-08-05.123456-0700/. Microseconds distinguish quick consecutive runs; the suffix records the UTC offset. Both launchers print the chosen path. An explicit directory overrides automatic naming and must be new. --operator and --task are optional labels. --print-dataflow shows the wiring without launching devices or creating a recording. The direct hardware graph also enables recording as described above.

#RGB camera recording

The bc-ur5e-sharpa profile selects wrist + ZED. bc-ur5e, Wuji, and Tianji defaults also include the D405 for explicit live-input recordings. The table shows the current UR5e + Sharpa preset; other rigs keep their own saved sizes. The same camera nodes serve MuJoCo, dummy, and supported hardware sinks.

Recording stream Stored RGB size Capture / saved FPS USB selection
zed_2i 1280 × 720, native left unrectified view 60 / 30 ZED 2i UVC interface
wrist 640 × 480, full view 90 / 30 HD USB Camera, video-index0
realsense_d405 256 × 256, color view 30 / 30 D405 serial 318523070403, video-index4

Only RGB images are acquired and saved from these cameras. Depth, infrared, point clouds, IMU data, and camera audio are not captured. The ZED's UVC frames contain both eyes side by side; zed2i:PATH selects the first half as the left view before encoding. The ZED requests 2560 × 720 YUYV capture at 60 FPS with four driver buffers. This is the documented ZED 2i HD720 mode. The full left 1280 × 720 view is saved at its original resolution without resizing or cropping. The node reads at 60 FPS and selects frames for saving at 30 FPS with their source timestamps. Startup rejects a negotiated mode below 60 FPS. A six-second passive check on 2026-09-23 opened the real ZED in this mode, extracted the left eye, and encoded and wrote 350 native 1280 × 720 JPEG files using the former 60 FPS save preset. The rate between first and last host read timestamps was 60.02 FPS (median interval 16.67 ms). This camera-only check does not include the current 30 FPS save setting, Dora session recorder, or full teleop load.

The wrist uses MJPG capture and four driver buffers. Its native 640 × 480 view is preserved without cropping. capture_fps: 90 paces reads, while fps: 30 selects frames for encoding and saving. A device may negotiate a faster native mode; the node still reads at the configured capture rate. A camera-only check of the former 60 FPS save setting measured 89.99 FPS capture and 60.06 FPS saved through the current USB hub.

UR5e per-camera settings live under recording.cameras in the profile YAML; Wuji and Tianji retain inline rig settings. Legacy source strings use recording.image_size and recording.camera_fps defaults. --image-size explicitly overrides all stored views to a square, and --camera-fps overrides both capture and saved rates for every selected camera. Replacing a named source, such as --camera wrist=/dev/video0, retains that camera's preset unless those overrides are supplied.

bash
# Live teleop recording with all RGB cameras (MuJoCo by default):
uv run dmani --robot ur5e --record

# Same cameras with UR5e + Wuji hand:
uv run dmani --robot ur5e-wuji --record

# Same cameras with Tianji + Sharpa (left, right, or bimanual):
uv run tianji --arm-mode right --input vive --record

# Inspect the graph without opening devices or creating an archive:
uv run dmani-record run --robot ur5e --input vive --print-dataflow

# Explicit camera options replace the selected camera list:
uv run dmani-record run --headless --camera zed_2i=synthetic --camera realsense_d405=synthetic --camera wrist=synthetic

# Record robot telemetry without opening cameras:
uv run dmani --robot ur5e --record --no-cameras

BC default sources use /dev/v4l/by-id/ paths; changing /dev/videoN numbers after reconnecting cannot silently select a different sensor or depth stream. Update the selected profile's recording.cameras if a camera is replaced. Images are stored under images/zed_2i/, images/realsense_d405/, and images/wrist/, with timestamps and RGB/view metadata in the session archive. Camera failures trigger the existing recording fault path. Bare --record with mock input or replay does not activate these defaults; select a named type or supply --camera to add views there. USB power/link faults must be resolved before collecting physical-camera data.

#Sharpa tactile recording

UR5e + Sharpa hardware recording enables the SDK tactile stream automatically, including hardware replay and policy recording. --no-cameras keeps tactile capture enabled. An explicit --no-record run skips tactile acquisition and its health checks along with session saving. Each fresh per-channel frame in a recording saves its available RAW, RAW_JPEG, DEFORM, F6, and CONTACT_POINT blocks. Image bytes and float32 sensor values are preserved in NPZ files; no image resizing, JPEG re-encoding, or tactile calibration is applied.

The hand_tactile_frame Parquet events index those files with the hand side, finger name, original SDK channel, frame ID when supplied, block shapes/dtypes, and unmodified SDK timestamp. sample_monotonic marks host callback receipt; it is not a hardware exposure timestamp. Left thumb-to-pinky channels are 9, 8, 7, 6, 5; right channels are 4, 3, 2, 1, 0.

Acquisition copies every fresh SDK callback frame into a bounded queue, then publishes all queued frames on the recorder's tactile tick and flushes the tail at shutdown. The recorder saves frames only during TELEOP; a frame captured before engagement is excluded even if it arrives after the episode opens. Out-of-episode completion frames are drained but not saved. The recorder waits for the completion marker before closing. Repeated SDK stamps are not saved. Queue overflow raises a recording fault rather than silently dropping frames. Every channel must produce data within five seconds of stream startup and continue advancing within two seconds; shutdown also requires at least one frame from every finger. Missing or frozen tactile feedback raises a fault. Simulation contact projections remain a separate stream/schema, and dummy mode does not synthesize hardware tactile readings.

Load a frame using the relative NPZ path in its event's text column:

python
with np.load(session_path / row["text"], allow_pickle=False) as frame:
    forces = frame["F6"]
    raw_image = frame["RAW"]  # When supplied by the SDK.

#Record only the Vive trajectory

For a wrist trajectory take, vive-record captures the filtered wrist source without starting Dora, a simulator, or any actuator driver:

bash
uv run vive-record

Press Space to start and again to stop. The UI shows tracking health, the trail, recording state, frame count, and destination. Closing an active take also saves it when at least two valid poses were captured. Recenter and trail clearing change only the display; the saved poses remain in the same Z-up, metre, xyzw frame used by live teleoperation.

The new directory contains trajectory.npz, config.yml, and recording.json. Replay remains source-level: the first pose is clutched to the arm, then the normal mapper, IK, state manager, and command gate run. The launcher automatically selects the take's config.yml unless --config explicitly overrides it. Preview it in MuJoCo and the non-connecting dummy backend before hardware:

bash
uv run dmani --robot ur5e --replay out/vive-recordings/<take>
uv run dmani --robot ur5e --mode dummy --replay out/vive-recordings/<take>
uv run dmani --robot ur5e --mode hw --replay out/vive-recordings/<take>

The hardware command is interactive: measured-pose hold and keyboard y confirmation are mandatory, followed by a after IDLE. It never sends recorded joint commands directly.

#Operator workflow

Key Action
y during BRINGUP Confirm live startup
a in IDLE or PAUSE Engage and open a new episode
a in TELEOP Pause, hold measured joints, and seal the episode
y in IDLE or PAUSE Keep the most recently sealed, unreviewed episode
x in IDLE or PAUSE Exclude that episode; retain its local data
b Park and end the current episode
c Shut down through the existing state machine

Repeated state messages do not create extra episodes. A new engagement clears the live review prompt; an unanswered take remains pending and can be reviewed offline. Episode numbers are never reused. Review keys do nothing during active motion. A fault or interrupted recording marks the active take incomplete, which prevents replay export and keep approval.

#What is saved

Each launched session contains:

text
session.json                  rig, joint order, episode paths, counters
config.yml                    rig snapshot with absolute asset paths and selected arm mode
episodes/episode_000001/events/00000000.parquet  atomically published TELEOP events
episodes/episode_000001/images/wrist/000000000123.jpg  optional camera frame
episodes/episode_000001/tactile/left/channel_9/000000000124.npz  SDK blocks
episodes/episode_000002/events/00000000.parquet  next TELEOP take
reviews/episode_000001.json    explicit keep/exclude decision and operator

Available subscribed streams are:

Tianji's current live/mock input graph supplies wrists and mock hand commands. Its hand commands and feedback are recorded; raw glove/landmark streams require an input graph that publishes them. UR5e live input supplies both glove and wrist streams. Camera selection and rates follow the rig presets above. Add --camera front=/dev/video0 (repeat for more views) to choose other RGB sources. Generic cameras without rig defaults use 256 square pixels at 30 Hz. --image-size and --camera-fps override the selected presets. Frames are saved as JPEG files before their timestamped Parquet entries. The camera timestamps identify host read completion rather than hardware exposure time.

See Policy Learning to export kept episodes into a training dataset and run a learned policy through the same robot sinks. Policy rollout archives are labelled source_kind: policy; training export excludes them unless explicitly requested.

The archive uses timestamped Parquet events with the project's existing PyArrow dependency. Continuous numeric streams save up to one real sample per topic on each 30 Hz episode-relative slot. State and safety events remain event driven; every physical SDK tactile frame is retained. The wrist camera selects frames at 30 FPS, and dataset export aligns all streams on a 30 Hz grid by default. It is not a URDS-compatible dataset.

Numeric payloads retain the bus's float32 values. Poses are metres and xyzw quaternions; joints are radians. Every event has separate receipt-monotonic, wall-clock, and transport timestamps. A producer's sample_monotonic is preserved when supplied, along with all event metadata. If it is absent, the source timestamp is null; receipt time is never labelled as capture time. Nonfinite payloads and invalid source timestamps remain available for diagnosis.

The recorder orders inputs within a 50 ms transport window. Events outside TELEOP are discarded; a delayed frame with a valid pre-engagement capture time is also discarded. Ordering errors on saved events are counted. A late state or review event makes episode boundaries ambiguous and blocks approval/export. This cannot detect or recover samples lost before receipt, including upstream/Dora queue eviction.

#System data viewer

Open the local Viser viewer for saved sessions or a specific episode:

bash
uv run dmani-view
uv run dmani-view out/recordings/2026-09-21_15-14-17.258762-0700
uv run dmani-view out/recordings/<session>/episodes/episode_000001
# Include captures saved outside the standard recordings directory:
uv run dmani-view --root out
# Open the page automatically if desired (default: print the URL only):
uv run dmani-view --browser --port 8768

Viser shows the robot and background without task objects, even when the saved simulation scene includes them. It displays an interactive 3D model with a docked sensor panel and one shared playback cursor. Choose a session and episode, play at 0.25–4×, scrub, or step by 100 ms. Orbit and zoom the robot with the mouse. Use Refresh archive to pick up published chunks from an unfinalized session. The viewer does not change review decisions or recording files.

All synchronization uses recorder receipt time. Every stream holds its latest preceding sample inside its episode, with no future samples borrowed and no interpolation. Playback is empty between takes; plots break at episode boundaries. Source-to-receipt ages appear only when a valid source timestamp exists and are not driver send timing metrics. Incomplete takes and published chunks from unfinalized recordings remain viewable for diagnosis.

The Viser server binds only to 127.0.0.1 (default port 8768). --port 0 selects a free port. It reads existing session manifests, Parquet events, JPEGs, and NPZ payloads. Robot geometry uses the saved configuration and local model assets; if geometry is unavailable, sensor data remains usable. Ctrl+C stops it.

#Review and save a LeRobot v3 dataset

bash
uv run lerobot out/recordings/2026-09-25_02-06-12.021251-0700
# Open the browser automatically if desired (default: print the local URL):
uv run lerobot out/recordings/<session> --browser --port 8769

The local dark page shows the recorded RGB cameras and five fingertip tactile streams: RAW and DEFORM frames plus F6 force readings. The robot view is hidden by default. Click Show 3D replay to display the recorded robot with a solid measured pose, a cyan IK command overlay, and the saved Vive tracker pose. The tracker is shown in room coordinates relative to its first sample in each episode and placed beside the robot for comparison. The view uses the same Viser robot scene as replay and follows the camera/tactile playback cursor when playing, scrubbing, or selecting another episode. Click again to hide it. Select each sealed episode, play or scrub its media, then click Keep this episode or Mark useless. An optional note is saved in reviews/episode_*.json with the decision; the recorded events and payloads are left intact. The review moves to the next pending take. Incomplete takes remain viewable but cannot be kept. The page requires a decision for every sealed take before enabling Save as LeRobot v3 dataset.

If the review buttons are dimmed, read the reason beneath them. An interrupted session may still say recording after its writer has stopped. A nonzero boundary_errors count means a state or review event arrived out of order, so the saved episode tags need an audit before review. The media remains viewable. Click Recover and audit saved takes on the review page, or Audit saved takes if the session was already recovered. Recovery checks that no recorder holds the session lock and rebuilds counts from published chunks; the audit checks every saved event against its episode's sequence and receipt-time range, including any incomplete trailing take, and checks camera files. If it passes, review the sealed takes and mark each one Keep or Mark useless. Incomplete takes remain excluded from the dataset, and the original boundary_errors value stays in the archive and export provenance.

For a session already marked closed or recovered, the equivalent audited override is also available when starting the reviewer:

bash
uv run lerobot out/recordings/<session> --trust-recorded-boundaries --task "Toss the object"

Dataset conversion still checks required streams, timing, and freshness; the output provenance records that the saved boundaries were explicitly trusted. The audit cannot recover the original timing of the late control event, so inspect the first and last frames of each take before keeping it. --task fills the shared task instruction field; you can edit it on the page. If no task is specified or recorded, the field defaults to Tossing putaozhi. The output directory must be new; the page suggests out/datasets/<session>-lerobot-v3. Saving runs in the background with a progress bar for frame conversion, video encoding, and finalization. It exports only explicitly kept, sealed episodes and uses the existing 30 Hz causal alignment and freshness checks, with measured arm/hand joint positions as observation.state, state-manager joint targets as action, and each recorded RGB camera as an MP4 observation.images.* stream. The result includes LeRobot v3 metadata, task labels, alignment timestamps, and dynamic-mani provenance. Tactile frames and raw glove payloads remain in the source session for inspection. No data is uploaded. Export errors appear in the page; an unsuccessful export does not publish a partial output directory.

#Standalone joint angle HTML

Generate one self-contained HTML page with every recorded arm and hand joint:

bash
uv run dmani-joints out/recordings/<session>/episodes/episode_000001
uv run dmani-joints out/recordings/<session> --episode 1 --mark-time 4 --output out/reports/joint_angles.html

The command prints the output path and does not open a browser. Open that file manually to compare IK/retargeted commands, state-manager targets, sent commands when recorded, and measured angles in degrees. The page has per-joint plots, joint-name filtering, time-range controls, and recorded arm limits. --mark-time uses seconds from the start of the selected episode and adds a marker plus before/after/close-up buttons. An episode path selects that take directly; use --episode with a session path containing multiple takes. The default output is out/reports/recording-<session>/episode_000001_joint_angles.html for episode 1. This reads only saved data and never connects to a robot.

#Inspect, review, and replay

To visualize saved robot feedback and commands together in MuJoCo:

bash
uv run dmani-record view out/recordings/<session> --episode 1
# Slow playback to half speed:
uv run dmani-record view out/recordings/<session> --episode 1 --speed 0.5

The solid robot follows recorded measured joints; the cyan overlay follows recorded arm_joint_cmd and hand_joint_cmd. This is offline kinematic playback using the saved rig configuration and recorder receipt timestamps. It opens no hardware connections and does not rerun IK or simulate a new robot response. Incomplete takes can be viewed without changing their review/export eligibility. Omit --episode to view all recorded takes. New archives contain no startup, idle, pause, fault, or shutdown samples; older full-session archives remain readable. An episode selection uses the stored tags even if boundary errors were reported. In the viewer, Space pauses/resumes, R restarts, and Q/Esc closes. Playback holds the final frame until restarted or closed.

bash
uv run dmani-record inspect out/recordings/2026-09-16_19-08-05.123456-0700
uv run dmani-record review out/recordings/2026-09-16_19-08-05.123456-0700 --episode 1 --decision keep --operator weison
uv run dmani-record review out/recordings/2026-09-16_19-08-05.123456-0700 --episode 2 --decision exclude
uv run dmani-record export out/recordings/2026-09-16_19-08-05.123456-0700 --episode 1 --output out/replay/task_001_episode_1
uv run dmani-replay out/replay/task_001_episode_1

Replay uses the current rig selected by dmani.env, including its tuned initial pose and active arms. Add --recorded-config to use the exported config.yml instead, or --config PATH to choose another rig YAML. The recording itself is unchanged. The selected config path is printed before launch.

At the end, replay holds the final arm and hand commands and keeps the program open. Press b to return to the configured default pose and rewind. Once parked, the robot waits in the same IDLE state as startup; a starts another pass and c shuts down. Automatic replay keeps these controls when a viewer is present. Headless replay holds until the process is stopped.

Episode numbers start at 1. Offline review requires that the writer has stopped; live review uses the keyboard. Only an explicit keep marks a sealed take eligible. There is no uploader, automatic deletion, or implicit keep on exit.

Export creates trajectory.npz, config.yml, and export.json in a new directory. It is an explicit replay view, so a sealed pending/excluded take can also be exported for investigation. Raw archive files remain unchanged.

Alignment uses each wrist timestamp and the latest preceding hand sample within --max-age (default 0.25 seconds). Both inputs use source timestamps if all are present; otherwise both use receipt timestamps, recorded in export.json. Initial hand warm-up is trimmed; internal or trailing stale gaps cause an error. Landmarks are preferred to raw glove input. Available telemetry is aligned with validity masks and original sample timestamps; it does not drive replay. Exports load through the existing replay source, mapper, IK, GHR retargeter, orchestrator, and safety gates. Deliberate playback holds remain fresh during startup ramps and pauses; repeated hand frames reuse their solved pose without advancing GHR filter history. A stalled new solve still expires normally.

Tianji wrist-only exports hold the hands idle during replay. Mock UR5e captures contain hand commands but no glove/landmark source, so they support inspection, not finger-source replay. Real UR5e captures provide the required source input. Source replay re-anchors each take at the configured idle pose and recomputes control; it does not promise identical measured motion.

Older wrist-only and IK-demo NPZ captures still work with dmani-replay. New tianji --record sessions use this archive format and need the explicit episode export above before replay.

#Durability and limits

Live buffering is bounded: 4,096 events in the ordering window, 4,096 in the writer queue, and at most 512 rows in a chunk. The writer also flushes every 0.5 seconds. Completed chunks are independently readable before the session ends. Queue overflow, disk errors, or disk space below the 256 MiB reserve raise an explicit recording failure and send SAFETY_STOP through the state manager.

After a crashed recorder, recover only the published prefix:

bash
uv run dmani-record recover out/recordings/interrupted_session

Recovery refuses a running writer, checks published per-episode chunks against the manifest, and marks an unfinished take incomplete. Older full-session archives still reconstruct boundaries from their state events. Pending chunks and an unflushed tail are not assumed to have survived. This is process-crash recovery, not a power-loss guarantee.

The config snapshot and hashes identify configured calibration/assets. Those external files must remain available; the archive does not bundle GHR, meshes, or all implicitly resolved GHR defaults. Matching calibration and retargeter settings remain necessary for replay.