1#![allow(clippy::iter_over_hash_type)]
2
3pub mod common;
14pub mod datasetv2;
15pub mod datasetv3;
16
17use std::{fmt, path::Path};
18
19use serde::{
20 Deserialize, Deserializer, Serialize,
21 de::{MapAccess, SeqAccess, Visitor},
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum LeRobotDatasetVersion {
26 V1,
27 V2,
28 V3,
29}
30
31impl LeRobotDatasetVersion {
32 pub fn find_version(path: impl AsRef<Path>) -> Option<Self> {
33 let path = path.as_ref();
34
35 if is_v3_lerobot_dataset(path) {
36 Some(Self::V3)
37 } else if is_v2_lerobot_dataset(path) {
38 Some(Self::V2)
39 } else if is_v1_lerobot_dataset(path) {
40 Some(Self::V1)
41 } else {
42 None
43 }
44 }
45}
46
47pub fn is_lerobot_dataset(path: impl AsRef<Path>) -> bool {
49 is_v1_lerobot_dataset(path.as_ref())
50 || is_v2_lerobot_dataset(path.as_ref())
51 || is_v3_lerobot_dataset(path.as_ref())
52}
53
54fn is_v3_lerobot_dataset(_path: impl AsRef<Path>) -> bool {
56 let path = _path.as_ref();
57
58 if !path.is_dir() {
59 return false;
60 }
61
62 has_sub_directories(&["meta", "data"], path) && path.join("meta").join("episodes").is_dir()
64}
65
66fn is_v2_lerobot_dataset(path: impl AsRef<Path>) -> bool {
68 let path = path.as_ref();
69
70 if !path.is_dir() {
71 return false;
72 }
73
74 has_sub_directories(&["meta", "data"], path)
77}
78
79fn is_v1_lerobot_dataset(path: impl AsRef<Path>) -> bool {
81 let path = path.as_ref();
82
83 if !path.is_dir() {
84 return false;
85 }
86
87 has_sub_directories(&["meta_data", "data"], path)
90}
91
92fn has_sub_directories(directories: &[&str], path: impl AsRef<Path>) -> bool {
93 directories.iter().all(|subdir| {
94 let subpath = path.as_ref().join(subdir);
95
96 subpath.is_dir()
98 && subpath
99 .read_dir()
100 .is_ok_and(|mut contents| contents.next().is_some())
101 })
102}
103
104#[derive(Serialize, Deserialize, Debug, Clone)]
112pub struct Feature {
113 pub dtype: DType,
114 pub shape: Vec<usize>,
115 pub names: Option<Names>,
116}
117
118impl Feature {
119 pub fn channel_dim(&self) -> usize {
130 if let Some(names) = &self.names
132 && let Some(channel_idx) = names.0.iter().position(|name| name == "channels")
133 {
134 if channel_idx < self.shape.len() {
136 return self.shape[channel_idx];
137 }
138 }
139
140 self.shape.last().copied().unwrap_or(0)
143 }
144}
145
146#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
148#[serde(rename_all = "snake_case")]
149pub enum DType {
150 Video,
151 Image,
152 Bool,
153 Float32,
154 Float64,
155 Int16,
156 Int64,
157 String,
158 Language,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
169pub struct Names(pub(crate) Vec<String>);
170
171impl Names {
172 pub fn name_for_index(&self, index: usize) -> Option<&String> {
176 self.0.get(index)
177 }
178}
179
180struct NamesVisitor;
190
191impl<'de> Visitor<'de> for NamesVisitor {
192 type Value = Names;
193
194 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
195 formatter.write_str(
196 "a string, a flat string array, a nested string array, or a single-entry object with a string array or null value",
197 )
198 }
199
200 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
202 where
203 E: serde::de::Error,
204 {
205 Ok(Names(vec![v.to_owned()]))
206 }
207
208 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
212 where
213 A: SeqAccess<'de>,
214 {
215 #[derive(Deserialize)]
217 #[serde(untagged)]
218 enum ListItem {
219 Str(String),
220 List(Vec<String>),
221 }
222
223 #[derive(PartialEq)]
225 enum ListType {
226 Undetermined,
227 Flat,
228 Nested,
229 }
230
231 let mut names = Vec::new();
232 let mut determined_type = ListType::Undetermined;
233
234 while let Some(item) = seq.next_element::<ListItem>()? {
235 match item {
236 ListItem::Str(s) => {
237 if determined_type == ListType::Nested {
238 return Err(serde::de::Error::custom(
239 "Cannot mix nested lists with flat strings within names array",
240 ));
241 }
242 determined_type = ListType::Flat;
243 names.push(s);
244 }
245 ListItem::List(list) => {
246 if determined_type == ListType::Flat {
247 return Err(serde::de::Error::custom(
248 "Cannot mix flat strings and nested lists within names array",
249 ));
250 }
251 determined_type = ListType::Nested;
252
253 names.extend(list);
255 }
256 }
257 }
258
259 Ok(Names(names))
260 }
261
262 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
264 where
265 A: MapAccess<'de>,
266 {
267 let mut names_vec: Option<Vec<String>> = None;
268 let mut entry_count = 0;
269
270 while let Some((_key, value)) = map.next_entry::<String, Option<Vec<String>>>()? {
272 entry_count += 1;
273 if entry_count > 1 {
274 while map
276 .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
277 .is_some()
278 {}
279
280 return Err(serde::de::Error::invalid_length(
281 entry_count,
282 &"a Names object with exactly one entry.",
283 ));
284 }
285
286 names_vec = Some(value.unwrap_or_default());
287 }
288
289 Ok(Names(names_vec.unwrap_or_default()))
290 }
291}
292
293impl<'de> Deserialize<'de> for Names {
294 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
295 where
296 D: Deserializer<'de>,
297 {
298 deserializer.deserialize_any(NamesVisitor)
299 }
300}
301
302#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
304#[serde(transparent)]
305pub struct EpisodeIndex(pub usize);
306
307#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
309#[serde(transparent)]
310pub struct TaskIndex(pub usize);
311
312#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
314#[serde(transparent)]
315pub struct SubtaskIndex(pub usize);
316
317#[derive(Debug, Serialize, Deserialize, Clone)]
321pub struct LeRobotDatasetTask {
322 #[serde(rename = "task_index")]
323 pub index: TaskIndex,
324 pub task: String,
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone)]
332pub struct LeRobotDatasetSubtask {
333 #[serde(rename = "subtask_index")]
334 pub index: SubtaskIndex,
335 pub subtask: String,
336}
337
338#[derive(thiserror::Error, Debug)]
340pub enum LeRobotError {
341 #[error("IO error occurred on path: {path}")]
342 IO {
343 #[source]
344 source: std::io::Error,
345 path: std::path::PathBuf,
346 },
347
348 #[error(transparent)]
349 Json(#[from] serde_json::Error),
350
351 #[error(transparent)]
352 Parquet(#[from] parquet::errors::ParquetError),
353
354 #[error(transparent)]
355 Arrow(#[from] arrow::error::ArrowError),
356
357 #[error("Invalid feature key: {0}")]
358 InvalidFeatureKey(String),
359
360 #[error("Missing dataset info: {0}")]
361 MissingDatasetInfo(String),
362
363 #[error("Invalid feature dtype, expected {key} to be of type {expected:?}, but got {actual:?}")]
364 InvalidFeatureDtype {
365 key: String,
366 expected: DType,
367 actual: DType,
368 },
369
370 #[error("Invalid chunk index: {0}")]
371 InvalidChunkIndex(usize),
372
373 #[error("Invalid episode index: {0:?}")]
374 InvalidEpisodeIndex(EpisodeIndex),
375
376 #[error("Episode {0:?} data file does not contain any records")]
377 EmptyEpisode(EpisodeIndex),
378
379 #[error(transparent)]
380 Chunk(#[from] re_chunk::ChunkError),
381
382 #[error("{}", re_error::format(.0))]
383 Other(#[from] anyhow::Error),
384}
385
386impl LeRobotError {
387 pub fn io(source: std::io::Error, path: impl Into<std::path::PathBuf>) -> Self {
389 Self::IO {
390 source,
391 path: path.into(),
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_deserialize_single_string() {
402 let json = r#""some_name""#;
403 let expected = Names(vec!["some_name".to_owned()]);
404 let names: Names = serde_json::from_str(json).unwrap();
405 assert_eq!(names, expected);
406 }
407
408 #[test]
409 fn test_deserialize_flat_list() {
410 let json = r#"["a", "b", "c"]"#;
411 let expected = Names(vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]);
412 let names: Names = serde_json::from_str(json).unwrap();
413 assert_eq!(names, expected);
414 }
415
416 #[test]
417 fn test_deserialize_nested_list() {
418 let json = r#"[["a", "b"], ["c"]]"#;
419 let expected = Names(vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]);
420 let names: Names = serde_json::from_str(json).unwrap();
421 assert_eq!(names, expected);
422 }
423
424 #[test]
425 fn test_deserialize_empty_nested_list() {
426 let json = r#"[[], []]"#;
427 let expected = Names(vec![]);
428 let names: Names = serde_json::from_str(json).unwrap();
429 assert_eq!(names, expected);
430 }
431
432 #[test]
433 fn test_deserialize_empty_list() {
434 let json = r#"[]"#;
435 let expected = Names(vec![]);
436 let names: Names = serde_json::from_str(json).unwrap();
437 assert_eq!(names, expected);
438 }
439
440 #[test]
441 fn test_deserialize_object_with_list() {
442 let json = r#"{ "axes": ["x", "y", "z"] }"#;
443 let expected = Names(vec!["x".to_owned(), "y".to_owned(), "z".to_owned()]);
444 let names: Names = serde_json::from_str(json).unwrap();
445 assert_eq!(names, expected);
446 }
447
448 #[test]
449 fn test_deserialize_object_with_empty_list() {
450 let json = r#"{ "motors": [] }"#;
451 let expected = Names(vec![]);
452 let names: Names = serde_json::from_str(json).unwrap();
453 assert_eq!(names, expected);
454 }
455
456 #[test]
457 fn test_deserialize_object_with_null() {
458 let json = r#"{ "axes": null }"#;
459 let expected = Names(vec![]); let names: Names = serde_json::from_str(json).unwrap();
461 assert_eq!(names, expected);
462 }
463
464 #[test]
465 fn test_deserialize_empty_object() {
466 let json = r#"{}"#;
468 let expected = Names(vec![]);
469 let names: Names = serde_json::from_str(json).unwrap();
470 assert_eq!(names, expected);
471 }
472
473 #[test]
474 fn test_deserialize_error_mixed_list() {
475 let json = r#"["a", ["b"]]"#; let result: Result<Names, _> = serde_json::from_str(json);
477 assert!(result.is_err());
478 assert!(
479 result
480 .unwrap_err()
481 .to_string()
482 .contains("Cannot mix flat strings and nested lists")
483 );
484 }
485
486 #[test]
487 fn test_deserialize_feature_with_language_dtype() {
488 let json = r#"{ "dtype": "language", "shape": [1], "names": null }"#;
491 let feature: Feature = serde_json::from_str(json).unwrap();
492 assert_eq!(feature.dtype, DType::Language);
493 }
494
495 #[test]
496 fn test_deserialize_error_object_multiple_entries() {
497 let json = r#"{ "axes": ["x"], "motors": ["m"] }"#;
498 let result: Result<Names, _> = serde_json::from_str(json);
499 assert!(result.is_err());
500 assert!(
501 result
502 .unwrap_err()
503 .to_string()
504 .contains("a Names object with exactly one entry")
505 );
506 }
507}