Pepijn commited on
Commit
f660c9b
·
unverified ·
2 Parent(s): bde0e4518b0652

Merge pull request #26 from huggingface/feat/speedup_visulization

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/deploy-release.yml +12 -1
  2. .gitignore +3 -1
  3. README.md +11 -2
  4. bun.lock +117 -0
  5. next.config.ts +7 -0
  6. package.json +6 -1
  7. public/urdf/openarm/assets/body_link0.dae +3 -0
  8. public/urdf/openarm/assets/body_link0.stl +3 -0
  9. public/urdf/openarm/assets/finger.dae +3 -0
  10. public/urdf/openarm/assets/finger.stl +3 -0
  11. public/urdf/openarm/assets/hand.dae +3 -0
  12. public/urdf/openarm/assets/hand.stl +3 -0
  13. public/urdf/openarm/assets/link0.dae +3 -0
  14. public/urdf/openarm/assets/link0.stl +3 -0
  15. public/urdf/openarm/assets/link1.dae +3 -0
  16. public/urdf/openarm/assets/link1.stl +3 -0
  17. public/urdf/openarm/assets/link2.dae +3 -0
  18. public/urdf/openarm/assets/link2.stl +3 -0
  19. public/urdf/openarm/assets/link3.dae +3 -0
  20. public/urdf/openarm/assets/link3.stl +3 -0
  21. public/urdf/openarm/assets/link4.dae +3 -0
  22. public/urdf/openarm/assets/link4.stl +3 -0
  23. public/urdf/openarm/assets/link5.dae +3 -0
  24. public/urdf/openarm/assets/link5.stl +3 -0
  25. public/urdf/openarm/assets/link6.dae +3 -0
  26. public/urdf/openarm/assets/link6.stl +3 -0
  27. public/urdf/openarm/assets/link7.dae +3 -0
  28. public/urdf/openarm/assets/link7.stl +3 -0
  29. public/urdf/openarm/openarm_bimanual.urdf +495 -0
  30. public/urdf/so101/assets/base_motor_holder_so101_v1.stl +3 -0
  31. public/urdf/so101/assets/base_so101_v2.stl +3 -0
  32. public/urdf/so101/assets/motor_holder_so101_base_v1.stl +3 -0
  33. public/urdf/so101/assets/motor_holder_so101_wrist_v1.stl +3 -0
  34. public/urdf/so101/assets/moving_jaw_so101_v1.stl +3 -0
  35. public/urdf/so101/assets/rotation_pitch_so101_v1.stl +3 -0
  36. public/urdf/so101/assets/sts3215_03a_no_horn_v1.stl +3 -0
  37. public/urdf/so101/assets/sts3215_03a_v1.stl +3 -0
  38. public/urdf/so101/assets/under_arm_so101_v1.stl +3 -0
  39. public/urdf/so101/assets/upper_arm_so101_v1.stl +3 -0
  40. public/urdf/so101/assets/waveshare_mounting_plate_so101_v2.stl +3 -0
  41. public/urdf/so101/assets/wrist_roll_follower_so101_v1.stl +3 -0
  42. public/urdf/so101/assets/wrist_roll_pitch_so101_v2.stl +3 -0
  43. public/urdf/so101/so100.urdf +406 -0
  44. public/urdf/so101/so101_new_calib.urdf +453 -0
  45. src/app/.well-known/appspecific/[...slug]/route.ts +5 -0
  46. src/app/[org]/[dataset]/[episode]/actions.ts +65 -0
  47. src/app/[org]/[dataset]/[episode]/episode-viewer.tsx +397 -89
  48. src/app/[org]/[dataset]/[episode]/fetch-data.ts +1326 -259
  49. src/app/[org]/[dataset]/[episode]/page.tsx +1 -1
  50. src/app/explore/page.tsx +3 -3
.github/workflows/deploy-release.yml CHANGED
@@ -16,7 +16,18 @@ jobs:
16
  with:
17
  fetch-depth: 0
18
  lfs: true
 
19
  - name: Push to hub
20
  env:
21
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
22
- run: git push https://mishig:$HF_TOKEN@huggingface.co/spaces/lerobot/visualize_dataset main -f
 
 
 
 
 
 
 
 
 
 
 
16
  with:
17
  fetch-depth: 0
18
  lfs: true
19
+ ref: main
20
  - name: Push to hub
21
  env:
22
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
23
+ run: |
24
+ set -euo pipefail
25
+
26
+ git lfs install --local
27
+
28
+ git remote remove hf 2>/dev/null || true
29
+ git remote add hf "https://mishig:${HF_TOKEN}@huggingface.co/spaces/lerobot/visualize_dataset"
30
+
31
+ # Push large files first, then refs.
32
+ git lfs push hf main
33
+ git push hf main -f
.gitignore CHANGED
@@ -2,6 +2,7 @@
2
 
3
  # dependencies
4
  /node_modules
 
5
  /.pnp
6
  .pnp.*
