From 731414ea565663ba9a44ae9815ac85ef3793423a Mon Sep 17 00:00:00 2001 From: Corbin Date: Sat, 18 Jul 2026 18:27:28 -0700 Subject: [PATCH 1/4] Add SmolVLA local training doc; ignore outputs/ Document the verified local SmolVLA training setup (Python 3.11 venv, vendored lerobot, the mandatory --dataset.video_backend=pyav workaround) and git-ignore outputs/ so training artifacts aren't committed. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 2 ++ SMOLVLA_TRAINING.md | 85 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 SMOLVLA_TRAINING.md diff --git a/.gitignore b/.gitignore index 6edce72..96f7df8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .venv/ +.venv-lerobot/ .venv-zed/ pyzed-*.whl PyOpenGL*.whl @@ -16,6 +17,7 @@ config/.env # Runtime / calibration artifacts runs/ +outputs/ data/calibration/*.npy data/calibration/*.json data/calibration/*.png diff --git a/SMOLVLA_TRAINING.md b/SMOLVLA_TRAINING.md new file mode 100644 index 0000000..50d9f4e --- /dev/null +++ b/SMOLVLA_TRAINING.md @@ -0,0 +1,85 @@ +# SmolVLA — local training (Track B) + +How to fine-tune a **SmolVLA** policy locally on this machine's GPU, using the +vendored LeRobot. This is the SmolVLA half of Track B; MolmoAct is trained +separately on Modal. + +Verified working on an Apple Silicon Mac (MPS) on 2026-07-18. + +--- + +## One-time setup + +The system default Python (3.13) is too new for the ML stack — use **Python 3.11**. + +```bash +# from the repo root +python3.11 -m venv .venv-lerobot +./.venv-lerobot/bin/python -m pip install --upgrade pip +./.venv-lerobot/bin/python -m pip install -e "./rebot_setup/vendor/rebot_lerobot/lerobot[smolvla]" +``` + +Confirm it worked: + +```bash +./.venv-lerobot/bin/lerobot-train --help # should print usage +./.venv-lerobot/bin/python -c "import torch; print('mps:', torch.backends.mps.is_available())" +``` + +## Important: the `pyav` flag is mandatory on this Mac + +The default video reader (`torchcodec`) is broken here — it can't link the system +FFmpeg libraries and crashes the moment training reads a recording. **Every** +training command must therefore include: + +``` +--dataset.video_backend=pyav +``` + +`pyav` is already installed and bundles its own FFmpeg, so no system install is +needed. Leave this flag out and training fails at the first batch. + +## Train + +```bash +HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ + --policy.type=smolvla \ + --dataset.repo_id= \ + --dataset.video_backend=pyav \ + --policy.device=mps \ + --policy.push_to_hub=false \ + --batch_size=8 \ + --steps=5000 \ + --save_freq=1000 \ + --output_dir=outputs/smolvla/ \ + --wandb.enable=false +``` + +- Replace `` with the recorded dataset (e.g. the LeRobot repo id + the data team uses; a locally recorded dataset lives at + `~/.cache/huggingface/lerobot/`). +- Checkpoints are written under `outputs/smolvla//checkpoints/`. The + `pretrained_model/` folder inside each checkpoint is what the inference/harness + side loads. +- `outputs/` is git-ignored — sync checkpoints off-machine (Drive / HF) rather + than committing them. + +## Smoke test (no real data needed) + +To prove the machine can train before real recordings exist, run a few steps on a +public SmolVLA example dataset: + +```bash +HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ + --policy.type=smolvla \ + --dataset.repo_id=lerobot/svla_so101_pickplace \ + --dataset.episodes='[0, 1]' \ + --dataset.video_backend=pyav \ + --policy.device=mps \ + --batch_size=2 --steps=20 --save_freq=20 --eval_freq=0 \ + --output_dir=outputs/smolvla/smoke_test \ + --wandb.enable=false +``` + +Success = loss prints and decreases, and a checkpoint appears under +`outputs/smolvla/smoke_test/checkpoints/`. From 43115888f46226e7e6a61a1cfa883c768ff77fa9 Mon Sep 17 00:00:00 2001 From: Corbin Date: Sat, 18 Jul 2026 19:05:52 -0700 Subject: [PATCH 2/4] SmolVLA: warm-start from smolvla_base instead of training from scratch Fine-tune the pretrained lerobot/smolvla_base (--policy.path) rather than --policy.type=smolvla, which trains from scratch and won't learn a usable policy on ~150 episodes. Matches the official "warm-start from SO-101 base" recipe. Co-Authored-By: Claude Opus 4.8 --- SMOLVLA_TRAINING.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/SMOLVLA_TRAINING.md b/SMOLVLA_TRAINING.md index 50d9f4e..8d25092 100644 --- a/SMOLVLA_TRAINING.md +++ b/SMOLVLA_TRAINING.md @@ -6,6 +6,13 @@ separately on Modal. Verified working on an Apple Silicon Mac (MPS) on 2026-07-18. +**Fine-tune, don't train from scratch.** We warm-start from the pretrained +`lerobot/smolvla_base` checkpoint — a SmolVLA that already knows how to move an +arm — and adapt it to our data. This matches the official hackathon recipe +("warm-start from the SO-101 base"). Training from scratch (`--policy.type=smolvla`) +will not learn a usable policy on ~150 episodes, so we always pass +`--policy.path=lerobot/smolvla_base` instead. + --- ## One-time setup @@ -43,7 +50,7 @@ needed. Leave this flag out and training fails at the first batch. ```bash HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ - --policy.type=smolvla \ + --policy.path=lerobot/smolvla_base \ --dataset.repo_id= \ --dataset.video_backend=pyav \ --policy.device=mps \ @@ -71,7 +78,7 @@ public SmolVLA example dataset: ```bash HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ - --policy.type=smolvla \ + --policy.path=lerobot/smolvla_base \ --dataset.repo_id=lerobot/svla_so101_pickplace \ --dataset.episodes='[0, 1]' \ --dataset.video_backend=pyav \ From 73f2f7d142874711a5aebf6e078f1a8aa8d31a58 Mon Sep 17 00:00:00 2001 From: Corbin Date: Sat, 18 Jul 2026 19:42:56 -0700 Subject: [PATCH 3/4] SmolVLA: document camera rename_map + empty_cameras for warm-start smolvla_base expects cameras camera1/2/3; our data is front/side. Add the required --rename_map and --policy.empty_cameras=1 to the train + smoke-test commands. Full warm-start recipe verified end-to-end on MPS. Co-Authored-By: Claude Opus 4.8 --- SMOLVLA_TRAINING.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/SMOLVLA_TRAINING.md b/SMOLVLA_TRAINING.md index 8d25092..c7e03bd 100644 --- a/SMOLVLA_TRAINING.md +++ b/SMOLVLA_TRAINING.md @@ -46,6 +46,26 @@ training command must therefore include: `pyav` is already installed and bundles its own FFmpeg, so no system install is needed. Leave this flag out and training fails at the first batch. +## Important: camera names must match the base model + +`smolvla_base` was pretrained expecting **three** cameras named +`observation.images.camera1`, `camera2`, `camera3`. Our dataset's cameras are +named differently (`front`, `side`) and there are only two of them, so training +stops with a "Feature mismatch" error unless you: + +1. **Rename** your camera keys to `camera1`/`camera2` with `--rename_map`, and +2. **Pad** the missing third camera with `--policy.empty_cameras=1`. + +For our reBot dataset (`front` = overhead, `side` = 45°) that is: + +``` +--rename_map='{"observation.images.front": "observation.images.camera1", "observation.images.side": "observation.images.camera2"}' +--policy.empty_cameras=1 +``` + +(Confirm the exact camera key names against the real dataset's `meta/info.json` +before the first real run — adjust the map if they differ.) + ## Train ```bash @@ -53,6 +73,8 @@ HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ --policy.path=lerobot/smolvla_base \ --dataset.repo_id= \ --dataset.video_backend=pyav \ + --rename_map='{"observation.images.front": "observation.images.camera1", "observation.images.side": "observation.images.camera2"}' \ + --policy.empty_cameras=1 \ --policy.device=mps \ --policy.push_to_hub=false \ --batch_size=8 \ @@ -82,11 +104,15 @@ HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ --dataset.repo_id=lerobot/svla_so101_pickplace \ --dataset.episodes='[0, 1]' \ --dataset.video_backend=pyav \ + --rename_map='{"observation.images.side": "observation.images.camera1", "observation.images.up": "observation.images.camera2"}' \ + --policy.empty_cameras=1 \ --policy.device=mps \ --batch_size=2 --steps=20 --save_freq=20 --eval_freq=0 \ --output_dir=outputs/smolvla/smoke_test \ --wandb.enable=false ``` -Success = loss prints and decreases, and a checkpoint appears under -`outputs/smolvla/smoke_test/checkpoints/`. +(This public dataset's cameras are `side`/`up`, hence the different rename map +than our real reBot data.) Success = loss prints and decreases, and a checkpoint +appears under `outputs/smolvla/smoke_test/checkpoints/`. Verified end-to-end on +MPS 2026-07-18. From 371a75eab03f0a9837b17e154300adc72b084af6 Mon Sep 17 00:00:00 2001 From: toyeshhm Date: Sat, 18 Jul 2026 23:44:37 -0700 Subject: [PATCH 4/4] feat: harden hackathon Rerun and training workflow --- SMOLVLA_TRAINING.md | 48 +++- docs/HACKATHON_SUBMISSION_DEMO.md | 151 +++++++++++ docs/Rerun_bounty_progress.md | 3 +- docs/TEAM.md | 8 +- docs/p5_rerun_port/README.md | 8 +- .../examples/hackathon_smoke_query_report.md | 29 +++ p5_rerun_port/README.md | 5 +- p5_rerun_port/constants.py | 8 + p5_rerun_port/takes.py | 31 ++- p5_rerun_port/tests/test_rerun_hardening.py | 244 ++++++++++++++++++ p5_rerun_port/urdf_log.py | 28 +- p5_rerun_port/viewer_blueprint.py | 59 +++++ .../teleop_gui/controlled_record.py | 13 +- .../teleop_gui/training_workspace.py | 13 +- requirements.txt | 1 + 15 files changed, 623 insertions(+), 26 deletions(-) create mode 100644 docs/HACKATHON_SUBMISSION_DEMO.md create mode 100644 docs/p5_rerun_port/examples/hackathon_smoke_query_report.md create mode 100644 p5_rerun_port/tests/test_rerun_hardening.py create mode 100644 p5_rerun_port/viewer_blueprint.py diff --git a/SMOLVLA_TRAINING.md b/SMOLVLA_TRAINING.md index c7e03bd..38b0a42 100644 --- a/SMOLVLA_TRAINING.md +++ b/SMOLVLA_TRAINING.md @@ -4,7 +4,9 @@ How to fine-tune a **SmolVLA** policy locally on this machine's GPU, using the vendored LeRobot. This is the SmolVLA half of Track B; MolmoAct is trained separately on Modal. -Verified working on an Apple Silicon Mac (MPS) on 2026-07-18. +Verified working on an Apple Silicon Mac (MPS) on 2026-07-18 using the +repository's standard LeRobot environment, +`rebot_setup/vendor/rebot_lerobot/.venv`. **Fine-tune, don't train from scratch.** We warm-start from the pretrained `lerobot/smolvla_base` checkpoint — a SmolVLA that already knows how to move an @@ -17,20 +19,34 @@ will not learn a usable policy on ~150 episodes, so we always pass ## One-time setup -The system default Python (3.13) is too new for the ML stack — use **Python 3.11**. +The system default Python (3.13) is too new for the ML stack. Build the +repository's pinned **Python 3.11** environment, then add the SmolVLA policy +dependencies to that same environment: ```bash # from the repo root -python3.11 -m venv .venv-lerobot -./.venv-lerobot/bin/python -m pip install --upgrade pip -./.venv-lerobot/bin/python -m pip install -e "./rebot_setup/vendor/rebot_lerobot/lerobot[smolvla]" +./rebot_setup/setup.sh +uv pip install \ + --python rebot_setup/vendor/rebot_lerobot/.venv/bin/python \ + 'transformers>=4.57.1,<5.0.0' \ + 'num2words>=0.5.14,<0.6.0' \ + 'accelerate>=1.7.0,<2.0.0' \ + 'safetensors>=0.4.3,<1.0.0' ``` +The `uv pip` form is intentional: the environment built by `setup.sh` does not +install the `pip` module. Also do not install the full vendored +`lerobot[smolvla]` extra into this shared environment: that package metadata +pins `rerun-sdk<0.27` and would downgrade the Rerun 0.34 runtime required by the +Query API integration. `setup.sh` has already installed vendored LeRobot; the +four packages above are the only SmolVLA additions it needs. + Confirm it worked: ```bash -./.venv-lerobot/bin/lerobot-train --help # should print usage -./.venv-lerobot/bin/python -c "import torch; print('mps:', torch.backends.mps.is_available())" +rebot_setup/vendor/rebot_lerobot/.venv/bin/lerobot-train --help +rebot_setup/vendor/rebot_lerobot/.venv/bin/python -c \ + "import torch; print('mps:', torch.backends.mps.is_available())" ``` ## Important: the `pyav` flag is mandatory on this Mac @@ -69,7 +85,7 @@ before the first real run — adjust the map if they differ.) ## Train ```bash -HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ +HF_HUB_ENABLE_HF_TRANSFER=1 rebot_setup/vendor/rebot_lerobot/.venv/bin/lerobot-train \ --policy.path=lerobot/smolvla_base \ --dataset.repo_id= \ --dataset.video_backend=pyav \ @@ -99,7 +115,7 @@ To prove the machine can train before real recordings exist, run a few steps on public SmolVLA example dataset: ```bash -HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ +HF_HUB_ENABLE_HF_TRANSFER=1 rebot_setup/vendor/rebot_lerobot/.venv/bin/lerobot-train \ --policy.path=lerobot/smolvla_base \ --dataset.repo_id=lerobot/svla_so101_pickplace \ --dataset.episodes='[0, 1]' \ @@ -107,12 +123,18 @@ HF_HUB_ENABLE_HF_TRANSFER=1 ./.venv-lerobot/bin/lerobot-train \ --rename_map='{"observation.images.side": "observation.images.camera1", "observation.images.up": "observation.images.camera2"}' \ --policy.empty_cameras=1 \ --policy.device=mps \ - --batch_size=2 --steps=20 --save_freq=20 --eval_freq=0 \ + --policy.push_to_hub=false \ + --batch_size=2 --steps=20 --log_freq=1 --save_freq=20 --eval_freq=0 \ + --num_workers=0 \ --output_dir=outputs/smolvla/smoke_test \ --wandb.enable=false ``` (This public dataset's cameras are `side`/`up`, hence the different rename map -than our real reBot data.) Success = loss prints and decreases, and a checkpoint -appears under `outputs/smolvla/smoke_test/checkpoints/`. Verified end-to-end on -MPS 2026-07-18. +than our real reBot data.) Success = all 20 steps report finite loss and +`checkpoints/000020/pretrained_model/model.safetensors` exists. Do not require a +monotonic loss curve from only 20 shuffled mini-batches. This exact command path +was verified end-to-end on MPS on 2026-07-18: it loaded 2 public episodes / 569 +frames, trained 100M of 450M parameters, printed finite losses for every step, +saved the step-20 checkpoint, and exited 0. It does not validate our reBot data +or autonomous arm motion. diff --git a/docs/HACKATHON_SUBMISSION_DEMO.md b/docs/HACKATHON_SUBMISSION_DEMO.md new file mode 100644 index 0000000..dc12d6a --- /dev/null +++ b/docs/HACKATHON_SUBMISSION_DEMO.md @@ -0,0 +1,151 @@ +# Hackathon submission and judge demo + +Use this as the short presentation layer. Detailed setup and safety procedures +remain in `docs/Rerun_bounty_progress.md`, `docs/p5_rerun_port/README.md`, and +`p3_vlm_orchestrator/PERSON4_RUNBOOK.md`. + +## 60-second pitch + +> We ported the Rerun SO-101 learning loop to reBot, a different seven-joint +> robot with Damiao motors, two named cameras, and an extra wrist-yaw degree of +> freedom. One episode flows from joint, goal, camera, and URDF logging into a +> local Rerun catalog; the Query API compares commanded and observed motion so +> we can reject bad takes; selected episodes export in a reBot LeRobot v3 +> schema; and the same trajectory can be replayed or handed to our guarded +> learned-policy runner. The important part is the loop, not a URDF screenshot: +> collect, inspect, curate, export, and close the loop. Our checked-in evidence +> proves that entire path with synthetic hardware. [Only after a successful +> rehearsal: We also ran the same path on the physical reBot.] The result is a +> reproducible non-SO-101 integration with auditable safety and data contracts. + +Do not say the bracketed live sentence unless the live row in the evidence +table below has been filled with a recording, command log, and outcome. + +## Prize-track mapping + +| Track | Submission position | Evidence and remaining gate | +|---|---|---| +| Rerun non-SO-101 end-to-end port ($1k) | **Primary** | `p5_rerun_port` covers log, record, catalog/query, LeRobot export, and replay for a 7-DOF reBot. Synthetic path is documented green; physical end-to-end remains a required venue proof. | +| Rerun Query API ($2k) | **Secondary** | `query_api_cli` uses the Rerun server/reader path for schema, entity reads, and goal-versus-position comparison. The tracked example is `docs/p5_rerun_port/examples/cans_query_report.md`; current proof is synthetic. | +| Interesting Rerun Viewer ($2k) | **Additional submission** | Interactive recordings activate a purpose-built Blueprint with synchronized front/side cameras, the 3D reBot URDF, goal-versus-position traces, and the time panel in one operator view. Rehearse the real Viewer before claiming live use. | + +## Evidence boundary + +| Capability | Synthetic evidence currently supported | Live evidence required before claiming it | +|---|---|---| +| reBot telemetry, cameras, goals, and URDF in Rerun | `log_rebot --fake --teleop`; tracked progress marks dry-run done | Saved live `.rrd` showing the physical seven-joint follower, both real cameras, and URDF | +| Episode recording and catalog | `record_episode --fake`; dry-run `.rrd`, trajectory sidecar, frames, and catalog path documented | One clean physical can-to-zone episode with matching `.rrd`, metadata, trajectory, and camera frames | +| Query API curation | Schema/entity/goal-vs-position commands and tracked example report | Run the same query against the physical episode and show the episode ID on screen | +| Central Viewer workflow | The checked-in Blueprint and real synthetic `.rrd` put both cameras, the URDF, and tracking error in one layout | Open the physical episode with the Blueprint active and use it during collection/curation, not only as a final screenshot | +| LeRobot export | Fallback export is documented green with reBot type, seven joints, `front`, then `side` | Load and validate an export made from the physical episode; record exact output path/revision | +| Replay / close loop | `replay_episode --fake` is documented green | Physical replay only after a supervised rehearsal, clear workspace, e-stop operator, and conservative speed | +| Learned autonomous pick | Guarded runner, offline/shadow/live gates, and tests exist | A real checkpoint must pass inspect, offline, shadow, and empty-workspace live gates before filming a can pick | +| Training quality or success rate | Training/evaluation tooling exists | A real checkpoint plus held-out trial report; do not infer success from training loss or code tests | + +## Judge demo sequence + +### 1. State the evidence level (5 seconds) + +Say either “This is the reproducible synthetic pipeline” or “This is the live +pipeline validated in rehearsal.” Never switch labels mid-demo. + +### 2. Show one complete synthetic episode (25 seconds) + +Run from the repository root. A timestamped dataset avoids reusing stale demo +artifacts: + +```bash +export DEMO_DATASET="hackathon-demo-$(date -u +%Y%m%dT%H%M%SZ)" + +python -m p5_rerun_port.record_episode \ + --fake --dataset "$DEMO_DATASET" \ + --task "Pick up one can and place it in the taped sorting zone" \ + --tag "Good episode" --seconds 5 --no-viewer + +rerun "recordings/$DEMO_DATASET/episode_01.rrd" +``` + +In Rerun, point out `follower/position`, `follower/goal`, the seven-joint arm, +and both camera streams. If any entity is absent, stop and use the last +rehearsed artifact without calling the new run successful. + +### 3. Query and curate (15 seconds) + +```bash +python -m p5_rerun_port.query_api_cli --dataset "$DEMO_DATASET" --schema +python -m p5_rerun_port.query_api_cli \ + --dataset "$DEMO_DATASET" --compare goal-vs-position +python -m p5_rerun_port.query_dataset \ + --dataset "$DEMO_DATASET" --tag "Good episode" +``` + +Explain that goal-versus-position error exposes lag, dropped samples, or bad +takes before they enter training. + +### 4. Export and close the synthetic loop (10 seconds) + +```bash +python -m p5_rerun_port.export_lerobot \ + --dataset "$DEMO_DATASET" --tag "Good episode" --fallback +python -m p5_rerun_port.replay_episode \ + --dataset "$DEMO_DATASET" --episode episode_01 \ + --fake --speed 0.5 --no-viewer +``` + +Call this a schema/export and fake-replay proof. Do not call it physical replay. + +### 5. Optional live closeout + +Only use this after the full preflight below and one successful private +rehearsal. Keep a dedicated operator on the physical e-stop/power cut. + +```bash +./rebot_operator_kit/01_check_hardware.command +./rebot_operator_kit/02_dual_camera_check.command + +python -m p5_rerun_port.log_rebot --teleop --seconds 20 +python -m p5_rerun_port.record_episode \ + --dataset cans-live-demo \ + --task "Pick up one can and place it in the taped sorting zone" \ + --tag "Good episode" +python -m p5_rerun_port.query_api_cli \ + --dataset cans-live-demo --compare goal-vs-position +python -m p5_rerun_port.export_lerobot \ + --dataset cans-live-demo --tag "Good episode" +``` + +Physical replay is a separate safety decision. Do not improvise it during the +judge session. If it was approved and rehearsed, use the documented command: + +```bash +python -m p5_rerun_port.replay_episode \ + --dataset cans-live-demo --episode episode_01 --speed 0.5 +``` + +For a learned checkpoint, follow all four gates in +`p3_vlm_orchestrator/PERSON4_RUNBOOK.md`; never jump directly to a can pick. + +## Preflight checklist + +- [ ] The presenter can identify every artifact as synthetic or live. +- [ ] `git status --short` has no unexplained source changes; runtime recordings remain ignored. +- [ ] `python -m p5_rerun_port.record_episode --help` and `python -m p5_rerun_port.query_api_cli --help` open successfully. +- [ ] The timestamped synthetic sequence above has been rehearsed from a fresh dataset name. +- [ ] Rerun opens the saved `.rrd`; both cameras, `follower/position`, and `follower/goal` are visible. +- [ ] Query API schema and goal-versus-position comparison return for the same episode. +- [ ] Export contains seven ordered joints and the `front`, then `side` image contract. +- [ ] Demo screen recording, terminal font size, and backup artifact are ready. +- [ ] Live only: hardware discovery and simultaneous camera checks pass immediately before the demo. +- [ ] Live only: fixed cameras, lighting, taped zone, cables, and arm bases have not moved. +- [ ] Live only: workspace is clear, follower is supported, and one person owns the physical e-stop/power cut. +- [ ] Live only: no other process owns follower or leader serial ports. +- [ ] Live only: one private full-path rehearsal produced a saved `.rrd` and command log. +- [ ] Learned policy only: checkpoint identity, processor files, calibration/profile digest, offline, shadow, and empty-workspace live gates all pass. +- [ ] Off-site copy of the chosen `.rrd`, export, checkpoint (if used), and demo video exists. + +## Failure fallback + +If live hardware, cameras, Rerun, or policy gating fails, stop motion and show +the last verified synthetic `.rrd` plus the tracked query report. State the +failure plainly. A reproducible dry-run with a clear live gap is stronger than +an unsafe or mislabeled “live” claim. diff --git a/docs/Rerun_bounty_progress.md b/docs/Rerun_bounty_progress.md index 915121c..40ca261 100644 --- a/docs/Rerun_bounty_progress.md +++ b/docs/Rerun_bounty_progress.md @@ -21,6 +21,7 @@ | Slice D2 — **Rerun Query API** | done (dry-run) | `query_api_cli`: Server + `reader()` + goal-vs-position; see `QUERY_API.md` | | Slice E — `export_lerobot` | done (dry-run) | `robot_type=seeed_b601_dm_follower`, 7 joints, front/side | | Slice F — `replay_episode` | done (dry-run) | Fake replay OK; live follower TODO on venue | +| Viewer Blueprint | done (synthetic) | Interactive recordings center both cameras, 3D reBot URDF, goal/position traces, and the time panel | | Live arm e2e (no `--fake`) | TODO | Venue: log → record → export → replay | | Demo video + 60s pitch | TODO | | | Off-site sync (RRDs / checkpoints) | TODO | | @@ -36,7 +37,7 @@ |-------|--------| | $1k non-SO-101 port | Code + dry-run; live e2e TODO | | $2k Query API | Implemented: `query_api_cli` + docs (dry-run on fake `.rrd`) | -| $2k interesting Viewer | Not implemented (no custom blueprints/views yet) | +| $2k interesting Viewer | Purpose-built operator Blueprint implemented and synthetic `.rrd` verified; live Viewer rehearsal TODO | --- diff --git a/docs/TEAM.md b/docs/TEAM.md index 4336af2..f66c60a 100644 --- a/docs/TEAM.md +++ b/docs/TEAM.md @@ -60,7 +60,13 @@ **Two model routes — start with the easy one:** -- **SmolVLA (do first — ready now, no cloud):** uses the vendored LeRobot at `rebot_setup/vendor/rebot_lerobot/lerobot/`. Install once: `pip install -e ".[smolvla]"` from that folder. Train: `lerobot-train --policy.type=smolvla --dataset.repo_id= --output_dir=outputs/smolvla_test --steps=200`. Expect auth/format errors on the first run — fixing them early is the whole point of starting at episode 10. +- **SmolVLA (do first — ready now, no cloud):** warm-start + `lerobot/smolvla_base` in the repository's vendored LeRobot Python 3.11 + environment; do not train from scratch with `--policy.type=smolvla` on this + small dataset. Use the tested install, camera rename/padding, PyAV, and MPS + commands in [`SMOLVLA_TRAINING.md`](../SMOLVLA_TRAINING.md). Expect + auth/format errors on the first real-data run — fixing them early is the + point of starting at episode 10. - **MolmoAct (bigger, more setup):** `python -m p5_training.build_dataset_mixture --dataset-root ` → `python -m p5_training.modal_finetune --mixture ...`. Runs on **Modal** (needs `pip install modal` + `modal token new` + credits). ⚠️ Runs a **placeholder trainer** until the real `train_cmd` + base checkpoint are set in `p5_training/configs/molmoact2_single_arm.yaml` — those come from the organizers. **Retrain** checkpoints as data hits 50 / 100 / 150. Log which model looks better. diff --git a/docs/p5_rerun_port/README.md b/docs/p5_rerun_port/README.md index db0e040..ac316b1 100644 --- a/docs/p5_rerun_port/README.md +++ b/docs/p5_rerun_port/README.md @@ -13,6 +13,11 @@ Leaves `p4_data_collection` (native LeRobot record) and `rebot_operator_kit` unt **Query API (post-record):** see [`QUERY_API.md`](./QUERY_API.md) — `python -m p5_rerun_port.query_api_cli`. +**Viewer workflow:** interactive logging/recording activates a purpose-built +Blueprint that keeps both synchronized cameras, the 3D reBot URDF, +goal-versus-position traces, and the time panel visible together. `--no-viewer` +remains fully headless and creates no Viewer or gRPC sink. + ## How it connects (flowchart) ![reBot pipeline: shared setup, GUI track, Rerun bounty loop, training](../assets/rebot_rerun_pipeline_flowchart.png) @@ -101,7 +106,8 @@ python -m p5_rerun_port.export_lerobot --dataset cans --tag "Good episode" --fal python -m p5_rerun_port.replay_episode --dataset cans --episode episode_01 --fake --speed 0.5 --no-viewer ``` -Open any `.rrd` in the Rerun viewer: `rerun recordings/cans/episode_01.rrd` +Interactive record commands open the purpose-built operator Blueprint. To +inspect a saved artifact later: `rerun recordings/cans/episode_01.rrd`. ## Venue (live arm) diff --git a/docs/p5_rerun_port/examples/hackathon_smoke_query_report.md b/docs/p5_rerun_port/examples/hackathon_smoke_query_report.md new file mode 100644 index 0000000..7ff3b58 --- /dev/null +++ b/docs/p5_rerun_port/examples/hackathon_smoke_query_report.md @@ -0,0 +1,29 @@ +# Rerun Query API report — `hackathon_smoke` + +Generated by `python -m p5_rerun_port.query_api_cli`. + +> Synthetic evidence only: this report was generated from a `--fake` episode +> to verify the Rerun Query API path. It is not a physical-arm evaluation or a +> measured can-picking result. + +## Catalog episodes + +| dataset | episode | tag | frames | dur_s | task | +|---|---|---|---:|---:|---| +| hackathon_smoke | episode_01 | Good episode | 6 | 0.9 | Pick up one can and place it in the taped sorting zone | + +## Compare `follower/goal` vs `follower/position` + +``` +episode=episode_01 rows=6 rms=2.4474 +joint mean|err| max|err| +shoulder_pan 2.0088 2.2416 +shoulder_lift 2.1277 2.7888 +elbow_flex 1.6884 2.9247 +wrist_flex 0.4094 1.2612 +wrist_yaw 0.8170 1.2826 +wrist_roll 1.7764 2.6569 +gripper 4.9601 5.9775 +``` + +High mean/max error can mean teleop lag, dropped frames, or a bad take — use this to curate before `export_lerobot`. diff --git a/p5_rerun_port/README.md b/p5_rerun_port/README.md index 49c91e4..9ed9b8e 100644 --- a/p5_rerun_port/README.md +++ b/p5_rerun_port/README.md @@ -2,4 +2,7 @@ Rerun non-SO-101 bounty port (log / record / Query API / export / replay). -**Docs:** [docs/p5_rerun_port/](../docs/p5_rerun_port/) · Query API: [docs/p5_rerun_port/QUERY_API.md](../docs/p5_rerun_port/QUERY_API.md) · progress: [docs/Rerun_bounty_progress.md](../docs/Rerun_bounty_progress.md) +**Docs:** [docs/p5_rerun_port/](../docs/p5_rerun_port/) · Query API: [docs/p5_rerun_port/QUERY_API.md](../docs/p5_rerun_port/QUERY_API.md) · demo/pitch: [docs/HACKATHON_SUBMISSION_DEMO.md](../docs/HACKATHON_SUBMISSION_DEMO.md) · progress: [docs/Rerun_bounty_progress.md](../docs/Rerun_bounty_progress.md) + +Interactive recordings activate a purpose-built Viewer Blueprint with both +cameras, the 3D reBot URDF, goal-versus-position traces, and the time panel. diff --git a/p5_rerun_port/constants.py b/p5_rerun_port/constants.py index f8736b4..892999a 100644 --- a/p5_rerun_port/constants.py +++ b/p5_rerun_port/constants.py @@ -40,6 +40,14 @@ # Prefer SDK URDF; fall back to env override. DEFAULT_URDF_CANDIDATES = ( + REPO_ROOT + / "rebot_setup" + / "vendor" + / "reBotArm_control_py" + / "urdf" + / "reBot-DevArm_fixend_description" + / "urdf" + / "reBot-DevArm_fixend.urdf", Path.home() / "reBotArm_control_py" / "urdf" / "00-arm-rs_asm-v3" / "urdf" / "00-arm-rs_asm-v3.urdf", Path(r"C:\Users\aaron\reBotArm_control_py\urdf\00-arm-rs_asm-v3\urdf\00-arm-rs_asm-v3.urdf"), ) diff --git a/p5_rerun_port/takes.py b/p5_rerun_port/takes.py index 27e0340..6e6235c 100644 --- a/p5_rerun_port/takes.py +++ b/p5_rerun_port/takes.py @@ -35,16 +35,43 @@ def begin_recording( rr = _import_rerun() path.parent.mkdir(parents=True, exist_ok=True) + blueprint = None + if spawn_viewer and not save_only: + try: + from p5_rerun_port.viewer_blueprint import build_hackathon_blueprint + + blueprint = build_hackathon_blueprint() + except Exception as exc: + # Recording must remain usable if a particular SDK build lacks a + # blueprint feature. The FileSink is the durable source of truth. + print(f"WARN: Viewer blueprint unavailable ({exc}); using automatic layout", flush=True) + # Prefer modern multi-sink API; fall back to init+save for older SDKs. try: rec = rr.RecordingStream(APP_ID, recording_id=f"{sanitize_name(dataset)}-{path.stem}") + if spawn_viewer and not save_only and hasattr(rec, "spawn"): + try: + # Spawn without connecting first: set_sinks below then tees the + # stream to both the Viewer and the durable .rrd FileSink. + rec.spawn( + connect=False, + hide_welcome_screen=True, + default_blueprint=blueprint, + ) + except Exception as exc: + print(f"WARN: Rerun Viewer did not start ({exc}); recording to file", flush=True) sinks: list[Any] = [rr.FileSink(str(path))] if spawn_viewer and not save_only: try: sinks.insert(0, rr.GrpcSink()) except Exception: pass - rec.set_sinks(*sinks) + rec.set_sinks(*sinks, default_blueprint=blueprint) + if blueprint is not None and hasattr(rec, "send_blueprint"): + try: + rec.send_blueprint(blueprint, make_active=True, make_default=True) + except Exception as exc: + print(f"WARN: Viewer blueprint was not activated ({exc})", flush=True) if hasattr(rec, "send_recording_name"): rec.send_recording_name(episode) if hasattr(rec, "send_property"): @@ -57,6 +84,8 @@ def begin_recording( except Exception: rr.init(APP_ID, spawn=spawn_viewer and not save_only) rr.save(str(path)) + if blueprint is not None and hasattr(rr, "send_blueprint"): + rr.send_blueprint(blueprint, make_active=True, make_default=True) rr.log("/task", rr.TextDocument(task or ""), static=True) return rr diff --git a/p5_rerun_port/tests/test_rerun_hardening.py b/p5_rerun_port/tests/test_rerun_hardening.py new file mode 100644 index 0000000..5dfc9a7 --- /dev/null +++ b/p5_rerun_port/tests/test_rerun_hardening.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from types import SimpleNamespace + +import numpy as np +import pytest + +from p5_rerun_port.config import resolve_urdf_path +from p5_rerun_port.constants import FOLLOWER, JOINT_NAMES, REPO_ROOT +from p5_rerun_port.rerun_query import ( + compare_goal_vs_position, + list_schema, + open_dataset_server, +) +from p5_rerun_port.urdf_log import log_urdf + + +def test_default_urdf_resolves_to_vendored_model_with_meshes() -> None: + path = resolve_urdf_path() + + assert path == ( + REPO_ROOT + / "rebot_setup/vendor/reBotArm_control_py/urdf" + / "reBot-DevArm_fixend_description/urdf/reBot-DevArm_fixend.urdf" + ) + assert path.is_file() + mesh_dir = path.parent.parent / "meshes" + assert mesh_dir.is_dir() + assert {p.name for p in mesh_dir.glob("*.STL")} >= { + "base_link.STL", + "link1.STL", + "link6.STL", + "end_link.STL", + } + + +def test_vendored_urdf_logs_real_mesh_entities(tmp_path: Path) -> None: + import rerun as rr + + rrd_path = tmp_path / "urdf.rrd" + rec = rr.RecordingStream("deskpartner-urdf-test") + rec.set_sinks(rr.FileSink(str(rrd_path))) + + used = log_urdf(rec) + rec.disconnect() + + assert used == resolve_urdf_path() + assert rrd_path.stat().st_size > 1_000_000 + with open_dataset_server("urdf-test", rrd_paths=[rrd_path]) as dataset: + entities = list_schema(dataset)["entities"] + + assert f"/{FOLLOWER}/urdf/base_link/visual_0" in entities + assert any(entity.endswith("/link6/visual_0") for entity in entities) + assert f"/{FOLLOWER}/urdf/source" in entities + + +def test_urdf_mesh_failure_falls_back_to_static_source_path( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + urdf_path = tmp_path / "robot.urdf" + urdf_path.write_text("", encoding="utf-8") + calls: list[tuple[str, object, bool]] = [] + + class BrokenLogger: + def __init__(self, *_args, **_kwargs) -> None: + raise RuntimeError("missing mesh") + + class TextDocument: + def __init__(self, text: str) -> None: + self.text = text + + rec = SimpleNamespace( + log=lambda entity, value, static=False: calls.append((entity, value, static)) + ) + monkeypatch.setitem(sys.modules, "rerun", SimpleNamespace(TextDocument=TextDocument)) + monkeypatch.setitem( + sys.modules, "rerun_loader_urdf", SimpleNamespace(URDFLogger=BrokenLogger) + ) + + assert log_urdf(rec, urdf_path) == urdf_path + assert len(calls) == 1 + entity, document, static = calls[0] + assert entity == f"{FOLLOWER}/urdf" + assert document.text == f"URDF path: {urdf_path}" + assert static is True + + +def test_query_api_compares_small_real_recording(tmp_path: Path) -> None: + import rerun as rr + + rrd_path = tmp_path / "tracking.rrd" + rec = rr.RecordingStream("deskpartner-query-test") + rec.set_sinks(rr.FileSink(str(rrd_path))) + offsets = np.asarray([1, 2, 3, 4, 5, 6, 7], dtype=np.float64) + for frame in range(3): + rec.set_time("time", sequence=frame) + position = np.full(len(JOINT_NAMES), frame, dtype=np.float64) + rec.log(f"{FOLLOWER}/position", rr.Scalars(position)) + rec.log(f"{FOLLOWER}/goal", rr.Scalars(position + offsets)) + rec.disconnect() + + with open_dataset_server("tracking", rrd_paths=[rrd_path]) as dataset: + schema = list_schema(dataset) + result = compare_goal_vs_position(dataset, episode="episode_smoke") + + assert f"/{FOLLOWER}/position" in schema["entities"] + assert f"/{FOLLOWER}/goal" in schema["entities"] + assert result.episode == "episode_smoke" + assert result.n_rows == 3 + np.testing.assert_allclose(result.mean_abs_error, offsets) + np.testing.assert_allclose(result.max_abs_error, offsets) + assert result.rms_error == pytest.approx(float(np.sqrt(np.mean(offsets**2)))) + + +def test_hackathon_blueprint_prioritizes_cameras_robot_and_tracking() -> None: + import rerun.blueprint as rrb + + from p5_rerun_port.viewer_blueprint import build_hackathon_blueprint + + blueprint = build_hackathon_blueprint() + assert isinstance(blueprint, rrb.Blueprint) + root = blueprint.root_container + views = [] + + def collect(part) -> None: + if hasattr(part, "class_identifier"): + views.append(part) + for child in getattr(part, "contents", ()): + if not isinstance(child, str): + collect(child) + + collect(root) + names = {str(view.name) for view in views} + assert names >= {"Front camera", "Side camera", "reBot URDF", "Goal vs position"} + origins = {str(view.origin) for view in views} + assert origins >= {"/camera/cam0", "/camera/cam1", "/follower/urdf", "/follower"} + + +def test_no_viewer_recording_never_creates_grpc_sink_or_blueprint( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + import p5_rerun_port.takes as takes + import p5_rerun_port.viewer_blueprint as viewer_blueprint + + events: list[tuple] = [] + + class Recording: + def spawn(self, **kwargs) -> None: + events.append(("spawn", kwargs)) + + def set_sinks(self, *sinks, **kwargs) -> None: + events.append(("set_sinks", sinks, kwargs)) + + def log(self, *_args, **_kwargs) -> None: + pass + + fake_rr = SimpleNamespace( + RecordingStream=lambda *_args, **_kwargs: Recording(), + FileSink=lambda path: ("file", path), + GrpcSink=lambda: pytest.fail("headless mode created a GrpcSink"), + TextDocument=lambda text: text, + ) + monkeypatch.setattr(takes, "_import_rerun", lambda: fake_rr) + monkeypatch.setattr( + viewer_blueprint, + "build_hackathon_blueprint", + lambda: pytest.fail("headless mode built a Viewer blueprint"), + ) + + takes.begin_recording( + tmp_path / "headless.rrd", + episode="episode_01", + dataset="cans", + task="pick can", + spawn_viewer=False, + save_only=True, + ) + + assert events == [ + ( + "set_sinks", + (("file", str(tmp_path / "headless.rrd")),), + {"default_blueprint": None}, + ) + ] + + +def test_interactive_recording_activates_hackathon_blueprint( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + import p5_rerun_port.takes as takes + import p5_rerun_port.viewer_blueprint as viewer_blueprint + + events: list[tuple] = [] + marker = object() + + class Recording: + def spawn(self, **kwargs) -> None: + events.append(("spawn", kwargs)) + + def set_sinks(self, *sinks, **kwargs) -> None: + events.append(("set_sinks", sinks, kwargs)) + + def send_blueprint(self, blueprint, **kwargs) -> None: + events.append(("send_blueprint", blueprint, kwargs)) + + def log(self, *_args, **_kwargs) -> None: + pass + + fake_rr = SimpleNamespace( + RecordingStream=lambda *_args, **_kwargs: Recording(), + FileSink=lambda path: ("file", path), + GrpcSink=lambda: ("grpc",), + TextDocument=lambda text: text, + ) + monkeypatch.setattr(takes, "_import_rerun", lambda: fake_rr) + monkeypatch.setattr(viewer_blueprint, "build_hackathon_blueprint", lambda: marker) + + takes.begin_recording( + tmp_path / "viewer.rrd", + episode="episode_01", + dataset="cans", + task="pick can", + spawn_viewer=True, + save_only=False, + ) + + assert events[0] == ( + "spawn", + { + "connect": False, + "hide_welcome_screen": True, + "default_blueprint": marker, + }, + ) + assert events[1][0] == "set_sinks" + assert events[1][2] == {"default_blueprint": marker} + assert events[2] == ( + "send_blueprint", + marker, + {"make_active": True, "make_default": True}, + ) diff --git a/p5_rerun_port/urdf_log.py b/p5_rerun_port/urdf_log.py index 30be4ad..2980938 100644 --- a/p5_rerun_port/urdf_log.py +++ b/p5_rerun_port/urdf_log.py @@ -10,7 +10,7 @@ def log_urdf(rec: Any, urdf_path: Path | None = None, entity: str = f"{FOLLOWER}/urdf") -> Path | None: - """Log the reBot URDF file into the recording (static). Returns path used or None.""" + """Log the reBot URDF meshes/transforms into the recording when available.""" path = resolve_urdf_path(urdf_path) if path is None: print("WARN: reBot URDF not found - set path or clone reBotArm_control_py", flush=True) @@ -18,11 +18,27 @@ def log_urdf(rec: Any, urdf_path: Path | None = None, entity: str = f"{FOLLOWER} try: import rerun as rr - # Path annotation only: mesh import often fails when STL siblings are - # missing/case-mismatched. Joint scalars remain the training signal. - rec.log(entity, rr.TextDocument(f"URDF path: {path}"), static=True) - print(f"urdf: referenced {path} under {entity}", flush=True) + from rerun_loader_urdf import URDFLogger + + logger = URDFLogger(str(path), entity_path_prefix=entity) + # The vendored SolidWorks export keeps ``meshes/`` beside ``urdf/`` + # while its XML uses package-root-relative paths. + package_root = path.parent.parent + if (package_root / "meshes").is_dir(): + logger.root_filepath = package_root + logger.log(recording=rec) + rec.log(f"{entity}/source", rr.TextDocument(f"URDF path: {path}"), static=True) + print(f"urdf: logged meshes and transforms from {path} under {entity}", flush=True) return path except Exception as exc: - print(f"WARN: URDF log failed ({exc}); continuing with joint scalars only", flush=True) + try: + import rerun as rr + + rec.log(entity, rr.TextDocument(f"URDF path: {path}"), static=True) + except Exception: + pass + print( + f"WARN: URDF mesh log failed ({exc}); recorded the source path and continued with joint scalars", + flush=True, + ) return path diff --git a/p5_rerun_port/viewer_blueprint.py b/p5_rerun_port/viewer_blueprint.py new file mode 100644 index 0000000..3c1c621 --- /dev/null +++ b/p5_rerun_port/viewer_blueprint.py @@ -0,0 +1,59 @@ +"""Purpose-built Rerun Viewer layout for collecting and evaluating reBot takes.""" + +from __future__ import annotations + +from typing import Any + +from p5_rerun_port.constants import CAMERA_TO_RERUN, FOLLOWER + + +def build_hackathon_blueprint() -> Any: + """Return the central operator layout used by interactive Rerun recordings. + + Imports stay lazy so ``--no-viewer`` recording and non-Rerun utilities do not + need to initialize the Viewer/blueprint stack. + """ + import rerun.blueprint as rrb + + cameras = rrb.Vertical( + rrb.Spatial2DView( + origin=f"/{CAMERA_TO_RERUN['front']}", + name="Front camera", + ), + rrb.Spatial2DView( + origin=f"/{CAMERA_TO_RERUN['side']}", + name="Side camera", + ), + row_shares=[1, 1], + name="Synchronized cameras", + ) + robot_and_metrics = rrb.Vertical( + rrb.Spatial3DView( + origin=f"/{FOLLOWER}/urdf", + name="reBot URDF", + line_grid=True, + ), + rrb.TimeSeriesView( + origin=f"/{FOLLOWER}", + contents=[ + f"/{FOLLOWER}/position", + f"/{FOLLOWER}/goal", + ], + name="Goal vs position", + ), + row_shares=[3, 2], + name="Embodiment and tracking", + ) + return rrb.Blueprint( + rrb.Horizontal( + cameras, + robot_and_metrics, + column_shares=[2, 3], + name="Can-sorting operator view", + ), + rrb.SelectionPanel(expanded=False), + rrb.BlueprintPanel(expanded=False), + rrb.TimePanel(expanded=True, timeline="time"), + auto_views=False, + auto_layout=False, + ) diff --git a/rebot_operator_kit/teleop_gui/controlled_record.py b/rebot_operator_kit/teleop_gui/controlled_record.py index 759d9e7..cbfe6c1 100644 --- a/rebot_operator_kit/teleop_gui/controlled_record.py +++ b/rebot_operator_kit/teleop_gui/controlled_record.py @@ -352,7 +352,18 @@ def write_profile_sidecar( RERUN_VIEWER_PORT = 9876 -RERUN_NATIVE_BIN = Path(rr.__file__).resolve().parents[1] / "rerun_cli" / "rerun" +_RERUN_CLI_ROOT = Path(rr.__file__).resolve().parents[1] / "rerun_cli" +RERUN_NATIVE_BIN = next( + ( + candidate + for candidate in ( + _RERUN_CLI_ROOT / "rerun", + _RERUN_CLI_ROOT / "Rerun.app" / "Contents" / "MacOS" / "Rerun", + ) + if candidate.is_file() + ), + _RERUN_CLI_ROOT / "rerun", +) def read_control_decision(args: argparse.Namespace, expected_action: str) -> dict[str, Any]: diff --git a/rebot_operator_kit/teleop_gui/training_workspace.py b/rebot_operator_kit/teleop_gui/training_workspace.py index 8101de9..0a65ee9 100644 --- a/rebot_operator_kit/teleop_gui/training_workspace.py +++ b/rebot_operator_kit/teleop_gui/training_workspace.py @@ -73,7 +73,18 @@ def _venv_site_packages() -> Path: SITE_PACKAGES = _venv_site_packages() -RERUN_NATIVE_BIN = SITE_PACKAGES / "rerun_sdk" / "rerun_cli" / "rerun" +_RERUN_CLI_ROOT = SITE_PACKAGES / "rerun_sdk" / "rerun_cli" +RERUN_NATIVE_BIN = next( + ( + candidate + for candidate in ( + _RERUN_CLI_ROOT / "rerun", + _RERUN_CLI_ROOT / "Rerun.app" / "Contents" / "MacOS" / "Rerun", + ) + if candidate.is_file() + ), + _RERUN_CLI_ROOT / "rerun", +) DATASET_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{2,47}$") PROFILE_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{2,95}$") diff --git a/requirements.txt b/requirements.txt index 4f2d41f..12dce25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ anthropic>=0.40 google-generativeai>=0.8 # Person 5 — Rerun reBot port (p5_rerun_port) rerun-sdk[datafusion]>=0.28 +rerun-loader-urdf>=0.1.1 pandas>=2.0 # Optional export niceties: # pyarrow>=14.0