mishig HF Staff commited on
Commit
1cce69a
·
verified ·
1 Parent(s): 85743ff

Sync from GitHub via hub-sync

Browse files
CLAUDE.md CHANGED
@@ -58,6 +58,9 @@ Three versions are supported. Version is detected from `meta/info.json` → `cod
58
  - Integer columns from parquet come out as **BigInt** — always use `bigIntToNumber()` from `src/utils/typeGuards.ts`
59
  - Row-range selection: `dataset_from_index` / `dataset_to_index` allow reading only the episode's rows from a shared parquet file
60
  - Fallback format uses numeric keys `"0"`.."9"` when column names are unavailable
 
 
 
61
 
62
  ### v2.x path construction
63
 
@@ -112,5 +115,26 @@ Built by `buildVersionedUrl(repoId, version, path)`. The `version` param is acce
112
 
113
  ## Excluded columns (not shown in charts)
114
 
115
- - v2.x: `timestamp`, `frame_index`, `episode_index`, `index`, `task_index`
116
- - v3.0: `index`, `task_index`, `episode_index`, `frame_index`, `next.done`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  - Integer columns from parquet come out as **BigInt** — always use `bigIntToNumber()` from `src/utils/typeGuards.ts`
59
  - Row-range selection: `dataset_from_index` / `dataset_to_index` allow reading only the episode's rows from a shared parquet file
60
  - Fallback format uses numeric keys `"0"`.."9"` when column names are unavailable
61
+ - Episode metadata can span **multiple chunks** (when episode count exceeds `chunks_size`). Always walk via the `iterateEpisodeMetadataFilesV3(repoId, version)` async generator in `fetch-data.ts` — it advances chunk-000 → chunk-001 → … and stops on the first missing `file-000`. Never hardcode `chunk-000`.
62
+ - Multi-task episodes: episode-metadata rows carry a `tasks` field (`list[str]`) — prefer it over the legacy single `task_index` lookup. `EpisodeMetadataV3.tasks?: string[]` exposes it.
63
+ - `meta/tasks.parquet` lookup: rows are **not** ordered by `task_index`, and the task string lives in a named pandas index (`__index_level_0__`). Always filter by the `task_index` **column** (`row.task_index === taskIndexNum`), never by row position.
64
 
65
  ### v2.x path construction
66
 
 
115
 
116
  ## Excluded columns (not shown in charts)
117
 
118
+ Reserved/bookkeeping columns from lerobot see `EXCLUDED_COLUMNS` in `src/utils/constants.ts`:
119
+
120
+ - v2.x: `timestamp`, `frame_index`, `episode_index`, `index`, `task_index`, `next.reward`, `next.done`, `next.truncated`
121
+ - v3.0: `index`, `task_index`, `episode_index`, `frame_index`, `next.reward`, `next.done`, `next.truncated`, `subtask_index`
122
+
123
+ ## 3D URDF viewer (`src/components/urdf-viewer.tsx`)
124
+
125
+ - URDFs and meshes are hosted in the HF bucket `lerobot/robot-urdfs` — base URL `https://huggingface.co/buckets/lerobot/robot-urdfs/resolve` (no `/main` segment; buckets are unbranched). Override with `NEXT_PUBLIC_URDF_BASE_URL` for local development.
126
+ - Asset layout under the bucket: `g1/`, `openarm/`, `so101/` (both SO-100 and SO-101 live here).
127
+ - **URDFLoader gotcha**: after our `loadMeshCb` returns, `URDFLoader.js` does `if (obj instanceof THREE.Mesh) obj.material = <urdf-material>`, overwriting any material we set. Workaround: wrap the loaded mesh in a `THREE.Group` so the `instanceof Mesh` check fails. DAE returns a Group already; STL must be wrapped explicitly.
128
+ - **STLLoader event ordering**: `manager.itemEnd(url)` fires _before_ the user `onLoad` callback, so `manager.onLoad` can fire before meshes are attached to the robot tree. Defer post-load work (auto-fit camera, shadow flags) with `setTimeout(..., 0)`. Don't try to rebuild materials in `manager.onLoad` — pick the archetype color directly inside `loadMeshCb`.
129
+ - **OpenArm DAE files ship 23 stray `PointLight`s** that drown out scene lighting. Strip non-`AmbientLight` lights from `collada.scene` before adding it to the robot.
130
+ - Scene setup: `<Canvas shadows>` with `ACESFilmicToneMapping` (exposure 0.9), 3-point directional + ambient lights, `<Environment preset="studio" background={false} />`, `<color attach="background" args={["#1a2433"]} />`. `<OrbitControls makeDefault />` is required so `useThree().controls` exposes the controls for auto-fit.
131
+
132
+ ## Design system
133
+
134
+ CSS tokens in `src/app/globals.css` (Tailwind v4 `@theme inline`):
135
+
136
+ - Surfaces: `--bg #0a0e17`, `--surface-0`, `--surface-1`, `--surface-2`
137
+ - Text: `--text-primary`, `--text-muted`, `--text-faint`
138
+ - Accent: `--accent #38bdf8` (cyan) — primary interactive color across UI
139
+ - Helpers: `.panel`, `.panel-raised`, `.tabular` (tabular-nums)
140
+ - **Color semantics**: cyan = primary/active, orange (`orange-400/500`) is reserved for **flagged-episode** UI only — don't reuse it for generic accents.
README.md CHANGED
@@ -7,6 +7,10 @@ sdk: docker
7
  app_port: 7860