7
  .yarn/*
@@ -16,7 +17,8 @@
16
  # next.js
17
  /.next/
18
  /out/
19
- /public
 
20
 
21
  # production
22
  /build
 
2
 
3
  # dependencies
4
  /node_modules
5
+ package-lock.json
6
  /.pnp
7
  .pnp.*
8
  .yarn/*
 
17
  # next.js
18
  /.next/
19
  /out/
20
+ /public/*
21
+ !/public/urdf/
22
 
23
  # production
24
  /build
README.md CHANGED
@@ -11,7 +11,7 @@ license: apache-2.0
11
 
12
  # LeRobot Dataset Visualizer
13
 
14
- LeRobot Dataset Visualizer is a web application for interactive exploration and visualization of robotics datasets, particularly those in the LeRobot format. It enables users to browse, view, and analyze episodes from large-scale robotics datasets, combining synchronized video playback with rich, interactive data graphs.
15
 
16
  ## Project Overview
17
 
@@ -20,13 +20,20 @@ This tool is designed to help robotics researchers and practitioners quickly ins
20
  - Navigating between organizations, datasets, and episodes
21
  - Watching episode videos
22
  - Exploring synchronized time-series data with interactive charts
 
 
23
  - Paginating through large datasets efficiently
24
 
25
  ## Key Features
26
 
27
  - **Dataset & Episode Navigation:** Quickly jump between organizations, datasets, and episodes using a sidebar and navigation controls.
28
  - **Synchronized Video & Data:** Video playback is synchronized with interactive data graphs for detailed inspection of sensor and control signals.
29
- - **Efficient Data Loading:** Uses parquet and JSON loading for large dataset support, with pagination and chunking.
 
 
 
 
 
30
  - **Responsive UI:** Built with React, Next.js, and Tailwind CSS for a fast, modern user experience.
31
 
32
  ## Technologies Used
@@ -34,6 +41,8 @@ This tool is designed to help robotics researchers and practitioners quickly ins
34
  - **Next.js** (App Router)
35
  - **React**
36
  - **Recharts** (for data visualization)
 
 
37
  - **hyparquet** (for reading Parquet files)
38
  - **Tailwind CSS** (styling)
39
 
 
11
 
12
  # LeRobot Dataset Visualizer
13
 
14
+ LeRobot Dataset Tool and Visualizer is a web application for interactive exploration and visualization of robotics datasets, particularly those in the LeRobot format. It enables users to browse, view, and analyze episodes from large-scale robotics datasets, combining synchronized video playback with rich, interactive data graphs.
15
 
16
  ## Project Overview
17
 
 
20
  - Navigating between organizations, datasets, and episodes
21
  - Watching episode videos
22
  - Exploring synchronized time-series data with interactive charts
23
+ - Analyzing action quality and identifying problematic episodes
24
+ - Visualizing robot poses in 3D using URDF models
25
  - Paginating through large datasets efficiently
26
 
27
  ## Key Features
28
 
29
  - **Dataset & Episode Navigation:** Quickly jump between organizations, datasets, and episodes using a sidebar and navigation controls.
30
  - **Synchronized Video & Data:** Video playback is synchronized with interactive data graphs for detailed inspection of sensor and control signals.
31
+ - **Overview Panel:** At-a-glance summary of dataset metadata, camera info, and episode details.
32
+ - **Statistics Panel:** Dataset-level statistics including episode count, total recording time, frames-per-second, and an episode-length histogram.
33
+ - **Action Insights Panel:** Data-driven analysis tools to guide training configuration — includes autocorrelation, state-action alignment, speed distribution, and cross-episode variance heatmap.
34
+ - **Filtering Panel:** Identify and flag problematic episodes (low movement, jerky motion, outlier length) for removal. Exports flagged episode IDs as a ready-to-run LeRobot CLI command.
35
+ - **3D URDF Viewer:** Visualize robot joint poses frame-by-frame in an interactive 3D scene, with end-effector trail rendering. Supports SO-100, SO-101, and OpenArm bimanual robots.
36
+ - **Efficient Data Loading:** Uses parquet and JSON loading for large dataset support, with pagination, chunking, and lazy-loaded panels for fast initial load.
37
  - **Responsive UI:** Built with React, Next.js, and Tailwind CSS for a fast, modern user experience.
38
 
39
  ## Technologies Used
 
41
  - **Next.js** (App Router)
42
  - **React**
43
  - **Recharts** (for data visualization)
44
+ - **Three.js** + **@react-three/fiber** + **@react-three/drei** (for 3D URDF visualization)
45
+ - **urdf-loader** (for parsing URDF robot models)
46
  - **hyparquet** (for reading Parquet files)
47
  - **Tailwind CSS** (styling)
48
 
bun.lock CHANGED
@@ -5,12 +5,17 @@
5
  "": {
6
  "name": "lerobot-viewer",
7
  "dependencies": {
 
 
 
8
  "hyparquet": "^1.12.1",
9
  "next": "15.3.6",
10
  "react": "^19.0.0",
11
  "react-dom": "^19.0.0",
12
  "react-icons": "^5.5.0",
13
  "recharts": "^2.15.3",
 
 
14
  },
15
  "devDependencies": {
16
  "@eslint/eslintrc": "^3",
@@ -31,6 +36,8 @@
31
 
32
  "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
33
 
 
 
34
  "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
35
 
36
  "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -123,6 +130,10 @@
123
 
124
  "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
125
 
 
 
 
 
126
  "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
127
 
128
  "@next/env": ["@next/env@15.3.6", "", {}, "sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw=="],
@@ -153,6 +164,10 @@
153
 
154
  "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
155
 
 
 
 
 
156
  "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
157
 
158
  "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="],
@@ -191,6 +206,8 @@
191
 
192
  "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
193
 
 
 
194
  "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
195
 
196
  "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -211,6 +228,8 @@
211
 
212
  "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
213
 
 
 
214
  "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
215
 
216
  "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -219,10 +238,20 @@
219
 
220
  "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
221
 
 
 
222
  "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
223
 
224
  "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
225
 
 
 
 
 
 
 
 
 
226
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
227
 
228
  "@typescript-eslint/parser": ["@typescript-eslint/parser@8.55.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw=="],
@@ -281,6 +310,12 @@
281
 
282
  "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
283
 
 
 
 
 
 
 
284
  "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
285
 
286
  "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -321,10 +356,16 @@
321
 
322
  "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
323
 
 
 
 
 
324
  "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
325
 
326
  "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
327
 
 
 
328
  "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
329
 
330
  "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -335,6 +376,8 @@
335
 
336
  "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
337
 
 
 
338
  "caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
339
 
340
  "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -349,6 +392,8 @@
349
 
350
  "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
351
 
 
 
352
  "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
353
 
354
  "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -393,12 +438,16 @@
393
 
394
  "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
395
 
 
 
396
  "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
397
 
398
  "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
399
 
400
  "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
401
 
 
 
402
  "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
403
 
404
  "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
@@ -471,6 +520,8 @@
471
 
472
  "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
473
 
 
 
474
  "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
475
 
476
  "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -505,6 +556,8 @@
505
 
506
  "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
507
 
 
 
508
  "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
509
 
510
  "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -523,10 +576,16 @@
523
 
524
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
525
 
 
 
526
  "hyparquet": ["hyparquet@1.25.0", "", {}, "sha512-isJx+RplYT3aJc5yhaG5CeOZSBJecHZgYsUi7NE6P/nAbxxA0hZcyul0tUsWCQLc9QXYQ2uFyYBrk61JbJO0cg=="],
527
 
 
 
528
  "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
529
 
 
 
530
  "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
531
 
532
  "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -569,6 +628,8 @@
569
 
570
  "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
571
 
 
 
572
  "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
573
 
574
  "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
@@ -593,6 +654,8 @@
593
 
594
  "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
595
 
 
 
596
  "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
597
 
598
  "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -617,6 +680,8 @@
617
 
618
  "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
619
 
 
 
620
  "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
621
 
622
  "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
@@ -649,12 +714,18 @@
649
 
650
  "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
651
 
 
 
652
  "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
653
 
654
  "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
655
 
656
  "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
657
 
 
 
 
 
658
  "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
659
 
660
  "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
@@ -711,10 +782,14 @@
711
 
712
  "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
713
 
 
 
714
  "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
715
 
716
  "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
717
 
 
 
718
  "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
719
 
720
  "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -733,6 +808,8 @@
733
 
734
  "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
735
 
 
 
736
  "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
737
 
738
  "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
@@ -741,6 +818,8 @@
741
 
742
  "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
743
 
 
 
744
  "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
745
 
746
  "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -785,6 +864,10 @@
785
 
786
  "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
787
 
 
 
 
 
788
  "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
789
 
790
  "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
@@ -811,22 +894,38 @@
811
 
812
  "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
813
 
 
 
814
  "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
815
 
816
  "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
817
 
 
 
 
 
 
 
818
  "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
819
 
820
  "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
821
 
822
  "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
823
 
 
 
 
 
 
 
824
  "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
825
 
826
  "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
827
 
828
  "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
829
 
 
 
830
  "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
831
 
832
  "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
@@ -845,10 +944,20 @@
845
 
846
  "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
847
 
 
 
848
  "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
849
 
 
 
 
 
850
  "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
851
 
 
 
 
 
852
  "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
853
 
854
  "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@@ -863,6 +972,8 @@
863
 
864
  "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
865
 
 
 
866
  "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
867
 
868
  "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
@@ -903,6 +1014,12 @@
903
 
904
  "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
905
 
 
 
 
 
 
 
906
  "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
907
  }
908
  }
 
5
  "": {
6
  "name": "lerobot-viewer",
7
  "dependencies": {
8
+ "@react-three/drei": "^10.7.7",
9
+ "@react-three/fiber": "^9.5.0",
10
+ "@types/three": "^0.182.0",
11
  "hyparquet": "^1.12.1",
12
  "next": "15.3.6",
13
  "react": "^19.0.0",
14
  "react-dom": "^19.0.0",
15
  "react-icons": "^5.5.0",
16
  "recharts": "^2.15.3",
17
+ "three": "^0.182.0",
18
+ "urdf-loader": "^0.12.6",
19
  },
20
  "devDependencies": {
21
  "@eslint/eslintrc": "^3",
 
36
 
37
  "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
38
 
39
+ "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
40
+
41
  "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
42
 
43
  "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
 
130
 
131
  "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
132
 
133
+ "@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="],
134
+
135
+ "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="],
136
+
137
  "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
138
 
139
  "@next/env": ["@next/env@15.3.6", "", {}, "sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw=="],
 
164
 
165
  "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
166
 
167
+ "@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="],
168
+
169
+ "@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="],
170
+
171
  "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
172
 
173
  "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="],
 
206
 
207
  "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
208
 
209
+ "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
210
+
211
  "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
212
 
213
  "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
 
228
 
229
  "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
230
 
231
+ "@types/draco3d": ["@types/draco3d@1.4.10", "", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="],
232
+
233
  "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
234
 
235
  "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 
238
 
239
  "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
240
 
241
+ "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
242
+
243
  "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
244
 
245
  "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
246
 
247
+ "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
248
+
249
+ "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
250
+
251
+ "@types/three": ["@types/three@0.182.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q=="],
252
+
253
+ "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
254
+
255
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
256
 
257
  "@typescript-eslint/parser": ["@typescript-eslint/parser@8.55.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw=="],
 
310
 
311
  "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
312
 
313
+ "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
314
+
315
+ "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="],
316
+
317
+ "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
318
+
319
  "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
320
 
321
  "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 
356
 
357
  "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
358
 
359
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
360
+
361
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
362
+
363
  "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
364
 
365
  "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
366
 
367
+ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
368
+
369
  "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
370
 
371
  "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
 
376
 
377
  "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
378
 
379
+ "camera-controls": ["camera-controls@3.1.2", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA=="],
380
+
381
  "caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
382
 
383
  "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 
392
 
393
  "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
394
 
395
+ "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
396
+
397
  "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
398
 
399
  "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
 
438
 
439
  "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
440
 
441
+ "detect-gpu": ["detect-gpu@5.0.70", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w=="],
442
+
443
  "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
444
 
445
  "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
446
 
447
  "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
448
 
449
+ "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="],
450
+
451
  "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
452
 
453
  "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
 
520
 
521
  "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
522
 
523
+ "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
524
+
525
  "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
526
 
527
  "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 
556
 
557
  "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
558
 
559
+ "glsl-noise": ["glsl-noise@0.0.0", "", {}, "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w=="],
560
+
561
  "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
562
 
563
  "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 
576
 
577
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
578
 
579
+ "hls.js": ["hls.js@1.6.15", "", {}, "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA=="],
580
+
581
  "hyparquet": ["hyparquet@1.25.0", "", {}, "sha512-isJx+RplYT3aJc5yhaG5CeOZSBJecHZgYsUi7NE6P/nAbxxA0hZcyul0tUsWCQLc9QXYQ2uFyYBrk61JbJO0cg=="],
582
 
583
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
584
+
585
  "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
586
 
587
+ "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
588
+
589
  "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
590
 
591
  "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
 
628
 
629
  "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
630
 
631
+ "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="],
632
+
633
  "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
634
 
635
  "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
 
654
 
655
  "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
656
 
657
+ "its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="],
658
+
659
  "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
660
 
661
  "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 
680
 
681
  "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
682
 
683
+ "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
684
+
685
  "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
686
 
687
  "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
 
714
 
715
  "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
716
 
717
+ "maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
718
+
719
  "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
720
 
721
  "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
722
 
723
  "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
724
 
725
+ "meshline": ["meshline@3.3.1", "", { "peerDependencies": { "three": ">=0.137" } }, "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ=="],
726
+
727
+ "meshoptimizer": ["meshoptimizer@0.22.0", "", {}, "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg=="],
728
+
729
  "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
730
 
731
  "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
 
782
 
783
  "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
784
 
785
+ "potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
786
+
787
  "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
788
 
789
  "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
790
 
791
+ "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="],
792
+
793
  "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
794
 
795
  "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
 
808
 
809
  "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
810
 
811
+ "react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" }, "optionalPeers": ["react-dom"] }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="],
812
+
813
  "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
814
 
815
  "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
 
818
 
819
  "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
820
 
821
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
822
+
823
  "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
824
 
825
  "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
 
864
 
865
  "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
866
 
867
+ "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
868
+
869
+ "stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
870
+
871
  "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
872
 
873
  "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
 
894
 
895
  "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
896
 
897
+ "suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="],
898
+
899
  "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
900
 
901
  "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
902
 
903
+ "three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="],
904
+
905
+ "three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="],
906
+
907
+ "three-stdlib": ["three-stdlib@2.36.1", "", { "dependencies": { "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", "@types/webxr": "^0.5.2", "draco3d": "^1.4.1", "fflate": "^0.6.9", "potpack": "^1.0.1" }, "peerDependencies": { "three": ">=0.128.0" } }, "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg=="],
908
+
909
  "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
910
 
911
  "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
912
 
913
  "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
914
 
915
+ "troika-three-text": ["troika-three-text@0.52.4", "", { "dependencies": { "bidi-js": "^1.0.2", "troika-three-utils": "^0.52.4", "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, "peerDependencies": { "three": ">=0.125.0" } }, "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg=="],
916
+
917
+ "troika-three-utils": ["troika-three-utils@0.52.4", "", { "peerDependencies": { "three": ">=0.125.0" } }, "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A=="],
918
+
919
+ "troika-worker-utils": ["troika-worker-utils@0.52.0", "", {}, "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="],
920
+
921
  "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
922
 
923
  "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
924
 
925
  "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
926
 
927
+ "tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
928
+
929
  "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
930
 
931
  "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
 
944
 
945
  "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
946
 
947
+ "urdf-loader": ["urdf-loader@0.12.6", "", { "peerDependencies": { "three": ">=0.152.0" } }, "sha512-EwpgOCPe6Tep2+MXoo/r13keHaKQXMcM+4s9+jX0NRxNS/QSNuP5JPdk5AIgWEoEB43AkEj9Vk+Nr53NkXgSbA=="],
948
+
949
  "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
950
 
951
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
952
+
953
+ "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
954
+
955
  "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
956
 
957
+ "webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="],
958
+
959
+ "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="],
960
+
961
  "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
962
 
963
  "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
 
972
 
973
  "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
974
 
975
+ "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
976
+
977
  "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
978
 
979
  "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
 
1014
 
1015
  "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
1016
 
1017
+ "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
1018
+
1019
+ "three-stdlib/fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="],
1020
+
1021
+ "tunnel-rat/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
1022
+
1023
  "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
1024
  }
1025
  }
next.config.ts CHANGED
@@ -2,6 +2,13 @@ import type { NextConfig } from "next";
2
  import packageJson from "./package.json";
3
 
4
  const nextConfig: NextConfig = {
 
 
 
 
 
 
 
5
  generateBuildId: () => packageJson.version,
6
  };
7
 
 
2
  import packageJson from "./package.json";
3
 
4
  const nextConfig: NextConfig = {
5
+ typescript: {
6
+ ignoreBuildErrors: true,
7
+ },
8
+ eslint: {
9
+ ignoreDuringBuilds: true,
10
+ },
11
+ transpilePackages: ["three"],
12
  generateBuildId: () => packageJson.version,
13
  };
14
 
package.json CHANGED
@@ -14,15 +14,20 @@
14
  "validate": "bun run type-check && bun run lint && bun run format:check"
15
  },
16
  "dependencies": {
 
 
17
  "hyparquet": "^1.12.1",
18
  "next": "15.3.6",
19
  "react": "^19.0.0",
20
  "react-dom": "^19.0.0",
21
  "react-icons": "^5.5.0",
22
- "recharts": "^2.15.3"
 
 
23
  },
24
  "devDependencies": {
25
  "@eslint/eslintrc": "^3",
 
26
  "@tailwindcss/postcss": "^4",
27
  "@types/node": "^20",
28
  "@types/react": "^19",
 
14
  "validate": "bun run type-check && bun run lint && bun run format:check"
15
  },
16
  "dependencies": {
17
+ "@react-three/drei": "^10.7.7",
18
+ "@react-three/fiber": "^9.5.0",
19
  "hyparquet": "^1.12.1",
20
  "next": "15.3.6",
21
  "react": "^19.0.0",
22
  "react-dom": "^19.0.0",
23
  "react-icons": "^5.5.0",
24
+ "recharts": "^2.15.3",
25
+ "three": "^0.182.0",
26
+ "urdf-loader": "^0.12.6"
27
  },
28
  "devDependencies": {
29
  "@eslint/eslintrc": "^3",
30
+ "@types/three": "^0.182.0",
31
  "@tailwindcss/postcss": "^4",
32
  "@types/node": "^20",
33
  "@types/react": "^19",
public/urdf/openarm/assets/body_link0.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a05f59379dcf694e5ff336b1ee3a47ea504a2425a2fa774af1b14e096d9a239a
3
+ size 10783948
public/urdf/openarm/assets/body_link0.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7ab0afa9a26bebbf5c6149894c41d96622caa649555ae7c5f0f2548f86148d91
3
+ size 7955034
public/urdf/openarm/assets/finger.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64492f6a80a551a7a53e3f769facc42ee4a9bce1634ff63300a9d7f5b3628cb0
3
+ size 6021838
public/urdf/openarm/assets/finger.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:25c115c7c55a422f30ad11581dc21576dc8fc4e09e659890772d86fe82ec04d7
3
+ size 432484
public/urdf/openarm/assets/hand.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7a1ff18be1b603544f36645b50e9842ee1ea94d1ba8cbccdeded50946f4bce7
3
+ size 1549205
public/urdf/openarm/assets/hand.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e5d373ebbd3fd001b506058644062ad71a68f1ced5ca5d5ed0f6de20137956b
3
+ size 18284
public/urdf/openarm/assets/link0.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29037f2e51d6b34dbbdbd1eecb557abd900aa1897be4d4a30a7114e649f987b9
3
+ size 5356744
public/urdf/openarm/assets/link0.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c77bdc9419947088e1dfc452e29c6092cc7b02b239ff4f2f5be3d77e393af185
3
+ size 4148234
public/urdf/openarm/assets/link1.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c25937f901a4c15730927e08bf5fd26a072e801c529f0bd9678ba9758330647d
3
+ size 7655919
public/urdf/openarm/assets/link1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2a182c7fd4d25226aff6d9e2dd9d1008a85fce7df8e16525722a5c5053f8b055
3
+ size 5741534
public/urdf/openarm/assets/link2.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d2a1ec857a22eaa180034e2dcd989c54611abab6ce6659eb4999570c34cc124
3
+ size 6288652
public/urdf/openarm/assets/link2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de1b190a56c16dea14546fb8e22c86eccabc2ca5e054819630c1932592381745
3
+ size 4543534
public/urdf/openarm/assets/link3.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97d73c4bae3f3f7570bf3e28306da719aa99a6bec662060c9e358e5625b11cce
3
+ size 6846210
public/urdf/openarm/assets/link3.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8d47d75d0c47a65021708ba63cb73162bf7c3b1d14e9cfc70dfa47336034ac76
3
+ size 4978834
public/urdf/openarm/assets/link4.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:59eb1cbf44b1250ad9533176b30084179f9164fcb998ee2623d89b48968ec426
3
+ size 6644484
public/urdf/openarm/assets/link4.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ca8149f2ce8b1b102270ec0b1a4b75c3e6f98c09b084430a450adce808607e1
3
+ size 4944684
public/urdf/openarm/assets/link5.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96fd3516df16a772a84c268529e5b53b05e6df07da6ccfe850bc091eba947156
3
+ size 9034473
public/urdf/openarm/assets/link5.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:30ea7abfdd3661b315f897bb82a5f34fd966357b358e890e155e928e931ea975
3
+ size 6322984
public/urdf/openarm/assets/link6.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:adcd37741f4e223bd6fe9925625e84191b525c9d27f151f9178b23989de05459
3
+ size 6133097
public/urdf/openarm/assets/link6.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e49f91279f109baecb9ff54f5041eeb4514f757ba6daa65c3ea01fb1991967e4
3
+ size 4818434
public/urdf/openarm/assets/link7.dae ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e0e1d59b1e09c5a73198cdf175dbb94844073ec067d4b491dcab8a9e1a7faade
3
+ size 6288812
public/urdf/openarm/assets/link7.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a91593b67d2dec16d1dfb6f1305df3ddcd214cdef02e97d5aba30bc633e775b2
3
+ size 5114784
public/urdf/openarm/openarm_bimanual.urdf ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <robot name="openarm">
3
+ <link name="world" />
4
+ <joint name="openarm_body_world_joint" type="fixed">
5
+ <parent link="world" />
6
+ <child link="openarm_body_link0" />
7
+ <origin rpy="0 0 0" xyz="0 0 0" />
8
+ </joint>
9
+ <link name="openarm_body_link0">
10
+ <visual name="openarm_body_link0_visual">
11
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 0.0" />
12
+ <geometry>
13
+ <mesh filename="assets/body_link0.dae" scale="0.001 0.001 0.001" />
14
+ </geometry>
15
+ </visual>
16
+ <collision name="openarm_body_link0_collision">
17
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 0.0" />
18
+ <geometry>
19
+ <mesh filename="assets/body_link0.dae" scale="0.001 0.001 0.001" />
20
+ </geometry>
21
+ </collision>
22
+ <inertial>
23
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 0.0" />
24
+ <mass value="13.89" />
25
+ <inertia ixx="1.653" ixy="0.0" ixz="0.0" iyy="1.653" iyz="0.0" izz="0.051" />
26
+ </inertial>
27
+ </link>
28
+
29
+ <!-- ═══ LEFT ARM ═══ -->
30
+ <joint name="openarm_left_openarm_body_link0_joint" type="fixed">
31
+ <parent link="openarm_body_link0" />
32
+ <child link="openarm_left_link0" />
33
+ <origin rpy="-1.5708 0 0" xyz="0.0 0.031 0.698" />
34
+ </joint>
35
+ <link name="openarm_left_link0">
36
+ <visual name="openarm_left_link0_visual">
37
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 0.0" />
38
+ <geometry>
39
+ <mesh filename="assets/link0.dae" scale="0.001 -0.001 0.001" />
40
+ </geometry>
41
+ </visual>
42
+ <inertial>
43
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0009483362816297526 -0.0001580207020448382 0.03076860287587199" />
44
+ <mass value="1.1432284943239561" />
45
+ <inertia ixx="0.001128" ixy="-4e-06" ixz="-3.3e-05" iyy="0.000962" iyz="-7e-06" izz="0.00147" />
46
+ </inertial>
47
+ </link>
48
+ <link name="openarm_left_link1">
49
+ <visual name="openarm_left_link1_visual">
50
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0 0.0 -0.0625" />
51
+ <geometry>
52
+ <mesh filename="assets/link1.dae" scale="0.001 -0.001 0.001" />
53
+ </geometry>
54
+ </visual>
55
+ <inertial>
56
+ <origin rpy="0.0 0.0 0.0" xyz="0.0011467657911800769 -3.319987657026362e-05 0.05395284380736254" />
57
+ <mass value="1.1416684646202298" />
58
+ <inertia ixx="0.001567" ixy="-1e-06" ixz="-2.9e-05" iyy="0.001273" iyz="1e-06" izz="0.001016" />
59
+ </inertial>
60
+ </link>
61
+ <joint name="openarm_left_joint1" type="revolute">
62
+ <origin rpy="0 0 0" xyz="0.0 0.0 0.0625" />
63
+ <parent link="openarm_left_link0" />
64
+ <child link="openarm_left_link1" />
65
+ <axis xyz="0 0 1" />
66
+ <limit effort="40" lower="-3.490659" upper="1.3962629999999998" velocity="16.754666" />
67
+ </joint>
68
+ <link name="openarm_left_link2">
69
+ <visual name="openarm_left_link2_visual">
70
+ <origin rpy="0.0 0.0 0.0" xyz="0.0301 0.0 -0.1225" />
71
+ <geometry>
72
+ <mesh filename="assets/link2.dae" scale="0.001 -0.001 0.001" />
73
+ </geometry>
74
+ </visual>
75
+ <inertial>
76
+ <origin rpy="0.0 0.0 0.0" xyz="0.00839629182351943 2.0145102027597523e-08 0.03256649300522363" />
77
+ <mass value="0.2775092746011571" />
78
+ <inertia ixx="0.000359" ixy="1e-06" ixz="-0.000109" iyy="0.000376" iyz="1e-06" izz="0.000232" />
79
+ </inertial>
80
+ </link>
81
+ <joint name="openarm_left_joint2" type="revolute">
82
+ <origin rpy="-1.57079632679 0 0" xyz="-0.0301 0.0 0.06" />
83
+ <parent link="openarm_left_link1" />
84
+ <child link="openarm_left_link2" />
85
+ <axis xyz="-1 0 0" />
86
+ <limit effort="40" lower="-3.3161253267948965" upper="0.17453267320510335" velocity="16.754666" />
87
+ </joint>
88
+ <link name="openarm_left_link3">
89
+ <visual name="openarm_left_link3_visual">
90
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0 -0.0 -0.18875" />
91
+ <geometry>
92
+ <mesh filename="assets/link3.dae" scale="0.001 -0.001 0.001" />
93
+ </geometry>
94
+ </visual>
95
+ <inertial>
96
+ <origin rpy="0.0 0.0 0.0" xyz="-0.002104752099628911 -0.0005549085042607548 0.09047470545721961" />
97
+ <mass value="1.073863338202347" />
98
+ <inertia ixx="0.004372" ixy="1e-06" ixz="1.1e-05" iyy="0.004319" iyz="-3.6e-05" izz="0.000661" />
99
+ </inertial>
100
+ </link>
101
+ <joint name="openarm_left_joint3" type="revolute">
102
+ <origin rpy="0 0 0" xyz="0.0301 0.0 0.06625" />
103
+ <parent link="openarm_left_link2" />
104
+ <child link="openarm_left_link3" />
105
+ <axis xyz="0 0 1" />
106
+ <limit effort="27" lower="-1.570796" upper="1.570796" velocity="5.445426" />
107
+ </joint>
108
+ <link name="openarm_left_link4">
109
+ <visual name="openarm_left_link4_visual">
110
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 -0.0315 -0.3425" />
111
+ <geometry>
112
+ <mesh filename="assets/link4.dae" scale="0.001 0.001 0.001" />
113
+ </geometry>
114
+ </visual>
115
+ <inertial>
116
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0029006831074562967 -0.03030575826634669 0.06339637422196209" />
117
+ <mass value="0.6348534566833373" />
118
+ <inertia ixx="0.000623" ixy="-1e-06" ixz="-1.9e-05" iyy="0.000511" iyz="3.8e-05" izz="0.000334" />
119
+ </inertial>
120
+ </link>
121
+ <joint name="openarm_left_joint4" type="revolute">
122
+ <origin rpy="0 0 0" xyz="-0.0 0.0315 0.15375" />
123
+ <parent link="openarm_left_link3" />
124
+ <child link="openarm_left_link4" />
125
+ <axis xyz="0 1 0" />
126
+ <limit effort="27" lower="0.0" upper="2.443461" velocity="5.445426" />
127
+ </joint>
128
+ <link name="openarm_left_link5">
129
+ <visual name="openarm_left_link5_visual">
130
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0 -0.0 -0.438" />
131
+ <geometry>
132
+ <mesh filename="assets/link5.dae" scale="0.001 -0.001 0.001" />
133
+ </geometry>
134
+ </visual>
135
+ <inertial>
136
+ <origin rpy="0.0 0.0 0.0" xyz="-0.003049665024221911 -0.0008866902457326625 0.043079803024980934" />
137
+ <mass value="0.6156588026168502" />
138
+ <inertia ixx="0.000423" ixy="-8e-06" ixz="6e-06" iyy="0.000445" iyz="-6e-06" izz="0.000324" />
139
+ </inertial>
140
+ </link>
141
+ <joint name="openarm_left_joint5" type="revolute">
142
+ <origin rpy="0 0 0" xyz="0.0 -0.0315 0.0955" />
143
+ <parent link="openarm_left_link4" />
144
+ <child link="openarm_left_link5" />
145
+ <axis xyz="0 0 1" />
146
+ <limit effort="7" lower="-1.570796" upper="1.570796" velocity="20.943946" />
147
+ </joint>
148
+ <link name="openarm_left_link6">
149
+ <visual name="openarm_left_link6_visual">
150
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0375 -0.0 -0.5585" />
151
+ <geometry>
152
+ <mesh filename="assets/link6.dae" scale="0.001 -0.001 0.001" />
153
+ </geometry>
154
+ </visual>
155
+ <inertial>
156
+ <origin rpy="0.0 0.0 0.0" xyz="-0.037136587005447405 -0.00033230528343419053 -9.498374522309838e-05" />
157
+ <mass value="0.475202773187987" />
158
+ <inertia ixx="0.000143" ixy="1e-06" ixz="1e-06" iyy="0.000157" iyz="1e-06" izz="0.000159" />
159
+ </inertial>
160
+ </link>
161
+ <joint name="openarm_left_joint6" type="revolute">
162
+ <origin rpy="0 0 0" xyz="0.0375 0.0 0.1205" />
163
+ <parent link="openarm_left_link5" />
164
+ <child link="openarm_left_link6" />
165
+ <axis xyz="1 0 0" />
166
+ <limit effort="7" lower="-0.785398" upper="0.785398" velocity="20.943946" />
167
+ </joint>
168
+ <link name="openarm_left_link7">
169
+ <visual name="openarm_left_link7_visual">
170
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 -0.0 -0.5585" />
171
+ <geometry>
172
+ <mesh filename="assets/link7.dae" scale="0.001 -0.001 0.001" />
173
+ </geometry>
174
+ </visual>
175
+ <inertial>
176
+ <origin rpy="0.0 0.0 0.0" xyz="6.875510271106056e-05 -0.01266175250761268 0.06951945409987448" />
177
+ <mass value="0.4659771327380578" />
178
+ <inertia ixx="0.000639" ixy="1e-06" ixz="1e-06" iyy="0.000497" iyz="8.9e-05" izz="0.000342" />
179
+ </inertial>
180
+ </link>
181
+ <joint name="openarm_left_joint7" type="revolute">
182
+ <origin rpy="0 0 0" xyz="-0.0375 0.0 0.0" />
183
+ <parent link="openarm_left_link6" />
184
+ <child link="openarm_left_link7" />
185
+ <axis xyz="0 -1 0" />
186
+ <limit effort="7" lower="-1.570796" upper="1.570796" velocity="20.943946" />
187
+ </joint>
188
+
189
+ <!-- Left hand -->
190
+ <link name="openarm_left_hand">
191
+ <visual name="openarm_left_hand_visual">
192
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 -0.6585" />
193
+ <geometry>
194
+ <mesh filename="assets/hand.dae" scale="0.001 0.001 0.001" />
195
+ </geometry>
196
+ </visual>
197
+ <inertial>
198
+ <origin rpy="0 0 0" xyz="0.0 0.002 0.03" />
199
+ <mass value="0.35" />
200
+ <inertia ixx="0.0002473" ixy="1e-06" ixz="1e-06" iyy="1.763e-05" iyz="1e-06" izz="0.0002521" />
201
+ </inertial>
202
+ </link>
203
+ <joint name="left_openarm_hand_joint" type="fixed">
204
+ <parent link="openarm_left_link7" />
205
+ <child link="openarm_left_hand" />
206
+ <origin rpy="0 0 0" xyz="0 -0.0 0.1001" />
207
+ </joint>
208
+ <link name="openarm_left_hand_tcp">
209
+ <inertial>
210
+ <origin xyz="0 0 0" rpy="0 0 0" />
211
+ <mass value="0.001" />
212
+ <inertia ixx="0.000001" ixy="0.0" ixz="0.0" iyy="0.000001" iyz="0.0" izz="0.000001" />
213
+ </inertial>
214
+ </link>
215
+ <joint name="openarm_left_hand_tcp_joint" type="fixed">
216
+ <origin rpy="0 0 0" xyz="0 -0.0 0.08" />
217
+ <parent link="openarm_left_hand" />
218
+ <child link="openarm_left_hand_tcp" />
219
+ </joint>
220
+ <link name="openarm_left_left_finger">
221
+ <visual name="openarm_left_left_finger_visual">
222
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 -0.05 -0.673001" />
223
+ <geometry>
224
+ <mesh filename="assets/finger.dae" scale="0.001 0.001 0.001" />
225
+ </geometry>
226
+ </visual>
227
+ <inertial>
228
+ <origin rpy="0 0 0" xyz="0.0064528 0.01702 0.0219685" />
229
+ <mass value="0.03602545343277134" />
230
+ <inertia ixx="2.3749999999999997e-06" ixy="1e-06" ixz="1e-06" iyy="2.3749999999999997e-06" iyz="1e-06" izz="7.5e-07" />
231
+ </inertial>
232
+ </link>
233
+ <link name="openarm_left_right_finger">
234
+ <visual name="openarm_left_right_finger_visual">
235
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.05 -0.673001" />
236
+ <geometry>
237
+ <mesh filename="assets/finger.dae" scale="0.001 -0.001 0.001" />
238
+ </geometry>
239
+ </visual>
240
+ <inertial>
241
+ <origin rpy="0 0 0" xyz="0.0064528 -0.01702 0.0219685" />
242
+ <mass value="0.03602545343277134" />
243
+ <inertia ixx="2.3749999999999997e-06" ixy="1e-06" ixz="1e-06" iyy="2.3749999999999997e-06" iyz="1e-06" izz="7.5e-07" />
244
+ </inertial>
245
+ </link>
246
+ <joint name="openarm_left_finger_joint1" type="prismatic">
247
+ <parent link="openarm_left_hand" />
248
+ <child link="openarm_left_right_finger" />
249
+ <origin rpy="0 0 0" xyz="0 -0.006 0.015" />
250
+ <axis xyz="0 -1 0" />
251
+ <limit effort="333" lower="0.0" upper="0.044" velocity="10.0" />
252
+ </joint>
253
+ <joint name="openarm_left_finger_joint2" type="prismatic">
254
+ <parent link="openarm_left_hand" />
255
+ <child link="openarm_left_left_finger" />
256
+ <origin rpy="0 0 0" xyz="0 0.006 0.015" />
257
+ <axis xyz="0 1 0" />
258
+ <limit effort="333" lower="0.0" upper="0.044" velocity="10.0" />
259
+ <mimic joint="openarm_left_finger_joint1" />
260
+ </joint>
261
+
262
+ <!-- ═══ RIGHT ARM ═══ -->
263
+ <joint name="openarm_right_openarm_body_link0_joint" type="fixed">
264
+ <parent link="openarm_body_link0" />
265
+ <child link="openarm_right_link0" />
266
+ <origin rpy="1.5708 0 0" xyz="0.0 -0.031 0.698" />
267
+ </joint>
268
+ <link name="openarm_right_link0">
269
+ <visual name="openarm_right_link0_visual">
270
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 0.0" />
271
+ <geometry>
272
+ <mesh filename="assets/link0.dae" scale="0.001 0.001 0.001" />
273
+ </geometry>
274
+ </visual>
275
+ <inertial>
276
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0009483362816297526 0.0001580207020448382 0.03076860287587199" />
277
+ <mass value="1.1432284943239561" />
278
+ <inertia ixx="0.001128" ixy="-4e-06" ixz="-3.3e-05" iyy="0.000962" iyz="-7e-06" izz="0.00147" />
279
+ </inertial>
280
+ </link>
281
+ <link name="openarm_right_link1">
282
+ <visual name="openarm_right_link1_visual">
283
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0 0.0 -0.0625" />
284
+ <geometry>
285
+ <mesh filename="assets/link1.dae" scale="0.001 0.001 0.001" />
286
+ </geometry>
287
+ </visual>
288
+ <inertial>
289
+ <origin rpy="0.0 0.0 0.0" xyz="0.0011467657911800769 3.319987657026362e-05 0.05395284380736254" />
290
+ <mass value="1.1416684646202298" />
291
+ <inertia ixx="0.001567" ixy="-1e-06" ixz="-2.9e-05" iyy="0.001273" iyz="1e-06" izz="0.001016" />
292
+ </inertial>
293
+ </link>
294
+ <joint name="openarm_right_joint1" type="revolute">
295
+ <origin rpy="0 0 0" xyz="0.0 0.0 0.0625" />
296
+ <parent link="openarm_right_link0" />
297
+ <child link="openarm_right_link1" />
298
+ <axis xyz="0 0 1" />
299
+ <limit effort="40" lower="-1.396263" upper="3.490659" velocity="16.754666" />
300
+ </joint>
301
+ <link name="openarm_right_link2">
302
+ <visual name="openarm_right_link2_visual">
303
+ <origin rpy="0.0 0.0 0.0" xyz="0.0301 0.0 -0.1225" />
304
+ <geometry>
305
+ <mesh filename="assets/link2.dae" scale="0.001 0.001 0.001" />
306
+ </geometry>
307
+ </visual>
308
+ <inertial>
309
+ <origin rpy="0.0 0.0 0.0" xyz="0.00839629182351943 -2.0145102027597523e-08 0.03256649300522363" />
310
+ <mass value="0.2775092746011571" />
311
+ <inertia ixx="0.000359" ixy="1e-06" ixz="-0.000109" iyy="0.000376" iyz="1e-06" izz="0.000232" />
312
+ </inertial>
313
+ </link>
314
+ <joint name="openarm_right_joint2" type="revolute">
315
+ <origin rpy="1.57079632679 0 0" xyz="-0.0301 0.0 0.06" />
316
+ <parent link="openarm_right_link1" />
317
+ <child link="openarm_right_link2" />
318
+ <axis xyz="-1 0 0" />
319
+ <limit effort="40" lower="-0.17453267320510335" upper="3.3161253267948965" velocity="16.754666" />
320
+ </joint>
321
+ <link name="openarm_right_link3">
322
+ <visual name="openarm_right_link3_visual">
323
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0 -0.0 -0.18875" />
324
+ <geometry>
325
+ <mesh filename="assets/link3.dae" scale="0.001 0.001 0.001" />
326
+ </geometry>
327
+ </visual>
328
+ <inertial>
329
+ <origin rpy="0.0 0.0 0.0" xyz="-0.002104752099628911 0.0005549085042607548 0.09047470545721961" />
330
+ <mass value="1.073863338202347" />
331
+ <inertia ixx="0.004372" ixy="1e-06" ixz="1.1e-05" iyy="0.004319" iyz="-3.6e-05" izz="0.000661" />
332
+ </inertial>
333
+ </link>
334
+ <joint name="openarm_right_joint3" type="revolute">
335
+ <origin rpy="0 0 0" xyz="0.0301 0.0 0.06625" />
336
+ <parent link="openarm_right_link2" />
337
+ <child link="openarm_right_link3" />
338
+ <axis xyz="0 0 1" />
339
+ <limit effort="27" lower="-1.570796" upper="1.570796" velocity="5.445426" />
340
+ </joint>
341
+ <link name="openarm_right_link4">
342
+ <visual name="openarm_right_link4_visual">
343
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 -0.0315 -0.3425" />
344
+ <geometry>
345
+ <mesh filename="assets/link4.dae" scale="0.001 0.001 0.001" />
346
+ </geometry>
347
+ </visual>
348
+ <inertial>
349
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0029006831074562967 -0.03030575826634669 0.06339637422196209" />
350
+ <mass value="0.6348534566833373" />
351
+ <inertia ixx="0.000623" ixy="-1e-06" ixz="-1.9e-05" iyy="0.000511" iyz="3.8e-05" izz="0.000334" />
352
+ </inertial>
353
+ </link>
354
+ <joint name="openarm_right_joint4" type="revolute">
355
+ <origin rpy="0 0 0" xyz="-0.0 0.0315 0.15375" />
356
+ <parent link="openarm_right_link3" />
357
+ <child link="openarm_right_link4" />
358
+ <axis xyz="0 1 0" />
359
+ <limit effort="27" lower="0.0" upper="2.443461" velocity="5.445426" />
360
+ </joint>
361
+ <link name="openarm_right_link5">
362
+ <visual name="openarm_right_link5_visual">
363
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0 -0.0 -0.438" />
364
+ <geometry>
365
+ <mesh filename="assets/link5.dae" scale="0.001 0.001 0.001" />
366
+ </geometry>
367
+ </visual>
368
+ <inertial>
369
+ <origin rpy="0.0 0.0 0.0" xyz="-0.003049665024221911 0.0008866902457326625 0.043079803024980934" />
370
+ <mass value="0.6156588026168502" />
371
+ <inertia ixx="0.000423" ixy="-8e-06" ixz="6e-06" iyy="0.000445" iyz="-6e-06" izz="0.000324" />
372
+ </inertial>
373
+ </link>
374
+ <joint name="openarm_right_joint5" type="revolute">
375
+ <origin rpy="0 0 0" xyz="0.0 -0.0315 0.0955" />
376
+ <parent link="openarm_right_link4" />
377
+ <child link="openarm_right_link5" />
378
+ <axis xyz="0 0 1" />
379
+ <limit effort="7" lower="-1.570796" upper="1.570796" velocity="20.943946" />
380
+ </joint>
381
+ <link name="openarm_right_link6">
382
+ <visual name="openarm_right_link6_visual">
383
+ <origin rpy="0.0 0.0 0.0" xyz="-0.0375 -0.0 -0.5585" />
384
+ <geometry>
385
+ <mesh filename="assets/link6.dae" scale="0.001 0.001 0.001" />
386
+ </geometry>
387
+ </visual>
388
+ <inertial>
389
+ <origin rpy="0.0 0.0 0.0" xyz="-0.037136587005447405 0.00033230528343419053 -9.498374522309838e-05" />
390
+ <mass value="0.475202773187987" />
391
+ <inertia ixx="0.000143" ixy="1e-06" ixz="1e-06" iyy="0.000157" iyz="1e-06" izz="0.000159" />
392
+ </inertial>
393
+ </link>
394
+ <joint name="openarm_right_joint6" type="revolute">
395
+ <origin rpy="0 0 0" xyz="0.0375 0.0 0.1205" />
396
+ <parent link="openarm_right_link5" />
397
+ <child link="openarm_right_link6" />
398
+ <axis xyz="1 0 0" />
399
+ <limit effort="7" lower="-0.785398" upper="0.785398" velocity="20.943946" />
400
+ </joint>
401
+ <link name="openarm_right_link7">
402
+ <visual name="openarm_right_link7_visual">
403
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 -0.0 -0.5585" />
404
+ <geometry>
405
+ <mesh filename="assets/link7.dae" scale="0.001 0.001 0.001" />
406
+ </geometry>
407
+ </visual>
408
+ <inertial>
409
+ <origin rpy="0.0 0.0 0.0" xyz="6.875510271106056e-05 0.01266175250761268 0.06951945409987448" />
410
+ <mass value="0.4659771327380578" />
411
+ <inertia ixx="0.000639" ixy="1e-06" ixz="1e-06" iyy="0.000497" iyz="8.9e-05" izz="0.000342" />
412
+ </inertial>
413
+ </link>
414
+ <joint name="openarm_right_joint7" type="revolute">
415
+ <origin rpy="0 0 0" xyz="-0.0375 0.0 0.0" />
416
+ <parent link="openarm_right_link6" />
417
+ <child link="openarm_right_link7" />
418
+ <axis xyz="0 1 0" />
419
+ <limit effort="7" lower="-1.570796" upper="1.570796" velocity="20.943946" />
420
+ </joint>
421
+
422
+ <!-- Right hand -->
423
+ <link name="openarm_right_hand">
424
+ <visual name="openarm_right_hand_visual">
425
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 -0.6585" />
426
+ <geometry>
427
+ <mesh filename="assets/hand.dae" scale="0.001 0.001 0.001" />
428
+ </geometry>
429
+ </visual>
430
+ <inertial>
431
+ <origin rpy="0 0 0" xyz="0.0 0.002 0.03" />
432
+ <mass value="0.35" />
433
+ <inertia ixx="0.0002473" ixy="1e-06" ixz="1e-06" iyy="1.763e-05" iyz="1e-06" izz="0.0002521" />
434
+ </inertial>
435
+ </link>
436
+ <joint name="right_openarm_hand_joint" type="fixed">
437
+ <parent link="openarm_right_link7" />
438
+ <child link="openarm_right_hand" />
439
+ <origin rpy="0 0 0" xyz="0 -0.0 0.1001" />
440
+ </joint>
441
+ <link name="openarm_right_hand_tcp">
442
+ <inertial>
443
+ <origin xyz="0 0 0" rpy="0 0 0" />
444
+ <mass value="0.001" />
445
+ <inertia ixx="0.000001" ixy="0.0" ixz="0.0" iyy="0.000001" iyz="0.0" izz="0.000001" />
446
+ </inertial>
447
+ </link>
448
+ <joint name="openarm_right_hand_tcp_joint" type="fixed">
449
+ <origin rpy="0 0 0" xyz="0 -0.0 0.08" />
450
+ <parent link="openarm_right_hand" />
451
+ <child link="openarm_right_hand_tcp" />
452
+ </joint>
453
+ <link name="openarm_right_left_finger">
454
+ <visual name="openarm_right_left_finger_visual">
455
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 -0.05 -0.673001" />
456
+ <geometry>
457
+ <mesh filename="assets/finger.dae" scale="0.001 0.001 0.001" />
458
+ </geometry>
459
+ </visual>
460
+ <inertial>
461
+ <origin rpy="0 0 0" xyz="0.0064528 0.01702 0.0219685" />
462
+ <mass value="0.03602545343277134" />
463
+ <inertia ixx="2.3749999999999997e-06" ixy="1e-06" ixz="1e-06" iyy="2.3749999999999997e-06" iyz="1e-06" izz="7.5e-07" />
464
+ </inertial>
465
+ </link>
466
+ <link name="openarm_right_right_finger">
467
+ <visual name="openarm_right_right_finger_visual">
468
+ <origin rpy="0.0 0.0 0.0" xyz="0.0 0.05 -0.673001" />
469
+ <geometry>
470
+ <mesh filename="assets/finger.dae" scale="0.001 -0.001 0.001" />
471
+ </geometry>
472
+ </visual>
473
+ <inertial>
474
+ <origin rpy="0 0 0" xyz="0.0064528 -0.01702 0.0219685" />
475
+ <mass value="0.03602545343277134" />
476
+ <inertia ixx="2.3749999999999997e-06" ixy="1e-06" ixz="1e-06" iyy="2.3749999999999997e-06" iyz="1e-06" izz="7.5e-07" />
477
+ </inertial>
478
+ </link>
479
+ <joint name="openarm_right_finger_joint1" type="prismatic">
480
+ <parent link="openarm_right_hand" />
481
+ <child link="openarm_right_right_finger" />
482
+ <origin rpy="0 0 0" xyz="0 -0.006 0.015" />
483
+ <axis xyz="0 -1 0" />
484
+ <limit effort="333" lower="0.0" upper="0.044" velocity="10.0" />
485
+ </joint>
486
+ <joint name="openarm_right_finger_joint2" type="prismatic">
487
+ <parent link="openarm_right_hand" />
488
+ <child link="openarm_right_left_finger" />
489
+ <origin rpy="0 0 0" xyz="0 0.006 0.015" />
490
+ <axis xyz="0 1 0" />
491
+ <limit effort="333" lower="0.0" upper="0.044" velocity="10.0" />
492
+ <mimic joint="openarm_right_finger_joint1" />
493
+ </joint>
494
+ </robot>
495
+
public/urdf/so101/assets/base_motor_holder_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424
3
+ size 1877084
public/urdf/so101/assets/base_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d
3
+ size 471584
public/urdf/so101/assets/motor_holder_so101_base_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b
3
+ size 1129384
public/urdf/so101/assets/motor_holder_so101_wrist_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011
3
+ size 1052184
public/urdf/so101/assets/moving_jaw_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d
3
+ size 1413584
public/urdf/so101/assets/rotation_pitch_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34
3
+ size 883684
public/urdf/so101/assets/sts3215_03a_no_horn_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887
3
+ size 865884
public/urdf/so101/assets/sts3215_03a_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0
3
+ size 954084
public/urdf/so101/assets/under_arm_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71
3
+ size 1975884
public/urdf/so101/assets/upper_arm_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f
3
+ size 1303484
public/urdf/so101/assets/waveshare_mounting_plate_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9
3
+ size 62784
public/urdf/so101/assets/wrist_roll_follower_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4
3
+ size 1439884
public/urdf/so101/assets/wrist_roll_pitch_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae
3
+ size 2699784
public/urdf/so101/so100.urdf ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+
3
+ <!-- Originally Generated using onshape-to-robot -->
4
+ <!-- Onshape
5
+ https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
6
+ <robot name="so100">
7
+ <!-- Materials -->
8
+ <material name="green">
9
+ <color rgba="0.06 0.4 0.1 1.0" />
10
+ </material>
11
+ <material name="black">
12
+ <color rgba="0.1 0.1 0.1 1.0" />
13
+ </material>
14
+
15
+ <!-- World link -->
16
+ <link name="world" />
17
+
18
+ <!-- Joint from world to base -->
19
+ <joint name="world_to_base" type="fixed">
20
+ <parent link="world" />
21
+ <child link="base" />
22
+ <origin xyz="0.163038 0.168068 -0.0300817" rpy="0 0 0" />
23
+ </joint>
24
+
25
+ <!-- Link base -->
26
+ <link name="base">
27
+ <inertial>
28
+ <origin xyz="-0.14932 -0.16812 0.065966" rpy="0 0 0" />
29
+ <mass value="0.147" />
30
+ <inertia ixx="0.000114686" ixy="-4.59787e-07" ixz="4.97151e-06" iyy="0.000136117"
31
+ iyz="9.75275e-08" izz="0.000130364" />
32
+ </inertial>
33
+ <!-- Part base_motor_holder_so101_v1 -->
34
+ <visual>
35
+ <origin xyz="-0.169402 -0.168167 0.0300817" rpy="1.5708 -1.67685e-15 1.5708" />
36
+ <geometry>
37
+ <mesh filename="assets/base_motor_holder_so101_v1.stl" />
38
+ </geometry>
39
+ <material name="green" />
40
+ </visual>
41
+ <collision>
42
+ <origin xyz="-0.169402 -0.168167 0.0300817" rpy="1.5708 -1.67685e-15 1.5708" />
43
+ <geometry>
44
+ <mesh filename="assets/base_motor_holder_so101_v1.stl" />
45
+ </geometry>
46
+ </collision>
47
+ <!-- Part base_so101_v2 -->
48
+ <visual>
49
+ <origin xyz="-0.169402 -0.168068 0.0300817" rpy="1.5708 -1.6144e-15 1.5708" />
50
+ <geometry>
51
+ <mesh filename="assets/base_so101_v2.stl" />
52
+ </geometry>
53
+ <material name="green" />
54
+ </visual>
55
+ <collision>
56
+ <origin xyz="-0.169402 -0.168068 0.0300817" rpy="1.5708 -1.6144e-15 1.5708" />
57
+ <geometry>
58
+ <mesh filename="assets/base_so101_v2.stl" />
59
+ </geometry>
60
+ </collision>
61
+ <!-- Part sts3215_03a_v1 -->
62
+ <visual>
63
+ <origin xyz="-0.136702 -0.168068 0.0761817" rpy="-8.21148e-16 7.84513e-18 1.249e-15" />
64
+ <geometry>
65
+ <mesh filename="assets/sts3215_03a_v1.stl" />
66
+ </geometry>
67
+ <material name="black" />
68
+ </visual>
69
+ <collision>
70
+ <origin xyz="-0.136702 -0.168068 0.0761817" rpy="-8.21148e-16 7.84513e-18 1.249e-15" />
71
+ <geometry>
72
+ <mesh filename="assets/sts3215_03a_v1.stl" />
73
+ </geometry>
74
+ </collision>
75
+ <!-- Part waveshare_mounting_plate_so101_v2 -->
76
+ <visual>
77
+ <origin xyz="-0.19402 -0.168267 0.0798817" rpy="1.5708 -1.35493e-14 1.5708" />
78
+ <geometry>
79
+ <mesh filename="assets/waveshare_mounting_plate_so101_v2.stl" />
80
+ </geometry>
81
+ <material name="green" />
82
+ </visual>
83
+ <collision>
84
+ <origin xyz="-0.19402 -0.168267 0.0798817" rpy="1.5708 -1.35493e-14 1.5708" />
85
+ <geometry>
86
+ <mesh filename="assets/waveshare_mounting_plate_so101_v2.stl" />
87
+ </geometry>
88
+ </collision>
89
+ </link>
90
+ <!-- Frame baseframe (dummy link + fixed joint) -->
91
+ <link name="baseframe">
92
+ <origin xyz="0 0 0" rpy="0 -0 0" />
93
+ <inertial>
94
+ <origin xyz="0 0 0" rpy="0 0 0" />
95
+ <mass value="1e-9" />
96
+ <inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0" />
97
+ </inertial>
98
+ </link>
99
+ <joint name="baseframe_frame" type="fixed">
100
+ <origin xyz="-0.163038 -0.168068 0.0324817" rpy="1.6144e-15 7.84513e-18 1.33799e-15" />
101
+ <parent link="base" />
102
+ <child link="baseframe" />
103
+ <axis xyz="0 0 0" />
104
+ </joint>
105
+ <!-- Link shoulder -->
106
+ <link name="shoulder">
107
+ <inertial>
108
+ <origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0" />
109
+ <mass value="0.100006" />
110
+ <inertia ixx="8.3759e-05" ixy="7.55525e-08" ixz="-1.16342e-06" iyy="8.10403e-05"
111
+ iyz="1.54663e-07" izz="2.39783e-05" />
112
+ </inertial>
113
+ <!-- Part sts3215_03a_v1_2 -->
114
+ <visual>
115
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0" />
116
+ <geometry>
117
+ <mesh filename="assets/sts3215_03a_v1.stl" />
118
+ </geometry>
119
+ <material name="black" />
120
+ </visual>
121
+ <collision>
122
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0" />
123
+ <geometry>
124
+ <mesh filename="assets/sts3215_03a_v1.stl" />
125
+ </geometry>
126
+ </collision>
127
+ <!-- Part motor_holder_so101_base_v1 -->
128
+ <visual>
129
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0" />
130
+ <geometry>
131
+ <mesh filename="assets/motor_holder_so101_base_v1.stl" />
132
+ </geometry>
133
+ <material name="green" />
134
+ </visual>
135
+ <collision>
136
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0" />
137
+ <geometry>
138
+ <mesh filename="assets/motor_holder_so101_base_v1.stl" />
139
+ </geometry>
140
+ </collision>
141
+ <!-- Part rotation_pitch_so101_v1 -->
142
+ <visual>
143
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0" />
144
+ <geometry>
145
+ <mesh filename="assets/rotation_pitch_so101_v1.stl" />
146
+ </geometry>
147
+ <material name="green" />
148
+ </visual>
149
+ <collision>
150
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0" />
151
+ <geometry>
152
+ <mesh filename="assets/rotation_pitch_so101_v1.stl" />
153
+ </geometry>
154
+ </collision>
155
+ </link>
156
+ <!-- Link upper_arm -->
157
+ <link name="upper_arm">
158
+ <inertial>
159
+ <origin xyz="-0.0898471 -0.00838224 0.0184089" rpy="0 0 0" />
160
+ <mass value="0.103" />
161
+ <inertia ixx="4.08002e-05" ixy="-1.97819e-05" ixz="-4.03016e-08" iyy="0.000147318"
162
+ iyz="8.97326e-09" izz="0.000142487" />
163
+ </inertial>
164
+ <!-- Part sts3215_03a_v1_3 -->
165
+ <visual>
166
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708" />
167
+ <geometry>
168
+ <mesh filename="assets/sts3215_03a_v1.stl" />
169
+ </geometry>
170
+ <material name="black" />
171
+ </visual>
172
+ <collision>
173
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708" />
174
+ <geometry>
175
+ <mesh filename="assets/sts3215_03a_v1.stl" />
176
+ </geometry>
177
+ </collision>
178
+ <!-- Part upper_arm_so101_v1 -->
179
+ <visual>
180
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30" />
181
+ <geometry>
182
+ <mesh filename="assets/upper_arm_so101_v1.stl" />
183
+ </geometry>
184
+ <material name="green" />
185
+ </visual>
186
+ <collision>
187
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30" />
188
+ <geometry>
189
+ <mesh filename="assets/upper_arm_so101_v1.stl" />
190
+ </geometry>
191
+ </collision>
192
+ </link>
193
+ <!-- Link lower_arm -->
194
+ <link name="lower_arm">
195
+ <inertial>
196
+ <origin xyz="-0.0980701 0.00324376 0.0182831" rpy="0 0 0" />
197
+ <mass value="0.104" />
198
+ <inertia ixx="2.87438e-05" ixy="7.41152e-06" ixz="1.26409e-06" iyy="0.000159844"
199
+ iyz="-4.90188e-08" izz="0.00014529" />
200
+ </inertial>
201
+ <!-- Part under_arm_so101_v1 -->
202
+ <visual>
203
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31" />
204
+ <geometry>
205
+ <mesh filename="assets/under_arm_so101_v1.stl" />
206
+ </geometry>
207
+ <material name="green" />
208
+ </visual>
209
+ <collision>
210
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31" />
211
+ <geometry>
212
+ <mesh filename="assets/under_arm_so101_v1.stl" />
213
+ </geometry>
214
+ </collision>
215
+ <!-- Part motor_holder_so101_wrist_v1 -->
216
+ <visual>
217
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -2.56146e-31" />
218
+ <geometry>
219
+ <mesh filename="assets/motor_holder_so101_wrist_v1.stl" />
220
+ </geometry>
221
+ <material name="green" />
222
+ </visual>
223
+ <collision>
224
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -2.56146e-31" />
225
+ <geometry>
226
+ <mesh filename="assets/motor_holder_so101_wrist_v1.stl" />
227
+ </geometry>
228
+ </collision>
229
+ <!-- Part sts3215_03a_v1_4 -->
230
+ <visual>
231
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159" />
232
+ <geometry>
233
+ <mesh filename="assets/sts3215_03a_v1.stl" />
234
+ </geometry>
235
+ <material name="black" />
236
+ </visual>
237
+ <collision>
238
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159" />
239
+ <geometry>
240
+ <mesh filename="assets/sts3215_03a_v1.stl" />
241
+ </geometry>
242
+ </collision>
243
+ </link>
244
+ <!-- Link wrist -->
245
+ <link name="wrist">
246
+ <inertial>
247
+ <origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0" />
248
+ <mass value="0.079" />
249
+ <inertia ixx="3.68263e-05" ixy="1.7893e-08" ixz="-5.28128e-08" iyy="2.5391e-05"
250
+ iyz="3.6412e-06" izz="2.1e-05" />
251
+ </inertial>
252
+ <!-- Part sts3215_03a_no_horn_v1 -->
253
+ <visual>
254
+ <origin xyz="5.55112e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0" />
255
+ <geometry>
256
+ <mesh filename="assets/sts3215_03a_no_horn_v1.stl" />
257
+ </geometry>
258
+ <material name="black" />
259
+ </visual>
260
+ <collision>
261
+ <origin xyz="5.55112e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0" />
262
+ <geometry>
263
+ <mesh filename="assets/sts3215_03a_no_horn_v1.stl" />
264
+ </geometry>
265
+ </collision>
266
+ <!-- Part wrist_roll_pitch_so101_v2 -->
267
+ <visual>
268
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0" />
269
+ <geometry>
270
+ <mesh filename="assets/wrist_roll_pitch_so101_v2.stl" />
271
+ </geometry>
272
+ <material name="green" />
273
+ </visual>
274
+ <collision>
275
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0" />
276
+ <geometry>
277
+ <mesh filename="assets/wrist_roll_pitch_so101_v2.stl" />
278
+ </geometry>
279
+ </collision>
280
+ </link>
281
+ <!-- Link gripper -->
282
+ <link name="gripper">
283
+ <inertial>
284
+ <origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0" />
285
+ <mass value="0.087" />
286
+ <inertia ixx="2.75087e-05" ixy="-3.35241e-07" ixz="-5.7352e-06" iyy="4.33657e-05"
287
+ iyz="-5.17847e-08" izz="3.45059e-05" />
288
+ </inertial>
289
+ <!-- Part sts3215_03a_v1_5 -->
290
+ <visual>
291
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16" />
292
+ <geometry>
293
+ <mesh filename="assets/sts3215_03a_v1.stl" />
294
+ </geometry>
295
+ <material name="black" />
296
+ </visual>
297
+ <collision>
298
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16" />
299
+ <geometry>
300
+ <mesh filename="assets/sts3215_03a_v1.stl" />
301
+ </geometry>
302
+ </collision>
303
+ <!-- Part wrist_roll_follower_so101_v1 -->
304
+ <visual>
305
+ <origin xyz="0 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0" />
306
+ <geometry>
307
+ <mesh filename="assets/wrist_roll_follower_so101_v1.stl" />
308
+ </geometry>
309
+ <material name="green" />
310
+ </visual>
311
+ <collision>
312
+ <origin xyz="0 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0" />
313
+ <geometry>
314
+ <mesh filename="assets/wrist_roll_follower_so101_v1.stl" />
315
+ </geometry>
316
+ </collision>
317
+ </link>
318
+ <!-- Frame gripperframe (dummy link + fixed joint) -->
319
+ <link name="gripperframe">
320
+ <origin xyz="0 0 0" rpy="0 -0 0" />
321
+ <inertial>
322
+ <origin xyz="0 0 0" rpy="0 0 0" />
323
+ <mass value="1e-9" />
324
+ <inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0" />
325
+ </inertial>
326
+ </link>
327
+ <joint name="gripperframe_frame" type="fixed">
328
+ <origin xyz="-0.0079 -0.000218121 -0.0981274" rpy="-0 1.5708 0" />
329
+ <parent link="gripper" />
330
+ <child link="gripperframe" />
331
+ <axis xyz="0 0 0" />
332
+ </joint>
333
+ <!-- Link moving_jaw_so101_v1 -->
334
+ <link name="moving_jaw_so101_v1">
335
+ <inertial>
336
+ <origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0" />
337
+ <mass value="0.012" />
338
+ <inertia ixx="6.61427e-06" ixy="-3.19807e-07" ixz="-5.90717e-09" iyy="1.89032e-06"
339
+ iyz="-1.09945e-07" izz="5.28738e-06" />
340
+ </inertial>
341
+ <!-- Part moving_jaw_so101_v1 -->
342
+ <visual>
343
+ <origin xyz="-5.55112e-17 0 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24" />
344
+ <geometry>
345
+ <mesh filename="assets/moving_jaw_so101_v1.stl" />
346
+ </geometry>
347
+ <material name="green" />
348
+ </visual>
349
+ <collision>
350
+ <origin xyz="-5.55112e-17 0 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24" />
351
+ <geometry>
352
+ <mesh filename="assets/moving_jaw_so101_v1.stl" />
353
+ </geometry>
354
+ </collision>
355
+ </link>
356
+
357
+ <!-- Joint from base to shoulder -->
358
+ <joint name="Rotation" type="revolute">
359
+ <origin xyz="-0.124202 -0.168068 0.0948817" rpy="3.14159 0 0" />
360
+ <parent link="base" />
361
+ <child link="shoulder" />
362
+ <axis xyz="0 0 1" />
363
+ <limit effort="10" velocity="10" lower="1.22014" upper="5.05986" />
364
+ </joint>
365
+ <!-- Joint from shoulder to upper_arm -->
366
+ <joint name="Pitch" type="revolute">
367
+ <origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 1.5692 0" />
368
+ <parent link="shoulder" />
369
+ <child link="upper_arm" />
370
+ <axis xyz="0 0 1" />
371
+ <limit effort="10" velocity="10" lower="1.39467" upper="4.88692" />
372
+ </joint>
373
+ <!-- Joint from upper_arm to lower_arm -->
374
+ <joint name="Elbow" type="revolute">
375
+ <origin xyz="-0.11257 -0.028 2.09886e-16" rpy="0 0 4.71239" />
376
+ <parent link="upper_arm" />
377
+ <child link="lower_arm" />
378
+ <axis xyz="0 0 1" />
379
+ <limit effort="10" velocity="10" lower="1.39626" upper="4.71239" />
380
+ </joint>
381
+ <!-- Joint from lower_arm to wrist -->
382
+ <joint name="Wrist_Pitch" type="revolute">
383
+ <origin xyz="-0.1349 0.0052 8.44651e-17" rpy="0 0 1.57079" />
384
+ <parent link="lower_arm" />
385
+ <child link="wrist" />
386
+ <axis xyz="0 0 1" />
387
+ <limit effort="10" velocity="10" lower="1.48353" upper="4.79965" />
388
+ </joint>
389
+ <!-- Joint from wrist to gripper -->
390
+ <joint name="Wrist_Roll" type="revolute">
391
+ <origin xyz="2.77556e-16 -0.0611 0.0181" rpy="1.5708 3.1902695 3.14159" />
392
+ <parent link="wrist" />
393
+ <child link="gripper" />
394
+ <axis xyz="0 0 1" />
395
+ <limit effort="10" velocity="10" lower="0.39774" upper="5.9828" />
396
+ </joint>
397
+ <!-- Joint from gripper to moving_jaw_so101_v1 -->
398
+ <joint name="Jaw" type="revolute">
399
+ <origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 3.315 0" />
400
+ <parent link="gripper" />
401
+ <child link="moving_jaw_so101_v1" />
402
+ <axis xyz="0 0 1" />
403
+ <limit effort="10" velocity="10" lower="3.14" upper="4.88692" />
404
+ </joint>
405
+ </robot>
406
+
public/urdf/so101/so101_new_calib.urdf ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generated using onshape-to-robot -->
3
+ <!-- Onshape https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
4
+ <robot name="so101_new_calib">
5
+
6
+ <!-- Materials -->
7
+ <material name="3d_printed">
8
+ <color rgba="1.0 0.82 0.12 1.0"/>
9
+ </material>
10
+ <material name="sts3215">
11
+ <color rgba="0.1 0.1 0.1 1.0"/>
12
+ </material>
13
+
14
+ <!-- Link base -->
15
+ <link name="base_link">
16
+ <inertial>
17
+ <origin xyz="0.0137179 -5.19711e-05 0.0334843" rpy="0 0 0"/>
18
+ <mass value="0.147"/>
19
+ <inertia ixx="0.000114686" ixy="-4.59787e-07" ixz="4.97151e-06" iyy="0.000136117" iyz="9.75275e-08" izz="0.000130364"/>
20
+ </inertial>
21
+ <!-- Part base_motor_holder_so101_v1 -->
22
+ <visual>
23
+ <origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
24
+ <geometry>
25
+ <mesh filename="assets/base_motor_holder_so101_v1.stl"/>
26
+ </geometry>
27
+ <material name="3d_printed"/>
28
+ </visual>
29
+ <collision>
30
+ <origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
31
+ <geometry>
32
+ <mesh filename="assets/base_motor_holder_so101_v1.stl"/>
33
+ </geometry>
34
+ </collision>
35
+ <!-- Part base_so101_v2 -->
36
+ <visual>
37
+ <origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
38
+ <geometry>
39
+ <mesh filename="assets/base_so101_v2.stl"/>
40
+ </geometry>
41
+ <material name="3d_printed"/>
42
+ </visual>
43
+ <collision>
44
+ <origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
45
+ <geometry>
46
+ <mesh filename="assets/base_so101_v2.stl"/>
47
+ </geometry>
48
+ </collision>
49
+ <!-- Part sts3215_03a_v1 -->
50
+ <visual>
51
+ <origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
52
+ <geometry>
53
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
54
+ </geometry>
55
+ <material name="sts3215"/>
56
+ </visual>
57
+ <collision>
58
+ <origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
59
+ <geometry>
60
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
61
+ </geometry>
62
+ </collision>
63
+ <!-- Part waveshare_mounting_plate_so101_v2 -->
64
+ <visual>
65
+ <origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
66
+ <geometry>
67
+ <mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
68
+ </geometry>
69
+ <material name="3d_printed"/>
70
+ </visual>
71
+ <collision>
72
+ <origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
73
+ <geometry>
74
+ <mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
75
+ </geometry>
76
+ </collision>
77
+ </link>
78
+
79
+ <!-- Link shoulder -->
80
+ <link name="shoulder_link">
81
+ <inertial>
82
+ <origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0"/>
83
+ <mass value="0.100006"/>
84
+ <inertia ixx="8.3759e-05" ixy="7.55525e-08" ixz="-1.16342e-06" iyy="8.10403e-05" iyz="1.54663e-07" izz="2.39783e-05"/>
85
+ </inertial>
86
+ <!-- Part sts3215_03a_v1_2 -->
87
+ <visual>
88
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
89
+ <geometry>
90
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
91
+ </geometry>
92
+ <material name="sts3215"/>
93
+ </visual>
94
+ <collision>
95
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
96
+ <geometry>
97
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
98
+ </geometry>
99
+ </collision>
100
+ <!-- Part motor_holder_so101_base_v1 -->
101
+ <visual>
102
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
103
+ <geometry>
104
+ <mesh filename="assets/motor_holder_so101_base_v1.stl"/>
105
+ </geometry>
106
+ <material name="3d_printed"/>
107
+ </visual>
108
+ <collision>
109
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
110
+ <geometry>
111
+ <mesh filename="assets/motor_holder_so101_base_v1.stl"/>
112
+ </geometry>
113
+ </collision>
114
+ <!-- Part rotation_pitch_so101_v1 -->
115
+ <visual>
116
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
117
+ <geometry>
118
+ <mesh filename="assets/rotation_pitch_so101_v1.stl"/>
119
+ </geometry>
120
+ <material name="3d_printed"/>
121
+ </visual>
122
+ <collision>
123
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
124
+ <geometry>
125
+ <mesh filename="assets/rotation_pitch_so101_v1.stl"/>
126
+ </geometry>
127
+ </collision>
128
+ </link>
129
+
130
+ <!-- Link upper_arm -->
131
+ <link name="upper_arm_link">
132
+ <inertial>
133
+ <origin xyz="-0.0898471 -0.00838224 0.0184089" rpy="0 0 0"/>
134
+ <mass value="0.103"/>
135
+ <inertia ixx="4.08002e-05" ixy="-1.97819e-05" ixz="-4.03016e-08" iyy="0.000147318" iyz="8.97326e-09" izz="0.000142487"/>
136
+ </inertial>
137
+ <!-- Part sts3215_03a_v1_3 -->
138
+ <visual>
139
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
140
+ <geometry>
141
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
142
+ </geometry>
143
+ <material name="sts3215"/>
144
+ </visual>
145
+ <collision>
146
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
147
+ <geometry>
148
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
149
+ </geometry>
150
+ </collision>
151
+ <!-- Part upper_arm_so101_v1 -->
152
+ <visual>
153
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
154
+ <geometry>
155
+ <mesh filename="assets/upper_arm_so101_v1.stl"/>
156
+ </geometry>
157
+ <material name="3d_printed"/>
158
+ </visual>
159
+ <collision>
160
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
161
+ <geometry>
162
+ <mesh filename="assets/upper_arm_so101_v1.stl"/>
163
+ </geometry>
164
+ </collision>
165
+ </link>
166
+
167
+ <!-- Link lower_arm -->
168
+ <link name="lower_arm_link">
169
+ <inertial>
170
+ <origin xyz="-0.0980701 0.00324376 0.0182831" rpy="0 0 0"/>
171
+ <mass value="0.104"/>
172
+ <inertia ixx="2.87438e-05" ixy="7.41152e-06" ixz="1.26409e-06" iyy="0.000159844" iyz="-4.90188e-08" izz="0.00014529"/>
173
+ </inertial>
174
+ <!-- Part under_arm_so101_v1 -->
175
+ <visual>
176
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
177
+ <geometry>
178
+ <mesh filename="assets/under_arm_so101_v1.stl"/>
179
+ </geometry>
180
+ <material name="3d_printed"/>
181
+ </visual>
182
+ <collision>
183
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
184
+ <geometry>
185
+ <mesh filename="assets/under_arm_so101_v1.stl"/>
186
+ </geometry>
187
+ </collision>
188
+ <!-- Part motor_holder_so101_wrist_v1 -->
189
+ <visual>
190
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
191
+ <geometry>
192
+ <mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
193
+ </geometry>
194
+ <material name="3d_printed"/>
195
+ </visual>
196
+ <collision>
197
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
198
+ <geometry>
199
+ <mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
200
+ </geometry>
201
+ </collision>
202
+ <!-- Part sts3215_03a_v1_4 -->
203
+ <visual>
204
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
205
+ <geometry>
206
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
207
+ </geometry>
208
+ <material name="sts3215"/>
209
+ </visual>
210
+ <collision>
211
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
212
+ <geometry>
213
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
214
+ </geometry>
215
+ </collision>
216
+ </link>
217
+
218
+ <!-- Link wrist -->
219
+ <link name="wrist_link">
220
+ <inertial>
221
+ <origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0"/>
222
+ <mass value="0.079"/>
223
+ <inertia ixx="3.68263e-05" ixy="1.7893e-08" ixz="-5.28128e-08" iyy="2.5391e-05" iyz="3.6412e-06" izz="2.1e-05"/>
224
+ </inertial>
225
+ <!-- Part sts3215_03a_no_horn_v1 -->
226
+ <visual>
227
+ <origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
228
+ <geometry>
229
+ <mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
230
+ </geometry>
231
+ <material name="sts3215"/>
232
+ </visual>
233
+ <collision>
234
+ <origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
235
+ <geometry>
236
+ <mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
237
+ </geometry>
238
+ </collision>
239
+ <!-- Part wrist_roll_pitch_so101_v2 -->
240
+ <visual>
241
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
242
+ <geometry>
243
+ <mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
244
+ </geometry>
245
+ <material name="3d_printed"/>
246
+ </visual>
247
+ <collision>
248
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
249
+ <geometry>
250
+ <mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
251
+ </geometry>
252
+ </collision>
253
+ </link>
254
+
255
+ <!-- Link gripper -->
256
+ <link name="gripper_link">
257
+ <inertial>
258
+ <origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0"/>
259
+ <mass value="0.087"/>
260
+ <inertia ixx="2.75087e-05" ixy="-3.35241e-07" ixz="-5.7352e-06" iyy="4.33657e-05" iyz="-5.17847e-08" izz="3.45059e-05"/>
261
+ </inertial>
262
+ <!-- Part sts3215_03a_v1_5 -->
263
+ <visual>
264
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
265
+ <geometry>
266
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
267
+ </geometry>
268
+ <material name="sts3215"/>
269
+ </visual>
270
+ <collision>
271
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
272
+ <geometry>
273
+ <mesh filename="assets/sts3215_03a_v1.stl"/>
274
+ </geometry>
275
+ </collision>
276
+ <!-- Part wrist_roll_follower_so101_v1 -->
277
+ <visual>
278
+ <origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
279
+ <geometry>
280
+ <mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
281
+ </geometry>
282
+ <material name="3d_printed"/>
283
+ </visual>
284
+ <collision>
285
+ <origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
286
+ <geometry>
287
+ <mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
288
+ </geometry>
289
+ </collision>
290
+ </link>
291
+
292
+ <!-- Gripper frame (dummy link + fixed joint) -->
293
+ <link name="gripper_frame_link">
294
+ <origin xyz="0 0 0" rpy="0 -0 0"/>
295
+ <inertial>
296
+ <origin xyz="0 0 0" rpy="0 0 0"/>
297
+ <mass value="1e-9"/>
298
+ <inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0"/>
299
+ </inertial>
300
+ </link>
301
+
302
+ <joint name="gripper_frame_joint" type="fixed">
303
+ <origin xyz="-0.0079 -0.000218121 -0.0981274" rpy="0 3.14159 0"/>
304
+ <parent link="gripper_link"/>
305
+ <child link="gripper_frame_link"/>
306
+ <axis xyz="0 0 0"/>
307
+ </joint>
308
+
309
+ <!-- Link moving_jaw_so101_v1 -->
310
+ <link name="moving_jaw_so101_v1_link">
311
+ <inertial>
312
+ <origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0"/>
313
+ <mass value="0.012"/>
314
+ <inertia ixx="6.61427e-06" ixy="-3.19807e-07" ixz="-5.90717e-09" iyy="1.89032e-06" iyz="-1.09945e-07" izz="5.28738e-06"/>
315
+ </inertial>
316
+ <!-- Part moving_jaw_so101_v1 -->
317
+ <visual>
318
+ <origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
319
+ <geometry>
320
+ <mesh filename="assets/moving_jaw_so101_v1.stl"/>
321
+ </geometry>
322
+ <material name="3d_printed"/>
323
+ </visual>
324
+ <collision>
325
+ <origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
326
+ <geometry>
327
+ <mesh filename="assets/moving_jaw_so101_v1.stl"/>
328
+ </geometry>
329
+ </collision>
330
+ </link>
331
+
332
+ <!-- Joint from gripper to moving_jaw_so101_v1 -->
333
+ <joint name="gripper" type="revolute">
334
+ <origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 -5.24284e-08 -1.41553e-15"/>
335
+ <parent link="gripper_link"/>
336
+ <child link="moving_jaw_so101_v1_link"/>
337
+ <axis xyz="0 0 1"/>
338
+ <limit effort="10" velocity="10" lower="-0.174533" upper="1.74533"/>
339
+ </joint>
340
+
341
+ <transmission name="gripper_trans">
342
+ <type>transmission_interface/SimpleTransmission</type>
343
+ <joint name="gripper">
344
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
345
+ </joint>
346
+ <actuator name="motor6">
347
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
348
+ <mechanicalReduction>1</mechanicalReduction>
349
+ </actuator>
350
+ </transmission>
351
+
352
+ <!-- Joint from wrist to gripper -->
353
+ <joint name="wrist_roll" type="revolute">
354
+ <origin xyz="5.55112e-17 -0.0611 0.0181" rpy="1.5708 0.0486795 3.14159"/>
355
+ <parent link="wrist_link"/>
356
+ <child link="gripper_link"/>
357
+ <axis xyz="0 0 1"/>
358
+ <limit effort="10" velocity="10" lower="-2.74385" upper="2.84121"/>
359
+ </joint>
360
+
361
+ <transmission name="wrist_roll_trans">
362
+ <type>transmission_interface/SimpleTransmission</type>
363
+ <joint name="wrist_roll">
364
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
365
+ </joint>
366
+ <actuator name="motor5">
367
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
368
+ <mechanicalReduction>1</mechanicalReduction>
369
+ </actuator>
370
+ </transmission>
371
+
372
+ <!-- Joint from lower_arm to wrist -->
373
+ <joint name="wrist_flex" type="revolute">
374
+ <origin xyz="-0.1349 0.0052 3.62355e-17" rpy="4.02456e-15 8.67362e-16 -1.5708"/>
375
+ <parent link="lower_arm_link"/>
376
+ <child link="wrist_link"/>
377
+ <axis xyz="0 0 1"/>
378
+ <limit effort="10" velocity="10" lower="-1.65806" upper="1.65806"/>
379
+ </joint>
380
+
381
+ <transmission name="wrist_flex_trans">
382
+ <type>transmission_interface/SimpleTransmission</type>
383
+ <joint name="wrist_flex">
384
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
385
+ </joint>
386
+ <actuator name="motor4">
387
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
388
+ <mechanicalReduction>1</mechanicalReduction>
389
+ </actuator>
390
+ </transmission>
391
+
392
+ <!-- Joint from upper_arm to lower_arm -->
393
+ <!-- Note: 5-degree calibration offset applied to joint limits -->
394
+ <joint name="elbow_flex" type="revolute">
395
+ <origin xyz="-0.11257 -0.028 1.73763e-16" rpy="-3.63608e-16 8.74301e-16 1.5708"/>
396
+ <parent link="upper_arm_link"/>
397
+ <child link="lower_arm_link"/>
398
+ <axis xyz="0 0 1"/>
399
+ <limit effort="10" velocity="10" lower="-1.69" upper="1.69"/>
400
+ </joint>
401
+
402
+ <transmission name="elbow_flex_trans">
403
+ <type>transmission_interface/SimpleTransmission</type>
404
+ <joint name="elbow_flex">
405
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
406
+ </joint>
407
+ <actuator name="motor3">
408
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
409
+ <mechanicalReduction>1</mechanicalReduction>
410
+ </actuator>
411
+ </transmission>
412
+
413
+ <!-- Joint from shoulder to upper_arm -->
414
+ <joint name="shoulder_lift" type="revolute">
415
+ <origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 -1.5708 0"/>
416
+ <parent link="shoulder_link"/>
417
+ <child link="upper_arm_link"/>
418
+ <axis xyz="0 0 1"/>
419
+ <limit effort="10" velocity="10" lower="-1.74533" upper="1.74533"/>
420
+ </joint>
421
+
422
+ <transmission name="shoulder_lift_trans">
423
+ <type>transmission_interface/SimpleTransmission</type>
424
+ <joint name="shoulder_lift">
425
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
426
+ </joint>
427
+ <actuator name="motor2">
428
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
429
+ <mechanicalReduction>1</mechanicalReduction>
430
+ </actuator>
431
+ </transmission>
432
+
433
+ <!-- Joint from base to shoulder -->
434
+ <joint name="shoulder_pan" type="revolute">
435
+ <origin xyz="0.0388353 -8.97657e-09 0.0624" rpy="3.14159 4.18253e-17 -3.14159"/>
436
+ <parent link="base_link"/>
437
+ <child link="shoulder_link"/>
438
+ <axis xyz="0 0 1"/>
439
+ <limit effort="10" velocity="10" lower="-1.91986" upper="1.91986"/>
440
+ </joint>
441
+
442
+ <transmission name="shoulder_pan_trans">
443
+ <type>transmission_interface/SimpleTransmission</type>
444
+ <joint name="shoulder_pan">
445
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
446
+ </joint>
447
+ <actuator name="motor1">
448
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
449
+ <mechanicalReduction>1</mechanicalReduction>
450
+ </actuator>
451
+ </transmission>
452
+
453
+ </robot>
src/app/.well-known/appspecific/[...slug]/route.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+
3
+ export function GET() {
4
+ return new NextResponse(null, { status: 404 });
5
+ }
src/app/[org]/[dataset]/[episode]/actions.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { getDatasetVersionAndInfo } from "@/utils/versionUtils";
4
+ import type { DatasetMetadata } from "@/utils/parquetUtils";
5
+ import {
6
+ loadAllEpisodeLengthsV3,
7
+ loadAllEpisodeFrameInfo,
8
+ loadCrossEpisodeActionVariance,
9
+ loadEpisodeFlatChartData,
10
+ type EpisodeLengthStats,
11
+ type EpisodeFramesData,
12
+ type CrossEpisodeVarianceData,
13
+ } from "./fetch-data";
14
+
15
+ export async function fetchEpisodeLengthStats(
16
+ org: string,
17
+ dataset: string,
18
+ ): Promise<EpisodeLengthStats | null> {
19
+ const repoId = `${org}/${dataset}`;
20
+ const { version, info } = await getDatasetVersionAndInfo(repoId);
21
+ if (version !== "v3.0") return null;
22
+ return loadAllEpisodeLengthsV3(repoId, version, info.fps);
23
+ }
24
+
25
+ export async function fetchEpisodeFrames(
26
+ org: string,
27
+ dataset: string,
28
+ ): Promise<EpisodeFramesData> {
29
+ const repoId = `${org}/${dataset}`;
30
+ const { version, info } = await getDatasetVersionAndInfo(repoId);
31
+ return loadAllEpisodeFrameInfo(
32
+ repoId,
33
+ version,
34
+ info as unknown as DatasetMetadata,
35
+ );
36
+ }
37
+
38
+ export async function fetchCrossEpisodeVariance(
39
+ org: string,
40
+ dataset: string,
41
+ ): Promise<CrossEpisodeVarianceData | null> {
42
+ const repoId = `${org}/${dataset}`;
43
+ const { version, info } = await getDatasetVersionAndInfo(repoId);
44
+ return loadCrossEpisodeActionVariance(
45
+ repoId,
46
+ version,
47
+ info as unknown as DatasetMetadata,
48
+ info.fps,
49
+ );
50
+ }
51
+
52
+ export async function fetchEpisodeChartData(
53
+ org: string,
54
+ dataset: string,
55
+ episodeId: number,
56
+ ): Promise<Record<string, number>[]> {
57
+ const repoId = `${org}/${dataset}`;
58
+ const { version, info } = await getDatasetVersionAndInfo(repoId);
59
+ return loadEpisodeFlatChartData(
60
+ repoId,
61
+ version,
62
+ info as unknown as DatasetMetadata,
63
+ episodeId,
64
+ );
65
+ }
src/app/[org]/[dataset]/[episode]/episode-viewer.tsx CHANGED
@@ -1,16 +1,46 @@
1
  "use client";
2
 
3
- import { useState, useEffect, useRef } from "react";
4
  import { useRouter, useSearchParams } from "next/navigation";
5
  import { postParentMessageWithParams } from "@/utils/postParentMessage";
6
  import { SimpleVideosPlayer } from "@/components/simple-videos-player";
7
  import DataRecharts from "@/components/data-recharts";
8
  import PlaybackBar from "@/components/playback-bar";
9
  import { TimeProvider, useTime } from "@/context/time-context";
 
10
  import Sidebar from "@/components/side-nav";
 
 
11
  import Loading from "@/components/loading-component";
12
- import { getAdjacentEpisodesVideoInfo } from "./fetch-data";
13
- import type { EpisodeData } from "@/types";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  export default function EpisodeViewer({
16
  data,
@@ -39,8 +69,10 @@ export default function EpisodeViewer({
39
  }
40
 
41
  return (
42
- <TimeProvider duration={data.duration}>
43
- <EpisodeViewerInner data={data} org={org} dataset={dataset} />
 
 
44
  </TimeProvider>
45
  );
46
  }
@@ -67,10 +99,127 @@ function EpisodeViewerInner({
67
  const [chartsReady, setChartsReady] = useState(false);
68
  const isLoading = !videosReady || !chartsReady;
69
 
 
 
 
 
 
 
 
 
 
70
  const router = useRouter();
71
  const searchParams = useSearchParams();
72
 
73
- // State
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  // Use context for time sync
75
  const { currentTime, setCurrentTime, setIsPlaying, isPlaying } = useTime();
76
 
@@ -83,20 +232,29 @@ function EpisodeViewerInner({
83
  currentPage * pageSize,
84
  );
85
 
86
- // Preload adjacent episodes' videos
87
  useEffect(() => {
88
  if (!org || !dataset) return;
 
89
 
90
- const preloadAdjacent = async () => {
91
- try {
92
- await getAdjacentEpisodesVideoInfo(org, dataset, episodeId, 2);
93
- // Preload adjacent episodes for smoother navigation
94
- } catch {
95
- // Skip preloading on error
96
- }
97
- };
 
 
 
 
 
 
98
 
99
- preloadAdjacent();
 
 
100
  }, [org, dataset, episodeId]);
101
 
102
  // Initialize based on URL time parameter
@@ -189,86 +347,236 @@ function EpisodeViewerInner({
189
  };
190
 
191
  return (
192
- <div className="flex h-screen max-h-screen bg-slate-950 text-gray-200">
193
- {/* Sidebar */}
194
- <Sidebar
195
- datasetInfo={datasetInfo}
196
- paginatedEpisodes={paginatedEpisodes}
197
- episodeId={episodeId}
198
- totalPages={totalPages}
199
- currentPage={currentPage}
200
- prevPage={prevPage}
201
- nextPage={nextPage}
202
- />
203
-
204
- {/* Content */}
205
- <div
206
- className={`flex max-h-screen flex-col gap-4 p-4 md:flex-1 relative ${isLoading ? "overflow-hidden" : "overflow-y-auto"}`}
207
- >
208
- {isLoading && <Loading />}
209
-
210
- <div className="flex items-center justify-start my-4">
211
- <a
212
- href="https://github.com/huggingface/lerobot"
213
- target="_blank"
214
- className="block"
215
- >
216
- <img
217
- src="https://github.com/huggingface/lerobot/raw/main/media/readme/lerobot-logo-thumbnail.png"
218
- alt="LeRobot Logo"
219
- className="w-32"
220
- />
221
- </a>
222
-
223
- <div>
224
- <a
225
- href={`https://huggingface.co/datasets/${datasetInfo.repoId}`}
226
- target="_blank"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  >
228
- <p className="text-lg font-semibold">{datasetInfo.repoId}</p>
229
- </a>
230
-
231
- <p className="font-mono text-lg font-semibold">
232
- episode {episodeId}
233
- </p>
234
- </div>
235
- </div>
236
 
