dwehr commited on
Commit
e111369
·
1 Parent(s): 9f818c5

Fix local LeRobot sample boundaries

Browse files
cosmos-framework/cosmos_framework/data/vfm/action/_lerobot_local.py CHANGED
@@ -196,22 +196,95 @@ def load_local_lerobot_collection(root: str | Path, subroots: list[str] | None =
196
  return roots, episodes, tasks, rows
197
 
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  def sample_count(rows: list[dict[str, Any]], chunk_length: int, sample_stride: int = 1) -> int:
200
  """Number of contiguous local samples that can provide T+1 observations."""
201
 
202
- return max(0, (len(rows) - int(chunk_length) + int(sample_stride) - 1) // int(sample_stride))
203
 
204
 
205
- def rows_for_index(rows: list[dict[str, Any]], idx: int, chunk_length: int, sample_stride: int = 1) -> list[dict[str, Any]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  """Select T+1 contiguous rows for a flat local index."""
207
 
208
- row_idx = int(idx) * int(sample_stride)
209
  selected = rows[row_idx : row_idx + int(chunk_length) + 1]
210
  if len(selected) < int(chunk_length) + 1:
211
  raise IndexError(f"Index {idx} does not have {chunk_length + 1} rows")
212
- episode_id = int(selected[0]["episode_index"])
213
- dataset_idx = int(selected[0].get("_dataset_idx", 0))
214
- if any(int(row["episode_index"]) != episode_id or int(row.get("_dataset_idx", 0)) != dataset_idx for row in selected):
215
  raise IndexError(f"Index {idx} crosses an episode or shard boundary")
216
  return selected
217
 
@@ -222,17 +295,13 @@ def rows_at_fps(
222
  chunk_length: int,
223
  fps: float,
224
  sample_stride: int = 1,
 
225
  ) -> list[dict[str, Any]]:
226
  """Select T+1 nearest timestamp rows at target FPS within one episode."""
227
 
228
- start_row = rows[int(start_idx) * int(sample_stride)]
229
- episode_id = int(start_row["episode_index"])
230
- dataset_idx = int(start_row.get("_dataset_idx", 0))
231
- episode_rows = [
232
- row
233
- for row in rows
234
- if int(row["episode_index"]) == episode_id and int(row.get("_dataset_idx", 0)) == dataset_idx
235
- ]
236
  timestamps = np.asarray([float(row["timestamp"]) for row in episode_rows], dtype=np.float64)
237
  start_ts = float(start_row["timestamp"])
238
  target_ts = start_ts + np.arange(int(chunk_length) + 1, dtype=np.float64) / float(fps)
 
196
  return roots, episodes, tasks, rows
197
 
198
 
199
+ def _sample_stride_value(sample_stride: int) -> int:
200
+ stride = int(sample_stride)
201
+ if stride < 1:
202
+ raise ValueError(f"sample_stride must be >= 1, got {stride}")
203
+ return stride
204
+
205
+
206
+ def _episode_key(row: dict[str, Any]) -> tuple[int, int]:
207
+ return int(row.get("_dataset_idx", 0)), int(row["episode_index"])
208
+
209
+
210
+ def contiguous_sample_starts(rows: list[dict[str, Any]], chunk_length: int, sample_stride: int = 1) -> list[int]:
211
+ """Start row indices whose T+1 contiguous window stays inside one episode."""
212
+
213
+ stride = _sample_stride_value(sample_stride)
214
+ span = int(chunk_length) + 1
215
+ if span < 1:
216
+ raise ValueError(f"chunk_length must be >= 0, got {chunk_length}")
217
+
218
+ starts: list[int] = []
219
+ for row_idx in range(0, len(rows) - span + 1, stride):
220
+ selected = rows[row_idx : row_idx + span]
221
+ key = _episode_key(selected[0])
222
+ if all(_episode_key(row) == key for row in selected):
223
+ starts.append(row_idx)
224
+ return starts
225
+
226
+
227
+ def fps_sample_starts(rows: list[dict[str, Any]], chunk_length: int, fps: float, sample_stride: int = 1) -> list[int]:
228
+ """Start row indices whose timestamp-sampled T+1 window fits in one episode."""
229
+
230
+ stride = _sample_stride_value(sample_stride)
231
+ fps_value = float(fps)
232
+ if fps_value <= 0:
233
+ raise ValueError(f"fps must be > 0, got {fps_value}")
234
+
235
+ episode_end_ts: dict[tuple[int, int], float] = {}
236
+ for row in rows:
237
+ episode_end_ts[_episode_key(row)] = float(row["timestamp"])
238
+
239
+ starts: list[int] = []
240
+ duration_s = float(chunk_length) / fps_value
241
+ for row_idx in range(0, len(rows), stride):
242
+ row = rows[row_idx]
243
+ if float(row["timestamp"]) + duration_s <= episode_end_ts[_episode_key(row)] + 1e-9:
244
+ starts.append(row_idx)
245
+ return starts
246
+
247
+
248
  def sample_count(rows: list[dict[str, Any]], chunk_length: int, sample_stride: int = 1) -> int:
249
  """Number of contiguous local samples that can provide T+1 observations."""
250
 
251
+ return len(contiguous_sample_starts(rows, chunk_length, sample_stride))
252
 
253
 
254
+ def _row_idx_for_sample(
255
+ rows: list[dict[str, Any]],
256
+ idx: int,
257
+ sample_stride: int,
258
+ sample_starts: list[int] | None,
259
+ ) -> int:
260
+ sample_idx = int(idx)
261
+ if sample_idx < 0:
262
+ raise IndexError(f"Index {idx} is negative")
263
+ if sample_starts is not None:
264
+ if sample_idx >= len(sample_starts):
265
+ raise IndexError(f"Index {idx} is out of range for {len(sample_starts)} samples")
266
+ return int(sample_starts[sample_idx])
267
+ row_idx = sample_idx * _sample_stride_value(sample_stride)
268
+ if row_idx >= len(rows):
269
+ raise IndexError(f"Index {idx} is out of range for {len(rows)} rows")
270
+ return row_idx
271
+
272
+
273
+ def rows_for_index(
274
+ rows: list[dict[str, Any]],
275
+ idx: int,
276
+ chunk_length: int,
277
+ sample_stride: int = 1,
278
+ sample_starts: list[int] | None = None,
279
+ ) -> list[dict[str, Any]]:
280
  """Select T+1 contiguous rows for a flat local index."""
281
 
282
+ row_idx = _row_idx_for_sample(rows, idx, sample_stride, sample_starts)
283
  selected = rows[row_idx : row_idx + int(chunk_length) + 1]
284
  if len(selected) < int(chunk_length) + 1:
285
  raise IndexError(f"Index {idx} does not have {chunk_length + 1} rows")
286
+ episode_key = _episode_key(selected[0])
287
+ if any(_episode_key(row) != episode_key for row in selected):
 
288
  raise IndexError(f"Index {idx} crosses an episode or shard boundary")
289
  return selected
290
 
 
295
  chunk_length: int,
296
  fps: float,
297
  sample_stride: int = 1,
298
+ sample_starts: list[int] | None = None,
299
  ) -> list[dict[str, Any]]:
300
  """Select T+1 nearest timestamp rows at target FPS within one episode."""
301
 
302
+ start_row = rows[_row_idx_for_sample(rows, start_idx, sample_stride, sample_starts)]
303
+ episode_key = _episode_key(start_row)
304
+ episode_rows = [row for row in rows if _episode_key(row) == episode_key]
 
 
 
 
 
305
  timestamps = np.asarray([float(row["timestamp"]) for row in episode_rows], dtype=np.float64)
306
  start_ts = float(start_row["timestamp"])
307
  target_ts = start_ts + np.arange(int(chunk_length) + 1, dtype=np.float64) / float(fps)
cosmos-framework/cosmos_framework/data/vfm/action/_lerobot_local_test.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: OpenMDW-1.1
3
+
4
+ import unittest
5
+
6
+ from cosmos_framework.data.vfm.action._lerobot_local import (
7
+ contiguous_sample_starts,
8
+ fps_sample_starts,
9
+ rows_at_fps,
10
+ rows_for_index,
11
+ sample_count,
12
+ )
13
+
14
+
15
+ def _rows(lengths: list[int], *, fps: float = 10.0) -> list[dict]:
16
+ rows = []
17
+ index = 0
18
+ for episode_index, length in enumerate(lengths):
19
+ for frame in range(length):
20
+ rows.append(
21
+ {
22
+ "index": index,
23
+ "episode_index": episode_index,
24
+ "timestamp": frame / fps,
25
+ }
26
+ )
27
+ index += 1
28
+ return rows
29
+
30
+
31
+ class LocalLeRobotSampleIndexTest(unittest.TestCase):
32
+ def test_contiguous_samples_skip_episode_boundaries(self) -> None:
33
+ rows = _rows([20, 20])
34
+ starts = contiguous_sample_starts(rows, chunk_length=16)
35
+
36
+ self.assertEqual(starts, [0, 1, 2, 3, 20, 21, 22, 23])
37
+ self.assertEqual(sample_count(rows, chunk_length=16), len(starts))
38
+
39
+ selected = rows_for_index(rows, 4, chunk_length=16, sample_starts=starts)
40
+ self.assertEqual({row["episode_index"] for row in selected}, {1})
41
+
42
+ with self.assertRaisesRegex(IndexError, "crosses an episode"):
43
+ rows_for_index(rows, 12, chunk_length=16)
44
+
45
+ def test_fps_samples_skip_episode_tails(self) -> None:
46
+ rows = _rows([20], fps=10.0)
47
+ starts = fps_sample_starts(rows, chunk_length=16, fps=10.0)
48
+
49
+ self.assertEqual(starts, [0, 1, 2, 3])
50
+ selected = rows_at_fps(rows, 3, chunk_length=16, fps=10.0, sample_starts=starts)
51
+
52
+ self.assertEqual([row["index"] for row in selected], list(range(3, 20)))
53
+ with self.assertRaisesRegex(IndexError, "out of range"):
54
+ rows_at_fps(rows, 4, chunk_length=16, fps=10.0, sample_starts=starts)
cosmos-framework/cosmos_framework/data/vfm/action/bridge_orig_lerobot_dataset.py CHANGED
@@ -14,11 +14,11 @@ from torch.utils.data import Dataset
14
  from cosmos_framework.data.vfm.action._lerobot_local import (
15
  build_result,
16
  choose_mode,
 
17
  load_local_lerobot,
18
  load_video_key,
19
  pick_caption,
20
  rows_for_index,
21
- sample_count,
22
  task_text,
23
  )
24
  from cosmos_framework.data.vfm.action.pose_utils import (
@@ -102,6 +102,7 @@ class BridgeOrigLeRobotDataset(Dataset):
102
  self._tolerance_s = float(tolerance_s)
103
  self._viewpoint = viewpoint
104
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
 
105
 
106
  @property
107
  def fps(self) -> float:
@@ -123,9 +124,12 @@ class BridgeOrigLeRobotDataset(Dataset):
123
  def action_dim(self) -> int:
124
  return 10
125
 
 
 
 
126
  def __getitem__(self, idx: int) -> dict[str, Any]:
127
  mode = choose_mode(self._mode)
128
- observation_rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride)
129
  episode = self._episodes[int(observation_rows[0]["episode_index"])]
130
  action_rows = observation_rows[: self._chunk_length]
131
 
@@ -180,6 +184,3 @@ class BridgeOrigLeRobotDataset(Dataset):
180
  viewpoint=self._viewpoint,
181
  **extras,
182
  )
183
-
184
- def __len__(self) -> int:
185
- return sample_count(self._rows, self._chunk_length, self._sample_stride)
 
14
  from cosmos_framework.data.vfm.action._lerobot_local import (
15
  build_result,
16
  choose_mode,
17
+ contiguous_sample_starts,
18
  load_local_lerobot,
19
  load_video_key,
20
  pick_caption,
21
  rows_for_index,
 
22
  task_text,
23
  )
24
  from cosmos_framework.data.vfm.action.pose_utils import (
 
102
  self._tolerance_s = float(tolerance_s)
103
  self._viewpoint = viewpoint
104
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
105
+ self._sample_starts = contiguous_sample_starts(self._rows, self._chunk_length, self._sample_stride)
106
 
107
  @property
108
  def fps(self) -> float:
 
124
  def action_dim(self) -> int:
125
  return 10
126
 
127
+ def __len__(self) -> int:
128
+ return len(self._sample_starts)
129
+
130
  def __getitem__(self, idx: int) -> dict[str, Any]:
131
  mode = choose_mode(self._mode)
132
+ observation_rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride, self._sample_starts)
133
  episode = self._episodes[int(observation_rows[0]["episode_index"])]
134
  action_rows = observation_rows[: self._chunk_length]
135
 
 
184
  viewpoint=self._viewpoint,
185
  **extras,
186
  )
 
 
 