8
  pinned: false
9
  license: apache-2.0
 
 
 
 
10
  ---
11
 
12
  # LeRobot Dataset Visualizer
 
7
  app_port: 7860
8
  pinned: false
9
  license: apache-2.0
10
+ hf_oauth: true
11
+ hf_oauth_scopes:
12
+ - read-repos
13
+ hf_oauth_expiration_minutes: 480
14
  ---
15
 
16
  # LeRobot Dataset Visualizer
bun.lock CHANGED
@@ -5,6 +5,7 @@
5
  "": {
6
  "name": "lerobot-viewer",
7
  "dependencies": {
 
8
  "@react-three/drei": "^10.7.7",
9
  "@react-three/fiber": "^9.5.0",
10
  "hyparquet": "^1.12.1",
@@ -63,6 +64,10 @@
63
 
64
  "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
65
 
 
 
 
 
66
  "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
67
 
68
  "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
@@ -325,6 +330,8 @@
325
 
326
  "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
327
 
 
 
328
  "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
329
 
330
  "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -387,6 +394,8 @@
387
 
388
  "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
389
 
 
 
390
  "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
391
 
392
  "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -621,6 +630,8 @@
621
 
622
  "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
623
 
 
 
624
  "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
625
 
626
  "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
@@ -877,6 +888,8 @@
877
 
878
  "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
879
 
 
 
880
  "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
881
 
882
  "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
@@ -889,6 +902,8 @@
889
 
890
  "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
891
 
 
 
892
  "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
893
 
894
  "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
@@ -1021,6 +1036,8 @@
1021
 
1022
  "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
1023
 
 
 
1024
  "three-stdlib/fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="],
1025
 
1026
  "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=="],
 