237
- {/* Videos */}
238
- {videosInfo.length && (
239
- <SimpleVideosPlayer
240
- videosInfo={videosInfo}
241
- onVideosReady={() => setVideosReady(true)}
 
 
 
 
 
 
 
 
 
242
  />
243
  )}
244
 
245
- {/* Language Instruction */}
246
- {task && (
247
- <div className="mb-6 p-4 bg-slate-800 rounded-lg border border-slate-600">
248
- <p className="text-slate-300">
249
- <span className="font-semibold text-slate-100">
250
- Language Instruction:
251
- </span>
252
- </p>
253
- <div className="mt-2 text-slate-300">
254
- {task.split("\n").map((instruction, index) => (
255
- <p key={index} className="mb-1">
256
- {instruction}
257
- </p>
258
- ))}
259
- </div>
260
- </div>
261
- )}
262
 
263
- {/* Graph */}
264
- <div className="mb-4">
265
- <DataRecharts
266
- data={chartDataGroups}
267
- onChartsReady={() => setChartsReady(true)}
268
- />
269
- </div>
 
 
 
 
 
 
 
270
 
271
- <PlaybackBar />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  </div>
273
  </div>
274
  );
 
1
  "use client";
2
 
3
+ import { useState, useEffect, useRef, lazy, Suspense } from "react";
4
  import { useRouter, useSearchParams } from "next/navigation";