cosmos-framework/cosmos_framework/data/vfm/action/droid_lerobot_dataset.py CHANGED
@@ -15,11 +15,11 @@ from torch.utils.data import Dataset
15
  from cosmos_framework.data.vfm.action._lerobot_local import (
16
  build_result,
17
  choose_mode,
 
18
  load_local_lerobot,
19
  load_video_key,
20
  pick_caption,
21
  rows_for_index,
22
- sample_count,
23
  task_text,
24
  )
25
  from cosmos_framework.data.vfm.action.pose_utils import (
@@ -91,6 +91,7 @@ class DROIDLeRobotDataset(Dataset):
91
  if not (root_path / "meta" / "info.json").exists() and (root_path / "success" / "meta" / "info.json").exists():
92
  root_path = root_path / "success"
93
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root_path)
 
94
 
95
  @property
96
  def fps(self) -> float:
@@ -114,7 +115,7 @@ class DROIDLeRobotDataset(Dataset):
114
 
115
  def __getitem__(self, idx: int) -> dict[str, Any]:
116
  mode = choose_mode(self._mode)
117
- observation_rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride)
118
  episode = self._episodes[int(observation_rows[0]["episode_index"])]
119
  action_rows = observation_rows[: self._chunk_length]
120
 
@@ -180,4 +181,4 @@ class DROIDLeRobotDataset(Dataset):
180
  )