5
  "": {
6
  "name": "lerobot-viewer",
7
  "dependencies": {
8
+ "@huggingface/hub": "^2.11.0",
9
  "@react-three/drei": "^10.7.7",
10
  "@react-three/fiber": "^9.5.0",
11
  "hyparquet": "^1.12.1",
 
64
 
65
  "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
66
 
67
+ "@huggingface/hub": ["@huggingface/hub@2.11.0", "", { "dependencies": { "@huggingface/tasks": "^0.19.90" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ=="],
68
+
69
+ "@huggingface/tasks": ["@huggingface/tasks@0.19.90", "", {}, "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA=="],
70
+
71
  "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
72
 
73
  "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
 
330
 
331
  "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
332
 
333
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
334
+
335
  "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
336
 
337
  "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 
394
 
395
  "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
396
 
397
+ "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="],
398
+
399
  "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
400
 
401
  "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
 
630
 
631
  "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
632
 
633
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
634
+
635
  "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
636
 
637
  "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 
888
 
889
  "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
890
 
891
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
892
+
893
  "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
894
 
895
  "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
 
902
 
903
  "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
904
 
905
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
906
+
907
  "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
908
 
909
  "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
 
1036
 
1037
  "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
1038
 
1039
+ "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
1040
+
1041
  "three-stdlib/fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="],
1042
 
1043
  "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=="],
package.json CHANGED
@@ -15,6 +15,7 @@
15
  "validate": "bun run type-check && bun run lint && bun run format:check && bun test"
16
  },
17
  "dependencies": {
 
18
  "@react-three/drei": "^10.7.7",
19
  "@react-three/fiber": "^9.5.0",
20
  "hyparquet": "^1.12.1",
 
15
  "validate": "bun run type-check && bun run lint && bun run format:check && bun test"
16
  },
17
  "dependencies": {
18
+ "@huggingface/hub": "^2.11.0",
19
  "@react-three/drei": "^10.7.7",
20
  "@react-three/fiber": "^9.5.0",
21
  "hyparquet": "^1.12.1",
src/app/api/auth/session/route.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ // Mirror of Hugging Face's default OAuth token lifetime so the cookie expires
4
+ // alongside the upstream token. README sets hf_oauth_expiration_minutes: 480.
5
+ const COOKIE_NAME = "hf_access_token";
6
+ const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 8;
7
+
8
+ export const runtime = "nodejs";
9
+ export const dynamic = "force-dynamic";
10
+
11
+ // POST sets the auth cookie from the Authorization header sent by the client
12
+ // after a successful OAuth flow. The cookie is HttpOnly so the access token
13
+ // is never exposed to JS — only the proxy route reads it.
14
+ export async function POST(req: NextRequest) {
15
+ const auth = req.headers.get("authorization");
16
+ if (!auth?.toLowerCase().startsWith("bearer ")) {
17
+ return new NextResponse("Missing bearer token", { status: 400 });
18
+ }
19
+ const token = auth.slice("bearer ".length).trim();
20
+ if (!token) {
21
+ return new NextResponse("Empty token", { status: 400 });
22
+ }
23
+
24
+ const res = new NextResponse(null, { status: 204 });
25
+ res.cookies.set(COOKIE_NAME, token, {
26
+ httpOnly: true,
27
+ secure: process.env.NODE_ENV === "production",
28
+ sameSite: "lax",
29
+ path: "/",
30
+ maxAge: COOKIE_MAX_AGE_SECONDS,
31
+ });
32
+ return res;
33
+ }
34
+
35
+ export async function DELETE() {
36
+ const res = new NextResponse(null, { status: 204 });
37
+ res.cookies.delete(COOKIE_NAME);
38
+ return res;
39
+ }
src/app/api/proxy/[...path]/route.ts ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest } from "next/server";
2
+
3
+ // Same-origin streaming proxy for huggingface.co. The native <video> element
4
+ // can't carry an Authorization header, so we proxy through this route, which
5
+ // pulls the user's HF access token from the HttpOnly `hf_access_token` cookie
6
+ // (set by /api/auth/session after OAuth) and forwards Range requests upstream.
7
+ //
8
+ // Public datasets work too — the upstream simply ignores the bearer token.
9
+ //
10
+ // Allowed path prefixes are constrained so this can't be turned into an open
11
+ // proxy for arbitrary huggingface.co URLs (e.g. user profile, billing pages).
12
+
13
+ const HF_HOST = "https://huggingface.co";
14
+ const COOKIE_NAME = "hf_access_token";
15
+ const ALLOWED_PREFIXES = ["datasets/", "buckets/"];
16
+
17
+ export const runtime = "nodejs";
18
+ export const dynamic = "force-dynamic";
19
+
20
+ const FORWARD_REQUEST_HEADERS = [
21
+ "range",
22
+ "if-modified-since",
23
+ "if-none-match",
24
+ "accept",
25
+ "accept-encoding",
26
+ ];
27
+
28
+ const FORWARD_RESPONSE_HEADERS = [
29
+ "content-type",
30
+ "content-length",
31
+ "content-range",
32
+ "accept-ranges",
33
+ "etag",
34
+ "last-modified",
35
+ "cache-control",
36
+ ];
37
+
38
+ export async function GET(
39
+ req: NextRequest,
40
+ ctx: { params: Promise<{ path: string[] }> },
41
+ ) {
42
+ const { path } = await ctx.params;
43
+ const subPath = path.join("/");
44
+
45
+ if (!ALLOWED_PREFIXES.some((p) => subPath.startsWith(p))) {
46
+ return new Response("Forbidden", { status: 403 });
47
+ }
48
+
49
+ const upstreamUrl = new URL(`${HF_HOST}/${subPath}`);
50
+ for (const [k, v] of req.nextUrl.searchParams) {
51
+ upstreamUrl.searchParams.set(k, v);
52
+ }
53
+
54
+ const headers = new Headers();
55
+ const token = req.cookies.get(COOKIE_NAME)?.value;
56
+ if (token) headers.set("authorization", `Bearer ${token}`);
57
+ for (const h of FORWARD_REQUEST_HEADERS) {
58
+ const v = req.headers.get(h);
59
+ if (v) headers.set(h, v);
60
+ }
61
+
62
+ const upstream = await fetch(upstreamUrl, {
63
+ method: "GET",
64
+ headers,
65
+ redirect: "follow",
66
+ cache: "no-store",
67
+ });
68
+
69
+ const respHeaders = new Headers();
70
+ for (const h of FORWARD_RESPONSE_HEADERS) {
71
+ const v = upstream.headers.get(h);
72
+ if (v) respHeaders.set(h, v);
73
+ }
74
+
75
+ return new Response(upstream.body, {
76
+ status: upstream.status,
77
+ statusText: upstream.statusText,
78
+ headers: respHeaders,
79
+ });
80
+ }
81
+
82
+ export async function HEAD(
83
+ req: NextRequest,
84
+ ctx: { params: Promise<{ path: string[] }> },
85
+ ) {
86
+ const { path } = await ctx.params;
87
+ const subPath = path.join("/");
88
+
89
+ if (!ALLOWED_PREFIXES.some((p) => subPath.startsWith(p))) {
90
+ return new Response(null, { status: 403 });
91
+ }
92
+
93
+ const upstreamUrl = new URL(`${HF_HOST}/${subPath}`);
94
+ for (const [k, v] of req.nextUrl.searchParams) {
95
+ upstreamUrl.searchParams.set(k, v);
96
+ }
97
+
98
+ const headers = new Headers();
99
+ const token = req.cookies.get(COOKIE_NAME)?.value;
100
+ if (token) headers.set("authorization", `Bearer ${token}`);
101
+
102
+ const upstream = await fetch(upstreamUrl, {
103
+ method: "HEAD",
104
+ headers,
105
+ redirect: "follow",
106
+ cache: "no-store",
107
+ });
108
+
109
+ const respHeaders = new Headers();
110
+ for (const h of FORWARD_RESPONSE_HEADERS) {
111
+ const v = upstream.headers.get(h);
112
+ if (v) respHeaders.set(h, v);
113
+ }
114
+
115
+ return new Response(null, {
116
+ status: upstream.status,
117
+ statusText: upstream.statusText,
118
+ headers: respHeaders,
119
+ });
120
+ }
src/app/layout.tsx CHANGED
@@ -1,6 +1,8 @@
1
  import type { Metadata } from "next";
2
  import { Inter } from "next/font/google";
3
  import "./globals.css";
 
 
4
 
5
  const inter = Inter({ subsets: ["latin"] });
6
 
@@ -16,7 +18,14 @@ export default function RootLayout({
16
  }) {
17
  return (
18
  <html lang="en">
19
- <body className={inter.className}>{children}</body>
 
 
 
 
 
 
 
20
  </html>
21
  );
22
  }
 
1
  import type { Metadata } from "next";
2
  import { Inter } from "next/font/google";
3
  import "./globals.css";
4
+ import { AuthProvider } from "@/context/auth-context";
5
+ import HfAuthButton from "@/components/hf-auth-button";
6
 
7
  const inter = Inter({ subsets: ["latin"] });
8
 
 
18
  }) {
19
  return (
20
  <html lang="en">
21
+ <body className={inter.className}>
22
+ <AuthProvider>
23
+ <div className="fixed top-3 right-3 z-50">
24
+ <HfAuthButton />
25
+ </div>
26
+ {children}
27
+ </AuthProvider>
28
+ </body>
29
  </html>
30
  );
31
  }