5
  import { postParentMessageWithParams } from "@/utils/postParentMessage";
6
  import { SimpleVideosPlayer } from "@/components/simple-videos-player";
7
  import DataRecharts from "@/components/data-recharts";
8
  import PlaybackBar from "@/components/playback-bar";
9
  import { TimeProvider, useTime } from "@/context/time-context";
10
+ import { FlaggedEpisodesProvider } from "@/context/flagged-episodes-context";
11
  import Sidebar from "@/components/side-nav";
12
+ import StatsPanel from "@/components/stats-panel";
13
+ import OverviewPanel from "@/components/overview-panel";
14
  import Loading from "@/components/loading-component";
15
+ import { hasURDFSupport } from "@/lib/so101-robot";
16
+ import {
17
+ getAdjacentEpisodesVideoInfo,
18
+ computeColumnMinMax,
19
+ type EpisodeData,
20
+ type ColumnMinMax,
21
+ type EpisodeLengthStats,
22
+ type EpisodeFramesData,
23
+ type CrossEpisodeVarianceData,
24
+ } from "./fetch-data";
25
+ import {
26
+ fetchEpisodeLengthStats,
27
+ fetchEpisodeFrames,
28
+ fetchCrossEpisodeVariance,
29
+ } from "./actions";
30
+
31
+ const URDFViewer = lazy(() => import("@/components/urdf-viewer"));
32
+ const ActionInsightsPanel = lazy(
33
+ () => import("@/components/action-insights-panel"),
34
+ );
35
+ const FilteringPanel = lazy(() => import("@/components/filtering-panel"));
36
+
37
+ type ActiveTab =
38
+ | "episodes"
39
+ | "statistics"
40
+ | "frames"
41
+ | "insights"
42
+ | "filtering"
43
+ | "urdf";
44
 
45
  export default function EpisodeViewer({
46
  data,
 
69
  }
70
 
71
  return (
72
+ <TimeProvider duration={data!.duration}>
73
+ <FlaggedEpisodesProvider>
74
+ <EpisodeViewerInner data={data!} org={org} dataset={dataset} />
75
+ </FlaggedEpisodesProvider>
76
  </TimeProvider>
77
  );
78
  }
 
99
  const [chartsReady, setChartsReady] = useState(false);
100
  const isLoading = !videosReady || !chartsReady;
101
 
102
+ const loadStartRef = useRef(performance.now());
103
+ useEffect(() => {
104
+ if (!isLoading) {
105
+ console.log(
106
+ `[perf] Loading complete in ${(performance.now() - loadStartRef.current).toFixed(0)}ms (videos: ${videosReady ? "✓" : "…"}, charts: ${chartsReady ? "✓" : "…"})`,
107
+ );
108
+ }
109
+ }, [isLoading]);
110
+
111
  const router = useRouter();
112
  const searchParams = useSearchParams();
113
 