181
 
182
  def __len__(self) -> int:
183
- return sample_count(self._rows, self._chunk_length, self._sample_stride)
 
15
  from cosmos_framework.data.vfm.action._lerobot_local import (
16
  build_result,
17
  choose_mode,
18
+ contiguous_sample_starts,
19
  load_local_lerobot,
20
  load_video_key,
21
  pick_caption,
22
  rows_for_index,
 
23
  task_text,
24
  )
25
  from cosmos_framework.data.vfm.action.pose_utils import (
 
91
  if not (root_path / "meta" / "info.json").exists() and (root_path / "success" / "meta" / "info.json").exists():
92
  root_path = root_path / "success"
93
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root_path)
94
+ self._sample_starts = contiguous_sample_starts(self._rows, self._chunk_length, self._sample_stride)
95
 
96
  @property
97
  def fps(self) -> float:
 
115
 
116
  def __getitem__(self, idx: int) -> dict[str, Any]:
117
  mode = choose_mode(self._mode)
118
+ observation_rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride, self._sample_starts)
119
  episode = self._episodes[int(observation_rows[0]["episode_index"])]
120
  action_rows = observation_rows[: self._chunk_length]
121
 
 
181
  )
182
 
183
  def __len__(self) -> int:
184
+ return len(self._sample_starts)
cosmos-framework/cosmos_framework/data/vfm/action/fractal.py CHANGED
@@ -14,11 +14,11 @@ from torch.utils.data import Dataset
14
  from cosmos_framework.data.vfm.action._lerobot_local import (
15
  build_result,
16
  choose_mode,
 
17
  load_local_lerobot,
18
  load_video_key,
19
  pick_caption,
20
  rows_for_index,
21
- sample_count,
22
  task_text,
23
  )
