# `@huggingface/lerobot`

用于解析托管在 Hugging Face Hub 上的 [LeRobot](https://github.com/huggingface/lerobot) 机器人数据集的解析器。

所有内容均通过范围 HTTP 请求读取，因此可在浏览器和 Node 中直接运行，且无需下载整个数据集即可回答相关问题。

## 为什么

LeRobot 数据集存在两种布局。在 `v2.0` / `v2.1` 中，每个 episode 都有独立的视频和 parquet 文件，且 episode 索引是一个 JSON Lines 文件。在 `v3.0` 中，episode 被拼接进共享文件，索引为 parquet 格式，因此一个 episode 是较大文件内的一个字节范围和一个时间范围。

该包通过统一的 `LeRobotEpisode` 结构隐藏了这一差异。

## 安装

```bash
npm install @huggingface/lerobot
```

[hyparquet](https://github.com/hyparam/hyparquet) 随附提供，用于读取 `v3.0` 剧集索引和帧数据。[fzstd](https://github.com/101arrowz/fzstd) 在 hyparquet 内置的 SNAPPY 之外增加了 ZSTD 支持。两者均按需导入，因此仅需调用 `info()` 或列出 `v2` 剧集的调用方永远不会加载它们。

## 用法

```ts
import { LeRobotDataset } from "@huggingface/lerobot";

const dataset = new LeRobotDataset("lerobot/svla_so101_pickplace");

const info = await dataset.info();
// {
//   codebaseVersion: "v3.0",
//   robotType: "so100_follower",
//   fps: 30,
//   totalEpisodes: 50,
//   totalFrames: 11939,
//   cameras: [{ key: "observation.images.up", width: 640, height: 480, codec: "av1" }, ...],
//   joints: { "observation.state": ["shoulder_pan.pos", "shoulder_lift.pos", ...] },
//   ...
// }

const episodes = await dataset.episodes({ limit: 10 });
// [
//   {
//     index: 0,
//     length: 303,
//     durationSec: 10.1,
//     tasks: ["pink lego brick into the transparent box"],
//     videos: [{ cameraKey: "observation.images.up", url: "https://...", fromSec: 0, toSec: 10.1 }, ...],
//     data: { url: "https://.../data/chunk-000/file-000.parquet", fromRow: 0, toRow: 303 },
//   },
//   ...
// ]

const episode = await dataset.episode(42);
// The episode whose `index` is 42, or undefined when there is none
```

`offset` 统计的是索引中的位置，只有当数据集按 0、1、2、... 对剧集编号时，这些位置才与剧集索引一致。有些数据集从 10 开始编号，或跳过了已删除的剧集，因此请使用 `episode(index)` 来打开指定的剧集。对于按 0、1、2、... 编号的数据集，其开销与 `episodes({ offset: index, limit: 1 })` 相同。

`videos[].fromSec` / `toSec` 是剧集在 `url` 中的偏移量。在 `v3.0` 中，多个剧集共享同一个文件，因此需定位到 `fromSec` 并在 `toSec` 处停止；而在 `v2.x` 中，每个剧集都有独立文件，该范围直接覆盖整个文件。

`data` 指向该 episode 的帧行，供希望自行绘制 `observation.state` 或 `action` 的调用方使用。`fromRow` 为包含边界，`toRow` 为排除边界；两者均为 `data.url` 内部的行偏移量，而非跨数据集的索引。对于 v3，当页面从数据文件中间开始加载时，列出 episodes 还会读取该文件的 parquet 页脚以定位其起始位置。当该文件无法读取或与 episode 元数据不一致时，`data` 将不存在。

在分页浏览 episode 时，只需保留一个 `LeRobotDataset`：与 `info()` 类似，它会记住已遍历的 `v3.0` 索引文件，因此后续的翻页或 `episode()` 查询会直接定位到包含该数据的文件。

### 固定修订版本

在结果被存储或缓存到任何位置时，始终传入 commit sha，这样后续的 push 操作就无法更改你解析的内容：

```ts
const dataset = new LeRobotDataset("lerobot/svla_so101_pickplace", {
	revision: "f641879e22172be7e8161d5e6c1503c2d2feb657",
});
```

### 私有和受控数据集

传入您自己的 `fetch` 以添加凭据：

```ts
const dataset = new LeRobotDataset("my-org/private-dataset", {
	additionalFetchHeaders: { Authorization: `Bearer ${accessToken}` },
});
```

在 `huggingface.co` 的浏览器中，会话 Cookie 就足够了：

```ts
const dataset = new LeRobotDataset(repoId, {
	fetch: (url, init) => fetch(url, { ...init, credentials: "same-origin" }),
});
```

## 不获取数据的情况下进行解析

`parseInfo` 是纯函数，适用于已经拥有字节数据的调用方：

```ts
import { parseInfo } from "@huggingface/lerobot";

const info = parseInfo(await readFile("meta/info.json", "utf8"));
```

对于任何无法使用的信息文件，包括 LFS 指针、截断的读取内容，或本包未实现的 `codebase_version`，它都会抛出异常。