114
+ // Tab state & lazy stats
115
+ const [activeTab, setActiveTab] = useState<ActiveTab>("episodes");
116
+ const [, setColumnMinMax] = useState<ColumnMinMax[] | null>(null);
117
+ const [episodeLengthStats, setEpisodeLengthStats] =
118
+ useState<EpisodeLengthStats | null>(null);
119
+ const [statsLoading, setStatsLoading] = useState(false);
120
+ const statsLoadedRef = useRef(false);
121
+ const [episodeFramesData, setEpisodeFramesData] =
122
+ useState<EpisodeFramesData | null>(null);
123
+ const [framesLoading, setFramesLoading] = useState(false);
124
+ const framesLoadedRef = useRef(false);
125
+ const [framesFlaggedOnly, setFramesFlaggedOnly] = useState(false);
126
+ const [sidebarFlaggedOnly, setSidebarFlaggedOnly] = useState(false);
127
+ const [crossEpData, setCrossEpData] =
128
+ useState<CrossEpisodeVarianceData | null>(null);
129
+ const [insightsLoading, setInsightsLoading] = useState(false);
130
+ const insightsLoadedRef = useRef(false);
131
+
132
+ // Hydrate UI state from sessionStorage after mount (avoids SSR/client mismatch)
133
+ useEffect(() => {
134
+ const stored = sessionStorage.getItem("activeTab");
135
+ if (
136
+ stored &&
137
+ [
138
+ "episodes",
139
+ "statistics",
140
+ "frames",
141
+ "insights",
142
+ "filtering",
143
+ "urdf",
144
+ ].includes(stored)
145
+ ) {
146
+ setActiveTab(stored as ActiveTab);
147
+ }
148
+ if (sessionStorage.getItem("framesFlaggedOnly") === "true")
149
+ setFramesFlaggedOnly(true);
150
+ if (sessionStorage.getItem("sidebarFlaggedOnly") === "true")
151
+ setSidebarFlaggedOnly(true);
152
+ }, []);
153
+
154
+ // Persist UI state across episode navigations
155
+ useEffect(() => {
156
+ sessionStorage.setItem("activeTab", activeTab);
157
+ }, [activeTab]);
158
+ useEffect(() => {
159
+ sessionStorage.setItem("sidebarFlaggedOnly", String(sidebarFlaggedOnly));
160
+ }, [sidebarFlaggedOnly]);
161
+ useEffect(() => {
162
+ sessionStorage.setItem("framesFlaggedOnly", String(framesFlaggedOnly));
163
+ }, [framesFlaggedOnly]);
164
+
165
+ const loadStats = () => {
166
+ if (statsLoadedRef.current) return;
167
+ statsLoadedRef.current = true;
168
+ setStatsLoading(true);
169
+ setColumnMinMax(computeColumnMinMax(data.chartDataGroups));
170
+ if (org && dataset) {
171
+ fetchEpisodeLengthStats(org, dataset)
172
+ .then((result) => setEpisodeLengthStats(result))
173
+ .catch(() => {})
174
+ .finally(() => setStatsLoading(false));
175
+ } else {
176
+ setStatsLoading(false);
177
+ }
178
+ };
179
+
180
+ const loadFrames = () => {
181
+ if (framesLoadedRef.current || !org || !dataset) return;
182
+ framesLoadedRef.current = true;
183
+ setFramesLoading(true);
184
+ fetchEpisodeFrames(org, dataset)
185
+ .then(setEpisodeFramesData)
186
+ .catch(() => setEpisodeFramesData({ cameras: [], framesByCamera: {} }))
187
+ .finally(() => setFramesLoading(false));
188
+ };
189
+
190
+ const loadInsights = () => {
191
+ if (insightsLoadedRef.current || !org || !dataset) return;
192
+ insightsLoadedRef.current = true;
193
+ setInsightsLoading(true);
194
+ fetchCrossEpisodeVariance(org, dataset)
195
+ .then(setCrossEpData)
196
+ .catch((err) => console.error("[cross-ep] Failed:", err))
197
+ .finally(() => setInsightsLoading(false));
198
+ };
199
+
200
+ // Re-trigger data loading for the restored tab on mount
201
+ useEffect(() => {
202
+ if (activeTab === "statistics") loadStats();
203
+ if (activeTab === "frames") loadFrames();
204
+ if (activeTab === "insights") loadInsights();
205
+ if (activeTab === "filtering") {
206
+ loadStats();
207
+ loadInsights();
208
+ }
209
+ // eslint-disable-next-line react-hooks/exhaustive-deps
210
+ }, []);
211
+
212
+ const handleTabChange = (tab: ActiveTab) => {
213
+ setActiveTab(tab);
214
+ if (tab === "statistics") loadStats();
215
+ if (tab === "frames") loadFrames();
216
+ if (tab === "insights") loadInsights();
217
+ if (tab === "filtering") {
218
+ loadStats();
219
+ loadInsights();
220
+ }
221
+ };
222
+
223
  // Use context for time sync
224
  const { currentTime, setCurrentTime, setIsPlaying, isPlaying } = useTime();
225
 
 
232
  currentPage * pageSize,
233
  );
234
 
235
+ // Preload adjacent episodes' videos via <link rel="preload"> tags
236
  useEffect(() => {
237
  if (!org || !dataset) return;
238
+ const links: HTMLLinkElement[] = [];
239
 
240
+ getAdjacentEpisodesVideoInfo(org, dataset, episodeId, 2)
241
+ .then((adjacentVideos) => {
242
+ for (const ep of adjacentVideos) {
243
+ for (const v of ep.videosInfo) {
244
+ const link = document.createElement("link");
245
+ link.rel = "preload";
246
+ link.as = "video";
247
+ link.href = v.url;
248
+ document.head.appendChild(link);
249
+ links.push(link);
250
+ }
251
+ }
252
+ })
253
+ .catch(() => {});
254
 
255
+ return () => {
256
+ links.forEach((l) => l.remove());
257
+ };
258
  }, [org, dataset, episodeId]);
259
 
260
  // Initialize based on URL time parameter
 
347
  };
348
 
349
  return (
350
+ <div className="flex flex-col h-screen max-h-screen bg-slate-950 text-gray-200">
351
+ {/* Top tab bar */}
352
+ <div className="flex items-center border-b border-slate-700 bg-slate-900 shrink-0">
353
+ <button
354
+ className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
355
+ activeTab === "episodes"
356
+ ? "text-orange-400"
357
+ : "text-slate-400 hover:text-slate-200"
358
+ }`}
359
+ onClick={() => handleTabChange("episodes")}
360
+ >
361
+ Episodes
362
+ {activeTab === "episodes" && (
363
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
364
+ )}
365
+ </button>
366
+ <button
367
+ className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
368
+ activeTab === "statistics"
369
+ ? "text-orange-400"
370
+ : "text-slate-400 hover:text-slate-200"
371
+ }`}
372
+ onClick={() => handleTabChange("statistics")}
373
+ >
374
+ Statistics
375
+ {activeTab === "statistics" && (
376
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
377
+ )}
378
+ </button>
379
+ <button
380
+ className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
381
+ activeTab === "filtering"
382
+ ? "text-orange-400"
383
+ : "text-slate-400 hover:text-slate-200"
384
+ }`}
385
+ onClick={() => handleTabChange("filtering")}
386
+ >
387
+ Filtering
388
+ {activeTab === "filtering" && (
389
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
390
+ )}
391
+ </button>
392
+ <button
393
+ className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
394
+ activeTab === "frames"
395
+ ? "text-orange-400"
396
+ : "text-slate-400 hover:text-slate-200"
397
+ }`}
398
+ onClick={() => handleTabChange("frames")}
399
+ >
400
+ Frames
401
+ {activeTab === "frames" && (
402
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
403
+ )}
404
+ </button>
405
+ <button
406
+ className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
407
+ activeTab === "insights"
408
+ ? "text-orange-400"
409
+ : "text-slate-400 hover:text-slate-200"
410
+ }`}
411
+ onClick={() => handleTabChange("insights")}
412
+ >
413
+ Action Insights
414
+ {activeTab === "insights" && (
415
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
416
+ )}
417
+ </button>
418
+ {hasURDFSupport(datasetInfo.robot_type) &&
419
+ datasetInfo.codebase_version >= "v3.0" && (
420
+ <button
421
+ className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
422
+ activeTab === "urdf"
423
+ ? "text-orange-400"
424
+ : "text-slate-400 hover:text-slate-200"
425
+ }`}
426
+ onClick={() => handleTabChange("urdf")}
427
  >
428
+ 3D Replay
429
+ {activeTab === "urdf" && (
430
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
431
+ )}
432
+ </button>
433
+ )}
434
+ </div>
 
435
 
436
+ {/* Body: sidebar + content */}
437
+ <div className="flex flex-1 min-h-0">
438
+ {/* Sidebar — only on Episodes tab */}
439
+ {activeTab === "episodes" && (
440
+ <Sidebar
441
+ datasetInfo={datasetInfo}
442
+ paginatedEpisodes={paginatedEpisodes}
443
+ episodeId={episodeId}
444
+ totalPages={totalPages}
445
+ currentPage={currentPage}
446
+ prevPage={prevPage}
447
+ nextPage={nextPage}
448
+ showFlaggedOnly={sidebarFlaggedOnly}
449
+ onShowFlaggedOnlyChange={setSidebarFlaggedOnly}
450
  />
451
  )}
452
 
453
+ {/* Main content */}
454
+ <div
455
+ className={`flex flex-col gap-4 p-4 flex-1 relative ${isLoading ? "overflow-hidden" : "overflow-y-auto"}`}
456
+ >
457
+ {isLoading && <Loading />}
 
 
 
 
 
 
 
 
 
 
 
 
458
 
459
+ {activeTab === "episodes" && (
460
+ <>
461
+ <div className="flex items-center justify-start my-4">
462
+ <a
463
+ href="https://github.com/huggingface/lerobot"
464
+ target="_blank"
465
+ className="block"
466
+ >
467
+ <img
468
+ src="https://github.com/huggingface/lerobot/raw/main/media/readme/lerobot-logo-thumbnail.png"
469
+ alt="LeRobot Logo"
470
+ className="w-32"
471
+ />
472
+ </a>
473
 
474
+ <div>
475
+ <a
476
+ href={`https://huggingface.co/datasets/${datasetInfo.repoId}`}
477
+ target="_blank"
478
+ >
479
+ <p className="text-lg font-semibold">
480
+ {datasetInfo.repoId}
481
+ </p>
482
+ </a>
483
+
484
+ <p className="font-mono text-lg font-semibold">
485
+ episode {episodeId}
486
+ </p>
487
+ </div>
488
+ </div>
489
+
490
+ {/* Videos */}
491
+ {videosInfo.length > 0 && (
492
+ <SimpleVideosPlayer
493
+ videosInfo={videosInfo}
494
+ onVideosReady={() => setVideosReady(true)}
495
+ />
496
+ )}
497
+
498
+ {/* Language Instruction */}
499
+ {task && (
500
+ <div className="mb-6 p-4 bg-slate-800 rounded-lg border border-slate-600">
501
+ <p className="text-slate-300">
502
+ <span className="font-semibold text-slate-100">
503
+ Language Instruction:
504
+ </span>
505
+ </p>
506
+ <div className="mt-2 text-slate-300">
507
+ {task
508
+ .split("\n")
509
+ .map((instruction: string, index: number) => (
510
+ <p key={index} className="mb-1">
511
+ {instruction}
512
+ </p>
513
+ ))}
514
+ </div>
515
+ </div>
516
+ )}
517
+
518
+ {/* Graph */}
519
+ <div className="mb-4">
520
+ <DataRecharts
521
+ data={chartDataGroups}
522
+ onChartsReady={() => setChartsReady(true)}
523
+ />
524
+ </div>
525
+
526
+ <PlaybackBar />
527
+ </>
528
+ )}
529
+
530
+ {activeTab === "statistics" && (
531
+ <StatsPanel
532
+ datasetInfo={datasetInfo}
533
+ episodeLengthStats={episodeLengthStats}
534
+ loading={statsLoading}
535
+ />
536
+ )}
537
+
538
+ {activeTab === "frames" && (
539
+ <OverviewPanel
540
+ data={episodeFramesData}
541
+ loading={framesLoading}
542
+ flaggedOnly={framesFlaggedOnly}
543
+ onFlaggedOnlyChange={setFramesFlaggedOnly}
544
+ />
545
+ )}
546
+
547
+ {activeTab === "insights" && (
548
+ <Suspense fallback={<Loading />}>
549
+ <ActionInsightsPanel
550
+ flatChartData={data.flatChartData}
551
+ fps={datasetInfo.fps}
552
+ crossEpisodeData={crossEpData}
553
+ crossEpisodeLoading={insightsLoading}
554
+ />
555
+ </Suspense>
556
+ )}
557
+
558
+ {activeTab === "filtering" && (
559
+ <Suspense fallback={<Loading />}>
560
+ <FilteringPanel
561
+ repoId={datasetInfo.repoId}
562
+ crossEpisodeData={crossEpData}
563
+ crossEpisodeLoading={insightsLoading}
564
+ episodeLengthStats={episodeLengthStats}
565
+ flatChartData={data.flatChartData}
566
+ onViewFlaggedEpisodes={() => {
567
+ setSidebarFlaggedOnly(true);
568
+ handleTabChange("episodes");
569
+ }}
570
+ />
571
+ </Suspense>
572
+ )}
573
+
574
+ {activeTab === "urdf" && (
575
+ <Suspense fallback={<Loading />}>
576
+ <URDFViewer data={data} org={org} dataset={dataset} />
577
+ </Suspense>
578
+ )}
579
+ </div>
580
  </div>
581
  </div>
582
  );
src/app/[org]/[dataset]/[episode]/fetch-data.ts CHANGED
@@ -1,32 +1,109 @@
1
  import {
2
- fetchJson,
3
  fetchParquetFile,
4
  formatStringWithVars,
5
- readParquetColumn,
6
  readParquetAsObjects,
7
  } from "@/utils/parquetUtils";
8
  import { pick } from "@/utils/pick";
9
- import { getDatasetVersion, buildVersionedUrl } from "@/utils/versionUtils";
 
 
 
10
  import { PADDING, CHART_CONFIG, EXCLUDED_COLUMNS } from "@/utils/constants";
11
  import {
12
  processChartDataGroups,
13
  groupRowBySuffix,
14
  } from "@/utils/dataProcessing";
15
- import { extractLanguageInstructions } from "@/utils/languageInstructions";
16
  import {
17
  buildV3VideoPath,
18
  buildV3DataPath,
19
  buildV3EpisodesMetadataPath,
20
  } from "@/utils/stringFormatting";
21
  import { bigIntToNumber } from "@/utils/typeGuards";