24
  from cosmos_framework.data.vfm.action.pose_utils import PoseConvention, build_abs_pose_from_components, pose_abs_to_rel
@@ -64,6 +64,7 @@ class FractalLeRobotDataset(Dataset):
64
  self._viewpoint = viewpoint
65
  self._sample_stride = int(sample_stride)
66
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
 
67
 
68
  @property
69
  def fps(self) -> float:
@@ -86,10 +87,10 @@ class FractalLeRobotDataset(Dataset):
86
  return 10
87
 
88
  def __len__(self) -> int:
89
- return sample_count(self._rows, self._chunk_length, self._sample_stride)
90
 
91
  def __getitem__(self, idx: int) -> dict[str, Any]:
92
- rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride)
93
  episode = self._episodes[int(rows[0]["episode_index"])]
94
  video = load_video_key(self._root, self._info, episode, rows, _IMAGE_FEATURE, tolerance_s=1e-4)
95
 
 
14
  from cosmos_framework.data.vfm.action._lerobot_local import (
15
  build_result,
16
  choose_mode,
17
+ contiguous_sample_starts,
18
  load_local_lerobot,
19
  load_video_key,
20
  pick_caption,
21
  rows_for_index,
 
22
  task_text,
23
  )
24
  from cosmos_framework.data.vfm.action.pose_utils import PoseConvention, build_abs_pose_from_components, pose_abs_to_rel
 