src/components/hf-auth-button.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useAuth } from "@/context/auth-context";
5
+
6
+ const SIGNIN_BADGE_URL =
7
+ "https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-md-dark.svg";
8
+
9
+ export default function HfAuthButton() {
10
+ const { oauth, isAuthAvailable, signIn, signOut } = useAuth();
11
+
12
+ if (!isAuthAvailable) return null;
13
+
14
+ if (oauth) {
15
+ const name =
16
+ oauth.userInfo?.preferred_username ?? oauth.userInfo?.name ?? "signed in";
17
+ const avatar = oauth.userInfo?.picture;
18
+ return (
19
+ <div className="flex items-center gap-2 panel-raised bg-[var(--surface-0)]/85 backdrop-blur px-2 py-1 text-xs text-slate-300">
20
+ {avatar && (
21
+ // eslint-disable-next-line @next/next/no-img-element
22
+ <img
23
+ src={avatar}
24
+ alt=""
25
+ width={18}
26
+ height={18}
27
+ className="rounded-full"
28
+ />
29
+ )}
30
+ <span className="tabular max-w-[10rem] truncate">{name}</span>
31
+ <button
32
+ onClick={signOut}
33
+ className="rounded-md px-2 py-0.5 text-[10px] uppercase tracking-wide text-slate-400 hover:text-slate-100 hover:bg-white/5 transition-colors"
34
+ title="Sign out of Hugging Face"
35
+ >
36
+ Sign out
37
+ </button>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <button
44
+ onClick={signIn}
45
+ title="Sign in to access your private datasets"
46
+ className="flex items-center gap-2 rounded-md transition-opacity hover:opacity-90"
47
+ >
48
+ {/* eslint-disable-next-line @next/next/no-img-element */}
49
+ <img
50
+ src={SIGNIN_BADGE_URL}
51
+ alt="Sign in with Hugging Face"
52
+ height={32}
53
+ className="h-8 w-auto"
54
+ />
55
+ <span className="text-xs text-slate-300">to access private datasets</span>
56
+ </button>
57
+ );
58
+ }
src/components/simple-videos-player.tsx CHANGED
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useCallback } from "react";
4
  import { useTime } from "../context/time-context";