22
- import type {
23
- DatasetMetadata,
24
- EpisodeData,
25
- EpisodeMetadataV3,
26
- VideoInfo,
27
- AdjacentEpisodeVideos,
28
- ChartDataGroup,
29
- } from "@/types";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  export async function getEpisodeData(
32
  org: string,
@@ -35,10 +112,10 @@ export async function getEpisodeData(
35
  ): Promise<EpisodeData> {
36
  const repoId = `${org}/${dataset}`;
37
  try {
38
- // Check for compatible dataset version (v3.0, v2.1, or v2.0)
39
- const version = await getDatasetVersion(repoId);
40
- const jsonUrl = buildVersionedUrl(repoId, version, "meta/info.json");
41
- const info = await fetchJson<DatasetMetadata>(jsonUrl);
42
 
43
  if (info.video_path === null) {
44
  throw new Error(
@@ -46,19 +123,39 @@ export async function getEpisodeData(
46
  );
47
  }
48
 
49
- // Handle different versions
50
- if (version === "v3.0") {
51
- return await getEpisodeDataV3(repoId, version, info, episodeId);
52
- } else {
53
- return await getEpisodeDataV2(repoId, version, info, episodeId);
54
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  } catch (err) {
56
  console.error("Error loading episode data:", err);
57
  throw err;
58
  }
59
  }
60
 
61
- // Get video info for adjacent episodes (for preloading)
62
  export async function getAdjacentEpisodesVideoInfo(
63
  org: string,
64
  dataset: string,
@@ -67,9 +164,8 @@ export async function getAdjacentEpisodesVideoInfo(
67
  ): Promise<AdjacentEpisodeVideos[]> {
68
  const repoId = `${org}/${dataset}`;
69
  try {
70
- const version = await getDatasetVersion(repoId);
71
- const jsonUrl = buildVersionedUrl(repoId, version, "meta/info.json");
72
- const info = await fetchJson<DatasetMetadata>(jsonUrl);
73
 
74
  const totalEpisodes = info.total_episodes;
75
  const adjacentVideos: AdjacentEpisodeVideos[] = [];
@@ -142,12 +238,16 @@ async function getEpisodeDataV2(
142
  ): Promise<EpisodeData> {
143
  const episode_chunk = Math.floor(0 / 1000);
144
 
145
- // Dataset information
146
- const datasetInfo = {
147
  repoId,
148
  total_frames: info.total_frames,
149
  total_episodes: info.total_episodes,
150
  fps: info.fps,
 
 
 
 
 
151
  };
152
 
153
  // Generate list of episodes
@@ -197,22 +297,17 @@ async function getEpisodeDataV2(
197
  const filteredColumns = columnNames.filter(
198
  (column) => !excludedColumns.includes(column.key),
199
  );
200
- const filteredColumnNames = [
201
- "timestamp",
202
- ...filteredColumns.map((column) => column.key),
203
- ];
204
-
205
- const columns = filteredColumns.map(({ key }) => {
206
- let column_names = info.features[key].names;
207
- while (typeof column_names === "object") {
208
  if (Array.isArray(column_names)) break;
209
- column_names = Object.values(column_names ?? {})[0];
210
  }
211
  return {
212
  key,
213
  value: Array.isArray(column_names)
214
  ? column_names.map(
215
- (name) => `${key}${CHART_CONFIG.SERIES_NAME_DELIMITER}${name}`,
216
  )
217
  : Array.from(
218
  { length: columnNames.find((c) => c.key === key)?.length ?? 1 },
@@ -233,32 +328,38 @@ async function getEpisodeDataV2(
233
  );
234
 
235
  const arrayBuffer = await fetchParquetFile(parquetUrl);
 
236
 
237
- // Extract task - first check for language instructions (preferred), then fallback to task field or tasks.jsonl
238
  let task: string | undefined;
239
- let allData: Record<string, unknown>[] = [];
240
 
241
- // Load data first
242
- try {
243
- allData = await readParquetAsObjects(arrayBuffer, []);
244
- } catch {
245
- // Could not read parquet data
246
- }
247
 
248
- // First check for language_instruction fields in the data (preferred)
249
- task = extractLanguageInstructions(allData);
 
250
 
251
- // If no language instructions found, try direct task field
252
- if (
253
- !task &&
254
- allData.length > 0 &&
255
- typeof allData[0].task === "string" &&
256
- allData[0].task
257
- ) {
 
 
 
 
 
 
 
 
 
258
  task = allData[0].task;
259
  }
260
 
261
- // If still no task found, try loading from tasks.jsonl metadata file (v2.x format)
262
  if (!task && allData.length > 0) {
263
  try {
264
  const tasksUrl = buildVersionedUrl(repoId, version, "meta/tasks.jsonl");
@@ -266,7 +367,6 @@ async function getEpisodeDataV2(
266
 
267
  if (tasksResponse.ok) {
268
  const tasksText = await tasksResponse.text();
269
- // Parse JSONL format (one JSON object per line)
270
  const tasksData = tasksText
271
  .split("\n")
272
  .filter((line) => line.trim())
@@ -274,13 +374,11 @@ async function getEpisodeDataV2(
274
 
275
  if (tasksData && tasksData.length > 0) {
276
  const taskIndex = allData[0].task_index;
277
-
278
- // Convert BigInt to number for comparison
279
  const taskIndexNum =
280
  typeof taskIndex === "bigint" ? Number(taskIndex) : taskIndex;
281
-
282
- // Find task by task_index
283
- const taskData = tasksData.find((t) => t.task_index === taskIndexNum);
284
  if (taskData) {
285
  task = taskData.task;
286
  }
@@ -291,20 +389,25 @@ async function getEpisodeDataV2(
291
  }
292
  }
293
 
294
- const data = await readParquetColumn(arrayBuffer, filteredColumnNames);
295
- // Flatten and map to array of objects for chartData
296
  const seriesNames = [
297
  "timestamp",
298
  ...columns.map(({ value }) => value).flat(),
299
  ];
300
 
301
- const chartData = data.map((row) => {
302
- const flatRow = row.flat();
303
  const obj: Record<string, number> = {};
304
- seriesNames.forEach((key, idx) => {
305
- const value = flatRow[idx];
306
- obj[key] = typeof value === "number" ? value : Number(value) || 0;
307
- });
 
 
 
 
 
 
 
308
  return obj;
309
  });
310
 
@@ -338,6 +441,7 @@ async function getEpisodeDataV2(
338
  episodeId,
339
  videosInfo,
340
  chartDataGroups,
 
341
  episodes,
342
  ignoredColumns,
343
  duration,
@@ -352,15 +456,18 @@ async function getEpisodeDataV3(
352
  info: DatasetMetadata,
353
  episodeId: number,
354
  ): Promise<EpisodeData> {
355
- // Create dataset info structure (like v2.x)
356
- const datasetInfo = {
357
  repoId,
358
  total_frames: info.total_frames,
359
  total_episodes: info.total_episodes,
360
  fps: info.fps,
 
 
 
 
 
361
  };
362
 
363
- // Generate episodes list based on total_episodes from dataset info
364
  const episodes = Array.from({ length: info.total_episodes }, (_, i) => i);
365
 
366
  // Load episode metadata to get timestamps for episode 0
@@ -379,25 +486,19 @@ async function getEpisodeDataV3(
379
  );
380
 
381
  // Load episode data for charts
382
- const { chartDataGroups, ignoredColumns, task } = await loadEpisodeDataV3(
383
- repoId,
384
- version,
385
- info,
386
- episodeMetadata,
387
- );
388
 
389
- // Calculate duration from episode length and FPS if available
390
- const episodeLength = bigIntToNumber(episodeMetadata.length);
391
- const duration = episodeLength
392
- ? episodeLength / info.fps
393
- : (episodeMetadata.video_to_timestamp || 0) -
394
- (episodeMetadata.video_from_timestamp || 0);
395
 
396
  return {
397
  datasetInfo,
398
  episodeId,
399
  videosInfo,
400
  chartDataGroups,
 
401
  episodes,
402
  ignoredColumns,
403
  duration,
@@ -412,7 +513,8 @@ async function loadEpisodeDataV3(
412
  info: DatasetMetadata,
413
  episodeMetadata: EpisodeMetadataV3,
414
  ): Promise<{
415
- chartDataGroups: ChartDataGroup[];
 
416
  ignoredColumns: string[];
417
  task?: string;
418
  }> {
@@ -427,9 +529,11 @@ async function loadEpisodeDataV3(
427
  const fullData = await readParquetAsObjects(arrayBuffer, []);
428
 
429
  // Extract the episode-specific data slice
430
- // Convert BigInt to number if needed
431
- const fromIndex = Number(episodeMetadata.dataset_from_index || 0);
432
- const toIndex = Number(episodeMetadata.dataset_to_index || fullData.length);
 
 
433
 
434
  // Find the starting index of this parquet file by checking the first row's index
435
  // This handles the case where episodes are split across multiple parquet files
@@ -445,29 +549,57 @@ async function loadEpisodeDataV3(
445
  const episodeData = fullData.slice(localFromIndex, localToIndex);
446
 
447
  if (episodeData.length === 0) {
448
- return { chartDataGroups: [], ignoredColumns: [], task: undefined };
 
 
 
 
 
449
  }
450
 
451
  // Convert to the same format as v2.x for compatibility with existing chart code
452
- const { chartDataGroups, ignoredColumns } = processEpisodeDataForCharts(
453
- episodeData,
454
- info,
455
- episodeMetadata,
456
- );
457
 
458
  // First check for language_instruction fields in the data (preferred)
459
- // Check multiple rows: first, middle, and last
460
- const sampleIndices = [
461
- 0,
462
- Math.floor(episodeData.length / 2),
463
- episodeData.length - 1,
464
- ];
465
- let task = extractLanguageInstructions(episodeData, sampleIndices);
466
-
467
- // If no language instructions found, fall back to tasks metadata
468
- if (!task) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  try {
470
- // Load tasks metadata
471
  const tasksUrl = buildVersionedUrl(
472
  repoId,
473
  version,
@@ -476,53 +608,28 @@ async function loadEpisodeDataV3(
476
  const tasksArrayBuffer = await fetchParquetFile(tasksUrl);
477
  const tasksData = await readParquetAsObjects(tasksArrayBuffer, []);
478
 
479
- if (
480
- episodeData.length > 0 &&
481
- tasksData &&
482
- tasksData.length > 0 &&
483
- "task_index" in episodeData[0]
484
- ) {
485
- const taskIndex = episodeData[0].task_index;
486
 
487
- // Convert BigInt to number for comparison
488
- const taskIndexNum =
489
- typeof taskIndex === "bigint"
490
- ? Number(taskIndex)
491
- : typeof taskIndex === "number"
492
- ? taskIndex
493
- : undefined;
494
-
495
- // Look up task by index
496
- if (
497
- taskIndexNum !== undefined &&
498
- taskIndexNum >= 0 &&
499
- taskIndexNum < tasksData.length
500
- ) {
501
  const taskData = tasksData[taskIndexNum];
502
- // Extract task from various possible fields
503
- if (
504
- taskData &&
505
- "__index_level_0__" in taskData &&
506
- typeof taskData.__index_level_0__ === "string"
507
- ) {
508
- task = taskData.__index_level_0__;
509
- } else if (
510
- taskData &&
511
- "task" in taskData &&
512
- typeof taskData.task === "string"
513
- ) {
514
- task = taskData.task;
515
- }
516
  }
517
  }
518
  } catch {
519
- // Could not load tasks metadata - dataset might not have language tasks
520
  }
521
  }
522
 
523
- return { chartDataGroups, ignoredColumns, task };
524
  } catch {
525
- return { chartDataGroups: [], ignoredColumns: [], task: undefined };
 
 
 
 
 
526
  }
527
  }
528
 
@@ -531,16 +638,11 @@ function processEpisodeDataForCharts(
531
  episodeData: Record<string, unknown>[],
532
  info: DatasetMetadata,
533
  episodeMetadata?: EpisodeMetadataV3,
534
- ): { chartDataGroups: ChartDataGroup[]; ignoredColumns: string[] } {
535
- // Get numeric column features (not currently used but kept for reference)
536
- // const columnNames = Object.entries(info.features)
537
- // .filter(
538
- // ([, value]) =>
539
- // ["float32", "int32"].includes(value.dtype) &&
540
- // value.shape.length === 1,
541
- // )
542
- // .map(([key, value]) => ({ key, value }));
543
-
544
  // Convert parquet data to chart format
545
  let seriesNames: string[] = [];
546
 
@@ -576,7 +678,7 @@ function processEpisodeDataForCharts(
576
  const excludedColumns = EXCLUDED_COLUMNS.V3 as readonly string[];
577
 
578
  // Create columns structure similar to V2.1 for proper hierarchical naming
579
- const columns = Object.entries(info.features)
580
  .filter(
581
  ([key, value]) =>
582
  ["float32", "int32"].includes(value.dtype) &&
@@ -584,16 +686,16 @@ function processEpisodeDataForCharts(
584
  !excludedColumns.includes(key),
585
  )
586
  .map(([key, feature]) => {
587
- let column_names = feature.names;
588
- while (typeof column_names === "object") {
589
  if (Array.isArray(column_names)) break;
590
- column_names = Object.values(column_names ?? {})[0];
591
  }
592
  return {
593
  key,
594
  value: Array.isArray(column_names)
595
  ? column_names.map(
596
- (name) => `${key}${CHART_CONFIG.SERIES_NAME_DELIMITER}${name}`,
597
  )
598
  : Array.from(
599
  { length: feature.shape[0] || 1 },
@@ -714,45 +816,6 @@ function processEpisodeDataForCharts(
714
  // Process chart data into organized groups using utility function
715
  const chartGroups = processChartDataGroups(seriesNames, chartData);
716
 
717
- // Utility function to group row keys by suffix (same as V2.1)
718
- function groupRowBySuffix(row: Record<string, number>): {
719
- timestamp: number;
720
- [key: string]: number | Record<string, number>;
721
- } {
722
- const result: {
723
- timestamp: number;
724
- [key: string]: number | Record<string, number>;
725
- } = {
726
- timestamp: 0,
727
- };
728
- const suffixGroups: Record<string, Record<string, number>> = {};
729
- for (const [key, value] of Object.entries(row)) {
730
- if (key === "timestamp") {
731
- result.timestamp = value;
732
- continue;
733
- }
734
- const parts = key.split(CHART_CONFIG.SERIES_NAME_DELIMITER);
735
- if (parts.length === 2) {
736
- const [prefix, suffix] = parts;
737
- if (!suffixGroups[suffix]) suffixGroups[suffix] = {};
738
- suffixGroups[suffix][prefix] = value;
739
- } else {
740
- result[key] = value;
741
- }
742
- }
743
- for (const [suffix, group] of Object.entries(suffixGroups)) {
744
- const keys = Object.keys(group);
745
- if (keys.length === 1) {
746
- // Use the full original name as the key
747
- const fullName = `${keys[0]}${CHART_CONFIG.SERIES_NAME_DELIMITER}${suffix}`;
748
- result[fullName] = group[keys[0]];
749
- } else {
750
- result[suffix] = group;
751
- }
752
- }
753
- return result;
754
- }
755
-
756
  const chartDataGroups = chartGroups.map((group) =>
757
  chartData.map((row) => {
758
  const grouped = groupRowBySuffix(pick(row, [...group, "timestamp"]));
@@ -765,7 +828,7 @@ function processEpisodeDataForCharts(
765
  }),
766
  );
767
 
768
- return { chartDataGroups, ignoredColumns };
769
  }
770
 
771
  // Video info extraction with segmentation for v3.0
@@ -786,18 +849,22 @@ function extractVideoInfoV3WithSegmentation(
786
  key.startsWith(`videos/${videoKey}/`),
787
  );
788
 
789
- let chunkIndex, fileIndex, segmentStart, segmentEnd;
 
 
 
 
 
 
790
 
791
  if (cameraSpecificKeys.length > 0) {
792
- // Use camera-specific metadata
793
- const chunkValue = episodeMetadata[`videos/${videoKey}/chunk_index`];
794
- const fileValue = episodeMetadata[`videos/${videoKey}/file_index`];
795
- chunkIndex = bigIntToNumber(chunkValue, 0);
796
- fileIndex = bigIntToNumber(fileValue, 0);
797
- segmentStart = episodeMetadata[`videos/${videoKey}/from_timestamp`] || 0;
798
- segmentEnd = episodeMetadata[`videos/${videoKey}/to_timestamp`] || 30;
799
  } else {
800
- // Fallback to generic video metadata
801
  chunkIndex = episodeMetadata.video_chunk_index || 0;
802
  fileIndex = episodeMetadata.video_file_index || 0;
803
  segmentStart = episodeMetadata.video_from_timestamp || 0;
@@ -899,70 +966,83 @@ function parseEpisodeRowSimple(
899
  // Check if this is v3.0 format with named keys
900
  if ("episode_index" in row) {
901
  // v3.0 format - use named keys
902
- const episodeData: Record<string, number | bigint | undefined> = {
903
- episode_index: bigIntToNumber(row["episode_index"], 0),
904
- data_chunk_index: bigIntToNumber(row["data/chunk_index"], 0),
905
- data_file_index: bigIntToNumber(row["data/file_index"], 0),
906
- dataset_from_index: bigIntToNumber(row["dataset_from_index"], 0),
907
- dataset_to_index: bigIntToNumber(row["dataset_to_index"], 0),
908
- length: bigIntToNumber(row["length"], 0),
 
 
 
 
 
 
909
  };
910
 
911
  // Handle video metadata - look for video-specific keys
912
  const videoKeys = Object.keys(row).filter(
913
  (key) => key.includes("videos/") && key.includes("/chunk_index"),
914
  );
 
 
 
 
915
  if (videoKeys.length > 0) {
916
- // Use the first video stream for basic info
917
- const firstVideoKey = videoKeys[0];
918
- const videoBaseName = firstVideoKey.replace("/chunk_index", "");
919
-
920
- episodeData.video_chunk_index = bigIntToNumber(
921
- row[`${videoBaseName}/chunk_index`],
922
- 0,
923
- );
924
- episodeData.video_file_index = bigIntToNumber(
925
- row[`${videoBaseName}/file_index`],
926
- 0,
927
- );
928
- episodeData.video_from_timestamp = bigIntToNumber(
929
- row[`${videoBaseName}/from_timestamp`],
930
- 0,
931
- );
932
- episodeData.video_to_timestamp = bigIntToNumber(
933
- row[`${videoBaseName}/to_timestamp`],
934
- 0,
935
- );
936
- } else {
937
- // Fallback video values
938
- episodeData.video_chunk_index = 0;
939
- episodeData.video_file_index = 0;
940
- episodeData.video_from_timestamp = 0;
941
- episodeData.video_to_timestamp = 30;
942
  }
943
 
944
- // Store the raw row data to preserve per-camera metadata
945
- // This allows extractVideoInfoV3WithSegmentation to access camera-specific timestamps
 
 
 
 
 
 
 
 
 
 
 
 
946
  Object.keys(row).forEach((key) => {
947
  if (key.startsWith("videos/")) {
948
- episodeData[key] = bigIntToNumber(row[key]);
 
 
 
 
 
 
949
  }
950
  });
951
 
952
  return episodeData as EpisodeMetadataV3;
953
  } else {
954
  // Fallback to numeric keys for compatibility
 
 
 
 
 
 
955
  return {
956
- episode_index: bigIntToNumber(row["0"], 0),
957
- data_chunk_index: bigIntToNumber(row["1"], 0),
958
- data_file_index: bigIntToNumber(row["2"], 0),
959
- dataset_from_index: bigIntToNumber(row["3"], 0),
960
- dataset_to_index: bigIntToNumber(row["4"], 0),
961
- video_chunk_index: bigIntToNumber(row["5"], 0),
962
- video_file_index: bigIntToNumber(row["6"], 0),
963
- video_from_timestamp: bigIntToNumber(row["7"], 0),
964
- video_to_timestamp: bigIntToNumber(row["8"], 30),
965
- length: bigIntToNumber(row["9"], 30),
966
  };
967
  }
968
  }
@@ -984,6 +1064,995 @@ function parseEpisodeRowSimple(
984
  return fallback;
985
  }
986
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
987
  // Safe wrapper for UI error display
988
  export async function getEpisodeDataSafe(
989
  org: string,
@@ -993,10 +2062,8 @@ export async function getEpisodeDataSafe(
993
  try {
994
  const data = await getEpisodeData(org, dataset, episodeId);
995
  return { data };
996
- } catch (err) {
997
- // Only expose the error message, not stack or sensitive info
998
- const errorMessage =
999
- err instanceof Error ? err.message : String(err) || "Unknown error";
1000
- return { error: errorMessage };
1001
  }
1002
  }
 
1
  import {
2
+ DatasetMetadata,
3
  fetchParquetFile,
4
  formatStringWithVars,
 
5
  readParquetAsObjects,
6
  } from "@/utils/parquetUtils";
7
  import { pick } from "@/utils/pick";
8
+ import {
9
+ getDatasetVersionAndInfo,
10
+ buildVersionedUrl,
11
+ } from "@/utils/versionUtils";
12
  import { PADDING, CHART_CONFIG, EXCLUDED_COLUMNS } from "@/utils/constants";
13
  import {
14
  processChartDataGroups,
15
  groupRowBySuffix,
16
  } from "@/utils/dataProcessing";
 
17
  import {
18
  buildV3VideoPath,
19
  buildV3DataPath,
20
  buildV3EpisodesMetadataPath,
21
  } from "@/utils/stringFormatting";
22
  import { bigIntToNumber } from "@/utils/typeGuards";
23
+ import type { VideoInfo, AdjacentEpisodeVideos } from "@/types";
24
+
25
+ const SERIES_NAME_DELIMITER = CHART_CONFIG.SERIES_NAME_DELIMITER;
26
+
27
+ export type CameraInfo = { name: string; width: number; height: number };
28
+
29
+ export type DatasetDisplayInfo = {
30
+ repoId: string;
31
+ total_frames: number;
32
+ total_episodes: number;
33
+ fps: number;
34
+ robot_type: string | null;
35
+ codebase_version: string;
36
+ total_tasks: number;
37
+ dataset_size_mb: number;
38
+ cameras: CameraInfo[];
39
+ };
40
+
41
+ export type ChartRow = Record<string, number | Record<string, number>>;
42
+
43
+ export type ColumnMinMax = {
44
+ column: string;
45
+ min: number;
46
+ max: number;
47
+ };
48
+
49
+ export type EpisodeLengthInfo = {
50
+ episodeIndex: number;
51
+ lengthSeconds: number;
52
+ frames: number;
53
+ };
54
+
55
+ export type EpisodeLengthStats = {
56
+ shortestEpisodes: EpisodeLengthInfo[];
57
+ longestEpisodes: EpisodeLengthInfo[];
58
+ allEpisodeLengths: EpisodeLengthInfo[];
59
+ meanEpisodeLength: number;
60
+ medianEpisodeLength: number;
61
+ stdEpisodeLength: number;
62
+ episodeLengthHistogram: { binLabel: string; count: number }[];
63
+ };
64
+
65
+ export type EpisodeFrameInfo = {
66
+ episodeIndex: number;
67
+ videoUrl: string;
68
+ firstFrameTime: number;
69
+ lastFrameTime: number | null; // null = seek to video.duration on client
70
+ };
71
+
72
+ export type EpisodeFramesData = {
73
+ cameras: string[];
74
+ framesByCamera: Record<string, EpisodeFrameInfo[]>;
75
+ };
76
+
77
+ export type EpisodeData = {
78
+ datasetInfo: DatasetDisplayInfo;
79
+ episodeId: number;
80
+ videosInfo: VideoInfo[];
81
+ chartDataGroups: ChartRow[][];
82
+ flatChartData: Record<string, number>[];
83
+ episodes: number[];
84
+ ignoredColumns: string[];
85
+ duration: number;
86
+ task?: string;
87
+ };
88
+
89
+ type EpisodeMetadataV3 = {
90
+ episode_index: number;
91
+ data_chunk_index: number;
92
+ data_file_index: number;
93
+ dataset_from_index: number;
94
+ dataset_to_index: number;
95
+ video_chunk_index: number;
96
+ video_file_index: number;
97
+ video_from_timestamp: number;
98
+ video_to_timestamp: number;
99
+ length: number;
100
+ [key: string]: string | number;
101
+ };
102
+
103
+ type ColumnDef = {
104
+ key: string;
105
+ value: string[];
106
+ };
107
 
108
  export async function getEpisodeData(
109
  org: string,
 
112
  ): Promise<EpisodeData> {
113
  const repoId = `${org}/${dataset}`;
114
  try {
115
+ console.time(`[perf] getDatasetVersionAndInfo`);
116
+ const { version, info: rawInfo } = await getDatasetVersionAndInfo(repoId);
117
+ console.timeEnd(`[perf] getDatasetVersionAndInfo`);
118
+ const info = rawInfo as unknown as DatasetMetadata;
119
 
120
  if (info.video_path === null) {
121
  throw new Error(
 
123
  );
124
  }
125
 
126
+ console.time(`[perf] getEpisodeData (${version})`);
127
+ const result =
128
+ version === "v3.0"
129
+ ? await getEpisodeDataV3(repoId, version, info, episodeId)
130
+ : await getEpisodeDataV2(repoId, version, info, episodeId);
131
+ console.timeEnd(`[perf] getEpisodeData (${version})`);
132
+
133
+ // Extract camera resolutions from features
134
+ const cameras: CameraInfo[] = Object.entries(rawInfo.features)
135
+ .filter(([, f]) => f.dtype === "video" && f.shape.length >= 2)
136
+ .map(([name, f]) => ({ name, height: f.shape[0], width: f.shape[1] }));
137
+
138
+ result.datasetInfo = {
139
+ ...result.datasetInfo,
140
+ robot_type: rawInfo.robot_type ?? null,
141
+ codebase_version: rawInfo.codebase_version,
142
+ total_tasks: rawInfo.total_tasks ?? 0,
143
+ dataset_size_mb:
144
+ Math.round(
145
+ ((rawInfo.data_files_size_in_mb ?? 0) +
146
+ (rawInfo.video_files_size_in_mb ?? 0)) *
147
+ 10,
148
+ ) / 10,
149
+ cameras,
150
+ };
151
+
152
+ return result;
153
  } catch (err) {
154
  console.error("Error loading episode data:", err);
155
  throw err;
156
  }
157
  }
158
 
 
159
  export async function getAdjacentEpisodesVideoInfo(
160
  org: string,
161
  dataset: string,
 
164
  ): Promise<AdjacentEpisodeVideos[]> {
165
  const repoId = `${org}/${dataset}`;
166
  try {
167
+ const { version, info: rawInfo } = await getDatasetVersionAndInfo(repoId);
168
+ const info = rawInfo as unknown as DatasetMetadata;
 
169
 
170
  const totalEpisodes = info.total_episodes;
171
  const adjacentVideos: AdjacentEpisodeVideos[] = [];
 
238
  ): Promise<EpisodeData> {
239
  const episode_chunk = Math.floor(0 / 1000);
240
 
241
+ const datasetInfo: DatasetDisplayInfo = {
 
242
  repoId,
243
  total_frames: info.total_frames,
244
  total_episodes: info.total_episodes,
245
  fps: info.fps,
246
+ robot_type: null,
247
+ codebase_version: version,
248
+ total_tasks: 0,
249
+ dataset_size_mb: 0,
250
+ cameras: [],
251
  };
252
 
253
  // Generate list of episodes
 
297
  const filteredColumns = columnNames.filter(
298
  (column) => !excludedColumns.includes(column.key),
299
  );
300
+ const columns: ColumnDef[] = filteredColumns.map(({ key }) => {
301
+ let column_names: unknown = info.features[key].names;
302
+ while (typeof column_names === "object" && column_names !== null) {
 
 
 
 
 
303
  if (Array.isArray(column_names)) break;
304
+ column_names = Object.values(column_names)[0];
305
  }
306
  return {
307
  key,
308
  value: Array.isArray(column_names)
309
  ? column_names.map(
310
+ (name: string) => `${key}${SERIES_NAME_DELIMITER}${name}`,
311
  )
312
  : Array.from(
313
  { length: columnNames.find((c) => c.key === key)?.length ?? 1 },
 
328
  );
329
 
330
  const arrayBuffer = await fetchParquetFile(parquetUrl);
331
+ const allData = await readParquetAsObjects(arrayBuffer, []);
332
 
333
+ // Extract task from language_instruction fields, task field, or tasks.jsonl
334
  let task: string | undefined;
 
335
 
336
+ if (allData.length > 0) {
337
+ const firstRow = allData[0];
338
+ const languageInstructions: string[] = [];
 
 
 
339
 
340
+ if (typeof firstRow.language_instruction === "string") {
341
+ languageInstructions.push(firstRow.language_instruction);
342
+ }
343
 
344
+ let instructionNum = 2;
345
+ while (
346
+ typeof firstRow[`language_instruction_${instructionNum}`] === "string"
347
+ ) {
348
+ languageInstructions.push(
349
+ firstRow[`language_instruction_${instructionNum}`] as string,
350
+ );
351
+ instructionNum++;
352
+ }
353
+
354
+ if (languageInstructions.length > 0) {
355
+ task = languageInstructions.join("\n");
356
+ }
357
+ }
358
+
359
+ if (!task && allData.length > 0 && typeof allData[0].task === "string") {
360
  task = allData[0].task;
361
  }
362
 
 
363
  if (!task && allData.length > 0) {
364
  try {
365
  const tasksUrl = buildVersionedUrl(repoId, version, "meta/tasks.jsonl");
 
367
 
368
  if (tasksResponse.ok) {
369
  const tasksText = await tasksResponse.text();
 
370
  const tasksData = tasksText
371
  .split("\n")
372
  .filter((line) => line.trim())
 
374
 
375
  if (tasksData && tasksData.length > 0) {
376
  const taskIndex = allData[0].task_index;
 
 
377
  const taskIndexNum =
378
  typeof taskIndex === "bigint" ? Number(taskIndex) : taskIndex;
379
+ const taskData = tasksData.find(
380
+ (t: Record<string, unknown>) => t.task_index === taskIndexNum,
381
+ );
382
  if (taskData) {
383
  task = taskData.task;
384
  }
 
389
  }
390
  }
391
 
392
+ // Build chart data from already-parsed allData (no second parquet parse)
 
393
  const seriesNames = [
394
  "timestamp",
395
  ...columns.map(({ value }) => value).flat(),
396
  ];
397
 
398
+ const chartData = allData.map((row) => {
 
399
  const obj: Record<string, number> = {};
400
+ obj["timestamp"] = Number(row.timestamp);
401
+ for (const col of columns) {
402
+ const rawVal = row[col.key];
403
+ if (Array.isArray(rawVal)) {
404
+ rawVal.forEach((v: unknown, i: number) => {
405
+ if (i < col.value.length) obj[col.value[i]] = Number(v);
406
+ });
407
+ } else if (rawVal !== undefined) {
408
+ obj[col.value[0]] = Number(rawVal);
409
+ }
410
+ }
411
  return obj;
412
  });
413
 
 
441
  episodeId,
442
  videosInfo,
443
  chartDataGroups,
444
+ flatChartData: chartData,
445
  episodes,
446
  ignoredColumns,
447
  duration,
 
456
  info: DatasetMetadata,
457
  episodeId: number,
458
  ): Promise<EpisodeData> {
459
+ const datasetInfo: DatasetDisplayInfo = {
 
460
  repoId,
461
  total_frames: info.total_frames,
462
  total_episodes: info.total_episodes,
463
  fps: info.fps,
464
+ robot_type: null,
465
+ codebase_version: version,
466
+ total_tasks: 0,
467
+ dataset_size_mb: 0,
468
+ cameras: [],
469
  };
470
 
 
471
  const episodes = Array.from({ length: info.total_episodes }, (_, i) => i);
472
 
473
  // Load episode metadata to get timestamps for episode 0
 
486
  );
487
 
488
  // Load episode data for charts
489
+ const { chartDataGroups, flatChartData, ignoredColumns, task } =
490
+ await loadEpisodeDataV3(repoId, version, info, episodeMetadata);
 
 
 
 
491
 
492
+ const duration = episodeMetadata.length
493
+ ? episodeMetadata.length / info.fps
494
+ : episodeMetadata.video_to_timestamp - episodeMetadata.video_from_timestamp;
 
 
 
495
 
496
  return {
497
  datasetInfo,
498
  episodeId,
499
  videosInfo,
500
  chartDataGroups,
501
+ flatChartData,
502
  episodes,
503
  ignoredColumns,
504
  duration,
 
513
  info: DatasetMetadata,
514
  episodeMetadata: EpisodeMetadataV3,
515
  ): Promise<{
516
+ chartDataGroups: ChartRow[][];
517
+ flatChartData: Record<string, number>[];
518
  ignoredColumns: string[];
519
  task?: string;
520
  }> {
 
529
  const fullData = await readParquetAsObjects(arrayBuffer, []);
530
 
531
  // Extract the episode-specific data slice
532
+ const fromIndex = bigIntToNumber(episodeMetadata.dataset_from_index, 0);
533
+ const toIndex = bigIntToNumber(
534
+ episodeMetadata.dataset_to_index,
535
+ fullData.length,
536
+ );
537
 
538
  // Find the starting index of this parquet file by checking the first row's index
539
  // This handles the case where episodes are split across multiple parquet files
 
549
  const episodeData = fullData.slice(localFromIndex, localToIndex);
550
 
551
  if (episodeData.length === 0) {
552
+ return {
553
+ chartDataGroups: [],
554
+ flatChartData: [],
555
+ ignoredColumns: [],
556
+ task: undefined,
557
+ };
558
  }
559
 
560
  // Convert to the same format as v2.x for compatibility with existing chart code
561
+ const { chartDataGroups, flatChartData, ignoredColumns } =
562
+ processEpisodeDataForCharts(episodeData, info, episodeMetadata);
 
 
 
563
 
564
  // First check for language_instruction fields in the data (preferred)
565
+ let task: string | undefined;
566
+ if (episodeData.length > 0) {
567
+ const languageInstructions: string[] = [];
568
+
569
+ const extractInstructions = (row: Record<string, unknown>) => {
570
+ if (typeof row.language_instruction === "string") {
571
+ languageInstructions.push(row.language_instruction);
572
+ }
573
+ let num = 2;
574
+ while (typeof row[`language_instruction_${num}`] === "string") {
575
+ languageInstructions.push(
576
+ row[`language_instruction_${num}`] as string,
577
+ );
578
+ num++;
579
+ }
580
+ };
581
+
582
+ extractInstructions(episodeData[0]);
583
+
584
+ // If no instructions in first row, check middle and last rows
585
+ if (languageInstructions.length === 0 && episodeData.length > 1) {
586
+ for (const idx of [
587
+ Math.floor(episodeData.length / 2),
588
+ episodeData.length - 1,
589
+ ]) {
590
+ extractInstructions(episodeData[idx]);
591
+ if (languageInstructions.length > 0) break;
592
+ }
593
+ }
594
+
595
+ if (languageInstructions.length > 0) {
596
+ task = languageInstructions.join("\n");
597
+ }
598
+ }
599
+
600
+ // Fall back to tasks metadata parquet
601
+ if (!task && episodeData.length > 0) {
602
  try {
 
603
  const tasksUrl = buildVersionedUrl(
604
  repoId,
605
  version,
 
608
  const tasksArrayBuffer = await fetchParquetFile(tasksUrl);
609
  const tasksData = await readParquetAsObjects(tasksArrayBuffer, []);
610
 
611
+ if (tasksData.length > 0) {
612
+ const taskIndexNum = bigIntToNumber(episodeData[0].task_index, -1);
 
 
 
 
 
613
 
614
+ if (taskIndexNum >= 0 && taskIndexNum < tasksData.length) {
 
 
 
 
 
 
 
 
 
 
 
 
 
615
  const taskData = tasksData[taskIndexNum];
616
+ const rawTask = taskData.__index_level_0__ ?? taskData.task;
617
+ task = typeof rawTask === "string" ? rawTask : undefined;
 
 
 
 
 
 
 
 
 
 
 
 
618
  }
619
  }
620
  } catch {
621
+ // Could not load tasks metadata
622
  }
623
  }
624
 
625
+ return { chartDataGroups, flatChartData, ignoredColumns, task };
626
  } catch {
627
+ return {
628
+ chartDataGroups: [],
629
+ flatChartData: [],
630
+ ignoredColumns: [],
631
+ task: undefined,
632
+ };
633
  }
634
  }
635
 
 
638
  episodeData: Record<string, unknown>[],
639
  info: DatasetMetadata,
640
  episodeMetadata?: EpisodeMetadataV3,
641
+ ): {
642
+ chartDataGroups: ChartRow[][];
643
+ flatChartData: Record<string, number>[];
644
+ ignoredColumns: string[];
645
+ } {
 
 
 
 
 
646
  // Convert parquet data to chart format
647
  let seriesNames: string[] = [];
648
 
 
678
  const excludedColumns = EXCLUDED_COLUMNS.V3 as readonly string[];
679
 
680
  // Create columns structure similar to V2.1 for proper hierarchical naming
681
+ const columns: ColumnDef[] = Object.entries(info.features)
682
  .filter(
683
  ([key, value]) =>
684
  ["float32", "int32"].includes(value.dtype) &&
 
686
  !excludedColumns.includes(key),
687
  )
688
  .map(([key, feature]) => {
689
+ let column_names: unknown = feature.names;
690
+ while (typeof column_names === "object" && column_names !== null) {
691
  if (Array.isArray(column_names)) break;
692
+ column_names = Object.values(column_names)[0];
693
  }
694
  return {
695
  key,
696
  value: Array.isArray(column_names)
697
  ? column_names.map(
698
+ (name: string) => `${key}${SERIES_NAME_DELIMITER}${name}`,
699
  )
700
  : Array.from(
701
  { length: feature.shape[0] || 1 },
 
816
  // Process chart data into organized groups using utility function
817
  const chartGroups = processChartDataGroups(seriesNames, chartData);
818
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  const chartDataGroups = chartGroups.map((group) =>
820
  chartData.map((row) => {
821
  const grouped = groupRowBySuffix(pick(row, [...group, "timestamp"]));
 
828
  }),
829
  );
830
 
831
+ return { chartDataGroups, flatChartData: chartData, ignoredColumns };
832
  }
833
 
834
  // Video info extraction with segmentation for v3.0
 
849
  key.startsWith(`videos/${videoKey}/`),
850
  );
851
 
852
+ let chunkIndex: number,
853
+ fileIndex: number,
854
+ segmentStart: number,
855
+ segmentEnd: number;
856
+
857
+ const toNum = (v: string | number): number =>
858
+ typeof v === "string" ? parseFloat(v) || 0 : v;
859
 
860
  if (cameraSpecificKeys.length > 0) {
861
+ chunkIndex = toNum(episodeMetadata[`videos/${videoKey}/chunk_index`]);
862
+ fileIndex = toNum(episodeMetadata[`videos/${videoKey}/file_index`]);
863
+ segmentStart =
864
+ toNum(episodeMetadata[`videos/${videoKey}/from_timestamp`]) || 0;
865
+ segmentEnd =
866
+ toNum(episodeMetadata[`videos/${videoKey}/to_timestamp`]) || 30;
 
867
  } else {
 
868
  chunkIndex = episodeMetadata.video_chunk_index || 0;
869
  fileIndex = episodeMetadata.video_file_index || 0;
870
  segmentStart = episodeMetadata.video_from_timestamp || 0;
 
966
  // Check if this is v3.0 format with named keys
967
  if ("episode_index" in row) {
968
  // v3.0 format - use named keys
969
+ // Convert BigInt values to numbers
970
+ const toBigIntSafe = (value: unknown): number => {
971
+ if (typeof value === "bigint") return Number(value);
972
+ if (typeof value === "number") return value;
973
+ if (typeof value === "string") return parseInt(value) || 0;
974
+ return 0;
975
+ };
976
+
977
+ const toNumSafe = (value: unknown): number => {
978
+ if (typeof value === "number") return value;
979
+ if (typeof value === "bigint") return Number(value);
980
+ if (typeof value === "string") return parseFloat(value) || 0;
981
+ return 0;
982
  };
983
 
984
  // Handle video metadata - look for video-specific keys
985
  const videoKeys = Object.keys(row).filter(
986
  (key) => key.includes("videos/") && key.includes("/chunk_index"),
987
  );
988
+ let videoChunkIndex = 0,
989
+ videoFileIndex = 0,
990
+ videoFromTs = 0,
991
+ videoToTs = 30;
992
  if (videoKeys.length > 0) {
993
+ const videoBaseName = videoKeys[0].replace("/chunk_index", "");
994
+ videoChunkIndex = toBigIntSafe(row[`${videoBaseName}/chunk_index`]);
995
+ videoFileIndex = toBigIntSafe(row[`${videoBaseName}/file_index`]);
996
+ videoFromTs = toNumSafe(row[`${videoBaseName}/from_timestamp`]);
997
+ videoToTs = toNumSafe(row[`${videoBaseName}/to_timestamp`]) || 30;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
998
  }
999
 
1000
+ const episodeData: EpisodeMetadataV3 = {
1001
+ episode_index: toBigIntSafe(row["episode_index"]),
1002
+ data_chunk_index: toBigIntSafe(row["data/chunk_index"]),
1003
+ data_file_index: toBigIntSafe(row["data/file_index"]),
1004
+ dataset_from_index: toBigIntSafe(row["dataset_from_index"]),
1005
+ dataset_to_index: toBigIntSafe(row["dataset_to_index"]),
1006
+ length: toBigIntSafe(row["length"]),
1007
+ video_chunk_index: videoChunkIndex,
1008
+ video_file_index: videoFileIndex,
1009
+ video_from_timestamp: videoFromTs,
1010
+ video_to_timestamp: videoToTs,
1011
+ };
1012
+
1013
+ // Store per-camera metadata for extractVideoInfoV3WithSegmentation
1014
  Object.keys(row).forEach((key) => {
1015
  if (key.startsWith("videos/")) {
1016
+ const val = row[key];
1017
+ episodeData[key] =
1018
+ typeof val === "bigint"
1019
+ ? Number(val)
1020
+ : typeof val === "number" || typeof val === "string"
1021
+ ? val
1022
+ : 0;
1023
  }
1024
  });
1025
 
1026
  return episodeData as EpisodeMetadataV3;
1027
  } else {
1028
  // Fallback to numeric keys for compatibility
1029
+ const toNum = (v: unknown, fallback = 0): number =>
1030
+ typeof v === "number"
1031
+ ? v
1032
+ : typeof v === "bigint"
1033
+ ? Number(v)
1034
+ : fallback;
1035
  return {
1036
+ episode_index: toNum(row["0"]),
1037
+ data_chunk_index: toNum(row["1"]),
1038
+ data_file_index: toNum(row["2"]),
1039
+ dataset_from_index: toNum(row["3"]),
1040
+ dataset_to_index: toNum(row["4"]),
1041
+ video_chunk_index: toNum(row["5"]),
1042
+ video_file_index: toNum(row["6"]),
1043
+ video_from_timestamp: toNum(row["7"]),
1044
+ video_to_timestamp: toNum(row["8"], 30),
1045
+ length: toNum(row["9"], 30),
1046
  };
1047
  }
1048
  }
 
1064
  return fallback;
1065
  }
1066
 
1067
+ // ─── Stats computation ───────────────────────────────────────────
1068
+
1069
+ /**
1070
+ * Compute per-column min/max values from the current episode's chart data.
1071
+ */
1072
+ export function computeColumnMinMax(
1073
+ chartDataGroups: ChartRow[][],
1074
+ ): ColumnMinMax[] {
1075
+ const stats: Record<string, { min: number; max: number }> = {};
1076
+
1077
+ for (const group of chartDataGroups) {
1078
+ for (const row of group) {
1079
+ for (const [key, value] of Object.entries(row)) {
1080
+ if (key === "timestamp") continue;
1081
+ if (typeof value === "number" && isFinite(value)) {
1082
+ if (!stats[key]) {
1083
+ stats[key] = { min: value, max: value };
1084
+ } else {
1085
+ if (value < stats[key].min) stats[key].min = value;
1086
+ if (value > stats[key].max) stats[key].max = value;
1087
+ }
1088
+ } else if (typeof value === "object" && value !== null) {
1089
+ // Nested group like { joint_0: 1.2, joint_1: 3.4 }
1090
+ for (const [subKey, subVal] of Object.entries(value)) {
1091
+ const fullKey = `${key} | ${subKey}`;
1092
+ if (typeof subVal === "number" && isFinite(subVal)) {
1093
+ if (!stats[fullKey]) {
1094
+ stats[fullKey] = { min: subVal, max: subVal };
1095
+ } else {
1096
+ if (subVal < stats[fullKey].min) stats[fullKey].min = subVal;
1097
+ if (subVal > stats[fullKey].max) stats[fullKey].max = subVal;
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+
1106
+ return Object.entries(stats).map(([column, { min, max }]) => ({
1107
+ column,
1108
+ min: Math.round(min * 1000) / 1000,
1109
+ max: Math.round(max * 1000) / 1000,
1110
+ }));
1111
+ }
1112
+
1113
+ /**
1114
+ * Load all episode lengths from the episodes metadata parquet files (v3.0).
1115
+ * Returns min/max/mean/median/std and a histogram, or null if unavailable.
1116
+ */
1117
+ export async function loadAllEpisodeLengthsV3(
1118
+ repoId: string,
1119
+ version: string,
1120
+ fps: number,
1121
+ ): Promise<EpisodeLengthStats | null> {
1122
+ try {
1123
+ const allEpisodes: { index: number; length: number }[] = [];
1124
+ let fileIndex = 0;
1125
+ const chunkIndex = 0;
1126
+
1127
+ while (true) {
1128
+ const path = `meta/episodes/chunk-${chunkIndex.toString().padStart(3, "0")}/file-${fileIndex.toString().padStart(3, "0")}.parquet`;
1129
+ const url = buildVersionedUrl(repoId, version, path);
1130
+ try {
1131
+ const buf = await fetchParquetFile(url);
1132
+ const rows = await readParquetAsObjects(buf, []);
1133
+ if (rows.length === 0 && fileIndex > 0) break;
1134
+ for (const row of rows) {
1135
+ const parsed = parseEpisodeRowSimple(row);
1136
+ allEpisodes.push({
1137
+ index: parsed.episode_index,
1138
+ length: parsed.length,
1139
+ });
1140
+ }
1141
+ fileIndex++;
1142
+ } catch {
1143
+ break;
1144
+ }
1145
+ }
1146
+
1147
+ if (allEpisodes.length === 0) return null;
1148
+
1149
+ const withSeconds = allEpisodes.map((ep) => ({
1150
+ episodeIndex: ep.index,
1151
+ frames: ep.length,
1152
+ lengthSeconds: Math.round((ep.length / fps) * 100) / 100,
1153
+ }));
1154
+
1155
+ const sortedByLength = [...withSeconds].sort(
1156
+ (a, b) => a.lengthSeconds - b.lengthSeconds,
1157
+ );
1158
+ const shortestEpisodes = sortedByLength.slice(0, 5);
1159
+ const longestEpisodes = sortedByLength.slice(-5).reverse();
1160
+
1161
+ const lengths = withSeconds.map((e) => e.lengthSeconds);
1162
+ const sum = lengths.reduce((a, b) => a + b, 0);
1163
+ const mean = Math.round((sum / lengths.length) * 100) / 100;
1164
+
1165
+ const sorted = [...lengths].sort((a, b) => a - b);
1166
+ const mid = Math.floor(sorted.length / 2);
1167
+ const median =
1168
+ sorted.length % 2 === 0
1169
+ ? Math.round(((sorted[mid - 1] + sorted[mid]) / 2) * 100) / 100
1170
+ : sorted[mid];
1171
+
1172
+ const variance =
1173
+ lengths.reduce((acc, l) => acc + (l - mean) ** 2, 0) / lengths.length;
1174
+ const std = Math.round(Math.sqrt(variance) * 100) / 100;
1175
+
1176
+ // Build histogram
1177
+ const histMin = Math.min(...lengths);
1178
+ const histMax = Math.max(...lengths);
1179
+
1180
+ if (histMax === histMin) {
1181
+ return {
1182
+ shortestEpisodes,
1183
+ longestEpisodes,
1184
+ allEpisodeLengths: withSeconds,
1185
+ meanEpisodeLength: mean,
1186
+ medianEpisodeLength: median,
1187
+ stdEpisodeLength: std,
1188
+ episodeLengthHistogram: [
1189
+ { binLabel: `${histMin.toFixed(1)}s`, count: lengths.length },
1190
+ ],
1191
+ };
1192
+ }
1193
+
1194
+ const p1 = sorted[Math.floor(sorted.length * 0.01)];
1195
+ const p99 = sorted[Math.ceil(sorted.length * 0.99) - 1];
1196
+ const range = p99 - p1 || 1;
1197
+
1198
+ const targetBins = Math.max(
1199
+ 10,
1200
+ Math.min(50, Math.ceil(Math.log2(lengths.length) + 1)),
1201
+ );
1202
+ const rawBinWidth = range / targetBins;
1203
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rawBinWidth)));
1204
+ const niceSteps = [1, 2, 2.5, 5, 10];
1205
+ const niceBinWidth =
1206
+ niceSteps.map((s) => s * magnitude).find((w) => w >= rawBinWidth) ??
1207
+ rawBinWidth;
1208
+
1209
+ const niceMin = Math.floor(p1 / niceBinWidth) * niceBinWidth;
1210
+ const niceMax = Math.ceil(p99 / niceBinWidth) * niceBinWidth;
1211
+ const actualBinCount = Math.max(
1212
+ 1,
1213
+ Math.round((niceMax - niceMin) / niceBinWidth),
1214
+ );
1215
+ const bins = Array.from({ length: actualBinCount }, () => 0);
1216
+
1217
+ for (const len of lengths) {
1218
+ let binIdx = Math.floor((len - niceMin) / niceBinWidth);
1219
+ if (binIdx < 0) binIdx = 0;
1220
+ if (binIdx >= actualBinCount) binIdx = actualBinCount - 1;
1221
+ bins[binIdx]++;
1222
+ }
1223
+
1224
+ const histogram = bins.map((count, i) => {
1225
+ const lo = niceMin + i * niceBinWidth;
1226
+ const hi = lo + niceBinWidth;
1227
+ return { binLabel: `${lo.toFixed(1)}–${hi.toFixed(1)}s`, count };
1228
+ });
1229
+
1230
+ return {
1231
+ shortestEpisodes,
1232
+ longestEpisodes,
1233
+ allEpisodeLengths: withSeconds,
1234
+ meanEpisodeLength: mean,
1235
+ medianEpisodeLength: median,
1236
+ stdEpisodeLength: std,
1237
+ episodeLengthHistogram: histogram,
1238
+ };
1239
+ } catch {
1240
+ return null;
1241
+ }
1242
+ }
1243
+
1244
+ /**
1245
+ * Load video frame info for all episodes across all cameras.
1246
+ * Returns camera names + a map of camera → EpisodeFrameInfo[].
1247
+ */
1248
+ export async function loadAllEpisodeFrameInfo(
1249
+ repoId: string,
1250
+ version: string,
1251
+ info: DatasetMetadata,
1252
+ ): Promise<EpisodeFramesData> {
1253
+ const videoFeatures = Object.entries(info.features).filter(
1254
+ ([, f]) => f.dtype === "video",
1255
+ );
1256
+ if (videoFeatures.length === 0) return { cameras: [], framesByCamera: {} };
1257
+
1258
+ const cameras = videoFeatures.map(([key]) => key);
1259
+ const framesByCamera: Record<string, EpisodeFrameInfo[]> = {};
1260
+ for (const cam of cameras) framesByCamera[cam] = [];
1261
+
1262
+ if (version === "v3.0") {
1263
+ let fileIndex = 0;
1264
+ while (true) {
1265
+ const path = `meta/episodes/chunk-000/file-${fileIndex.toString().padStart(3, "0")}.parquet`;
1266
+ try {
1267
+ const buf = await fetchParquetFile(
1268
+ buildVersionedUrl(repoId, version, path),
1269
+ );
1270
+ const rows = await readParquetAsObjects(buf, []);
1271
+ if (rows.length === 0 && fileIndex > 0) break;
1272
+ for (const row of rows) {
1273
+ const epIdx = Number(row["episode_index"] ?? 0);
1274
+ for (const cam of cameras) {
1275
+ const cIdx = Number(
1276
+ row[`videos/${cam}/chunk_index`] ?? row["video_chunk_index"] ?? 0,
1277
+ );
1278
+ const fIdx = Number(
1279
+ row[`videos/${cam}/file_index`] ?? row["video_file_index"] ?? 0,
1280
+ );
1281
+ const fromTs = Number(
1282
+ row[`videos/${cam}/from_timestamp`] ??
1283
+ row["video_from_timestamp"] ??
1284
+ 0,
1285
+ );
1286
+ const toTs = Number(
1287
+ row[`videos/${cam}/to_timestamp`] ??
1288
+ row["video_to_timestamp"] ??
1289
+ 30,
1290
+ );
1291
+ const videoPath = `videos/${cam}/chunk-${cIdx.toString().padStart(3, "0")}/file-${fIdx.toString().padStart(3, "0")}.mp4`;
1292
+ framesByCamera[cam].push({
1293
+ episodeIndex: epIdx,
1294
+ videoUrl: buildVersionedUrl(repoId, version, videoPath),
1295
+ firstFrameTime: fromTs,
1296
+ lastFrameTime: Math.max(0, toTs - 0.05),
1297
+ });
1298
+ }
1299
+ }
1300
+ fileIndex++;
1301
+ } catch {
1302
+ break;
1303
+ }
1304
+ }
1305
+ return { cameras, framesByCamera };
1306
+ }
1307
+
1308
+ // v2.x — construct URLs from template
1309
+ for (let i = 0; i < info.total_episodes; i++) {
1310
+ const chunk = Math.floor(i / (info.chunks_size || 1000));
1311
+ for (const cam of cameras) {
1312
+ const videoPath = formatStringWithVars(info.video_path, {
1313
+ video_key: cam,
1314
+ episode_chunk: chunk.toString().padStart(3, "0"),
1315
+ episode_index: i.toString().padStart(6, "0"),
1316
+ });
1317
+ framesByCamera[cam].push({
1318
+ episodeIndex: i,
1319
+ videoUrl: buildVersionedUrl(repoId, version, videoPath),
1320
+ firstFrameTime: 0,
1321
+ lastFrameTime: null,
1322
+ });
1323
+ }
1324
+ }
1325
+ return { cameras, framesByCamera };
1326
+ }
1327
+
1328
+ // ─── Cross-episode action variance ──────────────────────────────
1329
+
1330
+ export type LowMovementEpisode = {
1331
+ episodeIndex: number;
1332
+ totalMovement: number;
1333
+ };
1334
+
1335
+ export type AggVelocityStat = {
1336
+ name: string;
1337
+ std: number; // normalized by motor range
1338
+ maxAbs: number; // normalized by motor range
1339
+ bins: number[];
1340
+ lo: number; // normalized by motor range
1341
+ hi: number; // normalized by motor range
1342
+ motorRange: number;
1343
+ inactive?: boolean; // true if p95(|Δa|) < 1% of motor range
1344
+ discrete?: boolean; // true if motor has very few unique values (e.g. open/close gripper)
1345
+ };
1346
+
1347
+ export type AggAutocorrelation = {
1348
+ chartData: Record<string, number>[];
1349
+ suggestedChunk: number | null;
1350
+ shortKeys: string[];
1351
+ };
1352
+
1353
+ export type SpeedDistEntry = {
1354
+ episodeIndex: number;
1355
+ speed: number;
1356
+ };
1357
+
1358
+ export type AggAlignment = {
1359
+ ccData: { lag: number; max: number; mean: number; min: number }[];
1360
+ meanPeakLag: number;
1361
+ meanPeakCorr: number;
1362
+ maxPeakLag: number;
1363
+ maxPeakCorr: number;
1364
+ minPeakLag: number;
1365
+ minPeakCorr: number;
1366
+ lagRangeMin: number;
1367
+ lagRangeMax: number;
1368
+ numPairs: number;
1369
+ };
1370
+
1371
+ export type JerkyEpisode = {
1372
+ episodeIndex: number;
1373
+ meanAbsDelta: number;
1374
+ };
1375
+
1376
+ export type CrossEpisodeVarianceData = {
1377
+ actionNames: string[];
1378
+ timeBins: number[];
1379
+ variance: number[][];
1380
+ numEpisodes: number;
1381
+ lowMovementEpisodes: LowMovementEpisode[];
1382
+ aggVelocity: AggVelocityStat[];
1383
+ aggAutocorrelation: AggAutocorrelation | null;
1384
+ speedDistribution: SpeedDistEntry[];
1385
+ jerkyEpisodes: JerkyEpisode[];
1386
+ aggAlignment: AggAlignment | null;
1387
+ };
1388
+
1389
+ export async function loadCrossEpisodeActionVariance(
1390
+ repoId: string,
1391
+ version: string,
1392
+ info: DatasetMetadata,
1393
+ fps: number,
1394
+ maxEpisodes = 500,
1395
+ numTimeBins = 50,
1396
+ ): Promise<CrossEpisodeVarianceData | null> {
1397
+ const actionEntry = Object.entries(info.features).find(
1398
+ ([key, f]) => key === "action" && f.shape.length === 1,
1399
+ );
1400
+ if (!actionEntry) {
1401
+ console.warn(
1402
+ "[cross-ep] No action feature found. Available features:",
1403
+ Object.entries(info.features)
1404
+ .map(([k, f]) => `${k}(${f.dtype}, shape=${JSON.stringify(f.shape)})`)
1405
+ .join(", "),
1406
+ );
1407
+ return null;
1408
+ }
1409
+
1410
+ const [actionKey, actionMeta] = actionEntry;
1411
+ const actionDim = actionMeta.shape[0];
1412
+
1413
+ let names: unknown = actionMeta.names;
1414
+ while (typeof names === "object" && names !== null && !Array.isArray(names)) {
1415
+ names = Object.values(names)[0];
1416
+ }
1417
+ const actionNames = Array.isArray(names)
1418
+ ? (names as string[]).map((n) => `${actionKey}${SERIES_NAME_DELIMITER}${n}`)
1419
+ : Array.from(
1420
+ { length: actionDim },
1421
+ (_, i) => `${actionKey}${SERIES_NAME_DELIMITER}${i}`,
1422
+ );
1423
+
1424
+ // State feature for alignment computation
1425
+ const stateEntry = Object.entries(info.features).find(
1426
+ ([key, f]) => key === "observation.state" && f.shape.length === 1,
1427
+ );
1428
+ const stateKey = stateEntry?.[0] ?? null;
1429
+ const stateDim = stateEntry?.[1].shape[0] ?? 0;
1430
+
1431
+ // Collect episode metadata
1432
+ type EpMeta = {
1433
+ index: number;
1434
+ chunkIdx: number;
1435
+ fileIdx: number;
1436
+ from: number;
1437
+ to: number;
1438
+ };
1439
+ const allEps: EpMeta[] = [];
1440
+
1441
+ if (version === "v3.0") {
1442
+ let fileIndex = 0;
1443
+ while (true) {
1444
+ const path = `meta/episodes/chunk-000/file-${fileIndex.toString().padStart(3, "0")}.parquet`;
1445
+ try {
1446
+ const buf = await fetchParquetFile(
1447
+ buildVersionedUrl(repoId, version, path),
1448
+ );
1449
+ const rows = await readParquetAsObjects(buf, []);
1450
+ if (rows.length === 0 && fileIndex > 0) break;
1451
+ for (const row of rows) {
1452
+ const parsed = parseEpisodeRowSimple(row);
1453
+ allEps.push({
1454
+ index: parsed.episode_index,
1455
+ chunkIdx: parsed.data_chunk_index,
1456
+ fileIdx: parsed.data_file_index,
1457
+ from: parsed.dataset_from_index,
1458
+ to: parsed.dataset_to_index,
1459
+ });
1460
+ }
1461
+ fileIndex++;
1462
+ } catch {
1463
+ break;
1464
+ }
1465
+ }
1466
+ } else {
1467
+ for (let i = 0; i < info.total_episodes; i++) {
1468
+ allEps.push({ index: i, chunkIdx: 0, fileIdx: 0, from: 0, to: 0 });
1469
+ }
1470
+ }
1471
+
1472
+ if (allEps.length < 2) {
1473
+ console.warn(
1474
+ `[cross-ep] Only ${allEps.length} episode(s) found in metadata, need ≥2`,
1475
+ );
1476
+ return null;
1477
+ }
1478
+ console.log(
1479
+ `[cross-ep] Found ${allEps.length} episodes in metadata, sampling up to ${maxEpisodes}`,
1480
+ );
1481
+
1482
+ // Sample episodes evenly
1483
+ const sampled =
1484
+ allEps.length <= maxEpisodes
1485
+ ? allEps
1486
+ : Array.from(
1487
+ { length: maxEpisodes },
1488
+ (_, i) =>
1489
+ allEps[Math.round((i * (allEps.length - 1)) / (maxEpisodes - 1))],
1490
+ );
1491
+
1492
+ // Load action (and state) data per episode
1493
+ const episodeActions: { index: number; actions: number[][] }[] = [];
1494
+ const episodeStates: (number[][] | null)[] = [];
1495
+
1496
+ if (version === "v3.0") {
1497
+ const byFile = new Map<string, EpMeta[]>();
1498
+ for (const ep of sampled) {
1499
+ const key = `${ep.chunkIdx}-${ep.fileIdx}`;
1500
+ if (!byFile.has(key)) byFile.set(key, []);
1501
+ byFile.get(key)!.push(ep);
1502
+ }
1503
+
1504
+ for (const [, eps] of byFile) {
1505
+ const ep0 = eps[0];
1506
+ const dataPath = `data/chunk-${ep0.chunkIdx.toString().padStart(3, "0")}/file-${ep0.fileIdx.toString().padStart(3, "0")}.parquet`;
1507
+ try {
1508
+ const buf = await fetchParquetFile(
1509
+ buildVersionedUrl(repoId, version, dataPath),
1510
+ );
1511
+ const rows = await readParquetAsObjects(buf, []);
1512
+ const fileStart =
1513
+ rows.length > 0 && rows[0].index !== undefined
1514
+ ? Number(rows[0].index)
1515
+ : 0;
1516
+
1517
+ for (const ep of eps) {
1518
+ const localFrom = Math.max(0, ep.from - fileStart);
1519
+ const localTo = Math.min(rows.length, ep.to - fileStart);
1520
+ const actions: number[][] = [];
1521
+ const states: number[][] = [];
1522
+ for (let r = localFrom; r < localTo; r++) {
1523
+ const raw = rows[r]?.[actionKey];
1524
+ if (Array.isArray(raw)) actions.push(raw.map(Number));
1525
+ if (stateKey) {
1526
+ const sRaw = rows[r]?.[stateKey];
1527
+ if (Array.isArray(sRaw)) states.push(sRaw.map(Number));
1528
+ }
1529
+ }
1530
+ if (actions.length > 0) {
1531
+ episodeActions.push({ index: ep.index, actions });
1532
+ episodeStates.push(
1533
+ stateKey && states.length === actions.length ? states : null,
1534
+ );
1535
+ }
1536
+ }
1537
+ } catch {
1538
+ /* skip file */
1539
+ }
1540
+ }
1541
+ } else {
1542
+ const chunkSize = info.chunks_size || 1000;
1543
+ for (const ep of sampled) {
1544
+ const chunk = Math.floor(ep.index / chunkSize);
1545
+ const dataPath = formatStringWithVars(info.data_path, {
1546
+ episode_chunk: chunk.toString().padStart(3, "0"),
1547
+ episode_index: ep.index.toString().padStart(6, "0"),
1548
+ });
1549
+ try {
1550
+ const buf = await fetchParquetFile(
1551
+ buildVersionedUrl(repoId, version, dataPath),
1552
+ );
1553
+ const rows = await readParquetAsObjects(buf, []);
1554
+ const actions: number[][] = [];
1555
+ const states: number[][] = [];
1556
+ for (const row of rows) {
1557
+ const raw = row[actionKey];
1558
+ if (Array.isArray(raw)) {
1559
+ actions.push(raw.map(Number));
1560
+ } else {
1561
+ const vec: number[] = [];
1562
+ for (let d = 0; d < actionDim; d++) {
1563
+ const v = row[`${actionKey}.${d}`] ?? row[d];
1564
+ vec.push(typeof v === "number" ? v : Number(v) || 0);
1565
+ }
1566
+ actions.push(vec);
1567
+ }
1568
+ if (stateKey) {
1569
+ const sRaw = row[stateKey];
1570
+ if (Array.isArray(sRaw)) states.push(sRaw.map(Number));
1571
+ }
1572
+ }
1573
+ if (actions.length > 0) {
1574
+ episodeActions.push({ index: ep.index, actions });
1575
+ episodeStates.push(
1576
+ stateKey && states.length === actions.length ? states : null,
1577
+ );
1578
+ }
1579
+ } catch {
1580
+ /* skip */
1581
+ }
1582
+ }
1583
+ }
1584
+
1585
+ if (episodeActions.length < 2) {
1586
+ console.warn(
1587
+ `[cross-ep] Only ${episodeActions.length} episode(s) had loadable action data out of ${sampled.length} sampled`,
1588
+ );
1589
+ return null;
1590
+ }
1591
+ console.log(
1592
+ `[cross-ep] Loaded action data for ${episodeActions.length}/${sampled.length} episodes`,
1593
+ );
1594
+
1595
+ // Resample each episode to numTimeBins and compute variance
1596
+ const timeBins = Array.from(
1597
+ { length: numTimeBins },
1598
+ (_, i) => i / (numTimeBins - 1),
1599
+ );
1600
+ const sums = Array.from(
1601
+ { length: numTimeBins },
1602
+ () => new Float64Array(actionDim),
1603
+ );
1604
+ const sumsSq = Array.from(
1605
+ { length: numTimeBins },
1606
+ () => new Float64Array(actionDim),
1607
+ );
1608
+ const counts = new Uint32Array(numTimeBins);
1609
+
1610
+ for (const { actions: epActions } of episodeActions) {
1611
+ const T = epActions.length;
1612
+ for (let b = 0; b < numTimeBins; b++) {
1613
+ const srcIdx = Math.min(Math.round(timeBins[b] * (T - 1)), T - 1);
1614
+ const row = epActions[srcIdx];
1615
+ for (let d = 0; d < actionDim; d++) {
1616
+ const v = row[d] ?? 0;
1617
+ sums[b][d] += v;
1618
+ sumsSq[b][d] += v * v;
1619
+ }
1620
+ counts[b]++;
1621
+ }
1622
+ }
1623
+
1624
+ const variance: number[][] = [];
1625
+ for (let b = 0; b < numTimeBins; b++) {
1626
+ const row: number[] = [];
1627
+ const n = counts[b];
1628
+ for (let d = 0; d < actionDim; d++) {
1629
+ if (n < 2) {
1630
+ row.push(0);
1631
+ continue;
1632
+ }
1633
+ const mean = sums[b][d] / n;
1634
+ row.push(sumsSq[b][d] / n - mean * mean);
1635
+ }
1636
+ variance.push(row);
1637
+ }
1638
+
1639
+ // Per-episode average movement per frame: mean L2 norm of frame-to-frame action deltas
1640
+ const movementScores: LowMovementEpisode[] = episodeActions.map(
1641
+ ({ index, actions: ep }) => {
1642
+ if (ep.length < 2) return { episodeIndex: index, totalMovement: 0 };
1643
+ let total = 0;
1644
+ for (let t = 1; t < ep.length; t++) {
1645
+ let sumSq = 0;
1646
+ for (let d = 0; d < actionDim; d++) {
1647
+ const delta = (ep[t][d] ?? 0) - (ep[t - 1][d] ?? 0);
1648
+ sumSq += delta * delta;
1649
+ }
1650
+ total += Math.sqrt(sumSq);
1651
+ }
1652
+ const avgPerFrame = total / (ep.length - 1);
1653
+ return {
1654
+ episodeIndex: index,
1655
+ totalMovement: Math.round(avgPerFrame * 10000) / 10000,
1656
+ };
1657
+ },
1658
+ );
1659
+
1660
+ movementScores.sort((a, b) => a.totalMovement - b.totalMovement);
1661
+ const lowMovementEpisodes = movementScores.slice(0, 10);
1662
+
1663
+ // Precompute per-dimension normalization: motor range (max − min) and unique value count
1664
+ const motorRanges: number[] = new Array(actionDim);
1665
+ const motorUniqueCount: number[] = new Array(actionDim);
1666
+ const DISCRETE_THRESHOLD = 4; // ≤ this many unique values → discrete motor
1667
+ for (let d = 0; d < actionDim; d++) {
1668
+ let lo = Infinity,
1669
+ hi = -Infinity;
1670
+ const uniqueVals = new Set<number>();
1671
+ for (const { actions: ep } of episodeActions) {
1672
+ for (let t = 0; t < ep.length; t++) {
1673
+ const v = ep[t][d] ?? 0;
1674
+ if (v < lo) lo = v;
1675
+ if (v > hi) hi = v;
1676
+ if (uniqueVals.size <= DISCRETE_THRESHOLD) uniqueVals.add(v);
1677
+ }
1678
+ }
1679
+ motorRanges[d] = hi - lo || 1;
1680
+ motorUniqueCount[d] = uniqueVals.size;
1681
+ }
1682
+
1683
+ // Per-episode, per-dimension activity: p95(|Δa|) >= 1% of motor range
1684
+ const ACTIVITY_THRESHOLD = 0.001; // 0.1% of motor range
1685
+ // activeMap[episodeIdx][dimIdx] = true if motor d is active in that episode
1686
+ const activeMap: boolean[][] = episodeActions.map(({ actions: ep }) => {
1687
+ const flags: boolean[] = new Array(actionDim);
1688
+ for (let d = 0; d < actionDim; d++) {
1689
+ if (ep.length < 2) {
1690
+ flags[d] = false;
1691
+ continue;
1692
+ }
1693
+ const absDeltas: number[] = [];
1694
+ for (let t = 1; t < ep.length; t++) {
1695
+ absDeltas.push(Math.abs((ep[t][d] ?? 0) - (ep[t - 1][d] ?? 0)));
1696
+ }
1697
+ absDeltas.sort((a, b) => a - b);
1698
+ const p95 = absDeltas[Math.floor(absDeltas.length * 0.95)];
1699
+ flags[d] = p95 >= motorRanges[d] * ACTIVITY_THRESHOLD;
1700
+ }
1701
+ return flags;
1702
+ });
1703
+ // A motor is globally inactive only if inactive in all episodes
1704
+ const globallyActive: boolean[] = new Array(actionDim);
1705
+ for (let d = 0; d < actionDim; d++) {
1706
+ globallyActive[d] = activeMap.some((flags) => flags[d]);
1707
+ }
1708
+
1709
+ // Aggregated velocity stats: pool deltas from all episodes, normalized by motor range
1710
+ const shortName = (k: string) => {
1711
+ const p = k.split(SERIES_NAME_DELIMITER);
1712
+ return p.length > 1 ? p[p.length - 1] : k;
1713
+ };
1714
+
1715
+ const aggVelocity: AggVelocityStat[] = (() => {
1716
+ const binCount = 30;
1717
+ const results: AggVelocityStat[] = [];
1718
+ for (let d = 0; d < actionDim; d++) {
1719
+ const motorRange = motorRanges[d];
1720
+ const inactive = !globallyActive[d];
1721
+ // Collect all deltas (unfiltered) for histogram display
1722
+ const allDeltas: number[] = [];
1723
+ // Collect only deltas from active episodes for stats
1724
+ const activeDeltas: number[] = [];
1725
+ for (let ei = 0; ei < episodeActions.length; ei++) {
1726
+ const ep = episodeActions[ei].actions;
1727
+ for (let t = 1; t < ep.length; t++) {
1728
+ const delta = (ep[t][d] ?? 0) - (ep[t - 1][d] ?? 0);
1729
+ allDeltas.push(delta);
1730
+ if (activeMap[ei][d]) activeDeltas.push(delta);
1731
+ }
1732
+ }
1733
+ const deltas = activeDeltas.length > 0 ? activeDeltas : allDeltas;
1734
+ const nUnique = motorUniqueCount[d];
1735
+ const discrete = nUnique <= DISCRETE_THRESHOLD;
1736
+ if (deltas.length === 0) {
1737
+ results.push({
1738
+ name: shortName(actionNames[d]),
1739
+ std: 0,
1740
+ maxAbs: 0,
1741
+ bins: new Array(binCount).fill(0),
1742
+ lo: 0,
1743
+ hi: 0,
1744
+ motorRange,
1745
+ inactive,
1746
+ discrete,
1747
+ });
1748
+ continue;
1749
+ }
1750
+ let sum = 0,
1751
+ maxAbsRaw = 0,
1752
+ loRaw = Infinity,
1753
+ hiRaw = -Infinity;
1754
+ for (const v of deltas) {
1755
+ sum += v;
1756
+ const a = Math.abs(v);
1757
+ if (a > maxAbsRaw) maxAbsRaw = a;
1758
+ if (v < loRaw) loRaw = v;
1759
+ if (v > hiRaw) hiRaw = v;
1760
+ }
1761
+ const mean = sum / deltas.length;
1762
+ let varSum = 0;
1763
+ for (const v of deltas) varSum += (v - mean) ** 2;
1764
+ const rawStd = Math.sqrt(varSum / deltas.length);
1765
+ const std = rawStd / motorRange;
1766
+ const maxAbs = maxAbsRaw / motorRange;
1767
+ const lo = loRaw / motorRange;
1768
+ const hi = hiRaw / motorRange;
1769
+ const range = hi - lo || 1;
1770
+ const binW = range / binCount;
1771
+ const bins = new Array(binCount).fill(0);
1772
+ for (const v of deltas) {
1773
+ const normV = v / motorRange;
1774
+ let b = Math.floor((normV - lo) / binW);
1775
+ if (b >= binCount) b = binCount - 1;
1776
+ bins[b]++;
1777
+ }
1778
+ results.push({
1779
+ name: shortName(actionNames[d]),
1780
+ std,
1781
+ maxAbs,
1782
+ bins,
1783
+ lo,
1784
+ hi,
1785
+ motorRange,
1786
+ inactive,
1787
+ discrete,
1788
+ });
1789
+ }
1790
+ return results;
1791
+ })();
1792
+
1793
+ // Aggregated autocorrelation: average per-episode ACFs
1794
+ const aggAutocorrelation: AggAutocorrelation | null = (() => {
1795
+ const maxLag = Math.min(
1796
+ 100,
1797
+ Math.floor(
1798
+ episodeActions.reduce(
1799
+ (min, e) => Math.min(min, e.actions.length),
1800
+ Infinity,
1801
+ ) / 2,
1802
+ ),
1803
+ );
1804
+ if (maxLag < 2) return null;
1805
+
1806
+ const avgAcf: number[][] = Array.from({ length: actionDim }, () =>
1807
+ new Array(maxLag).fill(0),
1808
+ );
1809
+ let epCount = 0;
1810
+
1811
+ for (const { actions: ep } of episodeActions) {
1812
+ if (ep.length < maxLag * 2) continue;
1813
+ epCount++;
1814
+ for (let d = 0; d < actionDim; d++) {
1815
+ const vals = ep.map((row) => row[d] ?? 0);
1816
+ const n = vals.length;
1817
+ const m = vals.reduce((a, b) => a + b, 0) / n;
1818
+ const centered = vals.map((v) => v - m);
1819
+ const vari = centered.reduce((a, v) => a + v * v, 0);
1820
+ if (vari === 0) continue;
1821
+ for (let lag = 1; lag <= maxLag; lag++) {
1822
+ let s = 0;
1823
+ for (let t = 0; t < n - lag; t++)
1824
+ s += centered[t] * centered[t + lag];
1825
+ avgAcf[d][lag - 1] += s / vari;
1826
+ }
1827
+ }
1828
+ }
1829
+
1830
+ if (epCount === 0) return null;
1831
+ for (let d = 0; d < actionDim; d++)
1832
+ for (let l = 0; l < maxLag; l++) avgAcf[d][l] /= epCount;
1833
+
1834
+ const shortKeys = actionNames.map(shortName);
1835
+ const chartData = Array.from({ length: maxLag }, (_, lag) => {
1836
+ const row: Record<string, number> = {
1837
+ lag: lag + 1,
1838
+ time: (lag + 1) / fps,
1839
+ };
1840
+ shortKeys.forEach((k, d) => {
1841
+ row[k] = avgAcf[d][lag];
1842
+ });
1843
+ return row;
1844
+ });
1845
+
1846
+ // Suggested chunk: median lag where ACF drops below 0.5
1847
+ const lags = avgAcf
1848
+ .map((acf) => {
1849
+ const i = acf.findIndex((v) => v < 0.5);
1850
+ return i >= 0 ? i + 1 : null;
1851
+ })
1852
+ .filter(Boolean) as number[];
1853
+ const suggestedChunk =
1854
+ lags.length > 0
1855
+ ? lags.sort((a, b) => a - b)[Math.floor(lags.length / 2)]
1856
+ : null;
1857
+
1858
+ return { chartData, suggestedChunk, shortKeys };
1859
+ })();
1860
+
1861
+ // Per-episode jerkiness: mean |Δa| across dimensions active in that episode, normalized by motor range
1862
+ const jerkyEpisodes: JerkyEpisode[] = episodeActions
1863
+ .map(({ index, actions: ep }, ei) => {
1864
+ let sum = 0,
1865
+ count = 0;
1866
+ for (let t = 1; t < ep.length; t++) {
1867
+ for (let d = 0; d < actionDim; d++) {
1868
+ if (!activeMap[ei][d]) continue; // skip motors inactive in this episode
1869
+ sum +=
1870
+ Math.abs((ep[t][d] ?? 0) - (ep[t - 1][d] ?? 0)) / motorRanges[d];
1871
+ count++;
1872
+ }
1873
+ }
1874
+ return { episodeIndex: index, meanAbsDelta: count > 0 ? sum / count : 0 };
1875
+ })
1876
+ .sort((a, b) => b.meanAbsDelta - a.meanAbsDelta);
1877
+
1878
+ // Speed distribution: all episode movement scores (not just lowest 10)
1879
+ const speedDistribution: SpeedDistEntry[] = movementScores.map((s) => ({
1880
+ episodeIndex: s.episodeIndex,
1881
+ speed: s.totalMovement,
1882
+ }));
1883
+
1884
+ // Aggregated state-action alignment across episodes
1885
+ const aggAlignment: AggAlignment | null = (() => {
1886
+ if (!stateKey || stateDim === 0) return null;
1887
+
1888
+ let sNms: unknown = stateEntry![1].names;
1889
+ while (typeof sNms === "object" && sNms !== null && !Array.isArray(sNms))
1890
+ sNms = Object.values(sNms)[0];
1891
+ const stateNames = Array.isArray(sNms)
1892
+ ? (sNms as string[])
1893
+ : Array.from({ length: stateDim }, (_, i) => `${i}`);
1894
+ const actionSuffixes = actionNames.map((n) => {
1895
+ const p = n.split(SERIES_NAME_DELIMITER);
1896
+ return p[p.length - 1];
1897
+ });
1898
+
1899
+ // Match pairs by suffix, fall back to index
1900
+ const pairs: [number, number][] = [];
1901
+ for (let ai = 0; ai < actionDim; ai++) {
1902
+ const si = stateNames.findIndex((s) => s === actionSuffixes[ai]);
1903
+ if (si >= 0) pairs.push([ai, si]);
1904
+ }
1905
+ if (pairs.length === 0) {
1906
+ const count = Math.min(actionDim, stateDim);
1907
+ for (let i = 0; i < count; i++) pairs.push([i, i]);
1908
+ }
1909
+ if (pairs.length === 0) return null;
1910
+
1911
+ const maxLag = 30;
1912
+ const numLags = 2 * maxLag + 1;
1913
+ const corrSums = pairs.map(() => new Float64Array(numLags));
1914
+ const corrCounts = pairs.map(() => new Uint32Array(numLags));
1915
+
1916
+ for (let ei = 0; ei < episodeActions.length; ei++) {
1917
+ const states = episodeStates[ei];
1918
+ if (!states) continue;
1919
+ const { actions } = episodeActions[ei];
1920
+ const n = Math.min(actions.length, states.length);
1921
+ if (n < 10) continue;
1922
+
1923
+ for (let pi = 0; pi < pairs.length; pi++) {
1924
+ const [ai, si] = pairs[pi];
1925
+ const aVals = actions.slice(0, n).map((r) => r[ai] ?? 0);
1926
+ const sDeltas = Array.from(
1927
+ { length: n - 1 },
1928
+ (_, t) => (states[t + 1][si] ?? 0) - (states[t][si] ?? 0),
1929
+ );
1930
+ const effN = Math.min(aVals.length, sDeltas.length);
1931
+ const aM = aVals.slice(0, effN).reduce((a, b) => a + b, 0) / effN;
1932
+ const sM = sDeltas.slice(0, effN).reduce((a, b) => a + b, 0) / effN;
1933
+
1934
+ for (let li = 0; li < numLags; li++) {
1935
+ const lag = -maxLag + li;
1936
+ let sum = 0,
1937
+ aV = 0,
1938
+ sV = 0;
1939
+ for (let t = 0; t < effN; t++) {
1940
+ const sIdx = t + lag;
1941
+ if (sIdx < 0 || sIdx >= sDeltas.length) continue;
1942
+ const a = aVals[t] - aM,
1943
+ s = sDeltas[sIdx] - sM;
1944
+ sum += a * s;
1945
+ aV += a * a;
1946
+ sV += s * s;
1947
+ }
1948
+ const d = Math.sqrt(aV * sV);
1949
+ if (d > 0) {
1950
+ corrSums[pi][li] += sum / d;
1951
+ corrCounts[pi][li]++;
1952
+ }
1953
+ }
1954
+ }
1955
+ }
1956
+
1957
+ const avgCorrs = pairs.map((_, pi) =>
1958
+ Array.from({ length: numLags }, (_, li) =>
1959
+ corrCounts[pi][li] > 0 ? corrSums[pi][li] / corrCounts[pi][li] : 0,
1960
+ ),
1961
+ );
1962
+
1963
+ const ccData = Array.from({ length: numLags }, (_, li) => {
1964
+ const lag = -maxLag + li;
1965
+ const vals = avgCorrs.map((pc) => pc[li]);
1966
+ return {
1967
+ lag,
1968
+ max: Math.max(...vals),
1969
+ mean: vals.reduce((a, b) => a + b, 0) / vals.length,
1970
+ min: Math.min(...vals),
1971
+ };
1972
+ });
1973
+
1974
+ let meanPeakLag = 0,
1975
+ meanPeakCorr = -Infinity;
1976
+ let maxPeakLag = 0,
1977
+ maxPeakCorr = -Infinity;
1978
+ let minPeakLag = 0,
1979
+ minPeakCorr = -Infinity;
1980
+ for (const row of ccData) {
1981
+ if (row.max > maxPeakCorr) {
1982
+ maxPeakCorr = row.max;
1983
+ maxPeakLag = row.lag;
1984
+ }
1985
+ if (row.mean > meanPeakCorr) {
1986
+ meanPeakCorr = row.mean;
1987
+ meanPeakLag = row.lag;
1988
+ }
1989
+ if (row.min > minPeakCorr) {
1990
+ minPeakCorr = row.min;
1991
+ minPeakLag = row.lag;
1992
+ }
1993
+ }
1994
+
1995
+ const perPairPeakLags = avgCorrs.map((pc) => {
1996
+ let best = -Infinity,
1997
+ bestLag = 0;
1998
+ for (let li = 0; li < pc.length; li++) {
1999
+ if (pc[li] > best) {
2000
+ best = pc[li];
2001
+ bestLag = -maxLag + li;
2002
+ }
2003
+ }
2004
+ return bestLag;
2005
+ });
2006
+
2007
+ return {
2008
+ ccData,
2009
+ meanPeakLag,
2010
+ meanPeakCorr,
2011
+ maxPeakLag,
2012
+ maxPeakCorr,
2013
+ minPeakLag,
2014
+ minPeakCorr,
2015
+ lagRangeMin: Math.min(...perPairPeakLags),
2016
+ lagRangeMax: Math.max(...perPairPeakLags),
2017
+ numPairs: pairs.length,
2018
+ };
2019
+ })();
2020
+
2021
+ return {
2022
+ actionNames,
2023
+ timeBins,
2024
+ variance,
2025
+ numEpisodes: episodeActions.length,
2026
+ lowMovementEpisodes,
2027
+ aggVelocity,
2028
+ aggAutocorrelation,
2029
+ speedDistribution,
2030
+ jerkyEpisodes,
2031
+ aggAlignment,
2032
+ };
2033
+ }
2034
+
2035
+ // Load only flatChartData for a specific episode (used by URDF viewer episode switching)
2036
+ export async function loadEpisodeFlatChartData(
2037
+ repoId: string,
2038
+ version: string,
2039
+ info: DatasetMetadata,
2040
+ episodeId: number,
2041
+ ): Promise<Record<string, number>[]> {
2042
+ const episodeMetadata = await loadEpisodeMetadataV3Simple(
2043
+ repoId,
2044
+ version,
2045
+ episodeId,
2046
+ );
2047
+ const { flatChartData } = await loadEpisodeDataV3(
2048
+ repoId,
2049
+ version,
2050
+ info,
2051
+ episodeMetadata,
2052
+ );
2053
+ return flatChartData;
2054
+ }
2055
+
2056
  // Safe wrapper for UI error display
2057
  export async function getEpisodeDataSafe(
2058
  org: string,
 
2062
  try {
2063
  const data = await getEpisodeData(org, dataset, episodeId);
2064
  return { data };
2065
+ } catch (err: unknown) {
2066
+ const message = err instanceof Error ? err.message : String(err);
2067
+ return { error: message || "Unknown error" };
 
 
2068
  }
2069
  }
src/app/[org]/[dataset]/[episode]/page.tsx CHANGED
@@ -27,7 +27,7 @@ export default async function EpisodePage({
27
  const { data, error } = await getEpisodeDataSafe(org, dataset, episodeNumber);
28
  return (
29
  <Suspense fallback={null}>
30
- <EpisodeViewer data={data} error={error} />
31
  </Suspense>
32
  );
33
  }
 
27
  const { data, error } = await getEpisodeDataSafe(org, dataset, episodeNumber);
28
  return (
29
  <Suspense fallback={null}>
30
+ <EpisodeViewer data={data} error={error} org={org} dataset={dataset} />
31
  </Suspense>
32
  );
33
  }
src/app/explore/page.tsx CHANGED
@@ -2,7 +2,7 @@ import React from "react";
2
  import ExploreGrid from "./explore-grid";
3
  import { fetchJson, formatStringWithVars } from "@/utils/parquetUtils";
4
  import { getDatasetVersion, buildVersionedUrl } from "@/utils/versionUtils";
5
- import type { DatasetMetadata } from "@/types";
6
 
7
  export default async function ExplorePage({
8
  searchParams,
@@ -10,7 +10,7 @@ export default async function ExplorePage({
10
  searchParams: Promise<{ p?: string }>;
11
  }) {
12
  const params = await searchParams;
13
- let datasets: any[] = [];
14
  let currentPage = 1;
15
  let totalPages = 1;
16
  try {
@@ -40,7 +40,7 @@ export default async function ExplorePage({
40
  // Fetch episode 0 data for each dataset
41
  const datasetWithVideos = (
42
  await Promise.all(
43
- datasets.map(async (ds: any) => {
44
  try {
45
  const [org, dataset] = ds.id.split("/");
46
  const repoId = `${org}/${dataset}`;
 
2
  import ExploreGrid from "./explore-grid";
3
  import { fetchJson, formatStringWithVars } from "@/utils/parquetUtils";
4
  import { getDatasetVersion, buildVersionedUrl } from "@/utils/versionUtils";
5
+ import type { DatasetMetadata } from "@/utils/parquetUtils";
6
 
7
  export default async function ExplorePage({
8
  searchParams,
 
10
  searchParams: Promise<{ p?: string }>;
11
  }) {
12
  const params = await searchParams;
13
+ let datasets: { id: string }[] = [];
14
  let currentPage = 1;
15
  let totalPages = 1;
16
  try {
 
40
  // Fetch episode 0 data for each dataset
41
  const datasetWithVideos = (
42
  await Promise.all(
43
+ datasets.map(async (ds) => {
44
  try {
45
  const [org, dataset] = ds.id.split("/");
46
  const repoId = `${org}/${dataset}`;