64
  self._viewpoint = viewpoint
65
  self._sample_stride = int(sample_stride)
66
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
67
+ self._sample_starts = contiguous_sample_starts(self._rows, self._chunk_length, self._sample_stride)
68
 
69
  @property
70
  def fps(self) -> float:
 
87
  return 10
88
 
89
  def __len__(self) -> int:
90
+ return len(self._sample_starts)
91
 
92
  def __getitem__(self, idx: int) -> dict[str, Any]:
93
+ rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride, self._sample_starts)
94
  episode = self._episodes[int(rows[0]["episode_index"])]
95
  video = load_video_key(self._root, self._info, episode, rows, _IMAGE_FEATURE, tolerance_s=1e-4)
96
 
cosmos-framework/cosmos_framework/data/vfm/action/robomind_franka_dataset.py CHANGED
@@ -15,11 +15,11 @@ from torch.utils.data import Dataset
15
  from cosmos_framework.data.vfm.action._lerobot_local import (
16
  build_result,
17
  choose_mode,
 
18
  load_local_lerobot,
19
  load_video_key,
20
  pick_caption,
21
  rows_at_fps,
22
- sample_count,
23
  task_text,
24
  )
25
  from cosmos_framework.data.vfm.action.pose_utils import PoseConvention, build_abs_pose_from_components, pose_abs_to_rel
@@ -68,6 +68,7 @@ class RoboMINDFrankaDataset(Dataset):
68
  self._viewpoint = viewpoint
69
  self._sample_stride = int(sample_stride)
70
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
 
71
  self._to_opencv = _ROBOMIND_FRANKA_TO_OPENCV[:3, :3]
72
 
73
  @property
@@ -91,7 +92,7 @@ class RoboMINDFrankaDataset(Dataset):
91
  return 10 if self._embodiment_type == "robomind-franka" else 20
92
 
93
  def __len__(self) -> int:
94
- return sample_count(self._rows, self._chunk_length, self._sample_stride)
95
 
96
  def _camera_keys(self) -> tuple[str, str, str]:
97
  primary = "observation.images.camera_top" if self._embodiment_type == "robomind-franka" else "observation.images.camera_front"
@@ -143,7 +144,7 @@ class RoboMINDFrankaDataset(Dataset):
143
  return torch.from_numpy(action).float(), initial_pose_left, initial_pose_right
144
 
145
  def __getitem__(self, idx: int) -> dict[str, Any]:
146
- rows = rows_at_fps(self._rows, idx, self._chunk_length, self._fps, self._sample_stride)
147
  episode = self._episodes[int(rows[0]["episode_index"])]
148
  video = self._load_concat_video(episode, rows)
149
  built = self._build_action(rows)
 
15
  from cosmos_framework.data.vfm.action._lerobot_local import (
16
  build_result,
17
  choose_mode,
18
+ fps_sample_starts,
19
  load_local_lerobot,
20
  load_video_key,
21
  pick_caption,
22
  rows_at_fps,
 
23
  task_text,
24
  )
25
  from cosmos_framework.data.vfm.action.pose_utils import PoseConvention, build_abs_pose_from_components, pose_abs_to_rel
 
68
  self._viewpoint = viewpoint
69
  self._sample_stride = int(sample_stride)