5
  import { FaExpand, FaCompress, FaTimes, FaEye } from "react-icons/fa";
6
  import type { VideoInfo } from "@/types";
 
7
 
8
  const THRESHOLDS = {
9
  VIDEO_SYNC_TOLERANCE: 0.2,
@@ -316,7 +317,7 @@ export const SimpleVideosPlayer = ({
316
  isFirstVisible ? makeTimeUpdateHandler(idx) : undefined
317
  }
318
  >
319
- <source src={info.url} type="video/mp4" />
320
  Your browser does not support the video tag.
321
  </video>
322
  </div>
 
4
  import { useTime } from "../context/time-context";
5
  import { FaExpand, FaCompress, FaTimes, FaEye } from "react-icons/fa";
6
  import type { VideoInfo } from "@/types";
7
+ import { proxyHfUrl } from "@/utils/auth";
8
 
9
  const THRESHOLDS = {
10
  VIDEO_SYNC_TOLERANCE: 0.2,
 
317
  isFirstVisible ? makeTimeUpdateHandler(idx) : undefined
318
  }
319
  >
320
+ <source src={proxyHfUrl(info.url)} type="video/mp4" />
321
  Your browser does not support the video tag.
322
  </video>
323
  </div>
src/context/auth-context.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ useCallback,
9
+ } from "react";
10
+ import {
11
+ oauthLoginUrl,
12
+ oauthHandleRedirectIfPresent,
13
+ type OAuthResult,
14
+ } from "@huggingface/hub";
15
+ import { AUTH_STORAGE_KEY } from "@/utils/auth";
16
+
17
+ interface HfSpaceVariables {
18
+ OAUTH_CLIENT_ID?: string;
19
+ OAUTH_SCOPES?: string;
20
+ }
21
+
22
+ interface HfWindow extends Window {
23
+ huggingface?: { variables?: HfSpaceVariables };
24
+ }
25
+
26
+ interface AuthContextValue {
27
+ oauth: OAuthResult | null;
28
+ // Whether OAuth is configured for this deployment (i.e. running on an HF
29
+ // Space with hf_oauth enabled). When false, the button hides itself.
30
+ isAuthAvailable: boolean;
31
+ signIn: () => Promise<void>;
32
+ signOut: () => void;
33
+ }
34
+
35
+ const AuthContext = createContext<AuthContextValue>({
36
+ oauth: null,
37
+ isAuthAvailable: false,
38
+ signIn: async () => {},
39
+ signOut: () => {},
40
+ });
41
+
42
+ // Mirror the access token into an HttpOnly cookie so the same-origin
43
+ // /api/proxy route can attach it to <video> requests, which can't carry an
44
+ // Authorization header from JS.
45
+ async function setSessionCookie(accessToken: string): Promise<void> {
46
+ try {
47
+ await fetch("/api/auth/session", {
48
+ method: "POST",
49
+ headers: { Authorization: `Bearer ${accessToken}` },
50
+ });
51
+ } catch (err) {
52
+ console.error("Failed to set session cookie", err);
53
+ }
54
+ }
55
+
56
+ async function clearSessionCookie(): Promise<void> {
57
+ try {
58
+ await fetch("/api/auth/session", { method: "DELETE" });
59
+ } catch (err) {
60
+ console.error("Failed to clear session cookie", err);
61
+ }
62
+ }
63
+
64
+ function isExpired(result: OAuthResult): boolean {
65
+ const exp = result.accessTokenExpiresAt;
66
+ if (!exp) return false;
67
+ const expDate = exp instanceof Date ? exp : new Date(exp);
68
+ return expDate.getTime() <= Date.now();
69
+ }
70
+
71
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
72
+ const [oauth, setOauth] = useState<OAuthResult | null>(null);
73
+ const [isAuthAvailable, setIsAuthAvailable] = useState(false);
74
+
75
+ useEffect(() => {
76
+ const w = window as HfWindow;
77
+ const available = !!w.huggingface?.variables?.OAUTH_CLIENT_ID;
78
+ setIsAuthAvailable(available);
79
+ if (!available) return;
80
+
81
+ const stored = window.localStorage.getItem(AUTH_STORAGE_KEY);
82
+ if (stored) {
83
+ try {
84
+ const parsed = JSON.parse(stored) as OAuthResult;
85
+ if (isExpired(parsed)) {
86
+ window.localStorage.removeItem(AUTH_STORAGE_KEY);
87
+ clearSessionCookie();
88
+ } else {
89
+ setOauth(parsed);
90
+ setSessionCookie(parsed.accessToken);
91
+ return;
92
+ }
93
+ } catch {
94
+ window.localStorage.removeItem(AUTH_STORAGE_KEY);
95
+ }
96
+ }
97
+
98
+ oauthHandleRedirectIfPresent()
99
+ .then((result) => {
100
+ if (result) {
101
+ window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(result));
102
+ setOauth(result);
103
+ setSessionCookie(result.accessToken);
104
+ }
105
+ })
106
+ .catch((err) => {
107
+ console.error("OAuth redirect handling failed", err);
108
+ });
109
+ }, []);
110
+
111
+ const signIn = useCallback(async () => {
112
+ const w = window as HfWindow;
113
+ const scopes = w.huggingface?.variables?.OAUTH_SCOPES;
114
+ const url = await oauthLoginUrl(scopes ? { scopes } : {});
115
+ window.location.href = url + "&prompt=consent";
116
+ }, []);
117
+
118
+ const signOut = useCallback(() => {
119
+ window.localStorage.removeItem(AUTH_STORAGE_KEY);
120
+ setOauth(null);
121
+ clearSessionCookie();
122
+ // Strip ?code=... left in the URL by the OAuth redirect, if any.
123
+ const cleanUrl = window.location.href.replace(/\?.*$/, "");
124
+ if (cleanUrl !== window.location.href) {
125
+ window.history.replaceState(null, "", cleanUrl);
126
+ }
127
+ }, []);
128
+
129
+ return (
130
+ <AuthContext.Provider value={{ oauth, isAuthAvailable, signIn, signOut }}>
131
+ {children}
132
+ </AuthContext.Provider>
133
+ );
134
+ }
135
+
136
+ export function useAuth() {
137
+ return useContext(AuthContext);
138
+ }
src/utils/auth.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Client-side helpers for the HF OAuth flow. The token is owned by
2
+ // AuthProvider (src/context/auth-context.tsx); these read-only helpers exist
3
+ // so non-React fetch utilities can attach `Authorization: Bearer <token>`
4
+ // without going through React.
5
+
6
+ const STORAGE_KEY = "lerobot-viz-oauth";
7
+
8
+ export function getAuthToken(): string | null {
9
+ if (typeof window === "undefined") return null;
10
+ try {
11
+ const stored = window.localStorage.getItem(STORAGE_KEY);
12
+ if (!stored) return null;
13
+ const parsed = JSON.parse(stored) as { accessToken?: string };
14
+ return parsed.accessToken ?? null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export function authHeaders(): Record<string, string> {
21
+ const token = getAuthToken();
22
+ return token ? { Authorization: `Bearer ${token}` } : {};
23
+ }
24
+
25
+ export const AUTH_STORAGE_KEY = STORAGE_KEY;
26
+
27
+ // Native <video> elements can't carry an Authorization header. To play videos
28
+ // from private datasets, we route them through our same-origin /api/proxy
29
+ // endpoint, which reads the access token from an HttpOnly cookie set during
30
+ // sign-in and forwards the request to huggingface.co. Returns the original
31
+ // URL when running server-side or when the user is not signed in.
32
+ export function proxyHfUrl(url: string): string {
33
+ if (typeof window === "undefined") return url;
34
+ if (!getAuthToken()) return url;
35
+ try {
36
+ const parsed = new URL(url);
37
+ if (parsed.hostname !== "huggingface.co") return url;
38
+ return `/api/proxy${parsed.pathname}${parsed.search}`;
39
+ } catch {
40
+ return url;
41
+ }
42
+ }
src/utils/parquetUtils.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
  parquetReadObjects,
6
  type AsyncBuffer,
7
  } from "hyparquet";
 
8
 
9
  export interface DatasetMetadata {
10
  codebase_version: string;
@@ -31,7 +32,10 @@ export interface DatasetMetadata {
31
  }
32
 
33
  export async function fetchJson<T>(url: string): Promise<T> {
34
- const res = await fetch(url, { cache: "no-store" });
 
 
 
35
  if (!res.ok) {
36
  throw new Error(
37
  `Failed to fetch JSON ${url}: ${res.status} ${res.statusText}`,
@@ -58,7 +62,7 @@ export async function fetchParquetFile(url: string): Promise<ParquetFile> {
58
 
59
  const file = await asyncBufferFromUrl({
60
  url,
61
- requestInit: { cache: "no-store" },
62
  });
63
  const wrapped = cachedAsyncBuffer(file);
64
  parquetFileCache.set(url, wrapped);
 
5
  parquetReadObjects,
6
  type AsyncBuffer,
7
  } from "hyparquet";
8
+ import { authHeaders } from "./auth";
9
 
10
  export interface DatasetMetadata {
11
  codebase_version: string;
 
32
  }
33
 
34
  export async function fetchJson<T>(url: string): Promise<T> {
35
+ const res = await fetch(url, {
36
+ cache: "no-store",
37
+ headers: authHeaders(),
38
+ });
39
  if (!res.ok) {
40
  throw new Error(
41
  `Failed to fetch JSON ${url}: ${res.status} ${res.statusText}`,
 
62
 
63
  const file = await asyncBufferFromUrl({
64
  url,
65
+ requestInit: { cache: "no-store", headers: authHeaders() },
66
  });
67
  const wrapped = cachedAsyncBuffer(file);
68
  parquetFileCache.set(url, wrapped);
src/utils/versionUtils.ts CHANGED
@@ -2,6 +2,8 @@
2
  * Utility functions for checking dataset version compatibility
3
  */
4
 
 
 
5
  const DATASET_URL =
6
  process.env.DATASET_URL || "https://huggingface.co/datasets";
7
 
@@ -82,6 +84,7 @@ export async function getDatasetInfo(repoId: string): Promise<DatasetInfo> {
82
  method: "GET",
83
  cache: "no-store",
84
  signal: controller.signal,
 
85
  });
86
 
87
  clearTimeout(timeoutId);
 
2
  * Utility functions for checking dataset version compatibility
3
  */
4
 
5
+ import { authHeaders } from "./auth";
6
+
7
  const DATASET_URL =
8
  process.env.DATASET_URL || "https://huggingface.co/datasets";
9
 
 
84
  method: "GET",
85
  cache: "no-store",
86
  signal: controller.signal,
87
+ headers: authHeaders(),
88
  });
89
 
90
  clearTimeout(timeoutId);