70
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
71
+ self._sample_starts = fps_sample_starts(self._rows, self._chunk_length, self._fps, self._sample_stride)
72
  self._to_opencv = _ROBOMIND_FRANKA_TO_OPENCV[:3, :3]
73
 
74
  @property
 
92
  return 10 if self._embodiment_type == "robomind-franka" else 20
93
 
94
  def __len__(self) -> int:
95
+ return len(self._sample_starts)
96
 
97
  def _camera_keys(self) -> tuple[str, str, str]:
98
  primary = "observation.images.camera_top" if self._embodiment_type == "robomind-franka" else "observation.images.camera_front"
 
144
  return torch.from_numpy(action).float(), initial_pose_left, initial_pose_right
145
 
146
  def __getitem__(self, idx: int) -> dict[str, Any]:
147
+ rows = rows_at_fps(self._rows, idx, self._chunk_length, self._fps, self._sample_stride, self._sample_starts)
148
  episode = self._episodes[int(rows[0]["episode_index"])]
149
  video = self._load_concat_video(episode, rows)
150
  built = self._build_action(rows)
cosmos-framework/cosmos_framework/data/vfm/action/umi_lerobot_dataset.py CHANGED
@@ -14,11 +14,11 @@ from torch.utils.data import Dataset
14
  from cosmos_framework.data.vfm.action._lerobot_local import (
15
  build_result,
16
  choose_mode,
 
17
  load_local_lerobot,
18
  load_video_key,
19
  pick_caption,
20
  rows_for_index,
21
- sample_count,
22
  task_text,
23
  )
24
  from cosmos_framework.data.vfm.action.pose_utils import PoseConvention, build_abs_pose_from_components, pose_abs_to_rel
@@ -55,6 +55,7 @@ class UMIFastLeRobotDataset(Dataset):
55
  self._viewpoint = viewpoint
56
  self._sample_stride = int(sample_stride)
57
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
 
58
 
59
  # The packaged UMI trajectory is the right main camera trajectory. For
60
  # viewer overlays we want the actual gripper/end-effector pose, so
@@ -93,10 +94,10 @@ class UMIFastLeRobotDataset(Dataset):
93
  return 10
94
 
95
  def __len__(self) -> int:
96
- return sample_count(self._rows, self._chunk_length, self._sample_stride)
97
 
98
  def __getitem__(self, idx: int) -> dict[str, Any]:
99
- rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride)
100
  episode = self._episodes[int(rows[0]["episode_index"])]
101
  video = load_video_key(self._root, self._info, episode, rows, _IMAGE_FEATURE, tolerance_s=1e-4)
102
  pose = np.asarray([row[_POSE_FEATURE] for row in rows], dtype=np.float32)
 
14
  from cosmos_framework.data.vfm.action._lerobot_local import (
15
  build_result,
16
  choose_mode,
17
+ contiguous_sample_starts,
18
  load_local_lerobot,
19
  load_video_key,
20
  pick_caption,
21
  rows_for_index,
 
22
  task_text,
23
  )
24
  from cosmos_framework.data.vfm.action.pose_utils import PoseConvention, build_abs_pose_from_components, pose_abs_to_rel
 
55
  self._viewpoint = viewpoint
56
  self._sample_stride = int(sample_stride)
57
  self._root, self._info, self._episodes, self._tasks, self._rows = load_local_lerobot(root)
58
+ self._sample_starts = contiguous_sample_starts(self._rows, self._chunk_length, self._sample_stride)
59
 
60
  # The packaged UMI trajectory is the right main camera trajectory. For
61
  # viewer overlays we want the actual gripper/end-effector pose, so
 
94
  return 10
95
 
96
  def __len__(self) -> int:
97
+ return len(self._sample_starts)
98
 
99
  def __getitem__(self, idx: int) -> dict[str, Any]:
100
+ rows = rows_for_index(self._rows, idx, self._chunk_length, self._sample_stride, self._sample_starts)
101
  episode = self._episodes[int(rows[0]["episode_index"])]
102
  video = load_video_key(self._root, self._info, episode, rows, _IMAGE_FEATURE, tolerance_s=1e-4)
103
  pose = np.asarray([row[_POSE_FEATURE] for row in rows], dtype=np.float32)