<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harpreet Sahota</title>
    <description>The latest articles on DEV Community by Harpreet Sahota (@harpreetsahota).</description>
    <link>https://dev.to/harpreetsahota</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4213631%2Fae3b78ab-aed7-477c-bb84-5c293c70b9e9.png</url>
      <title>DEV Community: Harpreet Sahota</title>
      <link>https://dev.to/harpreetsahota</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harpreetsahota"/>
    <language>en</language>
    <item>
      <title>EmbeddingGemma 2 in FiftyOne: Search Video by Text, Sound, or Your Webcam</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:16:16 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/embeddinggemma-2-in-fiftyone-search-video-by-text-sound-or-your-webcam-k80</link>
      <guid>https://dev.to/harpreetsahota/embeddinggemma-2-in-fiftyone-search-video-by-text-sound-or-your-webcam-k80</guid>
      <description>&lt;p&gt;&lt;em&gt;One model, one 768-dimensional space, three LanceDB indexes: search 10,000 VGGSound clips with a sentence, a soundtrack, or a recording of your own, in a few lines of Python or a few clicks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have 10,000 video clips and a sound stuck in your head. A church bell, maybe. The clips have no labels for it, or the labels are wrong, or the thing you want lives in the audio track that no image model ever looked at.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://voxel51.com/embedding-and-semantic-search" rel="noopener noreferrer"&gt;Searching media by meaning&lt;/a&gt; has usually meant stitching models together: one for frames, one for audio, one for text, and a vector store for each. Then you compare scores across models that were never trained to agree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/google/embeddinggemma-2" rel="noopener noreferrer"&gt;EmbeddingGemma 2&lt;/a&gt;, released by Google DeepMind on October 6, 2026, removes the stitching. One 740M-parameter model maps text, images, video, and audio into a single 768-dimensional space. The sentence “church bell ringing,” a photo of a bell tower, and the sound of a bell all land near each other.&lt;/p&gt;

&lt;p&gt;This post runs it end to end in FiftyOne on &lt;a href="https://huggingface.co/datasets/harpreetsahota/VGGSound" rel="noopener noreferrer"&gt;VGGSound&lt;/a&gt;, a dataset of 10-second YouTube clips. You’ll build three LanceDB indexes over the same clips, search them with text, search pictures with sound, and then use the Cross-Modal Retrieval panel to do the same thing with an upload, your microphone, or your webcam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EmbeddingGemma 2 (740M parameters, Apache 2.0) embeds text, images, video, and audio into one 768-dimensional space, and its Matryoshka training lets you truncate vectors to 512, 256, or 128 dimensions.&lt;/li&gt;
&lt;li&gt;In FiftyOne, EmbeddingGemma 2 is a &lt;a href="https://github.com/harpreetsahota204/EmbeddingGemma2" rel="noopener noreferrer"&gt;remote zoo model&lt;/a&gt;: register it once, then &lt;code&gt;model.embed_dataset()&lt;/code&gt; fills one vector field per modality.&lt;/li&gt;
&lt;li&gt;With EmbeddingGemma 2, VGGSound ends up with three vector fields (&lt;code&gt;eg2_video&lt;/code&gt;, &lt;code&gt;eg2_audio&lt;/code&gt;, &lt;code&gt;eg2_av&lt;/code&gt;) and three LanceDB similarity indexes built with &lt;code&gt;fob.compute_similarity(..., backend="lancedb")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Because EmbeddingGemma 2 puts every modality in one space, a text query can search all three indexes, and a clip’s soundtrack can search other clips’ frames.&lt;/li&gt;
&lt;li&gt;The same EmbeddingGemma 2 vectors drive uniqueness, representativeness, &lt;a href="https://voxel51.com/blog/finding-the-best-embedding-model-for-image-classification" rel="noopener noreferrer"&gt;UMAP layouts&lt;/a&gt;, and zero-shot classification of VGGSound’s 309 sound classes.&lt;/li&gt;
&lt;li&gt;The Cross-Modal Retrieval panel installs with two &lt;code&gt;fiftyone plugin&lt;/code&gt; commands and lets you search by text, selected samples, or media you upload or record. You can also try it in a &lt;a href="https://huggingface.co/spaces/harpreetsahota/EmbeddingGemma-2-in-FiftyOne" rel="noopener noreferrer"&gt;Hugging Face Space&lt;/a&gt; without installing anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is EmbeddingGemma 2?
&lt;/h2&gt;

&lt;p&gt;EmbeddingGemma 2 is Google DeepMind’s open &lt;a href="https://voxel51.com/multimodal-embedding" rel="noopener noreferrer"&gt;multimodal embedding model&lt;/a&gt;. It’s built on the Gemma 4 architecture and combines a 270M-parameter text model with optional vision (170M) and audio (300M) encoders. You load only the encoders you need.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9hlza4iku1kg8mt32sf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9hlza4iku1kg8mt32sf.png" alt="Diagram of text, image, video, and audio inputs passing through EmbeddingGemma 2 into one shared space, where church bells, dog barking, and rain on a window each form a cluster of mixed-modality points." width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;EmbeddingGemma 2 maps text, images, video, and audio into one shared space where matching meanings sit together.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One space.&lt;/strong&gt; You can compare any vector to any other, so &lt;a href="https://voxel51.com/cosine-similarity" rel="noopener noreferrer"&gt;cosine similarity&lt;/a&gt; works between a sentence and a sound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matryoshka dimensions.&lt;/strong&gt; The first 512, 256, or 128 numbers of a vector are a usable embedding on their own, so you can use smaller indexes without retraining anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An 8K-token context.&lt;/strong&gt; Per the &lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/embeddinggemma-2/" rel="noopener noreferrer"&gt;launch post&lt;/a&gt;, that fits up to 5.5 minutes of audio, 29 images, or 58 video frames in a single input.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What can you do with EmbeddingGemma 2?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://voxel51.com/blog/multimodal-embeddings-physical-ai" rel="noopener noreferrer"&gt;One shared space&lt;/a&gt; turns several separate projects into the same three steps: embed, index, query.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://voxel51.com/blog/fiftyone-twelvelabs-natural-language-video-search" rel="noopener noreferrer"&gt;&lt;strong&gt;Find clips by describing them&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt; “Rain on a window” against a pile of unlabeled video.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://voxel51.com/blog/fiftyone-search-multimodal-data" rel="noopener noreferrer"&gt;&lt;strong&gt;Search across modalities&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt; A sound finds pictures, and a picture finds sounds. In a grouped dataset, a video’s vector can pull the music that fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick what to label or drop.&lt;/strong&gt; Uniqueness and representativeness scores computed from the vectors rank clips by how redundant or typical they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label without training.&lt;/strong&gt; Compare each clip’s vector to the text of your class names for &lt;a href="https://voxel51.com/zero-shot-labeling" rel="noopener noreferrer"&gt;zero-shot classification&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search with your own media.&lt;/strong&gt; Record a clap, a photo, or a few seconds of video and use it as the query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The dataset: VGGSound
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxdic6bhxqd0fxnmar7a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxdic6bhxqd0fxnmar7a.gif" alt="The VGGSound dataset in the FiftyOne App, with label counts, uniqueness and representativeness scores, and an embeddings view." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The VGGSound dataset in the FiftyOne App, with label counts, uniqueness and representativeness scores, and an embeddings view.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/harpreetsahota/VGGSound" rel="noopener noreferrer"&gt;VGGSound&lt;/a&gt; is a set of 10-second YouTube clips, each labeled with one of 309 sound classes. The sound source is visible in the clip. This FiftyOne build holds the first archive shard: 10,000 of the 199,467 clips in the full release.&lt;/p&gt;

&lt;p&gt;Sample fields you’ll use: &lt;code&gt;ground_truth&lt;/code&gt; (a &lt;code&gt;Classification&lt;/code&gt; with the sound class), &lt;code&gt;youtube_id&lt;/code&gt;, &lt;code&gt;start_sec&lt;/code&gt;, and &lt;code&gt;split&lt;/code&gt;. The Hub copy also carries the vectors this post computes: &lt;code&gt;eg2_video&lt;/code&gt;, &lt;code&gt;eg2_audio&lt;/code&gt;, and &lt;code&gt;eg2_av&lt;/code&gt;, each 768-dimensional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; VGGSound labels come from an automated pipeline (visual verification, audio verification, iterative noise filtering), not from human annotators. Treat &lt;code&gt;ground_truth&lt;/code&gt; as a strong signal that can still be wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Register the model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.brain&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fob&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.zoo&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foz&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ViewField&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="c1"&gt;# One-time: point FiftyOne at the remote zoo model source
&lt;/span&gt;&lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_zoo_model_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/harpreetsahota204/EmbeddingGemma2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load the vision and audio encoders so one model handles video, audio, and both
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_zoo_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/embeddinggemma-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modalities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weights download on first use, and the repo is public, so you don’t need a Hugging Face login. It runs on CPU, and a GPU makes embedding a video dataset much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Load VGGSound
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone.utils.huggingface&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_from_hub&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_from_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harpreetsahota/VGGSound&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good to know. Do you have to recompute the embeddings?&lt;/strong&gt; No.&amp;nbsp;The Hub copy ships with the three &lt;code&gt;eg2_*&lt;/code&gt; vector fields and the uniqueness and representativeness scores. Skip Step 3 and go straight to Step 4, which builds the LanceDB indexes on your machine from those vectors. Step 3 is the code to run on your own media. The full download is 16.6 GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Embed the clips three ways
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzroxmj5itqfql4abhbtm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzroxmj5itqfql4abhbtm.gif" alt="The three EmbeddingGemma 2 vector fields on VGGSound, explored as UMAP layouts in the Embeddings panel." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The three EmbeddingGemma 2 vector fields on VGGSound, explored as UMAP layouts in the Embeddings panel.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Frames only: sampled at 1 fps, up to 32 frames spread over the clip
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Soundtrack only: the first 30 seconds, resampled to 16 kHz mono
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Frames and soundtrack together, as one vector
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video+audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5lc59cri1rio0fa00mi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5lc59cri1rio0fa00mi.png" alt="Diagram of one video sample split three ways by EmbeddingGemma 2: a video vector from frames only, an audio vector from the soundtrack only, and a video plus audio vector from both." width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One clip, three indexes: video embeds the frames, audio the soundtrack, and video + audio both as one vector.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each call reads the &lt;code&gt;filepath&lt;/code&gt; of every sample and stores one &lt;code&gt;VectorField&lt;/code&gt;. FiftyOne’s &lt;code&gt;compute_embeddings()&lt;/code&gt; only accepts image and video collections, so &lt;code&gt;embed_dataset()&lt;/code&gt; is the route for audio and for video plus audio. Clips that fail to decode, such as a video with no audio track, get None and a logged warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Build a LanceDB index for each field
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;index_settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modalities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_kwargs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;index_settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;# reuse the stored vectors
&lt;/span&gt;        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/embeddinggemma-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# pass the zoo model name so text queries work
&lt;/span&gt;        &lt;span class="n"&gt;model_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lancedb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each index is an &lt;a href="https://docs.voxel51.com/user_guide/similarity.html" rel="noopener noreferrer"&gt;ordinary FiftyOne similarity index&lt;/a&gt; backed by its own LanceDB table, so image, video, audio, and video + audio indexes of the same dataset sit side by side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. Where do the tables live?&lt;/strong&gt; &lt;a href="https://docs.voxel51.com/integrations/lancedb.html" rel="noopener noreferrer"&gt;FiftyOne’s LanceDB backend&lt;/a&gt; reads its storage location from your brain config, and the default is &lt;code&gt;/tmp/lancedb&lt;/code&gt;. Many systems clear /tmp on restart. Set a permanent location in &lt;code&gt;~/.fiftyone/brain_config.json&lt;/code&gt; before you build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity_backends&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lancedb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/fiftyone/lancedb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why LanceDB for a multimodal index?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lancedb.com/" rel="noopener noreferrer"&gt;LanceDB&lt;/a&gt; is an open-source &lt;a href="https://voxel51.com/vector-database" rel="noopener noreferrer"&gt;vector database&lt;/a&gt; with FiftyOne Brain support built in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwzb17l95wlfqdxzuox3n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwzb17l95wlfqdxzuox3n.png" alt="Architecture diagram in which the eg2_video, eg2_audio, and eg2_av vector fields feed compute_similarity brain runs that each write a LanceDB table, searched by the panel, the App's similarity search, and Python." width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The dataset keeps the vectors, a brain run records the index, LanceDB holds one table per index, and the panel, the App, and Python all search it.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedded, no server.&lt;/strong&gt; It runs inside the FiftyOne process and stores tables as files, on local disk or in cloud storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built for ML data.&lt;/strong&gt; Tables use the Lance columnar format, designed for fast random access to large vectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scales past memory.&lt;/strong&gt; Tables live on disk, so an index doesn’t have to fit in RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One table per index.&lt;/strong&gt; A video, an audio, and a video + audio index of the same clips don’t interfere with each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Search with text
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fafvm8gb2aqkkve3mvk2e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fafvm8gb2aqkkve3mvk2e.gif" alt="Text queries in the Cross-Modal Retrieval panel return matches from the video, audio, and video + audio indexes at once." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Text queries in the Cross-Modal Retrieval panel return matches from the video, audio, and video + audio indexes at once.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;church bell ringing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text queries are prefixed with the model’s &lt;code&gt;SearchQuery&lt;/code&gt; prompt, which tells it the text is a search query rather than a document. Run the same query against each index, and each one ranks the clips by different evidence:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67w0iimyzk4g8qz67re8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67w0iimyzk4g8qz67re8.png" alt="Three bar charts of the top 8 cosine similarity results for the query church bell ringing in the video, audio, and video + audio indexes, with church bell clips highlighted in each." width="799" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The query “church bell ringing” against the video, audio, and video + audio indexes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this 100-clip sample, the video and video + audio indexes put four church bell clips in their top eight. The audio index puts an orchestra first and finds bells at ranks 3, 5, and 6. Colored bars are clips labeled &lt;code&gt;church bell ringing&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Let a sound search videos
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nbvxgtx4zfppjieg2nc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nbvxgtx4zfppjieg2nc.gif" alt="Audio and video queries in the Cross-Modal Retrieval panel find VGGSound clips by what they show and how they sound." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Audio and video queries in the Cross-Modal Retrieval panel find VGGSound clips by what they show and how they sound.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Take the soundtrack vector of one clip and search the frames-only index, which contains no sound at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A clip labeled "female singing"
&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground_truth.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;female singing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Its soundtrack vector queries the frames-only index; the clip itself is excluded
&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwpi9j2wcl1t4qv1w7pc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwpi9j2wcl1t4qv1w7pc.png" alt="Bar chart of the top 6 clips returned when a female singing clip's audio vector searches the frames-only video index, with three female singing clips among the results." width="799" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching the frames-only video index with a clip's audio vector finds other clips of the same class.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The chart above shows a sample of 100 clips. On the full 10,000 clips, the top 10 for a &lt;code&gt;female singing&lt;/code&gt; query is dominated by singing: &lt;code&gt;male singing&lt;/code&gt;, &lt;code&gt;female singing&lt;/code&gt;, and &lt;code&gt;child singing&lt;/code&gt;, plus a few neighbors such as &lt;code&gt;playing ukulele&lt;/code&gt; and &lt;code&gt;people cheering&lt;/code&gt;. The query never saw a frame, and the index never heard a sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Find the clips worth your time
&lt;/h2&gt;

&lt;p&gt;The vectors you just built also answer curation questions. &lt;code&gt;eg2_av&lt;/code&gt; embeds frames and sound together, so it’s the one to use here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Uniqueness: high scores mark clips with few close neighbors
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_uniqueness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uniqueness_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av_uniqueness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Representativeness: high scores mark clips near the center of a cluster
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_representativeness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;representativeness_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av_representativeness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster-center&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# A 2D layout you can explore in the Embeddings panel (needs umap-learn)
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_visualization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;umap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av_viz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sort by &lt;code&gt;eg2_av_uniqueness&lt;/code&gt; to find outliers worth a look. Sort by &lt;code&gt;eg2_av_representativeness&lt;/code&gt; to pick a small set that stands in for the whole. The Hub copy already stores both scores as sample fields for each of the three vector fields, for example, &lt;code&gt;eg2_audio_uniqueness&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Classify without training anything
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use the 309 VGGSound class names as the label set
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground_truth.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Compare stored vectors to the class text; no media is decoded again
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_zeroshot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Score the zero-shot labels against ground truth
&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate_classifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_zeroshot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gt_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground_truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eg2_eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Model Evaluation panel, and the confusion matrix shows which sound classes the model separates and which it mixes up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; &lt;code&gt;eg2_zeroshot&lt;/code&gt; is model output, not ground truth. Expect near-neighbor classes to mix. VGGSound lists &lt;code&gt;dog barking&lt;/code&gt;, &lt;code&gt;dog bow-wow&lt;/code&gt;, and &lt;code&gt;dog growling&lt;/code&gt; as separate classes, and a zero-shot model has little to separate them with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without code: the Cross-Modal Retrieval panel
&lt;/h2&gt;

&lt;p&gt;The Cross-Modal Retrieval panel runs the same searches from the App, and it lets you search with media you have in your hands right now. For how plugins and panels are built, see &lt;a href="https://voxel51.com/blog/fiftyone-plugins-mcap-lerobot-curation" rel="noopener noreferrer"&gt;FiftyOne Plugins for MCAP and LeRobot Data Curation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;harpreetsahota204&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cross_modal_similarity_search&lt;/span&gt;
&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt; &lt;span class="nd"&gt;@harpreetsahota&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cross&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;modal&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;retrieval&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FiftyOne 1.22.0 or later&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lancedb&lt;/code&gt;, &lt;code&gt;transformers&amp;gt;=5.18&lt;/code&gt;, &lt;code&gt;torch&lt;/code&gt;, &lt;code&gt;av&lt;/code&gt;, and &lt;code&gt;huggingface-hub&lt;/code&gt;, all installed by the second command&lt;/li&gt;
&lt;li&gt;The EmbeddingGemma 2 weights in your model zoo. Download them once (about 1.5 GB) so your first search doesn’t stall:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.zoo&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foz&lt;/span&gt;

&lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_zoo_model_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/harpreetsahota204/EmbeddingGemma2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_zoo_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/embeddinggemma-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A GPU is optional. The webcam and microphone options need the App on &lt;code&gt;localhost&lt;/code&gt; or https, which is a browser rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don’t want to install anything?
&lt;/h3&gt;

&lt;p&gt;Try the &lt;a href="https://huggingface.co/spaces/harpreetsahota/EmbeddingGemma-2-in-FiftyOne" rel="noopener noreferrer"&gt;EmbeddingGemma 2 in FiftyOne Space&lt;/a&gt; on Hugging Face and use the panel in your browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open the panel and build an index
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnk19tf69r0s47fcxbmyb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnk19tf69r0s47fcxbmyb.gif" alt="The Build Cross-Modal Index form, where you choose what part of each sample becomes its EmbeddingGemma 2 vector." width="640" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Build Cross-Modal Index form, where you choose what part of each sample becomes its EmbeddingGemma 2 vector.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Open a dataset in the App, click the + next to the grid’s tab, and choose &lt;strong&gt;Cross-Modal Search&lt;/strong&gt;. Indexes you built in Python with this model, such as the ones in Step 4, show up in the panel. To make a new one, click &lt;strong&gt;Build Cross-Modal Index&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you build an index, you choose what part of each sample becomes its vector:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embed the samples as&lt;/th&gt;
&lt;th&gt;What goes into the vector&lt;/th&gt;
&lt;th&gt;Finds samples by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;the image&lt;/td&gt;
&lt;td&gt;what they show&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;frames sampled from the video, with the sound ignored&lt;/td&gt;
&lt;td&gt;what they show&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;the soundtrack, or the audio file&lt;/td&gt;
&lt;td&gt;what they sound like&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video + audio&lt;/td&gt;
&lt;td&gt;frames and soundtrack together, as one vector&lt;/td&gt;
&lt;td&gt;both at once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The four ways to embed samples when you build a cross-modal index, with what goes into each vector and what it finds samples by.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The form also lets you pick &lt;strong&gt;Dimensions&lt;/strong&gt; (768, 512, 256, or 128), frame sampling for video, and how many seconds of audio to use. Building runs in the background if you have delegated operations set up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search with text
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvngoo3i0h5pl4z9z9bv9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvngoo3i0h5pl4z9z9bv9.gif" alt="Text searches in the Cross-Modal Retrieval panel, with a colored chip for each index's results." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Text searches in the Cross-Modal Retrieval panel, with a colored chip for each index's results.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;New Search&lt;/strong&gt;. Under &lt;strong&gt;Search against&lt;/strong&gt;, pick one or more indexes. Each returns its own matches. Choose &lt;strong&gt;Text&lt;/strong&gt;, describe what you want, and click &lt;strong&gt;Search&lt;/strong&gt;. Try “church bell ringing,” “rain on a window,” or “a crowd cheering.”&lt;/p&gt;

&lt;p&gt;The matches open in the grid, best first. A search across several indexes returns a colored chip for each index, and you can click chips to turn each index’s results on or off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search with other samples
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdh4yhzk4y4n79bhgadg0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdh4yhzk4y4n79bhgadg0.gif" alt="Searching with selected samples, which the panel averages into one query." width="640" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching with selected samples, which the panel averages into one query.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Select samples in the grid, then choose &lt;strong&gt;Samples&lt;/strong&gt; and search. The selected samples are excluded from the results. &lt;a href="https://voxel51.com/blog/few-shot-image-retrieval" rel="noopener noreferrer"&gt;Several selected samples are averaged into one query&lt;/a&gt;, so selecting three clips of dogs barking finds “dogs barking” better than a single example does.&lt;/p&gt;

&lt;p&gt;For video samples, &lt;strong&gt;embed your query as&lt;/strong&gt; chooses what part of the clip becomes the query: its frames, its sound, or both. Search an audio index with the clip's frames to find what sounds like what the clip shows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search with your own media
&lt;/h3&gt;

&lt;p&gt;Under &lt;strong&gt;Media&lt;/strong&gt;, use something that isn’t in the dataset. Nothing you upload or record is added to the dataset.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; an image, video, audio, or MCAP file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The microphone&lt;/strong&gt; records up to 30 seconds of audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The webcam&lt;/strong&gt; records up to 30 seconds of video with sound or takes a single photo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2ojlza5yyybazck0e45.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2ojlza5yyybazck0e45.gif" alt="A microphone recording searches the VGGSound clips directly, without being added to the dataset." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A microphone recording searches the VGGSound clips directly, without being added to the dataset.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The browser asks for permission the first time you use the webcam or microphone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tune the search and keep your results
&lt;/h3&gt;

&lt;p&gt;Set a &lt;strong&gt;Similarity threshold,&lt;/strong&gt; and the panel returns every sample from any index whose cosine similarity to your query meets it. The default is 0.65, which suits most searches, and strong cross-modal matches usually score between 0.6 and 0.9. Lower it if a search comes back empty. Each index returns at most 500 results.&lt;/p&gt;

&lt;p&gt;Every search is saved on the panel’s home page. Click one to bring its results back, &lt;strong&gt;Clone&lt;/strong&gt; one to tweak and rerun it, or &lt;strong&gt;Delete&lt;/strong&gt; the ones you don’t need.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmapuyygxbkm9snexu88b.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmapuyygxbkm9snexu88b.gif" alt="Saved searches on the Cross-Modal Retrieval panel's home page, ready to reopen, clone, or delete." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Saved searches on the Cross-Modal Retrieval panel's home page, ready to reopen, clone, or delete.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because every index is a standard FiftyOne similarity index, the App’s own similarity search and &lt;code&gt;view.sort_by_similarity()&lt;/code&gt; work on the same indexes the panel uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it honest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is one shard of VGGSound.&lt;/strong&gt; Everything here uses 10,000 of the 199,467 clips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read scores as a ranking.&lt;/strong&gt; Text-to-clip similarity sits in a narrow band, and matching clips score only slightly higher than the rest. Compare scores within one search, not across searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio-only is the weakest index on VGGSound.&lt;/strong&gt; The class names sound sources that are visible in the frames, so the video and video + audio indexes find them more reliably. Audio alone does better when the &lt;a href="https://voxel51.com/blog/whale-song-embeddings-fiftyone" rel="noopener noreferrer"&gt;classes are defined by sound&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;128 dimensions costs quality.&lt;/strong&gt; The model card reports a large drop for multimodal tasks at 128. Use 768 or 512 unless storage forces your hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval figures in this post come from a 100-clip sample.&lt;/strong&gt; The church bell and sound-to-frames charts illustrate behavior and aren’t benchmark results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/spaces/harpreetsahota/EmbeddingGemma-2-in-FiftyOne" rel="noopener noreferrer"&gt;EmbeddingGemma 2 in FiftyOne Space&lt;/a&gt;: the panel on VGGSound, no install&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/harpreetsahota204/cross_modal_similarity_search" rel="noopener noreferrer"&gt;Cross-Modal Retrieval plugin&lt;/a&gt;: the panel and operators&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/harpreetsahota204/EmbeddingGemma2" rel="noopener noreferrer"&gt;EmbeddingGemma 2 FiftyOne integration&lt;/a&gt;: the remote zoo model, with a notebook&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/datasets/harpreetsahota/VGGSound" rel="noopener noreferrer"&gt;VGGSound on Hugging Face&lt;/a&gt;: 10,000 clips with vectors, uniqueness, and representativeness scores&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/google/embeddinggemma-2" rel="noopener noreferrer"&gt;EmbeddingGemma 2 model card&lt;/a&gt; and &lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/embeddinggemma-2/" rel="noopener noreferrer"&gt;Google’s launch post&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.voxel51.com/user_guide/similarity.html" rel="noopener noreferrer"&gt;FiftyOne Brain similarity docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is EmbeddingGemma 2?
&lt;/h3&gt;

&lt;p&gt;EmbeddingGemma 2 is an open, 740M-parameter embedding model from Google DeepMind, released under Apache 2.0. It maps text, images, video, and audio into a single 768-dimensional vector space so that you can compare any modality with any other.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I use EmbeddingGemma 2 in FiftyOne?
&lt;/h3&gt;

&lt;p&gt;To use EmbeddingGemma 2 in FiftyOne, register the remote zoo source with &lt;code&gt;foz.register_zoo_model_source("https://github.com/harpreetsahota204/EmbeddingGemma2")&lt;/code&gt;, then load &lt;code&gt;google/embeddinggemma-2 with foz.load_zoo_model()&lt;/code&gt;. Use &lt;code&gt;dataset.compute_embeddings()&lt;/code&gt; for images and video, or &lt;code&gt;model.embed_dataset()&lt;/code&gt; for audio and video plus audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is cross-modal retrieval?
&lt;/h3&gt;

&lt;p&gt;Cross-modal retrieval is searching one media type with another, such as finding video clips with a text query or finding pictures with a sound. It works when one model embeds every modality into the same space, which EmbeddingGemma 2 does.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I build a multimodal index in FiftyOne with LanceDB?
&lt;/h3&gt;

&lt;p&gt;To build a multimodal index in FiftyOne with LanceDB, store embeddings in a vector field, then call &lt;code&gt;fob.compute_similarity(dataset, embeddings="eg2_av", model="google/embeddinggemma-2", model_kwargs={...}, backend="lancedb", brain_key="eg2_av")&lt;/code&gt;. Each index becomes its own LanceDB table so that you can keep video, audio, and video + audio indexes of the same dataset together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I search video by its sound?
&lt;/h3&gt;

&lt;p&gt;Yes, EmbeddingGemma 2 can search video by its sound. Build an audio index &lt;code&gt;(media_type="audio")&lt;/code&gt; or a video + audio index &lt;code&gt;(modalities=["vision", "audio"])&lt;/code&gt; and query with text, or query with another clip’s &lt;code&gt;eg2_audio&lt;/code&gt; vector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a GPU to run EmbeddingGemma 2?
&lt;/h3&gt;

&lt;p&gt;No. EmbeddingGemma 2 runs on CPU in float32 and uses bfloat16 on CUDA. A GPU makes embedding a large video dataset much faster. float16 is refused because it returns NaN or degraded vectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which embedding dimension should I use?
&lt;/h3&gt;

&lt;p&gt;Start with 768, the full EmbeddingGemma 2 dimension. Use 512 or 256 to cut storage and search time. At 128, multimodal quality drops noticeably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where does FiftyOne store LanceDB vectors?
&lt;/h3&gt;

&lt;p&gt;FiftyOne stores LanceDB vectors in the location set under &lt;code&gt;similarity_backends.lancedb.uri in ~/.fiftyone/brain_config.json&lt;/code&gt;, which defaults to &lt;code&gt;/tmp/lancedb&lt;/code&gt;. Point it somewhere permanent before you build indexes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why can’t I record from my webcam or microphone?
&lt;/h3&gt;

&lt;p&gt;Browsers allow camera and microphone access only on &lt;code&gt;localhost&lt;/code&gt; or https. Run the App locally or serve it over https.&lt;/p&gt;

</description>
      <category>embeddinggemma2</category>
      <category>computervision</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>One Camera Isn’t Enough: 9 Robotics Datasets You Can Play on One Timeline</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:10:05 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/one-camera-isnt-enough-9-robotics-datasets-you-can-play-on-one-timeline-1j1g</link>
      <guid>https://dev.to/harpreetsahota/one-camera-isnt-enough-9-robotics-datasets-you-can-play-on-one-timeline-1j1g</guid>
      <description>&lt;p&gt;&lt;em&gt;We combine event cameras, radar, thermal, tactile pads, and lidar into nine robotics datasets, each converted into FiftyOne multimodal episodes for synchronized sensor analysis.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the six sequences in the DSEC sample has a mean image brightness of 26.5. The other five range from 78.5 to 92.5.&lt;/p&gt;

&lt;p&gt;Same car, same cameras. The color images in that sequence are about a third as bright as the rest, and a pipeline tuned on the other five hasn’t seen anything like it. DSEC also records the road with a &lt;a href="https://voxel51.com/blog/exploring-octosense-multimodal-dataset-fiftyone" rel="noopener noreferrer"&gt;stereo pair of event cameras&lt;/a&gt; and ships lidar-derived disparity as ground truth, so when the color image degrades, you still have other sensors to check it against.&lt;/p&gt;

&lt;p&gt;That is the setup for every dataset in this post. A robot that leaves the lab encounters real-world conditions such as low light, water, snow, global navigation satellite system (GNSS) dropouts, and objects it must touch. These datasets, with ground truth or robot state, help you evaluate and improve your algorithms effectively.&lt;/p&gt;

&lt;p&gt;Each of the nine is a &lt;a href="https://voxel51.com/blog/fiftyone-supports-multimodal-data" rel="noopener noreferrer"&gt;FiftyOne multimodal dataset&lt;/a&gt;: one &lt;code&gt;.fo.mcap&lt;/code&gt; file per episode, with every sensor stream on &lt;a href="https://voxel51.com/sensor-synchronization" rel="noopener noreferrer"&gt;one shared clock&lt;/a&gt;, making it straightforward to access and work with the data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Sensors beyond RGB&lt;/th&gt;
&lt;th&gt;In this release&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DSEC&lt;/td&gt;
&lt;td&gt;Car&lt;/td&gt;
&lt;td&gt;Stereo event cameras, LiDAR&lt;/td&gt;
&lt;td&gt;6 of 53 sequences&lt;/td&gt;
&lt;td&gt;CC BY-SA 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M3ED&lt;/td&gt;
&lt;td&gt;Car, quadrotor, and Spot&lt;/td&gt;
&lt;td&gt;Stereo event cameras, LiDAR&lt;/td&gt;
&lt;td&gt;3 sequences, one per robot&lt;/td&gt;
&lt;td&gt;CC BY-SA 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edged-USLAM&lt;/td&gt;
&lt;td&gt;Quadrotor&lt;/td&gt;
&lt;td&gt;DAVIS346 event camera&lt;/td&gt;
&lt;td&gt;All 13 runs&lt;/td&gt;
&lt;td&gt;CC BY 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ColoRadar&lt;/td&gt;
&lt;td&gt;Handheld rig&lt;/td&gt;
&lt;td&gt;Two FMCW radars, LiDAR&lt;/td&gt;
&lt;td&gt;7 of 52 sequences&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CitrusFarm&lt;/td&gt;
&lt;td&gt;Clearpath Jackal&lt;/td&gt;
&lt;td&gt;Thermal, red-green-NIR, depth, LiDAR&lt;/td&gt;
&lt;td&gt;1 of 7 sequences&lt;/td&gt;
&lt;td&gt;CC BY-SA 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HapTile&lt;/td&gt;
&lt;td&gt;UR5e arm&lt;/td&gt;
&lt;td&gt;Vision-based tactile sensors, RGB-D&lt;/td&gt;
&lt;td&gt;1,699 episodes&lt;/td&gt;
&lt;td&gt;CC BY 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NTNU Underwater Multi-Camera&lt;/td&gt;
&lt;td&gt;BlueROV2 Heavy&lt;/td&gt;
&lt;td&gt;Five monochrome cameras, barometer, rangefinder&lt;/td&gt;
&lt;td&gt;All 8 runs&lt;/td&gt;
&lt;td&gt;BSD 3-Clause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GrandTour&lt;/td&gt;
&lt;td&gt;ANYmal D quadruped&lt;/td&gt;
&lt;td&gt;HDR and depth cameras, LiDAR, GNSS/INS&lt;/td&gt;
&lt;td&gt;3 of 49 missions&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TUM RGB-D&lt;/td&gt;
&lt;td&gt;Handheld and Pioneer robot&lt;/td&gt;
&lt;td&gt;Kinect color and depth&lt;/td&gt;
&lt;td&gt;47 main-table sequences&lt;/td&gt;
&lt;td&gt;CC BY 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The nine multimodal robotics datasets, with platform, sensors beyond RGB, how much of each release is included, and license.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DSEC, M3ED, and Edged-USLAM record every event as a &lt;code&gt;foxglove.PointCloud&lt;/code&gt; window of 1/30 s with &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;z&lt;/code&gt; (milliseconds since the window opened), and &lt;code&gt;polarity&lt;/code&gt;. In the &lt;code&gt;spot_indoor_stairwell&lt;/code&gt; sequence, it runs at 63.8 Hz, a high temporal resolution.&lt;/li&gt;
&lt;li&gt;ColoRadar, CitrusFarm, and HapTile cover radar, thermal and near-infrared (NIR), and touch: two frequency-modulated continuous-wave (FMCW) radars on a handheld rig, a thermal camera and a red-green-NIR camera on a Clearpath Jackal, and gel-pad tactile video on a UR5e gripper across 1,699 episodes.&lt;/li&gt;
&lt;li&gt;GrandTour and NTNU Underwater Multi-Camera test assumptions about the environment. The indoor GrandTour mission ETH-2 has 0 GNSS fixes, against 44,601 in the forest mission &lt;code&gt;ALB-3&lt;/code&gt;, and NTNU’s cameras sit behind a housing port that bends light underwater.&lt;/li&gt;
&lt;li&gt;TUM RGB-D offers 47 sequences spanning six categories, including nine with moving people and eight designed to separate structure from texture, showcasing the datasets' broad applicability for various research needs.&lt;/li&gt;
&lt;li&gt;Each of the nine robotics datasets is a FiftyOne dataset with &lt;code&gt;media_type="multimodal"&lt;/code&gt; and plain sample fields such as &lt;code&gt;peak_event_rate_mev_s&lt;/code&gt;, &lt;code&gt;lighting&lt;/code&gt;, &lt;code&gt;max_depth_m&lt;/code&gt;, and &lt;code&gt;haptic_feedback&lt;/code&gt;, so you can sort and filter recordings before opening a single file.&lt;/li&gt;
&lt;li&gt;Most of the nine robotics datasets are samples of larger releases: 6 of 53 DSEC sequences, 3 M3ED sequences, 7 of 52 ColoRadar sequences, 3 of 49 GrandTour missions, and 1 of 7 CitrusFarm sequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does a camera that reports changes record?
&lt;/h2&gt;

&lt;p&gt;An event camera doesn’t produce frames. Each pixel reports when its brightness changes, and nothing otherwise.&lt;/p&gt;

&lt;p&gt;Three of the nine datasets use one, and they use it in three different places.&lt;/p&gt;

&lt;h3&gt;
  
  
  DSEC: the driving sequence where the color image goes dark
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/DSEC-Sample" rel="noopener noreferrer"&gt;Voxel51/DSEC-Sample on Hugging Face&lt;/a&gt; | source: &lt;a href="https://dsec.ifi.uzh.ch/" rel="noopener noreferrer"&gt;DSEC&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4lla7nil7th6df394288.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4lla7nil7th6df394288.gif" alt="DSEC in the FiftyOne multimodal viewer" width="640" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DSEC in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DSEC is a driving dataset from the University of Zurich, featuring stereo Prophesee event cameras, global-shutter color cameras, LiDAR disparity ground truth, and &lt;a href="https://voxel51.com/optical-flow" rel="noopener noreferrer"&gt;optical flow&lt;/a&gt; in some sequences, totaling 6 sequences and over 2.2 billion events.&lt;/p&gt;

&lt;h3&gt;
  
  
  M3ED: one sensor head on a car, a quadrotor, and a Spot
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/M3ED-Sample" rel="noopener noreferrer"&gt;Voxel51/M3ED-Sample on Hugging Face&lt;/a&gt; | source: &lt;a href="https://m3ed.io/" rel="noopener noreferrer"&gt;m3ed.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4qo7b7ergegqtdpv2ls.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4qo7b7ergegqtdpv2ls.gif" alt="M3ED in the FiftyOne multimodal viewer" width="640" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;M3ED in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;M3ED comes from the GRASP Laboratory at the University of Pennsylvania. The sensor head has a stereo pair of Prophesee EVK4 HD event cameras at 1280x720, a grayscale stereo pair, a color camera at 1280x800, an inertial unit, and an Ouster OS1-64 LiDAR. The robot changes, and the rig doesn’t. The sample contains one sequence per robot: 192 seconds and 3,631,437,863 events total. The Spot sequence alone, &lt;code&gt;spot_indoor_stairwell&lt;/code&gt;, holds 1,883,139,312 events in 98.9 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edged-USLAM: what an event camera records as the lights drop below 5 lux
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/Edged-USLAM-Event-Camera" rel="noopener noreferrer"&gt;Voxel51/Edged-USLAM-Event-Camera on Hugging Face&lt;/a&gt; | source: &lt;a href="https://huggingface.co/datasets/sebnem-byte/drone-navigation-event-camera" rel="noopener noreferrer"&gt;sebnem-byte/drone-navigation-event-camera&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlwlcxr6idy5knw10w3g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlwlcxr6idy5knw10w3g.gif" alt="Edged-USLAM in the FiftyOne multimodal viewer" width="640" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Edged-USLAM in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Edged-USLAM is a DAVIS346 event camera on a quadrotor flying in a motion-capture room, recorded for an ICRA 2026 paper. Five of its 13 runs vary the lighting: 30% lighting, 60% lighting, blinking lights, strong side light, and under &lt;code&gt;5 lux&lt;/code&gt;. The &lt;code&gt;low_lit&lt;/code&gt; run has the lowest mean event rate of the five: 0.53 million events per second, compared with 0.69 to 0.80 for the others. The other eight runs include fly lines, squares, circles, aggressive turns, and manual flights, with the vehicle's Vicon pose recorded alongside them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. How are events stored in an MCAP file?&lt;/strong&gt; Each message holds one 1/30 s window as a &lt;code&gt;foxglove.PointCloud&lt;/code&gt;. &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; are pixel coordinates, &lt;code&gt;z&lt;/code&gt; is the time since the window opened (ms), and &lt;code&gt;polarity&lt;/code&gt; is 1 for a brightness increase and 0 for a decrease. The message is stamped when its window closes, so an event’s time is the stamp, minus 1/30 s, plus &lt;code&gt;z&lt;/code&gt;. A 3D panel set to the event's frame shows each window as a volume of pixel position against time. Each camera also gets an &lt;code&gt;/event-frames&lt;/code&gt; channel, a video render with ON events white and OFF events black on gray.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do radar, thermal, and touch see that a color camera can’t?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ColoRadar: two radars and a lidar, carried through an underground mine
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/ColoRadar-Sample" rel="noopener noreferrer"&gt;Voxel51/ColoRadar-Sample on Hugging Face&lt;/a&gt; | source: &lt;a href="https://arpg.colorado.edu/coloradar/" rel="noopener noreferrer"&gt;ColoRadar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fheb2xl5yi6y3su4az5o2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fheb2xl5yi6y3su4az5o2.gif" alt="ColoRadar in the FiftyOne multimodal viewer" width="640" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ColoRadar in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;ColoRadar comes from the Autonomous Robotics and Perception Group at the University of Colorado Boulder. A handheld rig carries two &lt;a href="https://voxel51.com/radar" rel="noopener noreferrer"&gt;FMCW radars&lt;/a&gt;, a TI MMWCAS-RF-EVM cascaded imaging radar and a TI AWR1843BOOST single-chip radar, next to an Ouster OS1-64 LiDAR and a Lord Microstrain 3DM-GX5-25 inertial unit. The release includes 52 sequences across seven settings: hallways, a lab, a large motion-capture space, outdoor built environments, the narrow and wide passages of an underground mine, and a fast ride along paths and roads. The sample takes the shortest sequence of each place: 7 sequences, 13m 46s, 8,263 LiDAR scans holding 382.6 million points, 4,145 cascaded radar frames, and 8,340 single-chip radar scans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; The cascaded radar publishes a heatmap of 128 range by 128 azimuth by 32 elevation cells, not a point cloud. The &lt;code&gt;/cascade-radar-points&lt;/code&gt; channel is derived from those heatmaps the way the release’s plotting tool derives them. It is not raw sensor output. The &lt;code&gt;/cascade-radar-bev&lt;/code&gt; channel is a top-down render of the same heatmaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  CitrusFarm: a Jackal in the orchard rows with thermal and near-infrared
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/CitrusFarm-Sample" rel="noopener noreferrer"&gt;Voxel51/CitrusFarm-Sample on Hugging Face&lt;/a&gt; | source: &lt;a href="https://ucr-robotics.github.io/Citrus-Farm-Dataset/" rel="noopener noreferrer"&gt;CitrusFarm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43ubqxusjue197x58ak1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43ubqxusjue197x58ak1.gif" alt="CitrusFarm in the FiftyOne multimodal viewer" width="640" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;CitrusFarm in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CitrusFarm is a ground robot in the rows of a citrus farm, from the ARCS Lab at the University of California Riverside. A Clearpath Jackal carries a FLIR Blackfly monochrome camera, a FLIR ADK thermal camera, a Mapir Survey3 camera that records red, green, and near-infrared, a Stereolabs ZED 2i with depth, a Velodyne LiDAR, a Microstrain inertial unit, and a Piksi GPS-RTK receiver. The sample is the smallest of seven sequences: &lt;code&gt;06_14B_Jackal&lt;/code&gt;, 5m 03s over 357 m, with 3,024 thermal frames, 3,025 red-green-NIR frames, 2,998 LiDAR scans, and 3,024 GPS-RTK fixes. The ZED depth holds values for 47% of pixels (&lt;code&gt;valid_depth_fraction =&lt;/code&gt; 0.4703).&lt;/p&gt;

&lt;h3&gt;
  
  
  HapTile: 1,699 episodes with the operator’s haptic feedback recorded
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/HapTile" rel="noopener noreferrer"&gt;Voxel51/HapTile on Hugging Face&lt;/a&gt; | source: &lt;a href="https://huggingface.co/datasets/HapTile2026/HapTile" rel="noopener noreferrer"&gt;HapTile2026/HapTile&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fal0ca43wmnozq3p61clr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fal0ca43wmnozq3p61clr.gif" alt="HapTile in the FiftyOne multimodal viewer" width="640" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;HapTile in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;HapTile moves the question from seeing to touching. A UR5e arm with a Robotiq 2F-85 gripper works through 38 contact-rich tabletop tasks under &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;teleoperation&lt;/a&gt;. Each finger carries a &lt;a href="https://voxel51.com/blog/tactile-datasets-robot-learning-lerobot" rel="noopener noreferrer"&gt;vision-based tactile sensor&lt;/a&gt; that films a marker-grid-printed gel pad, so contact shows up as markers moving. Two RGB-D cameras watch the scene, and we record the operator's haptic feedback alongside the robot state. That is 1,699 episodes, 664,045 frames, and 12.36 hours.&lt;/p&gt;

&lt;p&gt;The feedback levels live in a &lt;code&gt;haptic_feedback&lt;/code&gt; list on each sample. &lt;code&gt;Firm&lt;/code&gt; appears in 1,481 episodes, &lt;code&gt;soft&lt;/code&gt; in 575, and &lt;code&gt;mild&lt;/code&gt; in 414. The 47 &lt;code&gt;move_mobile_box&lt;/code&gt; episodes have no haptic channel at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks when the robot goes underwater, indoors, or onto snow?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NTNU Underwater Multi-Camera: five cameras behind a housing port
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/NTNU-Underwater-Multicam" rel="noopener noreferrer"&gt;Voxel51/NTNU-Underwater-Multicam on Hugging Face&lt;/a&gt; | source: &lt;a href="https://huggingface.co/datasets/ntnu-arl/underwater-datasets" rel="noopener noreferrer"&gt;ntnu-arl/underwater-datasets&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzgra2x72iww2n5s1085b.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzgra2x72iww2n5s1085b.gif" alt="NTNU Underwater Multi-Camera in the FiftyOne multimodal viewer" width="640" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NTNU Underwater Multi-Camera in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;NTNU’s dataset is Ariel, a BlueROV2 Heavy from the Autonomous Robots Lab, piloted by hand through the Trondheim Fjord (6 runs) and the lab’s Marine Cybernetics Laboratory pool (2 runs). An Alphasense rig records five monochrome cameras at 720x540 and 20 Hz. The autopilot logs a second inertial unit, a barometer, a downward rangefinder, the battery, and eight thruster outputs. Each run includes a reference trajectory the authors computed with ReAqROVIO. In total, that is 63m 19s, 379,896 camera frames, and 759,943 inertial samples. The fjord runs reach 7.1 to 7.7 m deep. The two pool runs reach 0.5 and 0.6 m.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; The release calibrates the stereo pair underwater and the other three cameras in air only. Refraction at the housing port changes intrinsics once the vehicle is submerged, so those three carry in-air values. The &lt;code&gt;depth_m&lt;/code&gt; track applies the release’s freshwater barometer mapping, which reads a little deep in the fjord’s salt water.&lt;/p&gt;

&lt;h3&gt;
  
  
  GrandTour: the same robot in a forest, a building, and snow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/GrandTour-Sample" rel="noopener noreferrer"&gt;Voxel51/GrandTour-Sample on Hugging Face&lt;/a&gt; | source: &lt;a href="https://huggingface.co/datasets/leggedrobotics/grand_tour_dataset" rel="noopener noreferrer"&gt;leggedrobotics/grand_tour_dataset&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7hpuifvicqkgsfpem5gg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7hpuifvicqkgsfpem5gg.gif" alt="GrandTour in the FiftyOne multimodal viewer" width="640" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GrandTour in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GrandTour is an ANYmal D quadruped from the Robotic Systems Lab at ETH Zurich carrying the Boxi sensor payload: HDR and depth cameras, a Hesai LiDAR, a tactical-grade inertial unit, and a GNSS/INS receiver, with a total station tracking a prism on the payload for reference. The release holds 49 missions. The sample carries 3 chosen to contrast: &lt;code&gt;ALB-3&lt;/code&gt; in a forest, &lt;code&gt;ETH-2&lt;/code&gt; inside the ETH main building, and &lt;code&gt;SNOW-1&lt;/code&gt; on the Jungfraujoch.&lt;/p&gt;

&lt;p&gt;The indoor mission is where GNSS disappears. The &lt;code&gt;GNSS&lt;/code&gt; field reads &lt;code&gt;Working&lt;/code&gt; for the forest and the snow and &lt;code&gt;No&lt;/code&gt; for &lt;code&gt;ETH-2&lt;/code&gt;, which has 0 fixes against 44,601 and 37,201. The robot’s &lt;code&gt;/legged-odometry&lt;/code&gt;, &lt;code&gt;/lidar-odometry estimate&lt;/code&gt;, and &lt;code&gt;/gnss-ins-odometry&lt;/code&gt; solution are separate pose channels, so you can overlay three estimates of the same walk and see where each one drifts from the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; The sample includes only the three HDR cameras and the upper front depth camera. The Alphasense cameras, ZED 2i, other depth cameras, Livox and Velodyne LiDARs, and the point cloud maps are in the full release but not here. For &lt;code&gt;ETH-2&lt;/code&gt;, the release notes say its HDR timestamps are imprecise because they use the GMSL2 arrival time. That note is carried in the &lt;code&gt;release_notes&lt;/code&gt; field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you grade a trajectory against?
&lt;/h2&gt;

&lt;p&gt;Every dataset in this post ships an answer key, and they are not the same kind of key. TUM RGB-D is the plainest case.&lt;/p&gt;

&lt;h3&gt;
  
  
  TUM RGB-D: a Kinect graded by an eight-camera motion-capture system
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/TUM-RGBD" rel="noopener noreferrer"&gt;Voxel51/TUM-RGBD on Hugging Face&lt;/a&gt; | source: &lt;a href="https://cvg.cit.tum.de/data/datasets/rgbd-dataset" rel="noopener noreferrer"&gt;TUM RGB-D benchmark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foh5nefiw824l4sf1tje5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foh5nefiw824l4sf1tje5.gif" alt="TUM RGB-D in the FiftyOne multimodal viewer" width="640" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TUM RGB-D in the FiftyOne multimodal viewer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TUM RGB-D comes from the Computer Vision Group at the Technical University of Munich. A Microsoft Kinect records color and depth at 640x480 and 30 Hz while an eight-camera motion-capture system tracks it at 100 Hz. Three Kinects, named &lt;code&gt;freiburg1&lt;/code&gt;, &lt;code&gt;freiburg2&lt;/code&gt;, and &lt;code&gt;freiburg3&lt;/code&gt;, are carried handheld and on a Pioneer robot over scenes built to test structure against texture, moving people, and object reconstruction.&lt;/p&gt;

&lt;p&gt;This conversion includes the 47 sequences in the benchmark’s main table: 9 &lt;code&gt;freiburg1&lt;/code&gt;, 18 &lt;code&gt;freiburg2&lt;/code&gt;, and 20 &lt;code&gt;freiburg3&lt;/code&gt;, totaling 48m 12s, with 81,413 color frames and 80,683 depth frames. The &lt;code&gt;category&lt;/code&gt; field groups them into Handheld SLAM (11 sequences), 3D Object Reconstruction (11), Dynamic Objects (9), Structure vs.&amp;nbsp;Texture (8), Robot SLAM (4), and Testing and Debugging (4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; The validation sequences, whose ground truth the benchmark withholds, are not included. The &lt;code&gt;freiburg3&lt;/code&gt; sequences have no accelerometer samples because the benchmark’s archives hold none.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each dataset grades you against
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;Ground truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DSEC&lt;/td&gt;
&lt;td&gt;LiDAR-derived disparity for both stereo pairs, and optical flow on some sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M3ED&lt;/td&gt;
&lt;td&gt;Left event camera ground-truth poses and depth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edged-USLAM&lt;/td&gt;
&lt;td&gt;Vicon pose of the vehicle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ColoRadar&lt;/td&gt;
&lt;td&gt;LiDAR-inertial trajectory, plus a Vicon pose on &lt;code&gt;aspen_run9&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CitrusFarm&lt;/td&gt;
&lt;td&gt;A ground-truth position track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HapTile&lt;/td&gt;
&lt;td&gt;Robot state: end-effector pose, joints, commanded targets, and gripper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NTNU&lt;/td&gt;
&lt;td&gt;ReAqROVIO reference trajectory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GrandTour&lt;/td&gt;
&lt;td&gt;Total-station prism positions, plus the three odometry estimates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TUM RGB-D&lt;/td&gt;
&lt;td&gt;Motion-capture pose of the Kinect at 100 Hz&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The ground truth or reference signal that ships with each of the nine multimodal robotics datasets.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Load a multimodal robotics dataset from Hugging Face
&lt;/h2&gt;

&lt;p&gt;Each dataset loads from the &lt;a href="https://docs.voxel51.com/integrations/huggingface.html" rel="noopener noreferrer"&gt;Hugging Face Hub&lt;/a&gt; with one call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.utils.huggingface&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fouh&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ViewField&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;dsec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fouh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_from_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/DSEC-Sample&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DSEC-Sample&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# multimodal
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsec&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;# 6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One sample is one sequence, and &lt;code&gt;filepath&lt;/code&gt; points to its &lt;a href="https://voxel51.com/blog/mcap-import-fiftyone-agent-skills" rel="noopener noreferrer"&gt;&lt;code&gt;.fo.mcap file&lt;/code&gt;&lt;/a&gt;. The datasets have no FiftyOne label fields. Every signal, including ground truth, is a channel inside the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Query the sample fields before you open a file
&lt;/h2&gt;

&lt;p&gt;The sample fields summarize each recording, so you can find the one you want first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The sequence with the darkest color images
&lt;/span&gt;&lt;span class="n"&gt;dsec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_image_brightness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;  &lt;span class="c1"&gt;# zurich_city_09_b
&lt;/span&gt;
&lt;span class="c1"&gt;# The Edged-USLAM run flown under 5 lux
&lt;/span&gt;&lt;span class="n"&gt;edged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fouh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_from_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/Edged-USLAM-Event-Camera&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Edged-USLAM-Event-Camera&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;edged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lighting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;under 5 lux&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# HapTile episodes where the operator felt firm contact
&lt;/span&gt;&lt;span class="n"&gt;haptile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fouh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_from_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/HapTile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HapTile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;firm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;haptile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;haptic_feedback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 1,481 episodes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then launch the App and open a sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;multimodal viewer&lt;/a&gt; lays out the channels as tiles with a shared playback clock, so a color frame, an event window, and a ground-truth plot move together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Each dataset above links to its Hugging Face repo and its source release. Pick the one that breaks your current pipeline, load it with the snippet in Step 1, and &lt;a href="https://voxel51.com/blog/view-mcap-files-fiftyone" rel="noopener noreferrer"&gt;open a sample in the App&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a FiftyOne multimodal dataset?
&lt;/h3&gt;

&lt;p&gt;A FiftyOne multimodal dataset is a dataset with &lt;code&gt;media_type="multimodal"&lt;/code&gt;, where each sample is one recording stored as an &lt;a href="https://voxel51.com/mcap" rel="noopener noreferrer"&gt;MCAP file&lt;/a&gt;. The App’s viewer reads the file's channels and displays them as tiles on a single timeline. Each sample also carries ordinary fields, such as &lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;num_events&lt;/code&gt;, or &lt;code&gt;ground_truth_path_m&lt;/code&gt;, that you can query like any other FiftyOne field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do these datasets have labels I can evaluate a model against?
&lt;/h3&gt;

&lt;p&gt;The nine robotics datasets have ground truth, but not as FiftyOne label fields. The ground truth, such as disparity, poses, depth, or reference trajectories, comes from each source release and lives in channels such as &lt;code&gt;/ground-truth&lt;/code&gt; and &lt;code&gt;/disparity-event&lt;/code&gt;. Nothing here is model output. The one derived channel is &lt;code&gt;/cascade-radar-points&lt;/code&gt; in ColoRadar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are these the full datasets?
&lt;/h3&gt;

&lt;p&gt;Four of the nine robotics datasets are complete, and five are samples. TUM RGB-D carries the benchmark’s 47 main-table sequences, NTNU carries all 8 runs, Edged-USLAM carries all 13 runs, and HapTile carries 1,699 episodes. DSEC (6 of 53 sequences), M3ED (3 sequences), ColoRadar (7 of 52), GrandTour (3 of 49), and CitrusFarm (1 of 7) are samples. The cards for those say what was left out.&lt;/p&gt;

&lt;h3&gt;
  
  
  What licenses do they use?
&lt;/h3&gt;

&lt;p&gt;The nine robotics datasets use the licenses of their source releases. NTNU is BSD 3-Clause. M3ED, DSEC, and CitrusFarm are CC BY-SA 4.0. TUM RGB-D, Edged-USLAM, and HapTile are CC BY 4.0. ColoRadar is Apache 2.0, and GrandTour is MIT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one should I start with?
&lt;/h3&gt;

&lt;p&gt;Start with the robotics dataset that breaks your current pipeline. Low light points to Edged-USLAM, GNSS-denied walking to GrandTour ETH-2, underwater refraction to NTNU, and contact to HapTile.&lt;/p&gt;

</description>
      <category>eventcameras</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>python</category>
    </item>
    <item>
      <title>Robots Can See. They Still Can't Feel: 8 Tactile LeRobot Datasets in FiftyOne</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:09:04 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/robots-can-see-they-still-cant-feel-8-tactile-lerobot-datasets-in-fiftyone-159b</link>
      <guid>https://dev.to/harpreetsahota/robots-can-see-they-still-cant-feel-8-tactile-lerobot-datasets-in-fiftyone-159b</guid>
      <description>&lt;p&gt;&lt;em&gt;A field guide to touch as a robot-learning modality, plus 8 LeRobot v3.0 datasets that record it (1,679 episodes of gel-sensor video, taxel grids, and fingertip force/torque), loaded into FiftyOne with their exact keys and quirks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Try tying your shoes with numb fingers. You can watch your hands the whole time and still fumble it, because the information you need lives at the point of contact. Is the lace slipping? Is the knot tight? How hard am I pulling? Your eyes can't answer any of that. Your fingertips answer it constantly, and you never notice.&lt;/p&gt;

&lt;p&gt;That's roughly where most robot learning sits today. &lt;a href="https://voxel51.com/vision-language-action-model-vla" rel="noopener noreferrer"&gt;Vision-language-action models&lt;/a&gt; have gotten good at the "where": where's the cup, where's the gripper, where should the arm go next. They're still mostly blind to what's happening at the contact surface.&lt;/p&gt;

&lt;p&gt;Tactile sensing fills that gap. Getting good tactile data, and then making sense of it, is one of the more interesting data problems in &lt;a href="https://voxel51.com/physical-ai" rel="noopener noreferrer"&gt;physical AI&lt;/a&gt; right now.&lt;/p&gt;

&lt;p&gt;We pulled together 8 tactile datasets that ship as &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;LeRobot&lt;/a&gt; v3.0, from a bimanual Flexiv rig packing earbuds into cases to a handheld gripper with two taxel pads, and loaded each one into FiftyOne. This guide covers why touch matters, what tactile data looks like on disk, why it's hard to collect and harder to read, and what to check before you train on it. Each dataset links to its FiftyOne version, its source repo, and its license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;8 tactile LeRobot datasets, 1,679 episodes, and 1,877,818 frames, all loadable with &lt;a href="https://docs.voxel51.com/integrations/huggingface.html" rel="noopener noreferrer"&gt;&lt;code&gt;fiftyone.utils.huggingface.load_from_hub&lt;/code&gt;&lt;/a&gt;. Xense's earbud-case insertion set alone holds 1,213,518 of those frames (65%).&lt;/li&gt;
&lt;li&gt;Tactile data shows up in three shapes. 7 of the 8 store gel-sensor touch as video under &lt;code&gt;observation.images.*&lt;/code&gt;, 4 carry force/torque vectors, and only Grabette stores taxel grids (6×6 and 4×8 int16). Tactile video plays in FiftyOne like any camera, and numeric tactile streams can be plotted in the episode viewer, but they stay in parquet and don't become sample fields you can filter on.&lt;/li&gt;
&lt;li&gt;There's no naming standard for tactile keys, even inside these 8 datasets. &lt;code&gt;observation.tactile&lt;/code&gt; is a 60-D fingertip force/torque vector in SharpaDex and the prefix of a taxel grid in Grabette.&lt;/li&gt;
&lt;li&gt;In the one SharpaDex season here (&lt;code&gt;assemble_gears_on_base&lt;/code&gt;, 410 episodes), the right index finger and right thumb register contact in 61% and 42% of 302,020 frames. Five of the ten fingertips rarely do.&lt;/li&gt;
&lt;li&gt;Licenses: Apache-2.0 for the three Xense sets and Grabette, CC-BY-4.0 for SharpaDex, and none declared for the three Intern sets.&lt;/li&gt;
&lt;li&gt;The evidence that tactile input helps robot policies is real but conditional. LeFlexiTac reports 7/30 → 23/30 on an in-bag pen task, FreeTacMan reports 21% → 71% average success, and an open T-Rex issue measured an offline benefit within ±0.9% on the full Sharpa data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why touch matters for robot learning
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbhzokas7pklbnt1zvqbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbhzokas7pklbnt1zvqbb.png" alt="Four-column diagram comparing what a camera sees with what a fingertip tactile sensor measures for occlusion, slip, force, and material. The camera frames look the same in each case, while the tactile signal shows the contact patch, rising shear, normal force, and stiffness." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In all four cases the camera frames look the same. The tactile signal is where the difference shows up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vision fails at the moment of contact.&lt;/strong&gt; When a gripper closes on an object, the fingers usually block the camera's view of what you care about. Inserting a plug, threading a cable, unscrewing a cap: the critical action happens in a region the camera can't see well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slip is invisible.&lt;/strong&gt; An object starting to slide out of a grasp often doesn't look like anything until it's already falling. A tactile sensor picks up the shear well before that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Force is not a pixel.&lt;/strong&gt; Wiping a table, peeling a sticker, or holding an egg all need the right amount of pressure. You can't reliably &lt;a href="https://voxel51.com/blog/egocentric-emg-force-dataset-fiftyone" rel="noopener noreferrer"&gt;infer pressure from an RGB frame&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Materials feel different than they look.&lt;/strong&gt; A soft plastic bottle and a rigid one can look identical. They respond very differently when squeezed.&lt;/p&gt;

&lt;p&gt;The early results back this up. LeFlexiTac added a FlexiTac sensor (a 12×32 taxel map) to the low-cost SO-100/SO-101 arm and fed it to four policy families through a single &lt;code&gt;observation.tactile.primary&lt;/code&gt; stream. On an in-bag pen retrieval task, where the camera can't see what the gripper is reaching for, vision-only Action Chunking with Transformers (ACT) succeeded 7 of 30 times. With tactile input, it succeeded 23 of 30. FreeTacMan ran five contact-rich tasks with ACT: vision-only averaged 21% success, adding tactile input raised that to 55%, and adding their visuo-tactile pretraining raised it to 71%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; An open issue on the T-Rex GitHub repo reports training T-Rex on the full Sharpa dataset (64 GPUs, six epochs) and comparing real tactile input against neutral input on 64 fixed samples. Overall action mean squared error (MSE) dropped from 0.0768 to 0.0295 across checkpoints, but the benefit of real touch over neutral input swung between −0.90% and +0.89%. That's offline action prediction, not closed-loop success, which is where touch is supposed to pay off. My read isn't that touch doesn't help. Touch helps when the task needs it and when the data is clean enough for a model to learn from it. That second condition is where most of the work is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three shapes of tactile data
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfpmcu6r1zwg3jm0j7fo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfpmcu6r1zwg3jm0j7fo.png" alt="Three cards showing the shapes tactile data takes on disk: image or video from camera-based gel sensors (7 of 8 datasets), taxel grids from pads of pressure cells (1 of 8), and force/torque vectors (4 of 8), each with an example and how to view it." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The three shapes of tactile data across these 8 LeRobot datasets, with an example of each and how to view it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Tactile" isn't one data type. Depending on the sensor, it lands on disk in one of three shapes, and each one needs a different way of looking at it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Images or video&lt;/strong&gt; from camera-based gel sensors. These at least look like something, though a raw gel frame is hard to read without context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-resolution arrays&lt;/strong&gt; from taxel pads and gloves, such as a 12×32 or 6×6 grid of pressure values. You can't view these as an image until you turn them into a heatmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time series of force and torque.&lt;/strong&gt; These only make sense plotted over time, next to what the robot was doing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How a gel sensor turns a press into an image
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmz7nfjltparsh0hpg4nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmz7nfjltparsh0hpg4nt.png" alt="Cross-section of a camera-based gel tactile sensor. An object presses into a soft gel with a reflective coating, LEDs light the gel from the sides through a clear support, and a camera underneath films it. On the right, the raw frame is stored as tactile_raw, and the difference from a no-contact frame as tactile_deform or tactile_*_aug_diff." width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How a camera-based gel sensor turns a press into an image, and which LeRobot keys store the raw frame and the difference image.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most of the touch in this roundup comes from camera-based sensors. A soft gel with a reflective coating sits on a clear support. LEDs light it from the edges, often in three colors, and a camera underneath films the coating. When an object presses in, the coating bends, the shading shifts, and the camera records it. Subtract a frame taken with nothing touching the sensor, and you get a map of where the gel moved.&lt;/p&gt;

&lt;p&gt;That's why datasets often ship more than one stream per sensor. SharpaDex stores &lt;code&gt;observation.images.tactile_raw&lt;/code&gt; (the camera's view), &lt;code&gt;observation.images.tactile_deform&lt;/code&gt; (a deformation map), and a 60-D force/torque signal in &lt;code&gt;observation.tactile&lt;/code&gt;. The Intern sets store &lt;code&gt;observation.images.tactile_*_aug_diff&lt;/code&gt;, which by its name looks like a difference image, though the card only calls them "touch images."&lt;/p&gt;

&lt;h3&gt;
  
  
  No standard sensor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzu66xq69whdjn1q1ar3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzu66xq69whdjn1q1ar3.png" alt="Comparison of four tactile sensor families (camera-based gel, taxel pad, force/torque, and pressure glove) by output, resolution, what each is good at, how each wears out or fails, and examples such as GelSight, DIGIT, Xense, FlexiTac, and Sharpa." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Four tactile sensor families, what each one outputs, and how each one wears out or fails.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Vision has converged on "an RGB camera." Touch hasn't converged on anything. You've got camera-based gel sensors (GelSight, DIGIT, Xense), taxel pads, fingertip sensors that report six-axis force and torque, and full-hand pressure gloves. The OmniViTac dataset alone uses four different visuo-tactile sensor types. Each produces data with a different shape, resolution, and meaning, and each fails in its own way.&lt;/p&gt;

&lt;p&gt;Gels tear and degrade. The lighting inside camera-based sensors shifts. Baselines move between sessions. If you've done any work with lab assays, you'll recognize this immediately: it's a batch effect. Data collected on Tuesday with a fresh gel doesn't look like data collected on Friday with a worn one, and a model will happily learn that difference instead of the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tactile data is hard to collect and harder to read
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79d96gz4yg419c64tcog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79d96gz4yg419c64tcog.png" alt="Pipeline from Collect to Store to View to Train, listing three tactile data failure points at each stage, such as operators who can't feel the contact, one modality with many key names, taxel grids that aren't images, and frame-random splits that leak, plus what each stage costs you." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where tactile data goes wrong at each stage, from collection to training.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teleoperators can't feel what the robot feels.&lt;/strong&gt; Most demonstrations come from &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;a human driving a robot remotely&lt;/a&gt;. Without haptic feedback, operators guess at contact forces, so demonstrations are only as good as those guesses. Projects such as HapTile are starting to feed haptic signals back to the operator, but that's still the exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human gloves have an embodiment gap.&lt;/strong&gt; Tactile gloves let you collect natural human manipulation at scale. But a human hand isn't a robot hand, and a glove's pressure map doesn't translate cleanly to a two-finger gripper or a 22-degree-of-freedom (DoF) dexterous hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale is still small.&lt;/strong&gt; &lt;a href="https://voxel51.com/open-x-embodiment" rel="noopener noreferrer"&gt;Open X-Embodiment&lt;/a&gt; pooled more than 1 million real robot trajectories across 22 embodiments. The largest public tactile datasets are in the thousands to tens of thousands of episodes: SharpaDex v1.0 has 28,993, and T-Rex has 5,464. The 8 datasets in this roundup add up to 1,679. Several announced tactile datasets, such as RoboTacDex and DexViTac, say they'll release their data later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Formats are inconsistent, even within one standard.&lt;/strong&gt; &lt;a href="https://voxel51.com/blog/lerobot-dataset-support-voxel51" rel="noopener noreferrer"&gt;LeRobot has become the default format for robot-learning data&lt;/a&gt;, which helps. It doesn't settle where touch goes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7hrij7n06m264e7veiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7hrij7n06m264e7veiv.png" alt="Tactile keys from the 8 LeRobot datasets mapped to three data shapes. Xense, Intern, and SharpaDex gel streams map to image/video, Intern's observation.wrench and SharpaDex's observation.tactile map to force/torque, and Grabette's observation.tactile.right_sensor_1 and right_sensor_2 map to taxel grids." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same key, observation.tactile, holds a force vector in SharpaDex and a taxel-grid prefix in Grabette.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Inside these 8 datasets alone, &lt;code&gt;observation.tactile&lt;/code&gt; means a force vector in one and a taxel-grid prefix in another. Xense names its overhead camera head in two sets and top in the third. All three Intern sources stored their task text as a pandas index in &lt;code&gt;meta/tasks.parquet&lt;/code&gt;, which had to be rewritten before the standard loader would read it. Their tactile video was MJPEG inside MP4, which browsers can't play, so the FiftyOne versions are H.264 re-encodes. Outside this list, &lt;a href="https://voxel51.com/blog/humanoid-lerobot-datasets-vla-training" rel="noopener noreferrer"&gt;T-Rex encodes its tactile streams losslessly&lt;/a&gt; in an H.264 profile most browsers can't decode either.&lt;/p&gt;

&lt;p&gt;None of these are bugs exactly. They're reasonable decisions made by different people. Together they mean that before you can learn anything from tactile data, you spend a lot of time getting it to show up correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synchronization is easy to get wrong.&lt;/strong&gt; Tactile sensors, cameras, and robot state often run at different rates. If the tactile spike from a grasp lands a few frames away from the gripper-close event, the model learns the wrong cause and effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FiftyOne LeRobot schema for tactile data
&lt;/h2&gt;

&lt;p&gt;All 8 datasets are loaded into FiftyOne with &lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt;, the same way as in our &lt;a href="https://voxel51.com/blog/lerobot-v3-datasets-vla-training-fiftyone" rel="noopener noreferrer"&gt;11-dataset LeRobot v3.0 roundup&lt;/a&gt;. Each one is a &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;multimodal dataset&lt;/a&gt; where &lt;strong&gt;one sample equals one episode&lt;/strong&gt;, with the same sample-level fields: &lt;code&gt;media_reference&lt;/code&gt;, &lt;code&gt;episode_index&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;tasks&lt;/code&gt;, &lt;code&gt;length&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;robot_type&lt;/code&gt;, and &lt;code&gt;fps&lt;/code&gt;. Per-frame data stays in the LeRobot &lt;code&gt;data/*.parquet&lt;/code&gt; and &lt;code&gt;videos/*/*.mp4&lt;/code&gt; files, and media_reference points into them.&lt;/p&gt;

&lt;p&gt;What changes for touch is where each shape ends up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Tactile shape&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Example key&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;In FiftyOne&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gel-sensor video&lt;/td&gt;
&lt;td&gt;&lt;code&gt;observation.images.left_tactile_left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A video stream in the episode viewer, next to the RGB cameras&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force/torque vector&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;observation.tactile&lt;/code&gt; (SharpaDex), &lt;code&gt;observation.wrench&lt;/code&gt; (Intern)&lt;/td&gt;
&lt;td&gt;Per-dimension series in the viewer's Plot and Message tiles. Stays in &lt;code&gt;data/*.parquet&lt;/code&gt;, not a sample field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taxel grid&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;observation.tactile.right_sensor_1&lt;/code&gt; (Grabette)&lt;/td&gt;
&lt;td&gt;Flattened to one series per taxel (36 for a 6×6 grid) in the Plot and Message tiles. Stays in &lt;code&gt;data/*.parquet&lt;/code&gt;, not a sample field&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Where touch lands when a LeRobot dataset is loaded into FiftyOne.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Where touch lands when a LeRobot dataset is loaded into FiftyOne.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. Can you filter on numeric tactile data?&lt;/strong&gt; Not directly. The episode viewer reads every numeric feature from parquet, so you can plot any taxel or force axis over time and step through exact values. FiftyOne's queries and aggregations, though, run on sample fields in its database, and LeRobot per-frame data deliberately stays in the source files behind &lt;code&gt;media_reference&lt;/code&gt;. To sort or filter episodes by touch, reduce each stream to a few per-episode numbers and write them back as fields (see the curation section below). A plot tile also shows a grid as 36 separate lines, not a heatmap, so render it yourself if you want to see the contact patch.&lt;/p&gt;

&lt;p&gt;Loading any of the 8 takes two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone.utils.huggingface&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_from_hub&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_from_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/xense-strap-tape-120ep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 8 tactile datasets at a glance
&lt;/h2&gt;

&lt;p&gt;All 8 are real-world data, and 7 are complete imports of their source repos. The exception is SharpaDex: the FiftyOne version is one collection season (19.6 GB) of a 3.4 TB release.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dataset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Task&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Tactile streams&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Episodes/frames&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Robot&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Xense Earbud Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Insert earbuds into cases, close the lid, and box them&lt;/td&gt;
&lt;td&gt;4 gel videos (400×700)&lt;/td&gt;
&lt;td&gt;834 / 1,213,518&lt;/td&gt;
&lt;td&gt;Bimanual Flexiv Rizon 4&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Xense Strap Tape&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Grab the tape and strap it&lt;/td&gt;
&lt;td&gt;4 gel videos (400×700)&lt;/td&gt;
&lt;td&gt;120 / 191,261&lt;/td&gt;
&lt;td&gt;Bimanual Flexiv Rizon 4&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Xense Tron2 RT Pick and Place&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Put the block into the box&lt;/td&gt;
&lt;td&gt;4 gel videos (400×700)&lt;/td&gt;
&lt;td&gt;115 / 48,999&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tron2_rt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intern Pour&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pour water from a cup into a beaker&lt;/td&gt;
&lt;td&gt;2 touch-image videos (700×400) + 6-D wrench&lt;/td&gt;
&lt;td&gt;51 / 44,042&lt;/td&gt;
&lt;td&gt;7-DoF arm + Robotiq gripper, GELLO teleop&lt;/td&gt;
&lt;td&gt;None declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intern Screw&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unscrew a cap from a bottle a person is holding&lt;/td&gt;
&lt;td&gt;2 touch-image videos + 6-D wrench&lt;/td&gt;
&lt;td&gt;50 / 31,960&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;None declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intern Shiguan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Insert a test tube into a rack&lt;/td&gt;
&lt;td&gt;2 touch-image videos + 6-D wrench&lt;/td&gt;
&lt;td&gt;51 / 26,665&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;None declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SharpaDex Assemble Gears&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gear assembly on a base&lt;/td&gt;
&lt;td&gt;Raw + deformation video, 60-D fingertip force/torque&lt;/td&gt;
&lt;td&gt;410 / 302,020&lt;/td&gt;
&lt;td&gt;Bimanual 7-DoF arms + 22-DoF hands&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grabette Tactile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Put the white cube in the correct cup&lt;/td&gt;
&lt;td&gt;2 taxel grids (6×6, 4×8 int16)&lt;/td&gt;
&lt;td&gt;48 / 19,353&lt;/td&gt;
&lt;td&gt;Grabette handheld gripper&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The 8 tactile LeRobot v3.0 datasets, with task, tactile streams, size, robot, and license.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The 8 tactile LeRobot v3.0 datasets, with task, tactile streams, size, robot, and license.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8 tactile datasets in detail
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Xense: three bimanual sets, four tactile streams each
&lt;/h3&gt;

&lt;p&gt;Xense Robotics builds visuo-tactile sensors and publishes datasets recorded with its own LeRobot fork, &lt;a href="https://github.com/XenseRobotics-AI/lerobot-xense" rel="noopener noreferrer"&gt;&lt;code&gt;lerobot-xense&lt;/code&gt;&lt;/a&gt;, a LeRobot v5.1 fork that adds Flexiv Rizon4, Elite CS66, and ARX5 arms plus tactile grippers. All three sets share one layout: 3 RGB cameras, 4 tactile video streams (a left and right pad on each gripper, 400×700), and a 20-D state and action vector (&lt;code&gt;left_tcp.{x,y,z,r1..r6}&lt;/code&gt;, &lt;code&gt;right_tcp.{x,y,z,r1..r6}&lt;/code&gt;, and both gripper positions), all at 30 fps. Every stream is standard H.264 yuv420p, so the tactile video plays in a browser with no transcoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; None of the three cards name the tactile sensor or say what the tactile pixels physically mean.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Xense Earbud Case: 834 episodes of long-horizon insertion&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwphcimu4xmkrebh8gpg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwphcimu4xmkrebh8gpg.gif" alt="An Xense Earbud Case episode in FiftyOne, with the RGB cameras and tactile streams playing in sync." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An Xense Earbud Case episode in FiftyOne, with the RGB cameras and tactile streams playing in sync.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/xense-earbud-case-834ep" rel="noopener noreferrer"&gt;Voxel51/xense-earbud-case-834ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Xense/earbud_case_insertion_teleop_0515" rel="noopener noreferrer"&gt;&lt;code&gt;Xense/earbud_case_insertion_teleop_0515&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the biggest dataset in the roundup by a wide margin: 834 teleoperated episodes and 1,213,518 frames on a bimanual Flexiv Rizon 4 (&lt;code&gt;bi_flexiv_rizon4_rt&lt;/code&gt;). Each episode runs the whole sequence: pick up an earbud case from the stands, insert the matching earbuds, close the lid, and place the case in a box. Episodes run 32.3 to 78.1 seconds. During insertion, fingers hide the contact from the cameras, making this a good set for asking what the tactile streams add.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 RGB streams (&lt;code&gt;head&lt;/code&gt;, &lt;code&gt;left_wrist&lt;/code&gt;, &lt;code&gt;right_wrist&lt;/code&gt;, 480×640), 4 tactile streams (&lt;code&gt;left_tactile_left&lt;/code&gt;, &lt;code&gt;left_tactile_right&lt;/code&gt;, &lt;code&gt;right_tactile_left&lt;/code&gt;, &lt;code&gt;right_tactile_right&lt;/code&gt;, 400×700), and the 20-D &lt;code&gt;observation.state&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; The source repo is still growing. An earlier snapshot listed 726 episodes and 1,065,060 frames, and this import has 834, so pin a revision if you need reproducible numbers. At 6.9 GB, it's also the largest download of the three Xense sets.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Xense Strap Tape: deformable tape, the longest episodes&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5twdsvw9gmm20681910e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5twdsvw9gmm20681910e.gif" alt="Xense Strap Tape in FiftyOne, with gripper positions plotted next to the four tactile streams." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Xense Strap Tape in FiftyOne, with gripper positions plotted next to the four tactile streams.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/xense-strap-tape-120ep" rel="noopener noreferrer"&gt;Voxel51/xense-strap-tape-120ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Xense/strap-tape-taccap-0929" rel="noopener noreferrer"&gt;&lt;code&gt;Xense/strap-tape-taccap-0929&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;120 episodes of "Grab the tape and strap the tape" on the same bimanual Flexiv rig, 191,261 frames in total. Tape is deformable, so the grip changes as it stretches and sticks. Episodes run 33.9 to 95.2 seconds, the longest in the roundup. At 0.84 GB, it's the smallest download with tactile video and the easiest place to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; the same 3 RGB streams, 4 tactile streams, and 20-D state and action as the earbud set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; The &lt;code&gt;taccap&lt;/code&gt; in the repo name probably refers to Xense's TacCap-Gripper, a wearable two-finger tactile data-collection device the company debuted at ICRA 2026, but the card doesn't say so. The episode count has also changed over time (an earlier snapshot showed 98).&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Xense Tron2 RT Pick and Place: short episodes, a different robot&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgslg83l234qm26787srk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgslg83l234qm26787srk.gif" alt="Xense Tron2 RT Pick and Place in FiftyOne, from the sample grid to a single episode." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Xense Tron2 RT Pick and Place in FiftyOne, from the sample grid to a single episode.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/xense-tron2rt-pnp-115ep" rel="noopener noreferrer"&gt;Voxel51/xense-tron2rt-pnp-115ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Xense/tron2rt-pnp-0918" rel="noopener noreferrer"&gt;&lt;code&gt;Xense/tron2rt-pnp-0918&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;115 episodes of "Put the block into the box" on robot type &lt;code&gt;tron2_rt&lt;/code&gt;, 48,999 frames in total. Episodes are short (10.7 to 22.2 seconds), so this is the quickest Xense set to scrub through when you want to see what a grasp looks like across all four tactile pads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 RGB streams (&lt;code&gt;top&lt;/code&gt;, &lt;code&gt;left_wrist&lt;/code&gt;, &lt;code&gt;right_wrist&lt;/code&gt;), 4 tactile streams, and the same 20-D tool center point (TCP) state and action layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; The overhead camera is &lt;code&gt;observation.images.top&lt;/code&gt; here and &lt;code&gt;observation.images.head&lt;/code&gt; in the other two Xense sets, so code that hard-codes camera keys will break across them. Unlike its siblings, the card carries no code link or citation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intern: three GELLO-teleoperated tasks with touch images and wrench
&lt;/h3&gt;

&lt;p&gt;The three Intern sets are community uploads by &lt;code&gt;cloudfan&lt;/code&gt; that share one robot schema, &lt;code&gt;intern_gello_7dof_robotiq&lt;/code&gt;: a 7-DoF arm with a Robotiq gripper, teleoperated with a GELLO leader arm. Each episode carries two RGB views (&lt;code&gt;cam_high&lt;/code&gt;, &lt;code&gt;cam_front&lt;/code&gt;), two tactile image streams (&lt;code&gt;tactile_left_aug_diff&lt;/code&gt;, &lt;code&gt;tactile_right_aug_diff&lt;/code&gt;, 700×400), a 6-D &lt;code&gt;observation.wrench&lt;/code&gt; with per-axis &lt;code&gt;observation.wrench_valid&lt;/code&gt; flags, an 8-D joint &lt;code&gt;observation.state&lt;/code&gt;, an 8-D GELLO &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;observation.eef_pose&lt;/code&gt;, and the original &lt;code&gt;source_timestamp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The source README says these are meant for training with the InternVLA A-series project. The robot config that ships with them maps only the two RGB views and describes the tactile and wrench channels as "preserved as auxiliary data, not automatically added to the RGB baseline." Out of the box, the touch data is along for the ride.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No license is declared on any of the three source repos, so you can't assume reuse rights.&lt;/li&gt;
&lt;li&gt;No episode was filtered for success. The task string is the goal, not a guarantee that the demonstration achieved it.&lt;/li&gt;
&lt;li&gt;The FiftyOne versions are lossy H.264 re-encodes (&lt;code&gt;libx264&lt;/code&gt;, &lt;code&gt;-crf 18&lt;/code&gt;) of the source MJPEG video. Frame counts and timestamps match the source, but the pixels don't match bit for bit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Intern Pour: 51 pouring demonstrations, 3 with missing force&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78fe4rbu1q4cpscnu47a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78fe4rbu1q4cpscnu47a.gif" alt="Intern Pour in FiftyOne, with the tactile streams and gripper command plotted over a pour." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Intern Pour in FiftyOne, with the tactile streams and gripper command plotted over a pour.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/intern-pour-lerobot-51ep" rel="noopener noreferrer"&gt;Voxel51/intern-pour-lerobot-51ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/cloudfan/intern-pour-lerobot" rel="noopener noreferrer"&gt;&lt;code&gt;cloudfan/intern-pour-lerobot&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; None declared in the source repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;51 demonstrations of "Pick up the small glass cup by its handle, pour the water into the large beaker, and place the cup back on the table," 44,042 frames at 30 fps, 21.6 to 39.3 seconds each. Pouring changes the weight in the gripper as the water leaves, which is a force signal no camera frame carries directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; the shared Intern layout above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; Episodes 13, 34, and 42 have missing force readings that were filled with zeros, and &lt;code&gt;observation.wrench_valid&lt;/code&gt; flags them. Filter on the validity flags, not on the zeros, or those frames will look like moments of no contact.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Intern Screw: unscrewing a cap from a bottle a person holds&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcskpguumwy7smh9i1e07.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcskpguumwy7smh9i1e07.gif" alt="Intern Screw in FiftyOne, with the gripper command plotted next to the tactile streams as the cap comes off." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Intern Screw in FiftyOne, with the gripper command plotted next to the tactile streams as the cap comes off.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/intern-screw-lerobot-50ep" rel="noopener noreferrer"&gt;Voxel51/intern-screw-lerobot-50ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/cloudfan/intern-screw-lerobot" rel="noopener noreferrer"&gt;&lt;code&gt;cloudfan/intern-screw-lerobot&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; None declared in the source repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;50 demonstrations of "Unscrew the cap from the bottle held by the person and place the cap on the table," 31,960 frames, 17.6 to 43.5 seconds each. The bottle is held by a person, not clamped, so the counter-force comes from a moving human hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; the shared Intern layout above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; None beyond the shared ones. Every frame has valid wrench readings.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Intern Shiguan: test-tube insertion&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5eeto47s10rrudwzgu1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5eeto47s10rrudwzgu1.gif" alt="Intern Shiguan in FiftyOne, with wrench forces plotted during a test-tube insertion." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Intern Shiguan in FiftyOne, with wrench forces plotted during a test-tube insertion.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/intern-shiguan-lerobot-51ep" rel="noopener noreferrer"&gt;Voxel51/intern-shiguan-lerobot-51ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/cloudfan/intern-shiguan-lerobot" rel="noopener noreferrer"&gt;&lt;code&gt;cloudfan/intern-shiguan-lerobot&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; None declared in the source repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Shiguan" is the pinyin for 试管, test tube. 51 demonstrations of "Pick up the test tube from the left side of the rack and insert it into the hole at the right end of the rack," 26,665 frames, 11.0 to 26.0 seconds each. It's the shortest of the three Intern tasks and the closest to a classic peg-in-hole insertion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; the shared Intern layout above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; None beyond the shared ones. Every frame has valid wrench readings.&lt;/p&gt;

&lt;h3&gt;
  
  
  SharpaDex Assemble Gears: dexterous hands, ten fingertip sensors
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7853dtxvfgrfg4wtoche.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7853dtxvfgrfg4wtoche.gif" alt="SharpaDex Assemble Gears in FiftyOne, with the raw and deformation tactile streams next to the RGB views." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SharpaDex Assemble Gears in FiftyOne, with the raw and deformation tactile streams next to the RGB views.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/sharpa-assemble-gears-410ep" rel="noopener noreferrer"&gt;Voxel51/sharpa-assemble-gears-410ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Sharpa-Robotics/SharpaDex-v1.0" rel="noopener noreferrer"&gt;&lt;code&gt;Sharpa-Robotics/SharpaDex-v1.0&lt;/code&gt;&lt;/a&gt; (28,993 episodes, 59 tasks, 495 seasons, about 300 hours)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SharpaDex v1.0 is teleoperated bimanual dexterous manipulation on a rig with two 7-DoF arms, two 22-DoF hands, four RGB cameras, and tactile sensors on every fingertip. The full release covers assembly, tool use, deformable objects, cleaning, material transfer, and long-horizon tasks, exported as both LeRobot v3.0 and v2.1.&lt;/p&gt;

&lt;p&gt;This FiftyOne version is one collection season (&lt;code&gt;season_POC22027_2026_04_30_15_04_50_train&lt;/code&gt;) of the &lt;code&gt;assemble_gears_on_base&lt;/code&gt; task: 410 episodes, 302,020 frames at 30 fps, 17.4 to 36.7 seconds each. It's a single contiguous season, not a random sample of the task or the release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 4 RGB streams (&lt;code&gt;head_left&lt;/code&gt;, &lt;code&gt;head_right&lt;/code&gt;, &lt;code&gt;wrist_left&lt;/code&gt;, &lt;code&gt;wrist_right&lt;/code&gt;, 480×480), 2 tactile video streams (&lt;code&gt;tactile_deform&lt;/code&gt; at 480×1200 and &lt;code&gt;tactile_raw&lt;/code&gt; at 480×1600), a 65-D &lt;code&gt;observation.state&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, and &lt;code&gt;observation.state.joint_torque&lt;/code&gt; (left arm, left hand, right arm, right hand, and torso), a 24-D &lt;code&gt;observation.state.tcp&lt;/code&gt; (pose and force/torque per arm), observed and commanded TCP poses, a per-frame &lt;code&gt;subtask_index&lt;/code&gt;, and the 60-D &lt;code&gt;observation.tactile&lt;/code&gt;: left and right &lt;code&gt;thumb&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;middle&lt;/code&gt;, &lt;code&gt;ring&lt;/code&gt;, and &lt;code&gt;little&lt;/code&gt; fingertips, each with &lt;code&gt;fx&lt;/code&gt;, &lt;code&gt;fy&lt;/code&gt;, &lt;code&gt;fz&lt;/code&gt;, &lt;code&gt;tx&lt;/code&gt;, &lt;code&gt;ty&lt;/code&gt;, &lt;code&gt;tz&lt;/code&gt;. The task field holds structured text with &lt;code&gt;Task&lt;/code&gt;, &lt;code&gt;Instruction&lt;/code&gt;, &lt;code&gt;Scene&lt;/code&gt;, and &lt;code&gt;Success&lt;/code&gt; sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Touch is concentrated in two fingers. With a contact threshold of 1.0 on per-fingertip force magnitude, the right index finger crosses it in 61% of frames and the right thumb in 42%. Left ring (9.5%) and left little (2.2%) come next, and the other five fingertips rarely cross it. That's consistent with a right-hand pinch doing the gear work, but check it against the tactile video before you trust the channel labels.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robot_type&lt;/code&gt; is the literal string robot in the source &lt;code&gt;info.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The source README says the export doesn't fully declare TCP units, reference frames, or rotation convention, so don't infer Euler angles or quaternions from vector width alone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subtask_index&lt;/code&gt; survives in the parquet, but the subtask text and skill labels it points to live in the source's &lt;code&gt;meta/subtasks.parquet&lt;/code&gt;, which the FiftyOne LeRobot exporter doesn't carry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; The T-Rex issue mentioned earlier, the one that found almost no offline benefit from real touch, trained on the full Sharpa dataset. A 410-episode slice can't settle that question, but it's a good place to look at what the tactile channels contain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grabette Tactile: a handheld gripper with two taxel pads
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl77yzqqd4cvwlsw0foyc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl77yzqqd4cvwlsw0foyc.gif" alt="Grabette Tactile in FiftyOne. Taxel values plot as one series per cell, and episode 2 reads zero throughout." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Grabette Tactile in FiftyOne. Taxel values plot as one series per cell, and episode 2 reads zero throughout.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/grabette-tactile-full-synced-48ep" rel="noopener noreferrer"&gt;Voxel51/grabette-tactile-full-synced-48ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/CarolinePascal/grabette-tactile-full-synced" rel="noopener noreferrer"&gt;&lt;code&gt;CarolinePascal/grabette-tactile-full-synced&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://huggingface.co/blog/grabette" rel="noopener noreferrer"&gt;Grabette release post&lt;/a&gt; · ACT policy trained on a derived version: &lt;a href="https://huggingface.co/CarolinePascal/act_tactile_grabette" rel="noopener noreferrer"&gt;CarolinePascal/act_tactile_grabette&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grabette is an open, low-cost handheld gripper from Pollen Robotics, released on the Hugging Face blog in July 2026 and inspired by Stanford's Universal Manipulation Interface (UMI). You hold it, perform the task by hand, and its cameras, inertial measurement unit (IMU), and gripper encoders record the demonstration. Simultaneous localization and mapping (SLAM) recovers the 6-DoF trajectory, and the output is a standard LeRobot dataset. A matching motorized gripper, Gripette, runs the learned policy on a robot arm.&lt;/p&gt;

&lt;p&gt;This dataset adds two taxel sensors to the right gripper. It has 48 episodes of "Pick up the white cube and put it in the correct cup," 19,353 frames at 50 fps, 2.9 to 10.7 seconds each. It's the only dataset in the roundup that stores touch as a raw pressure grid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 2 RGB streams (&lt;code&gt;right_cam0&lt;/code&gt;, &lt;code&gt;right_cam1&lt;/code&gt;, 960×720), an 8-D action (&lt;code&gt;right_x&lt;/code&gt;, &lt;code&gt;right_y&lt;/code&gt;, &lt;code&gt;right_z&lt;/code&gt;, &lt;code&gt;right_ax&lt;/code&gt;, &lt;code&gt;right_ay&lt;/code&gt;, &lt;code&gt;right_az&lt;/code&gt;, &lt;code&gt;right_proximal&lt;/code&gt;, &lt;code&gt;right_distal&lt;/code&gt;), an &lt;code&gt;is_lost&lt;/code&gt; flag, and two int16 taxel grids, &lt;code&gt;observation.tactile.right_sensor_1&lt;/code&gt; (6×6) and &lt;code&gt;observation.tactile.right_sensor_2&lt;/code&gt; (4×8).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The taxel grids show up in the episode viewer as one plottable series per taxel, but they aren't sample fields (see the schema section above). The arrays are in &lt;code&gt;data/chunk-000/file-000.parquet&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Episode 2 has no tactile signal at all: all 68 taxels read 0 for all 145 frames. It's also the shortest episode, so it may be an aborted take.&lt;/li&gt;
&lt;li&gt;The card doesn't state units, taxel layout, or the sensor model. Across all 19,353 frames, the raw values range from 0 to 1,774 on &lt;code&gt;right_sensor_1&lt;/code&gt; and 0 to 2,133 on &lt;code&gt;right_sensor_2&lt;/code&gt;, and every taxel varies at some point, so no cells are dead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_lost&lt;/code&gt; is 0 in every frame. It likely flags SLAM tracking loss, which the Grabette pipeline checks for before conversion, but the card doesn't define it.&lt;/li&gt;
&lt;li&gt;The release post describes Grabette as a robot-free, by-hand capture device and doesn't mention tactile sensing. The card doesn't say how they recorded these 48 episodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Curating tactile data: where the real gains are
&lt;/h2&gt;

&lt;p&gt;This is the part I care most about, because it's where a small amount of effort pays off the most.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1vadeox2xx8qnn87cesm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1vadeox2xx8qnn87cesm.png" alt="Six signal traces illustrating tactile data quality checks: a dead sensor reading zero, a signal saturated at the sensor max, baseline drift across three sessions, an episode that never crosses the contact threshold, tactile onset lagging the gripper close, and a brief slip event." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Six checks to run on any tactile dataset before you train on it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://voxel51.com/blog/robot-episode-quality-triage" rel="noopener noreferrer"&gt;Six things worth checking in any tactile dataset&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dead or flatlined sensors.&lt;/strong&gt; A taxel that reads zero for an entire episode, or a gel camera that's frozen, is pure noise. Flag those channels and drop them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturation.&lt;/strong&gt; If a sensor is pinned at its maximum, you've lost the information about how hard the contact actually was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift across sessions.&lt;/strong&gt; Compare at-rest baselines across collection days. If they wander, normalize per session, or at least know which episodes came from which sensor state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Episodes where touch never happens.&lt;/strong&gt; If the tactile signal barely moves during an episode, that episode teaches the model nothing about contact. It may be fine for vision training and useless for tactile learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact alignment.&lt;/strong&gt; Check that tactile onset lines up with what the cameras and gripper state say is happening. A misalignment is a sync problem, not a learning problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rare, valuable moments.&lt;/strong&gt; Slips, regrasps, and near-failures are where touch matters most, and they're usually a small fraction of the data. Find them and make sure they're represented.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Split by episode, not by frame.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhglo3pqf06zun1iulyqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhglo3pqf06zun1iulyqe.png" alt="Two panels comparing a frame-random split, where frames from one episode land in both train and test, with an episode split that holds out whole episodes. Below, RCT's tactile-to-text Recall@1 drops from 80.0% to 62.3% when contact sequences are held out." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Frame-random splits leak near-identical frames into the test set. Holding out whole contact sequences cost RCT 17.7 points of Recall@1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Consecutive tactile frames are nearly identical. The RCT authors measured what that does to evaluation: with the encoder held fixed, and only the test split changed, tactile-to-text Recall@1 was 80.0% under a frame-random split and 62.3% once whole contact sequences were held out, a 17.7-point drop. Random frame splits will make your tactile model look much better than it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Turn numeric touch into per-episode fields
&lt;/h3&gt;

&lt;p&gt;FiftyOne's queries and aggregations run on sample fields, and per-frame LeRobot data stays in parquet, so this step uses pandas to read the frames and FiftyOne to store, sort, and filter the results. Here's the pattern on SharpaDex: read &lt;code&gt;observation.tactile&lt;/code&gt;, compute per-fingertip force magnitude, and write two per-episode fields back onto the samples.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sharpa_assemble_gears&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sharpa-assemble-gears-410ep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# local snapshot of the Hub repo
&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;episode_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation.tactile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/data/*/*.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# (frames, 10 fingertips, fx fy fz tx ty tz)
&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation.tactile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;force&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ft&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;force&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;episode_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;peak_force&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;contact_fraction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;episode_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_tactile_force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact_fraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact_fraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact_fraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold of 1.0 is a starting point, not a calibrated contact definition, since the README doesn't declare units. Plot the distribution before you pick one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Look at the extremes
&lt;/h3&gt;

&lt;p&gt;In this season, every one of the 410 episodes has contact for between 39% and 93% of its frames (median 64%). Per-episode peak force ranges from 10.9 to 48.9 in the sensor's raw units. Sorting by &lt;code&gt;contact_fraction&lt;/code&gt; puts episodes 327, 318, and 326 at the top, all near 39%. Those are the first ones to open in the App and watch next to their tactile video. A low value could mean a fast, clean assembly or a fumbled grasp, and only watching tells you which.&lt;/p&gt;

&lt;p&gt;The same pattern works for the other datasets. On the Intern sets, compute the fraction of frames where &lt;code&gt;observation.wrench_valid&lt;/code&gt; is all ones. On Grabette, compute the per-episode peak and the per-taxel variance of each grid, so a dead cell shows up as a zero.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's the same discipline that worked for image datasets: &lt;a href="https://voxel51.com/blog/robot-data-quality-scoring" rel="noopener noreferrer"&gt;look at your data&lt;/a&gt;, find the problems, fix them, find the gaps, fill them. For touch, that means catching drift, dead sensors, and misaligned timestamps before they reach training.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is tactile data in robot learning?
&lt;/h3&gt;

&lt;p&gt;Tactile data is anything a robot measures at the point of contact. In LeRobot datasets, it shows up as video from camera-based gel sensors (for example &lt;code&gt;observation.images.left_tactile_left&lt;/code&gt; in the Xense sets), low-resolution pressure grids (&lt;code&gt;observation.tactile.right_sensor_1&lt;/code&gt; in Grabette), or force/torque vectors (&lt;code&gt;observation.tactile&lt;/code&gt; in SharpaDex, &lt;code&gt;observation.wrench&lt;/code&gt; in the Intern sets).&lt;/p&gt;

&lt;h3&gt;
  
  
  Which tactile datasets can I load directly into FiftyOne?
&lt;/h3&gt;

&lt;p&gt;All 8 in this roundup load with &lt;code&gt;load_from_hub("Voxel51/&amp;lt;repo&amp;gt;")&lt;/code&gt;. They are &lt;code&gt;xense-earbud-case-834ep&lt;/code&gt;, &lt;code&gt;xense-strap-tape-120ep&lt;/code&gt;, &lt;code&gt;xense-tron2rt-pnp-115ep&lt;/code&gt;, &lt;code&gt;intern-pour-lerobot-51ep&lt;/code&gt;, &lt;code&gt;intern-screw-lerobot-50ep&lt;/code&gt;, &lt;code&gt;intern-shiguan-lerobot-51ep&lt;/code&gt;, &lt;code&gt;sharpa-assemble-gears-410ep&lt;/code&gt;, and &lt;code&gt;grabette-tactile-full-synced-48ep&lt;/code&gt;. Each one is a multimodal FiftyOne dataset with one sample per episode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I see Grabette's tactile grids in the FiftyOne App?
&lt;/h3&gt;

&lt;p&gt;Yes, as time series. The episode viewer reads every numeric LeRobot feature, so &lt;code&gt;observation.tactile.right_sensor_1&lt;/code&gt; (6×6) and &lt;code&gt;observation.tactile.right_sensor_2&lt;/code&gt; (4×8) appear as one series per taxel in the Plot and Message tiles. They aren't sample fields, so to filter episodes by touch, compute per-episode statistics with pandas and write them back with &lt;code&gt;set_values&lt;/code&gt;. For a spatial view of the contact patch, render the grids as heatmaps yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is observation.tactile the same thing across datasets?
&lt;/h3&gt;

&lt;p&gt;No. In SharpaDex, &lt;code&gt;observation.tactile&lt;/code&gt; is a 60-D vector: 10 fingertips × 6-axis force/torque. In Grabette, &lt;code&gt;observation.tactile.*&lt;/code&gt; is the prefix for two int16 taxel grids. Check &lt;code&gt;meta/info.json&lt;/code&gt; for shape and dtype before you assume what a tactile key holds.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does aug_diff mean in the Intern tactile streams?
&lt;/h3&gt;

&lt;p&gt;The cards don't define it. The source README calls &lt;code&gt;tactile_left_aug_diff&lt;/code&gt; and &lt;code&gt;tactile_right_aug_diff&lt;/code&gt; "700 x 400 RGB touch images." The name suggests a difference from a reference frame, which is a common way to show where a gel sensor deformed, but treat that as a guess until the uploader documents it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why were the Intern videos re-encoded?
&lt;/h3&gt;

&lt;p&gt;The source stores video as MJPEG inside MP4, which browsers can't decode, so it wouldn't play in the FiftyOne App. All four streams in every episode were re-encoded to H.264 (&lt;code&gt;libx264&lt;/code&gt;, &lt;code&gt;-crf 18&lt;/code&gt;, yuv420p). Frame counts and timestamps match the source, but it's a lossy re-encode, so go back to the source repo if you need the original pixels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which of these datasets can I use commercially?
&lt;/h3&gt;

&lt;p&gt;The three Xense sets and Grabette are Apache-2.0, and SharpaDex is CC-BY-4.0. Both licenses permit commercial use under their terms (attribution for CC-BY). The three Intern sets declare no license, so you can't assume any reuse rights.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does adding tactile input always improve a robot policy?
&lt;/h3&gt;

&lt;p&gt;No. LeFlexiTac (7/30 → 23/30 on an in-bag pen task) and FreeTacMan (21% → 71% average success across five tasks) report large gains on contact-rich tasks. An open issue on the T-Rex repo found that the offline benefit of real touch over neutral input on the full Sharpa data stayed within ±0.9%. Touch tends to help when the task depends on contact and when the tactile data is clean.&lt;/p&gt;

</description>
      <category>lerobot</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>python</category>
    </item>
    <item>
      <title>How to Find Edited Copies of Images: Image Copy Detection with pHash, CLIP, and FiftyOne</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:02:58 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/how-to-find-edited-copies-of-images-image-copy-detection-with-phash-clip-and-fiftyone-2524</link>
      <guid>https://dev.to/harpreetsahota/how-to-find-edited-copies-of-images-image-copy-detection-with-phash-clip-and-fiftyone-2524</guid>
      <description>&lt;p&gt;&lt;em&gt;Exact-duplicate search misses a mirrored, cropped, or filtered copy of an image, so this post combines a perceptual hash and a CLIP embedding into one rule you control and shows the copies as a graph in FiftyOne.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your model's test metrics look great. Then someone opens the test set and finds a mirrored copy of a training image. Then another one, with a filter on it. Then a third, cropped and covered in emoji.&lt;/p&gt;

&lt;p&gt;You run a duplicate check. It finds nothing.&lt;/p&gt;

&lt;p&gt;That's because duplicate checks look for the same bytes, and an edited copy has different bytes. A mirrored JPEG is a different file from the original. To a hash of the file contents, the two are strangers. To a person, they're the same picture.&lt;/p&gt;

&lt;p&gt;The task is called &lt;strong&gt;image copy detection&lt;/strong&gt;. Given a query image, decide whether it's an edited copy of an image in a reference collection, and say which one. This post walks through one practical approach: compute two signals per image, combine them with a rule you control, and read the result as a graph where every link comes with evidence. It's packaged as an open-source FiftyOne plugin called Image Link Lab, but the idea carries over to any dataset where copies hide behind edits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd07bfr62admkc8oxynhn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd07bfr62admkc8oxynhn.gif" alt="Image Link Lab in FiftyOne. Tune the pHash and CLIP limits, and the copy graph redraws as you drag." width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Image Link Lab in FiftyOne. Tune the pHash and CLIP limits, and the copy graph redraws as you drag.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Image copy detection asks whether a query image is an edited copy of a reference image, and which one. &lt;code&gt;compute_exact_duplicates&lt;/code&gt; can't answer that, because edits change the bytes.&lt;/li&gt;
&lt;li&gt;A perceptual hash (pHash) summarizes pixel layout in 64 bits. It tolerates recompression and small blurs and breaks on mirrors, rotations, and big crops.&lt;/li&gt;
&lt;li&gt;A CLIP embedding (&lt;code&gt;clip-vit-base32-torch&lt;/code&gt; from the FiftyOne model zoo) summarizes what the picture shows. It survives mirrors and crops and gets fooled by look-alikes.&lt;/li&gt;
&lt;li&gt;Each signal's blind spot is the other's strength, so the plugin computes both and lets you choose how they combine: link a pair if either signal passes (&lt;code&gt;combine: "any"&lt;/code&gt;), or only if both do (&lt;code&gt;"all"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Moving a limit changes the answer. The Image Link Lab panel redraws the links as you drag the sliders and shows the numbers behind every line. Given a truth field, it scores each rule with FiftyOne's native evaluation so you can compare rules in Model Evaluation.&lt;/li&gt;
&lt;li&gt;The output is ordinary FiftyOne data: a phash field, a &lt;code&gt;copy_of Classification&lt;/code&gt; field, and &lt;code&gt;copies_*&lt;/code&gt; evaluation runs. You can tag copies, save views, and filter a test split cleanly with the code you already use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is image copy detection?
&lt;/h2&gt;

&lt;p&gt;Image copy detection is the task of deciding whether a query image is a modified copy of any image in a reference corpus. The edits can be mundane (crop, resize, or re-compress) or hostile (mirror, overlay text, blend with another image, or screenshot it on a different device).&lt;/p&gt;

&lt;p&gt;The standard benchmark is DISC21, the dataset behind the Image Similarity Challenge at NeurIPS 2021. Its authors describe the goal as determining "whether a query image is a modified copy of any image in a reference corpus of size 1 million," with edits that include automated transformations, hand-crafted changes, and machine-learning-based manipulations. Both the query set and the reference set are mostly distractors that match nothing, a needle-in-a-haystack setting like real life. The paper is &lt;a href="https://arxiv.org/abs/2106.09672" rel="noopener noreferrer"&gt;The 2021 Image Similarity Dataset and Challenge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Copy detection is a different question from "which images look like this one?" A &lt;a href="https://voxel51.com/blog/image-similarity-search-unlocking-pattern-detection-in-visual-data" rel="noopener noreferrer"&gt;similarity search&lt;/a&gt; returns neighbors. Copy detection has to commit: this query is a copy of that original, or it isn't a copy of anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyewcxgxbcf9j5u2f0qw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyewcxgxbcf9j5u2f0qw.png" alt="One beach photo and five edited copies (mirrored, cropped, blurred, text overlay, and grayscale with rotation) stored on five different devices. Exact-duplicate search finds none of them. A side panel lists why finding them matters: train/test leakage, labeling cost, licensing, and forensics." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Five edited copies of one image are five different files, so exact-duplicate search finds none of them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where edited copies cost you
&lt;/h2&gt;

&lt;p&gt;You don't need a content moderation team to care about this. Four situations come up again and again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Train/test leakage.&lt;/strong&gt; A mirrored copy of a training image in your test set inflates your metrics. You don't find out until the model meets real data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labeling cost.&lt;/strong&gt; You pay to label the same picture several times, sometimes with conflicting answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing and attribution.&lt;/strong&gt; You need to know where an image came from, and a cropped copy hides its source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moderation and forensics.&lt;/strong&gt; A known-bad image comes back filtered, or shows up on a second device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two hit any training pipeline. The last two are what DISC21 was built around: its authors say the benchmark mimics social media problems such as misinformation and objectionable content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why exact duplicates and single embeddings fall short
&lt;/h2&gt;

&lt;p&gt;FiftyOne already handles two nearby problems. &lt;a href="https://docs.voxel51.com/brain/index.html#brain-exact-duplicates" rel="noopener noreferrer"&gt;&lt;code&gt;compute_exact_duplicates&lt;/code&gt;&lt;/a&gt; finds byte-identical files. &lt;a href="https://docs.voxel51.com/brain/index.html#brain-near-duplicates" rel="noopener noreferrer"&gt;&lt;code&gt;compute_near_duplicates&lt;/code&gt;&lt;/a&gt;, along with the Similarity Search panel, finds neighbors by a single embedding.&lt;/p&gt;

&lt;p&gt;Neither is built for edited copies. Exact matching fails the moment a pixel changes. A single embedding gives you a ranked list of look-alikes, with one similarity score per pair and no way to say which kind of evidence you trust. If the score is wrong, you can't tell whether the embedding was fooled or the copy was over-edited.&lt;/p&gt;

&lt;p&gt;What's missing is a way to combine two different kinds of similarity with a rule you control, and a way to see which image is linked to which, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two signals, two blind spots
&lt;/h2&gt;

&lt;p&gt;The approach starts from a simple observation. Pixels and meaning fail on different edits.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;perceptual hash&lt;/strong&gt; (pHash) summarizes an image's pixel layout in 64 bits. Two images whose hashes differ in only a few bits look the same at a glance. It's fast, needs no model, and shrugs off recompression and a small blur. It's defeated by anything that moves pixels around: a mirror, a rotation, or a big crop.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://voxel51.com/embedding" rel="noopener noreferrer"&gt;&lt;strong&gt;CLIP embedding&lt;/strong&gt;&lt;/a&gt; summarizes what the picture shows. Two images with a high &lt;a href="https://voxel51.com/cosine-similarity" rel="noopener noreferrer"&gt;cosine similarity&lt;/a&gt; are about the same thing. It survives mirrors and crops. It's fooled by look-alikes: two different photos of the same kind of scene can score higher than a heavily blurred copy of the same photo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbedou0rk4o73mgbix11g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbedou0rk4o73mgbix11g.png" alt="Four DISC21 image pairs scored by pHash and CLIP: a mirrored copy only CLIP catches, a striped and blurred copy only pHash catches, a look-alike CLIP nearly links by mistake, and a flipped, blurred, and filtered copy both signals miss." width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pixels and meaning fail on different edits, so pHash and CLIP each catch copies the other misses.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The four pairs above show it. A mirrored copy is invisible to pHash and obvious to CLIP. A striped and blurred copy is the reverse. A look-alike that isn't a copy nearly gets linked by CLIP. And a flipped, blurred, filtered copy beats both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. Why not train a copy-detection model?&lt;/strong&gt; You can, and the DISC21 challenge exists to push that line of work. The two-signal approach is for the common case where you have a dataset on disk, no labeled copy pairs, and a question to answer this afternoon. Both signals work off the shelf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine them with a rule you control
&lt;/h2&gt;

&lt;p&gt;Each signal gets a limit. For pHash, it's the most bits two hashes may differ by. For CLIP, it's the least cosine similarity two images must reach. Then you choose how the signals combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Either signal is enough&lt;/strong&gt; (&lt;code&gt;any&lt;/code&gt;): link the pair if pHash &lt;em&gt;or&lt;/em&gt; CLIP passes. You get more links, and more wrong ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both must agree&lt;/strong&gt; (&lt;code&gt;all&lt;/code&gt;): link only if both pass. You get fewer links, and almost all of them right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving a limit is the whole game. Loosen the pHash limit, and you catch more heavily edited copies and start linking images that only look alike. Tighten it to all, and the wrong links disappear along with most of the right ones. No setting gets everything.&lt;/p&gt;

&lt;p&gt;That's why the plugin doesn't give you a single "is copy" score. It shows you where the trade-off sits for your data and lets you pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; There is no universal threshold. A limit that works on re-uploaded product photos will behave differently on a collection full of heavy crops and overlays. If you have even a small set of known copy pairs, use it to calibrate the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FiftyOne and its plugin ecosystem fit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/voxel51/fiftyone" rel="noopener noreferrer"&gt;FiftyOne&lt;/a&gt; is an open-source tool for building high-quality datasets and visual AI models. It gives you the dataset layer: samples with fields you can filter and sort on, views, tags, the App for looking at your images, and native tools for similarity search and model evaluation.&lt;/p&gt;

&lt;p&gt;It can't ship every check that depends on your data. That's the job of plugins.&lt;/p&gt;

&lt;p&gt;A FiftyOne plugin is a folder FiftyOne discovers at runtime. It includes a manifest (&lt;code&gt;fiftyone.yml&lt;/code&gt;), Python code, and optionally a React front end. You add a button, a form, a background job, or a whole panel to the App without forking FiftyOne or waiting for a release. Plugins come in two kinds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operators&lt;/strong&gt; are actions. You trigger one, it runs, it finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Panels&lt;/strong&gt; are UI that stays open next to your data and reacts when you filter the grid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugin code runs on the FiftyOne server in Python so that it can use NumPy, PyTorch, a GPU, or any library you can &lt;code&gt;pip install&lt;/code&gt;. Long jobs can be delegated to a background worker so the App stays free. Every operator also has a URI, so anything that can call it, from a button to a notebook to an AI agent, runs the same code. For a deeper tour of how operators and panels work, see &lt;a href="https://voxel51.com/blog/fiftyone-plugins-mcap-lerobot-curation" rel="noopener noreferrer"&gt;Your Robot Data Needs Custom Tools. Build Them as FiftyOne Plugins&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Image Link Lab is built from standard parts: two operators (&lt;code&gt;compute_signals&lt;/code&gt;, &lt;code&gt;find_copies&lt;/code&gt;), one hybrid panel (&lt;code&gt;image_link_lab&lt;/code&gt;), native similarity indexes, and native evaluation runs. The plugin adds only what FiftyOne lacks: a perceptual hash, a rule over two signals, and the graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhor2bxumbdsjlqnow9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhor2bxumbdsjlqnow9p.png" alt="The five-step Image Link Lab workflow: compute signals, find copies, open the panel, score the rule, and act. Each step lists what is plugin code and what is native FiftyOne." width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The plugin adds three things FiftyOne lacks: a perceptual hash, a rule over two signals, and the graph. Everything else is native.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the plugin and compute signals
&lt;/h2&gt;

&lt;p&gt;Install the plugin and its requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;harpreetsahota204&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;image_link_lab&lt;/span&gt;
&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt; &lt;span class="nd"&gt;@harpreetsahota&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;image_link_lab&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin needs &lt;code&gt;fiftyone&amp;gt;=1.22.1&lt;/code&gt;. CLIP runs on CPU but is much faster on a GPU.&lt;/p&gt;

&lt;p&gt;Open any image dataset in the App, click + next to Samples, and add the &lt;strong&gt;Image Link Lab&lt;/strong&gt; panel under Custom. Click &lt;strong&gt;Compute signals&lt;/strong&gt;, keep both boxes ticked, and execute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrzad2ig3bodm29zao22.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrzad2ig3bodm29zao22.gif" alt="Computing the pHash and CLIP signals with the Image Link Lab operator." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Computing the pHash and CLIP signals with the Image Link Lab operator.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The operator writes three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;phash&lt;/code&gt; field&lt;/td&gt;
&lt;td&gt;the 64-bit pHash of each image, as hex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;brain key &lt;code&gt;phash&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the hash bits as a native similarity index, so sort-by-similarity in the App works on pHash too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;brain key &lt;code&gt;clip&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a native CLIP similarity index (&lt;code&gt;clip-vit-base32-torch&lt;/code&gt; from the model zoo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;What the compute signals operator writes to your dataset: a phash field and two native similarity indexes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It runs delegated by default, so start a worker with &lt;code&gt;fiftyone delegated launch&lt;/code&gt; or choose "Execute now" in the form. pHash takes about a minute per 10,000 images, plus CLIP time. Signals that are already computed are left alone unless you tick them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Find copies
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Find copies&lt;/strong&gt; and fill in the form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8k140m9i5zzq9mxv34z3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8k140m9i5zzq9mxv34z3.gif" alt="Filling in the find copies form: queries, candidate originals, an optional truth field, and the starting rule." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Filling in the find copies form: queries, candidate originals, an optional truth field, and the starting rule.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query images&lt;/strong&gt; are the ones you want explained. Each is linked to at most one original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Candidate originals&lt;/strong&gt; are where to look. Both can be the current view, the whole dataset, or a tag. They can overlap, and an image is never linked to itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ID field&lt;/strong&gt; (optional) is what to call each image, if your dataset identifies images by something other than the sample ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truth field&lt;/strong&gt; (optional) holds, for each query, the ID of its known original. This is your &lt;a href="https://voxel51.com/ground-truth" rel="noopener noreferrer"&gt;ground truth&lt;/a&gt;, supplied by your dataset, not computed by the plugin. Set it, and you get scoring. Leave it empty, and the plugin works unsupervised: links are drawn, nothing is judged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule&lt;/strong&gt; sets the starting pHash limit, CLIP limit, and &lt;code&gt;any&lt;/code&gt; or &lt;code&gt;all&lt;/code&gt;. You'll change these in the panel, so this is just where the first result comes from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behind the form, the operator does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gathers candidates.&lt;/strong&gt; For each query, it takes the 5 nearest originals by pHash and the 5 nearest by CLIP, plus the true original if a truth field is set. It stores both signals' values for every candidate. This is the slow step, about 4 seconds for 1,000 queries against 5,000 originals, and every later run reuses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applies the rule&lt;/strong&gt; and writes &lt;code&gt;copy_of&lt;/code&gt;, a &lt;code&gt;Classification&lt;/code&gt; per query whose label is the linked original's ID, or none. &lt;code&gt;copy_of&lt;/code&gt; is a prediction from your rule, not a human label. When several candidates pass, the one passing the most signals wins, then the highest CLIP similarity, then the fewest differing bits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Scores&lt;/strong&gt;, if a truth field is set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flin98v9gmqo0gd236gw4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flin98v9gmqo0gd236gw4.png" alt="The find copies form beside the four steps its execute function performs: gather candidates once, apply the rule, score it with a native binary evaluation, and remember the settings for the panel." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The slow step, gathering candidates, runs once. Every rule you try after that reuses them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Read the graph
&lt;/h2&gt;

&lt;p&gt;The panel draws whatever queries are in the current grid view, up to 40 at a time. Filter the grid and the graph redraws.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nodes are images.&lt;/strong&gt; Copies have a gray border. Originals have an orange border and, when more than one copy points at them, a badge with the count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lines are candidate pairs the rule has something to say about.&lt;/strong&gt; With a truth field set, green is a correct link, red is an incorrect link, and dashed amber is a missed original. Thick lines pass both signals, and thin lines pass one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images joined by lines form families,&lt;/strong&gt; laid out as a star with the original in the middle. A loose rule joins families through wrong links, and you'll see red lines bridging two stars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries with nothing to draw&lt;/strong&gt; sit in a strip at the bottom. For a distractor with no original, that's the correct outcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click an image and the grid filters to that image and everything it has a line to, including originals your current view doesn't contain. Click a line and an evidence pane opens with the copy and the original side by side, then one row per signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pHash   pixels    30 bits differ    limit ≤ 10    fails
CLIP    meaning   0.731 similar     limit ≥ 0.90  fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below the rows is a verdict in a sentence, such as "Not linked: pHash and CLIP are outside the limit. This is the true original, so the rule missed it." You can check every statement against the numbers above it. No model decides behind the scenes.&lt;/p&gt;

&lt;p&gt;Move the sliders. The rule runs in the browser against the stored candidate values, so lines and counts update as you drag, with no server round trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. Does the graph work without ground truth?&lt;/strong&gt; Yes. Without a truth field, every link is drawn green, and nothing is dashed. The colors mean "linked," not "right."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Score the rule and compare alternatives
&lt;/h2&gt;

&lt;p&gt;If you set a truth field, click &lt;strong&gt;Score this rule&lt;/strong&gt;. The plugin runs the rule over every query, not just the 40 on screen, writes &lt;code&gt;copy_of&lt;/code&gt;, and records a native FiftyOne evaluation named after the rule, such as &lt;code&gt;copies_phash10_or_clip90&lt;/code&gt;. Then it opens &lt;a href="https://voxel51.com/blog/unified-model-insights-with-fiftyone-model-evaluation-workflows" rel="noopener noreferrer"&gt;Model Evaluation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The evaluation is binary, per query. Ground truth is copy if the query has a known original and unique if it doesn't. The prediction is copy if the rule linked the query to its true original, and unique otherwise. FiftyOne computes precision, recall, F1, and a 2×2 &lt;a href="https://voxel51.com/confusion-matrix" rel="noopener noreferrer"&gt;confusion matrix&lt;/a&gt; from those two labels.&lt;/p&gt;

&lt;p&gt;One definition matters: a copy linked to the wrong original counts as a miss, not a false positive. The evaluation asks whether you found the true original, and the answer is no.&lt;/p&gt;

&lt;p&gt;The loop looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Score the starting rule and open its evaluation in Model Evaluation.&lt;/li&gt;
&lt;li&gt;Click the false-negative cell. The grid filters to the misses, and the graph redraws them with dashed amber lines to their originals. Click a line to see which signal failed and by how much. That tells you which slider might help.&lt;/li&gt;
&lt;li&gt;Change a limit, score again, and you get a second evaluation key.&lt;/li&gt;
&lt;li&gt;Open one and compare it with the other.&lt;/li&gt;
&lt;li&gt;Mark the rule you keep as Reviewed and delete the rest with &lt;code&gt;dataset.delete_evaluation(key)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The false-positive cell is the other one worth clicking. Those are distractors the rule linked, usually look-alikes, which is CLIP's failure mode. Comparing a pHash-only run with a CLIP-only run shows which edits each signal survives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; Model Evaluation scenarios, which break results down by edit type, are a Voxel51 feature. In open-source FiftyOne, filter the grid by an edit-type field and read the chips, or compute the numbers in Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Act on the results
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;copy_of&lt;/code&gt; is an ordinary label field, so everything else in FiftyOne works on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ViewField&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;copies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copy_of.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;copies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag_samples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;copies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Keep a test split clean
&lt;/span&gt;&lt;span class="n"&gt;clean_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copy_of.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Everything linked to one original
&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copy_of.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;R003513&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical setups differ only in what you pass as queries and originals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Queries&lt;/th&gt;
&lt;th&gt;Originals&lt;/th&gt;
&lt;th&gt;Truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dedup a collection&lt;/td&gt;
&lt;td&gt;whole dataset&lt;/td&gt;
&lt;td&gt;whole dataset&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check a test split for leakage&lt;/td&gt;
&lt;td&gt;tag &lt;code&gt;test&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;tag &lt;code&gt;train&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Match seized images against a known-bad list&lt;/td&gt;
&lt;td&gt;the seized images&lt;/td&gt;
&lt;td&gt;the known-bad list&lt;/td&gt;
&lt;td&gt;none, or a case label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark a rule&lt;/td&gt;
&lt;td&gt;the edited images&lt;/td&gt;
&lt;td&gt;the originals&lt;/td&gt;
&lt;td&gt;the original's ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Typical Image Link Lab setups, with what to pass as queries, originals, and truth for each.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the same workflow from Python
&lt;/h2&gt;

&lt;p&gt;Everything the App does is an operator, so a notebook, a script, or an agent can run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.operators&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disc21_link_lab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Step 1: signals (delegate=False runs it in this process)
&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_operator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@harpreetsahota/image_link_lab/compute_signals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delegate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: copies, with a rule, scored against a truth field
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_operator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@harpreetsahota/image_link_lab/find_copies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag:final_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# or "view", "dataset", "tag:&amp;lt;tag&amp;gt;"
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;originals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag:reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id_field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disc21_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# optional
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;truth_field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# optional; turns on scoring
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phash_max&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip_min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;combine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;# "any" or "all"
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delegate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The results are ordinary FiftyOne fields and runs
&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_evaluation_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copies_phash10_or_clip90&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;print_report&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;find_copies&lt;/code&gt; again with a different rule and the candidates are reused, so it takes about a second and adds another evaluation run to compare.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f5njxtceffesdtcsk42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1f5njxtceffesdtcsk42.png" alt="One operator URI per step, callable from panel buttons, the find copies form, a notebook, an AI agent, or a background worker. Native tools close the loop between the grid, the Image Link Lab panel, scoring, and Model Evaluation." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every step is an operator with one URI, so the App, a notebook, and an AI agent all run the same code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both operators have typed inputs and show up through the FiftyOne Model Context Protocol (MCP) server (&lt;code&gt;list_operators&lt;/code&gt;, &lt;code&gt;get_operator_schema&lt;/code&gt;, &lt;code&gt;execute_operator&lt;/code&gt;), so an AI agent can compute signals, try a rule, and read back precision and recall.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;The plugin is small on purpose. The Python operators and the pure-Python core (hashing, candidate gathering, the rule) are tested with pytest. The panel UI is built in React with VOODO, the component library the FiftyOne App itself uses.&lt;/p&gt;

&lt;p&gt;The rule exists twice on purpose: in Python for the operators and in TypeScript so the sliders can run it in the browser. A shared fixture, &lt;code&gt;tests/fixtures/rule_cases.json&lt;/code&gt;, feeds both test suites, so the two can't drift.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckdbjh6v6s88b0gnydso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckdbjh6v6s88b0gnydso.png" alt="The image_link_lab plugin folder and its four layers (browser, framework, engine, and core), each calling only the layer below it, with the rule mirrored between rule.py and rule.ts." width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Four layers, each calling only the one below. A shared test fixture keeps the Python and TypeScript rules in sync.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything else is native FiftyOne. CLIP comes from &lt;code&gt;compute_similarity&lt;/code&gt;, the pHash bits are registered as a second similarity index, results are plain &lt;code&gt;Classification&lt;/code&gt; fields, scoring is &lt;a href="https://docs.voxel51.com/user_guide/evaluation/classifications.html" rel="noopener noreferrer"&gt;&lt;code&gt;evaluate_classifications&lt;/code&gt;&lt;/a&gt;, and rule history lives in evaluation runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; The demo dataset, &lt;code&gt;disc21_link_lab&lt;/code&gt;, is a subset of DISC21: 1,000 edited queries and 5,000 originals, with a known original for 500 of the queries. The rest are distractors. The full benchmark has a reference corpus of 1 million images, so don't read results from this subset as benchmark numbers. DISC21's edits are also deliberately brutal (heavy crops, overlays, and blends), so recall will be low on it. On a collection of ordinary re-uploads, pHash alone will catch most copies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/harpreetsahota204/image_link_lab" rel="noopener noreferrer"&gt;Image Link Lab plugin on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/spaces/harpreetsahota/image-link-lab" rel="noopener noreferrer"&gt;Try the plugin on the DISC21 dataset on this Hugging Face space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/voxel51/fiftyone" rel="noopener noreferrer"&gt;FiftyOne on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.voxel51.com/plugins/index.html" rel="noopener noreferrer"&gt;FiftyOne plugin docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2106.09672" rel="noopener noreferrer"&gt;The 2021 Image Similarity Dataset and Challenge (paper)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://voxel51.com/blog/fiftyone-plugins-mcap-lerobot-curation" rel="noopener noreferrer"&gt;Your Robot Data Needs Custom Tools. Build Them as FiftyOne Plugins&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is image copy detection?
&lt;/h3&gt;

&lt;p&gt;Image copy detection is the task of deciding whether a query image is an edited copy of an image in a reference collection, and identifying which one. It differs from similarity search, which returns neighbors without committing to a match.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I find edited or modified copies of an image in a dataset?
&lt;/h3&gt;

&lt;p&gt;Compute more than one kind of similarity, since no single signal survives every edit. Image Link Lab computes a perceptual hash in the phash field and a CLIP similarity index, then links pairs with a rule over both. &lt;code&gt;find_copies&lt;/code&gt; writes the result to a &lt;code&gt;copy_of&lt;/code&gt; field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can perceptual hashing detect mirrored or cropped images?
&lt;/h3&gt;

&lt;p&gt;Not reliably. A pHash summarizes how pixels are laid out, so anything that moves pixels around (a mirror, a rotation, or a big crop) defeats it. A CLIP embedding survives those edits, which is why the plugin uses both.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between exact duplicates, near-duplicates, and copy detection in FiftyOne?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;compute_exact_duplicates&lt;/code&gt; finds byte-identical files. &lt;code&gt;compute_near_duplicates&lt;/code&gt; finds neighbors by a single embedding. Image Link Lab links each query to at most one original using two signals and a rule you set, and shows the evidence for every link.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check a test split for train/test leakage?
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;find_copies&lt;/code&gt; with &lt;code&gt;queries&lt;/code&gt; set to &lt;code&gt;tag:test&lt;/code&gt; and &lt;code&gt;originals&lt;/code&gt; set to &lt;code&gt;tag:train&lt;/code&gt;. Then match &lt;code&gt;copy_of.label != "none"&lt;/code&gt; on the test split to see which test images link to a training image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need ground truth labels?
&lt;/h3&gt;

&lt;p&gt;No. Without a &lt;code&gt;truth_field&lt;/code&gt;, the plugin draws links, and nothing is judged. With one, you get right, wrong, and missed colors, counts, and a native evaluation per rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  How large a dataset can it handle?
&lt;/h3&gt;

&lt;p&gt;Candidates are gathered with dense distance matrices, which works fine for tens of thousands of originals. Beyond that, narrow the originals to a tag or a view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a different embedding than CLIP?
&lt;/h3&gt;

&lt;p&gt;Not from the form. The plugin is deliberately opinionated: one signal for pixels, one for meaning. To swap models, change &lt;code&gt;CLIP_MODEL&lt;/code&gt; in &lt;code&gt;constants.py&lt;/code&gt; to &lt;a href="https://voxel51.com/blog/finding-the-best-embedding-model-for-image-classification" rel="noopener noreferrer"&gt;any zoo model that produces embeddings&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is CLIP slow?
&lt;/h3&gt;

&lt;p&gt;It's the only model in the pipeline, and it runs once. On CPU, budget about a second per image. On a GPU, a few minutes for several thousand.&lt;/p&gt;

</description>
      <category>imagecopydetection</category>
      <category>computervision</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>You Can’t Watch 10,000 Episodes. Measure These Instead: Curation Metrics for LeRobot Datasets</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:01:57 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/you-cant-watch-10000-episodes-measure-these-instead-curation-metrics-for-lerobot-datasets-30p</link>
      <guid>https://dev.to/harpreetsahota/you-cant-watch-10000-episodes-measure-these-instead-curation-metrics-for-lerobot-datasets-30p</guid>
      <description>&lt;p&gt;&lt;em&gt;A field guide to the metrics worth computing on a LeRobot dataset before you train a policy or fine-tune a&lt;/em&gt; &lt;a href="https://voxel51.com/vision-language-action-model-vla" rel="noopener noreferrer"&gt;&lt;em&gt;vision-language-action (VLA) model&lt;/em&gt;&lt;/a&gt;&lt;em&gt;: what each one measures, when to trust it, where it misleads you, and how to run the whole workflow in FiftyOne.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You recorded 400 &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;teleop episodes&lt;/a&gt; last week. You &lt;a href="https://voxel51.com/blog/lerobot-v3-datasets-vla-training-fiftyone" rel="noopener noreferrer"&gt;pulled another 6,000 from the Hugging Face Hub&lt;/a&gt; because the task looked close to yours. Now you want to fine-tune a VLA.&lt;/p&gt;

&lt;p&gt;You can’t watch all of it. At 30 seconds per episode, 10,000 episodes is about 83 hours of video, and that’s before you open a second camera. So you scrub through a handful, they look fine, and you hit train.&lt;/p&gt;

&lt;p&gt;Then the policy hesitates at the first grasp. Somewhere in there is an episode with a frozen wrist camera. Another has 11 seconds of dead time before the arm moves. A third has the task string "Hold". A fourth has a video track that runs 0.4 seconds behind the actions. None of these show up when you watch one episode at a time because none of them is visible in a single episode. They show up when you compare an episode to its peers.&lt;/p&gt;

&lt;p&gt;That’s the gap this post is about. A viewer shows you data. Curation tells you which episode to open next.&lt;/p&gt;

&lt;p&gt;This guide walks through the metrics that do that job for LeRobot and VLA datasets. For each one, it covers what it measures, why you’d compute it, when to use it, how to read it, and where it lies to you. It also shows where each metric lives in a FiftyOne panel, because a metric you can’t filter, tag, and export is just a number in a notebook.&lt;/p&gt;

&lt;p&gt;The post isn’t about any one dataset. The metrics apply to anything in &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;LeRobot v3 format&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Curation metrics answer six different questions: is the episode broken (integrity), is the motion clean (smoothness), is it wasting frames (time), did the robot do what was commanded (tracking), can you trust the cameras (vision), and can a model learn from the instruction (language). Start with the cheapest.&lt;/li&gt;
&lt;li&gt;Most of these metrics need no model and no GPU. Integrity checks read metadata and Parquet. Motion, time, tracking, gripper, and consistency metrics are signal math on &lt;code&gt;action&lt;/code&gt; and &lt;code&gt;observation.state&lt;/code&gt;. Only the camera metrics decode video, at roughly 0.4 s per camera per episode.&lt;/li&gt;
&lt;li&gt;Compare episodes to their own task group, not to the whole dataset. A fold-a-shirt episode and a pick-a-cube episode have different “normal” motion, and a pooled score will rank one task as the problem.&lt;/li&gt;
&lt;li&gt;Correlated metrics should share one vote. Five time metrics that all fire on the same pause are one problem, not five.&lt;/li&gt;
&lt;li&gt;The published evidence is mixed. Spectral arc length (SPARC) smoothness filtering raised RoboMimic success by 16% using one-sixth of the data (RINSE, arXiv:2604.23000). A separate audit (arXiv:2606.10229) found 5 of 7 curation metrics exploited episode length, and that detection accuracy and policy success were uncorrelated. Use scores to order a review queue, and validate with a training run before you delete anything.&lt;/li&gt;
&lt;li&gt;FiftyOne’s role is the workflow around the metrics: one sample per episode, a panel that re-ranks as you filter, click-to-filter charts, &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html#temporal-tags" rel="noopener noreferrer"&gt;temporal tags&lt;/a&gt; on the episode timeline, and a lossless &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html#multimodal-lerobot-export" rel="noopener noreferrer"&gt;export&lt;/a&gt; of the kept subset as a LeRobot v3 dataset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, the LeRobot v3 format, in one paragraph
&lt;/h2&gt;

&lt;p&gt;A LeRobot v3 dataset doesn’t store one file per episode. Frames from many episodes are concatenated into shared Parquet files (&lt;code&gt;data/chunk-000/file-000.parquet&lt;/code&gt;), and each camera’s video is concatenated into shared MP4s. Metadata in &lt;code&gt;meta/info.json&lt;/code&gt; holds the schema and &lt;code&gt;fps&lt;/code&gt;, &lt;code&gt;meta/stats.json&lt;/code&gt; holds normalization statistics, and &lt;code&gt;meta/episodes/&lt;/code&gt; holds one row per episode with its row range and, per camera, the video time window.&lt;/p&gt;

&lt;p&gt;Two consequences matter for curation. First, “episode N” points into shared files, so bookkeeping can drift without anything crashing. Second, the timestamp column is written from the nominal &lt;code&gt;fps&lt;/code&gt;, not from a logged clock, so classic sensor-health checks such as jitter and clock drift don’t apply. You check structure and alignment instead.&lt;/p&gt;

&lt;p&gt;FiftyOne’s native &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html#multimodal-lerobot-datasets" rel="noopener noreferrer"&gt;&lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt;&lt;/a&gt; (FiftyOne 1.22 and later) imports each episode as one &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;multimodal sample&lt;/a&gt;, with a synchronized episode viewer on top (&lt;a href="https://docs.voxel51.com/api/fiftyone.utils.lerobot.html" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;). It reads only the metadata up front and pulls Parquet rows and video frames on demand, which is what makes dataset-level scoring practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you have to tell the tool
&lt;/h2&gt;

&lt;p&gt;Nothing about your robot should be guessed. A 14-dimensional action vector could be two 7-&lt;a href="https://voxel51.com/degrees-of-freedom-dof" rel="noopener noreferrer"&gt;degree-of-freedom (DoF)&lt;/a&gt; arms, or one arm plus a hand, or &lt;a href="https://voxel51.com/end-effector" rel="noopener noreferrer"&gt;end-effector&lt;/a&gt; deltas. If a tool silently picks, every smoothness score downstream is wrong in a way you can’t see.&lt;/p&gt;

&lt;p&gt;So the compute form asks for five things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which arrays are state and action.&lt;/strong&gt; They default to &lt;code&gt;observation.state&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; when the dataset uses the LeRobot standard names, and stay unset otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the action means.&lt;/strong&gt; Is it absolute joint positions in the same space as the state, or something else (deltas, end-effector poses)? This is a yes/no question, and it only appears when the two arrays have the same size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robot and joints.&lt;/strong&gt; Single or dual arm, whether there’s a multi-joint hand, and which joints are the arm and which are the gripper. You pick them by name, with each joint’s observed range beside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gripper open direction.&lt;/strong&gt; Does a high value mean open or closed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cameras.&lt;/strong&gt; Which video cameras to score. None are selected by default, because decoding is the slow part.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything you leave unanswered switches off the metrics that need it, and the form lists exactly which. It never blocks the run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkt9wi5s8tw9lpto0mh4u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkt9wi5s8tw9lpto0mh4u.gif" alt="The compute form asks what your arrays mean instead of guessing, and lists which metrics stay off until you answer." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The compute form asks what your arrays mean instead of guessing, and lists which metrics stay off until you answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. Why does “what the action means” matter so much?&lt;/strong&gt; Tracking, acceleration-spike, and joint-limit metrics compare the commanded &lt;code&gt;action&lt;/code&gt; to the achieved &lt;code&gt;state&lt;/code&gt;. That comparison only means something if both live in the same joint space. If your action is end-effector deltas, a gap between the two is just a unit mismatch, so those metrics stay off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LeRobot curation metrics
&lt;/h2&gt;

&lt;p&gt;Every metric below follows the same pattern: what it &lt;strong&gt;measures&lt;/strong&gt;, &lt;strong&gt;why&lt;/strong&gt; it matters, when to &lt;strong&gt;use&lt;/strong&gt; it, how to &lt;strong&gt;read&lt;/strong&gt; it, and the &lt;strong&gt;pitfall&lt;/strong&gt; to watch for. The harder ones get an intuition-level explanation.&lt;/p&gt;

&lt;p&gt;One convention first. Inside a score, every metric is oriented so that higher means worse. The tables and charts show each metric’s raw direction, and the scoring section explains how they combine.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Integrity: is this episode broken?
&lt;/h2&gt;

&lt;p&gt;Integrity checks don’t say an episode is worse. They say it’s broken. They produce pass, warn, or fail flags and counts. They never enter the ranking score, and they get their own verdict. They’re also the cheapest checks you’ll run, since they read metadata and Parquet and decode nothing.&lt;/p&gt;

&lt;p&gt;Run them first. There’s no point ranking the smoothness of an episode whose rows don’t match its video.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7i856qe6q9btxchi4hkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7i856qe6q9btxchi4hkz.png" alt="A LeRobot v3 episode as a row range in a shared Parquet file and a time window in a shared MP4, with the six integrity checks: frame_gaps, length_mismatch, video_window_mismatch, nonfinite_values, too_short, and schema_mismatch." width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. A v3 episode is a row range in a shared Parquet file plus a time window in a shared MP4 per camera. Each integrity check catches a case where a pointer stopped matching the data. Illustrative values.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame_gaps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non-contiguous or duplicated &lt;code&gt;frame_index&lt;/code&gt; values. Any nonzero count means a broken episode.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;length_mismatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The episode’s declared &lt;code&gt;length&lt;/code&gt; disagrees with the actual number of Parquet rows. Metadata and data disagree.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;video_window_mismatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per camera, the video window duration disagrees with &lt;code&gt;length/fps&lt;/code&gt;. Video and actions differ in length.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nonfinite_values&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NaN or inf in &lt;code&gt;action&lt;/code&gt; or &lt;code&gt;state&lt;/code&gt;. Any nonzero count breaks training.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;too_short&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Episodes under a minimum duration or frame count. Catches one-frame and near-empty episodes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;schema_mismatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When you merge sources: differences in feature shapes and names, &lt;code&gt;fps&lt;/code&gt;, units, camera keys, or resolution. Flags a dataset that won’t collate cleanly.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The six integrity checks and the broken-episode case each one catches.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;timestamp_dev&lt;/code&gt; (the largest deviation of consecutive timestamps from &lt;code&gt;1/fps&lt;/code&gt;) is reported as information only. Because LeRobot writes timestamps from &lt;code&gt;fps&lt;/code&gt;, it’s usually about zero and doesn’t affect the verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; The Hugging Face team’s write-up on community datasets lists these defects as recurring: episodes with one or very few frames, Parquet files deleted without reindexing, and inconsistent action or state dimensions for the same robot. Each one fails silently until a dataloader hits it, or worse, doesn’t fail at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; &lt;code&gt;video_window_mismatch&lt;/code&gt; compares durations, so it can’t see a constant offset. An episode whose video starts 400 ms late passes. That’s what the camera lag metric later in this guide is for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjy6cbw7ioqpbiygaci1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjy6cbw7ioqpbiygaci1.gif" alt="Integrity verdicts and task coverage in the LeRobot Curation panel. Click a row to open that episode in the inspector." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Integrity verdicts and task coverage in the LeRobot Curation panel. Click a row to open that episode in the inspector.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Motion: is the motion clean?
&lt;/h2&gt;

&lt;p&gt;Motion metrics measure the shape of the movement an operator commanded. This is where most of the curation literature lives, and it’s also where the most care is needed, so the preprocessing matters as much as the metric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What signal it reads.&lt;/strong&gt; The action array over the joints you put in the Arm (and Hand) fields. action is what a policy learns to output. On leader-follower rigs such as SO-100, Koch, and ALOHA, it’s the operator’s hand, not the robot’s arm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the signal is prepared.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each joint is range-normalized before speeds are combined, so a joint with a large range doesn’t dominate and units don’t matter.&lt;/li&gt;
&lt;li&gt;Gripper and hand dimensions are excluded from arm speed. A 22-joint dexterous hand is scored as its own signal so it never swamps a 7-joint arm.&lt;/li&gt;
&lt;li&gt;The low-pass cutoff is &lt;code&gt;min(10 Hz, 0.4 × fps)&lt;/code&gt;. At 30 fps that’s 10 Hz, well away from the Nyquist frequency of 15 Hz.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Metrics run on fixed-length windows because some are sensitive to episode duration. Each scored metric also keeps a worst-window value, so one bad stretch isn’t averaged into a fine episode.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;sparc&lt;/code&gt;: how many corrections does the motion contain?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; The spectral arc length of the arm’s speed profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intuition.&lt;/strong&gt; Take the arm's speed over time. A clean reach is one smooth bump: speed rises, peaks, falls. Take that curve’s frequency spectrum and trace along it. A single bump has a compact spectrum, so the traced path is short. Every extra correction, hesitation, or stop-and-go adds frequency content, and the path gets longer. SPARC is the negative of that path length, so smooth motion sits near 0, and fragmented motion goes very negative.&lt;/p&gt;

&lt;p&gt;In its standard form, SPARC is the negative arc length of the normalized speed spectrum up to a cutoff frequency:&lt;/p&gt;

&lt;p&gt;SPARC = -∫ sqrt( (1/ω_c)² + (dV̂(ω)/dω)² ) dω (0 to ω_c)&lt;/p&gt;

&lt;p&gt;where V̂(ω) is the magnitude spectrum of the speed profile, normalized by its peak. You don’t need to compute that by hand to use the metric. The picture matters: more corrections mean a longer trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; It’s the most direct validation in the literature. RINSE (arXiv:2604.23000) reports that SPARC-based filtering gave 16% higher RoboMimic success using one-sixth of the data. It also reports SPARC as about 10 times more noise-robust than log dimensionless jerk (LDLJ).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when.&lt;/strong&gt; You want one primary smoothness number per episode. It’s scale-invariant, so it works across arms of different sizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; Closer to 0 is smoother. Very negative means fragmented, hesitant motion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; SPARC responds to stop-and-go motion, not to additive white noise. A jittery signal from a noisy encoder can still look smooth to SPARC. That’s what the next metric is for.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ldlj&lt;/code&gt;: how much jerk, once you account for size and speed?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; Log dimensionless jerk of the speed profile. Jerk is the rate of change of acceleration. It’s integrated over the movement, made dimensionless using the movement’s duration and peak speed, then log-transformed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; It catches what SPARC doesn’t: high-frequency noise and abrupt changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when.&lt;/strong&gt; You suspect noisy teleop hardware or a bad leader-arm encoder, alongside sparc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; Closer to 0 is smoother.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; It’s noisier than SPARC and duration-sensitive, which is why the windows are fixed-length and why it carries half weight in the score.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfc3a248d3mtnlx57voj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfc3a248d3mtnlx57voj.png" alt="Speed profiles and spectra for a smooth, a stop-and-go, and a jittery version of the same two reaches, with SPARC and LDLJ values for each." width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The same two reaches, moved three ways. SPARC: smooth -2.57, stop-and-go -5.25, jittery -2.56. LDLJ: smooth -8.87, stop-and-go -14.45, jittery -14.71. SPARC separates stop-and-go from the other two. LDLJ penalizes both stop-and-go and noise. Synthetic signals at 30 fps. Values computed from them.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;sparc_phase&lt;/code&gt;: smoothness within each phase of the grasp (opt-in)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; SPARC computed inside each gripper-delimited phase (approach, grasp, transport, and release), rolled up by median.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; Contact is inherently jerky. When the gripper closes on an object, speed changes abruptly for good reasons. Scoring the whole episode as one signal counts those transitions as roughness. Scoring per phase stops that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when.&lt;/strong&gt; Your task has clear grasp and release events, and you’ve declared gripper joints and the open direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; Same as &lt;code&gt;sparc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; It adapts the gripper-phase idea from RINSE and isn’t the paper’s exact method. It’s covered by synthetic tests only. It skips phases with too few samples.&lt;/p&gt;

&lt;p&gt;Two more motion metrics exist for review only and are never scored. &lt;code&gt;jerk_rms&lt;/code&gt; is the root mean square (RMS) jerk after a low-pass filter. It’s noise-sensitive and correlated with the other metrics. &lt;code&gt;psd_lf_hf&lt;/code&gt; is the log ratio of low- to high-frequency power in the speed profile, and it’s unreliable at LeRobot frame rates, where tremor sits near Nyquist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; RINSE assumed its episodes were already filtered for task success, and it tested &lt;a href="https://voxel51.com/imitation-learning" rel="noopener noreferrer"&gt;behavior cloning&lt;/a&gt; with proprioceptive signals, not VLAs. Smoothness helps. It isn’t sufficient. &lt;a href="https://voxel51.com/blog/motion-smoothness-task-success-robolab-egox" rel="noopener noreferrer"&gt;A smooth episode can still show the wrong task&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F065n2cn206yb8mjljf1v.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F065n2cn206yb8mjljf1v.gif" alt="Motion, time, tracking, and gripper metrics as click-to-filter histograms on the Motion &amp;amp; Action tab." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Motion, time, tracking, and gripper metrics as click-to-filter histograms on the Motion &amp;amp; Action tab.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Time efficiency: is the episode wasting frames?
&lt;/h2&gt;

&lt;p&gt;Time metrics measure dead time. They read the commanded motion (&lt;code&gt;action&lt;/code&gt;), with an idle threshold set as a multiple of the episode’s own moving speed. That per-episode threshold means a slow, careful operator isn’t flagged just for being slow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;th&gt;Read it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idle_lead_s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seconds of stillness at the start&lt;/td&gt;
&lt;td&gt;High means dead time before the task begins. Also stores a suggested trim start (&lt;code&gt;keep_from_s&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idle_trail_s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seconds of stillness at the end&lt;/td&gt;
&lt;td&gt;High means a long dead tail. Stores a suggested trim end (&lt;code&gt;keep_to_s&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;longest_pause_s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Longest idle run that isn’t at either end&lt;/td&gt;
&lt;td&gt;High means a mid-task stall or hesitation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;end_motion_ratio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Speed in the last second relative to typical moving speed&lt;/td&gt;
&lt;td&gt;High means the episode probably ended mid-motion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;length_z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Robust z of episode duration within its task group&lt;/td&gt;
&lt;td&gt;Scored as &lt;code&gt;abs(z)&lt;/code&gt;. Positive means unusually long (struggle). Negative means unusually short (abandoned).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Time efficiency metrics, what each one measures, and how to read a high value.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkmuyz0hdsygq02sw3wl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkmuyz0hdsygq02sw3wl.png" alt="Arm speed over one episode with the idle lead, a mid-episode pause, and the idle trail marked, plus a histogram of episode durations within one task group with length_z warn lines." width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. Left: one episode's arm speed with the three idle stretches the time metrics measure, and the keep window they suggest. Right: length_z places an episode's duration against its task group. Synthetic signals.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; Two published pipelines treat this as signal. OpenVLA removed all-zero no-op actions from its training data. SCIZOR’s removed samples included “manipulation failures, slow motion, and pauses.” Both pipelines treated dead frames as removable, and either way they cost training compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when.&lt;/strong&gt; Your data comes from human teleop, where operators pause to think, reposition, or wait for a timer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfalls.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trailing idle and &lt;code&gt;end_motion_ratio&lt;/code&gt; get half weight, because fixed-duration recording setups produce a dead tail from the timer itself.&lt;/li&gt;
&lt;li&gt;A pause next to a regrasp counts as recovery and is excluded from &lt;code&gt;longest_pause_s&lt;/code&gt;. A hesitation that precedes a retry is the operator fixing a problem, not wasting time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;idle_frac&lt;/code&gt; (the overall fraction of idle frames) is shown for context and never scored. A high value can be a legitimate pause. &lt;code&gt;path_length_z&lt;/code&gt; (opt-in) is also never scored, since it correlates with &lt;code&gt;length_z&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;length_z&lt;/code&gt;, &lt;code&gt;path_length_z&lt;/code&gt;, and &lt;code&gt;idle_frac&lt;/code&gt; depend on episode length by construction. Only &lt;code&gt;length_z&lt;/code&gt; is scored, and it shares one vote with the idle metrics. That design decision comes straight from the length-confound audit discussed later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trimming is a suggestion.&lt;/strong&gt; &lt;code&gt;keep_from_s&lt;/code&gt; and &lt;code&gt;keep_to_s&lt;/code&gt; store a proposed range. Nothing is written back to the dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tracking and contact: did the robot do what was commanded?
&lt;/h2&gt;

&lt;p&gt;On a leader-follower rig, &lt;code&gt;action&lt;/code&gt; is where the operator’s leader arm is, and &lt;code&gt;observation.state&lt;/code&gt; is where the follower robot actually is. When the two agree, the robot is doing what it was told. When they diverge, something physical happened: a collision, a stall, a slipping joint, or a following arm that couldn’t keep up.&lt;/p&gt;

&lt;p&gt;These metrics read the state, so they run only when you’ve declared the action to be absolute joint positions in the state space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs26f6sibwnfyelbtas4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs26f6sibwnfyelbtas4a.png" alt="Action versus state for one joint on a leader-follower rig, with a stall, the tracking residual after lag alignment, and the cross-correlation curve that estimates the lag." width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. The follower trails the leader by a few frames (right), then stalls while the command keeps moving. The residual after lag alignment lights up exactly where the stall is, and the catch-up produces an acceleration spike. Synthetic signals.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;track_resid&lt;/code&gt;: how far is the follower from the command?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; The per-joint, normalized gap between action and state after lag alignment, with each joint’s median offset removed first. Arm joints only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; Calibration offsets are constant and harmless. You want the part of the gap that varies, where contact and slippage live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; High means the robot isn’t reaching the commanded position. It also raises interval flags that become spans on the timeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;track_lag_ms&lt;/code&gt;: how late is the follower?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; The delay between command and response, estimated by sliding one signal against the other and picking the offset with the highest cross-correlation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lag = argmax_k corr( action[t], state[t + k] )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; Mostly it’s a hardware property. A servo with a slow control loop lags every episode by about the same amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; It’s summarized per dataset or session, and only episodes far from the dataset median are flagged. It never enters a score.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;accel_spike_frac&lt;/code&gt;: sudden jolts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; The fraction of frames where any joint’s state acceleration exceeds median + k × MAD (median absolute deviation), with an absolute floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; It’s a proxy for collisions and contact. The absolute floor stops idle joints from turning every frame into a spike.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;joint_limit_frac (opt-in)&lt;/code&gt;: operating near the edge
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; The share of (frame, joint) pairs within a margin (default 2% of range) of a joint limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; It uses your limits if you give them, and otherwise falls back to &lt;code&gt;meta/stats.json&lt;/code&gt; min and max. That’s a weak proxy, because observed extremes always sit at the edge of the observed range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; These metrics have no published downstream validation. Treat them as sanity checks. Related work (RoboDrop, arXiv:2609.10021) shows observation-action temporal misalignment is a major corruption that action-only methods handle poorly, and a teleoperation paper (arXiv:2605.26349) flags stalls and operation near joint limits, but its user study had 3 operators. That’s weak evidence, not a validated benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Gripper: is the operator fighting the grasp?
&lt;/h2&gt;

&lt;p&gt;Gripper metrics read the gripper dimension (averaged into one signal if you picked several), and they need the open direction you declared.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;th&gt;Read it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gripper_flips_per_s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open/close transitions per second, with hysteresis so noise near the threshold doesn’t count&lt;/td&gt;
&lt;td&gt;High means chatter or a hesitant operator.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;missed_grasp_frac&lt;/code&gt; (opt-in)&lt;/td&gt;
&lt;td&gt;Fraction of close commands where the follower closes fully with no stall residual, meaning nothing was held&lt;/td&gt;
&lt;td&gt;High means empty grasps. Heuristic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;recovery_count&lt;/code&gt; (never scored)&lt;/td&gt;
&lt;td&gt;Number of detected regrasps: open, then close again near the same place&lt;/td&gt;
&lt;td&gt;Neutral or positive. Writes a &lt;code&gt;recovery&lt;/code&gt; tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Gripper metrics for chatter, empty grasps, and regrasp recovery.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; A gripper that flips ten times in four seconds is either a noisy sensor or an operator who can’t get a grip. Both are worth a look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjuypjrtwqng0rpnua3ng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjuypjrtwqng0rpnua3ng.png" alt="Three gripper open and close traces: a clean grasp, chatter, and a regrasp recovery, each labeled with its flips per second." width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. Hysteresis keeps noise near the threshold from counting as a transition. Chatter scores 0.88 flips per second against 0.25 for a clean grasp. The regrasp has more transitions than the clean grasp and is tagged as a recovery instead of being penalized. Synthetic signals.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The design choice worth knowing.&lt;/strong&gt; Several clean open-close cycles are often a recovery, an operator correcting a failed grasp. That’s behavior you may want a policy to learn. So &lt;code&gt;recovery_count&lt;/code&gt; is never penalized, and a pause next to a regrasp isn’t counted against the time metrics either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; The gripper and tracking metrics have no published downstream validation. &lt;code&gt;missed_grasp_frac&lt;/code&gt; and &lt;code&gt;recovery_count&lt;/code&gt; are covered by synthetic tests only.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Consistency: does this demonstration agree with its peers?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;action_divergence&lt;/code&gt;: same situation, different action
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; For frames in this episode, find the nearest &lt;code&gt;state&lt;/code&gt; neighbors in other episodes of the same task group, and measure the variance of their &lt;code&gt;action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intuition.&lt;/strong&gt; If five demonstrations are in nearly the same state and four push left while one pushes right, the policy sees contradictory labels for the same input. Belkhale et al.&amp;nbsp;(NeurIPS 2023) call this &lt;em&gt;action divergence&lt;/em&gt; and report that “making actions more consistent tends to increase policy success.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when.&lt;/strong&gt; You have enough episodes per task group for neighbors to exist (about 20).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; High means this demonstration takes a different action from peers in a similar state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall.&lt;/strong&gt; Similar states can legitimately need different actions. A task with two valid strategies produces divergence that isn’t a defect. Treat it as a review signal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4a271r8m4037ai4dmev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4a271r8m4037ai4dmev.png" alt="Neighboring episodes' actions from a similar state, agreeing on the left (low divergence) and contradicting each other on the right (high divergence)." width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6. Each arrow is the action another episode took from a state near this episode's. Variance of those actions: 0.00 on the left, 0.59 on the right. Schematic, in a 2D stand-in for the state space.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;&lt;code&gt;stats_leverage&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;(never scored): how much one episode bends the normalization&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures.&lt;/strong&gt; How many feature dimensions this episode alone stretches beyond the rest of the dataset’s &lt;code&gt;meta/stats.json&lt;/code&gt; bounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters.&lt;/strong&gt; Training normally normalizes with &lt;code&gt;stats.json&lt;/code&gt;. One episode with a sensor glitch that reports a joint value of 400 where everything else lives in -1 to 1 distorts that normalization for every other episode, and you’d never see it in a single-episode viewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Camera: can you trust the pixels?
&lt;/h2&gt;

&lt;p&gt;Camera metrics are opt-in because they decode video. They’re pixel and signal arithmetic on a few decoded frames, with no model, so there’s nothing to download.&lt;/p&gt;

&lt;p&gt;We store every value per camera and compare it with the same camera in other episodes. A wrist camera is never judged against a top camera. An episode’s camera score is its worst camera.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;th&gt;Read it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The 10th percentile of Laplacian variance over 12 sampled frames, at a fixed size (longer side 480 px), reported as &lt;code&gt;log10(1 + variance)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lower is blurrier. The log scale exists because raw variance is heavy-tailed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exposure_err&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Distance of mean luminance from mid-gray, from 0 (mid-gray) to 1 (black or white). Median over sampled frames&lt;/td&gt;
&lt;td&gt;High means the image is too dark or too bright.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clipped_frac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Share of pixels at pure black or pure white&lt;/td&gt;
&lt;td&gt;High means blown highlights or crushed shadows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frozen_frac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Of 6 short bursts, the share where frames are nearly identical while the state shows the robot moving&lt;/td&gt;
&lt;td&gt;High means a frozen or dropped feed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;video_action_lag_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Absolute offset between motion in the video and arm speed in the action, from cross-correlation over the central 30 s&lt;/td&gt;
&lt;td&gt;Near 0 is in sync.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Camera metrics, computed from a few decoded frames and compared with the same camera in other episodes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ku20g6xi5euy1hdtae5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ku20g6xi5euy1hdtae5.png" alt="Five synthetic camera frames showing the reference, blur, exposure, clipping, and frozen-feed values used by the camera metrics." width="799" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 7. The frame metrics on a synthetic scene, with values computed at the tile's resolution. The plugin measures at a fixed size (longer side 480 px) and compares each camera with the same camera in other episodes. A live camera on a static scene still shows frame-to-frame change (about 0.8 gray levels here). A frozen feed shows none.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;frozen_frac&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;is built the way it is.&lt;/strong&gt; A still scene with a still robot isn’t a frozen camera. So a burst counts only when the robot is moving, and the pixels aren’t. A live camera on a static scene reads about 0.3 gray levels of frame-to-frame change or more, and the threshold (0.05) sits far below that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;video_action_lag_ms&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;is the one to take seriously.&lt;/strong&gt; If your video runs 400 ms behind your actions, the policy is trained to predict the action from an observation that’s already stale. It’s the failure that integrity checks can’t see, since durations match and every file is intact. The metric is relative to peers: a lag shared by every episode sets the baseline, and only episodes that differ from it stand out. Nothing is reported when the video doesn’t track the action (correlation under 0.3), for example when the robot is out of view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgddph0f27rexiepkv8ap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgddph0f27rexiepkv8ap.png" alt="Arm speed and video motion energy offset in time, the cross-correlation peak at 400 ms, and a histogram of video-action lag across episodes with a few late outliers." width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 8. Video motion energy trails the arm speed by 400 ms here. Cross-correlation finds the offset. Across episodes of one camera, the lag everyone shares is the baseline, and the episodes that differ from it stand out. Synthetic signals.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use them when.&lt;/strong&gt; You merge data from several sessions, several rigs, or the Hub. A camera problem that hits one session or one rig is easy to miss when you sample a few episodes, and obvious once you rank them all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfalls.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A scene that’s dark by design reads as high &lt;code&gt;exposure_err&lt;/code&gt;, and black borders count as clipped. The per-camera comparison absorbs both, but a view that pools many robots will flag more episodes than a single-task dataset.&lt;/li&gt;
&lt;li&gt;Cameras stored as images rather than video can’t be read, and aren’t offered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Frame metrics share one decode of 12 frames, so enabling all three costs the same as enabling one. On the development set, a video takes about 0.05 s for the frame metrics and about 0.9 s per episode (all cameras) for the lag. Budget about 0.4 s per camera per episode when you run it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0k97kx5h1v1nabktpyvq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0k97kx5h1v1nabktpyvq.gif" alt="The Vision tab compares each camera with the same camera in other episodes." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Vision tab compares each camera with the same camera in other episodes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Language: can a VLA learn from this instruction?
&lt;/h2&gt;

&lt;p&gt;For a VLA, the task string is the conditioning input. It’s what the model sees alongside the images. A clean trajectory with the instruction &lt;code&gt;"Hold"&lt;/code&gt; teaches the model very little about language.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;task_missing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Empty, whitespace-only, or null task string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;task_generic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fewer than 3 tokens, a placeholder (&lt;code&gt;"task desc"&lt;/code&gt;, &lt;code&gt;"Hold"&lt;/code&gt;), or no action verb&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The two language checks run on each episode’s task string.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Task strings are canonicalized (case, whitespace, and punctuation) before grouping, so &lt;code&gt;"Pick up the cube."&lt;/code&gt; and &lt;code&gt;"pick up the cube"&lt;/code&gt; land in one group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s separate.&lt;/strong&gt; Language flags never touch the score, the verdict, or the flag count. They get their own verdict and their own column. A VLA user reads it next to the score. A policy trained without language conditioning can ignore it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; Whether generic instructions hurt VLA language grounding hasn’t been measured in anything we reviewed. &lt;code&gt;task_generic&lt;/code&gt; rests on the Hugging Face community’s list of these strings as real defects, which they documented while curating data for SmolVLA. Static checks also can’t tell you that a perfectly grammatical instruction describes the wrong video. That’s a model-based check, covered below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfnwanb8emvqx7xjn4vq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfnwanb8emvqx7xjn4vq.gif" alt="Language verdicts flag missing or generic task strings without touching the score." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Language verdicts flag missing or generic task strings without touching the score.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Outliers: unusual is not the same as bad
&lt;/h2&gt;

&lt;p&gt;Outlier metrics are never scored.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iforest_score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Isolation-forest anomaly score over the episode’s metric vector, fit within its task group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;novelty_knn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mean distance to the nearest same-group episodes in metric-vector space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_outlier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Either score at z ≥ 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The three outlier metrics, fit within each task group and never scored.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use them for.&lt;/strong&gt; A second look at episodes that no single metric flags but that sit far from everything else in the group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read them.&lt;/strong&gt; &lt;code&gt;novelty_knn&lt;/code&gt; is two-sided. Very high means unlike the rest. Very low means near-duplicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why they never score.&lt;/strong&gt; The length-confound audit (arXiv:2606.10229) found that isolation-forest curation matched no curation at all, and that &lt;a href="https://voxel51.com/outlier-detection" rel="noopener noreferrer"&gt;outlier detectors&lt;/a&gt; can rank defective episodes as normal. It’s a single-author preprint on 80 scripted demonstrations, so read it as a caution rather than a verdict. The practical takeaway is this: a “weird” episode might be your most valuable edge case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1dzarbzzpmw5et078euh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1dzarbzzpmw5et078euh.gif" alt="Outliers get their own scatter on the Overview tab, for information only. They never enter the score." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Outliers get their own scatter on the Overview tab, for information only. They never enter the score.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Master reference table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Reads&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;In score?&lt;/th&gt;
&lt;th&gt;Use it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;frame_gaps&lt;/code&gt;, &lt;code&gt;length_mismatch&lt;/code&gt;, &lt;code&gt;nonfinite_values&lt;/code&gt;, &lt;code&gt;too_short&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;metadata, Parquet&lt;/td&gt;
&lt;td&gt;any nonzero is bad&lt;/td&gt;
&lt;td&gt;Own verdict&lt;/td&gt;
&lt;td&gt;Always, first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;video_window_mismatch&lt;/code&gt;, &lt;code&gt;schema_mismatch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;metadata, video windows&lt;/td&gt;
&lt;td&gt;any mismatch is bad&lt;/td&gt;
&lt;td&gt;Own verdict&lt;/td&gt;
&lt;td&gt;Merging sources, any camera data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sparc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closer to 0 is smoother&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ldlj&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closer to 0 is smoother&lt;/td&gt;
&lt;td&gt;Yes, half weight&lt;/td&gt;
&lt;td&gt;Noisy hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sparc_phase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;action&lt;/code&gt; + gripper&lt;/td&gt;
&lt;td&gt;closer to 0 is smoother&lt;/td&gt;
&lt;td&gt;Yes (opt-in)&lt;/td&gt;
&lt;td&gt;Grasp-and-release tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;jerk_rms&lt;/code&gt;, &lt;code&gt;psd_lf_hf&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lower / higher is smoother&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Review only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;idle_lead_s&lt;/code&gt;, &lt;code&gt;longest_pause_s&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Human teleop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;idle_trail_s&lt;/code&gt;, &lt;code&gt;end_motion_ratio&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes, half weight&lt;/td&gt;
&lt;td&gt;Human teleop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;length_z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;metadata&lt;/td&gt;
&lt;td&gt;abs(z), lower is better&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Mixed-quality sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;idle_frac&lt;/code&gt;, &lt;code&gt;path_length_z&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;context&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Review only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;track_resid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;action&lt;/code&gt; + &lt;code&gt;state&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Leader-follower rigs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;accel_spike_frac&lt;/code&gt;, &lt;code&gt;joint_limit_frac&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Contact-heavy tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;track_lag_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;action&lt;/code&gt; + &lt;code&gt;state&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;far from median is flagged&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Comparing rigs and sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gripper_flips_per_s&lt;/code&gt;, &lt;code&gt;missed_grasp_frac&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;gripper&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Grasp tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;recovery_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gripper&lt;/td&gt;
&lt;td&gt;neutral&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Understanding recovery behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;action_divergence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;state&lt;/code&gt; + &lt;code&gt;action&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;20+ episodes per task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stats_leverage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta/stats.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Before training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;blur&lt;/code&gt;, &lt;code&gt;exposure_err&lt;/code&gt;, &lt;code&gt;clipped_frac&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;video frames&lt;/td&gt;
&lt;td&gt;see above&lt;/td&gt;
&lt;td&gt;Yes (camera group)&lt;/td&gt;
&lt;td&gt;Multi-session data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frozen_frac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;video + &lt;code&gt;state&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;lower is better&lt;/td&gt;
&lt;td&gt;Yes (camera group)&lt;/td&gt;
&lt;td&gt;Always, if you can afford to decode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;video_action_lag_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;video + &lt;code&gt;action&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;near 0 is in sync&lt;/td&gt;
&lt;td&gt;Yes (camera group)&lt;/td&gt;
&lt;td&gt;Multi-rig data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;task_missing&lt;/code&gt;, &lt;code&gt;task_generic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;task string&lt;/td&gt;
&lt;td&gt;flag&lt;/td&gt;
&lt;td&gt;Own verdict&lt;/td&gt;
&lt;td&gt;Fine-tuning a VLA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;iforest_score&lt;/code&gt;, &lt;code&gt;novelty_knn&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;metric vector&lt;/td&gt;
&lt;td&gt;context&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Second-look queue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Every curation metric in this guide: what it reads, which direction is worse, whether it enters the score, and when to use it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond heuristics: the model-based metrics
&lt;/h2&gt;

&lt;p&gt;Everything above runs without a model. That’s useful: nothing to download, nothing to host, and cheap, repeatable results. It also means certain questions stay unanswered. A heuristic can’t tell you that an instruction describes the wrong video, that two episodes are near-duplicates in what they show, or whether the robot actually completed the task.&lt;/p&gt;

&lt;p&gt;The research literature has methods for each. They cost more, and their evidence is a mix of strong and thin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing to be clear about: the plugin this post demonstrates computes none of these.&lt;/strong&gt; It’s deliberately heuristic-only. FiftyOne provides the infrastructure for the model-based tier. It has embeddings and similarity through the &lt;a href="https://docs.voxel51.com/brain/index.html" rel="noopener noreferrer"&gt;FiftyOne Brain&lt;/a&gt;, a &lt;a href="https://docs.voxel51.com/model_zoo/index.html" rel="noopener noreferrer"&gt;model zoo&lt;/a&gt; (including &lt;a href="https://docs.voxel51.com/model_zoo/remote.html" rel="noopener noreferrer"&gt;remote zoo models&lt;/a&gt;) for running vision-language models (VLMs), and the same panel and &lt;a href="https://docs.voxel51.com/user_guide/app.html#tags-and-tagging" rel="noopener noreferrer"&gt;tagging&lt;/a&gt; workflow to review whatever those produce. You’d build these as additional &lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html#operators" rel="noopener noreferrer"&gt;operators&lt;/a&gt; on the same pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Instruction-video agreement (VLM).&lt;/strong&gt; Sample frames, ask a vision-language model whether the video shows the instruction, and store the answer, a confidence score, and a proposed rewrite. SmolVLA’s data pipeline did a version of this, using Qwen2.5-VL-3B to rewrite task strings from sample frames plus the original label, with a prompt asking for under 30 characters starting with an action verb (“Pick,” “Place,” “Open”). &lt;strong&gt;Watch out:&lt;/strong&gt; VLM judges are noisy, and OpenGVL’s benchmark found open VLMs lag proprietary ones. Keep the result as its own verdict, never folded into a score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Embedding-based duplicates and novelty.&lt;/strong&gt; Embed sampled frames and the action trajectory, then find near-duplicates by cosine similarity. SCIZOR does this at the transition level, using a joint state-action embedding, and reports that its two thresholds (εs = 0.58, εd = 0.99) transferred across RoboMimic, Open X-Embodiment, and real data. &lt;strong&gt;Watch out:&lt;/strong&gt; near-duplicates in a table-top task might be exactly the repetition that robust policies need. Duplication needs a human decision about how much is too much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Task progress.&lt;/strong&gt; Ask a model to order shuffled frames by progress, then measure how well that order matches the true frame order (Value-Order Correlation, from GVL). OpenGVL applied this to LeRobot Hub data and found task-definition problems, labeling ambiguity, and failed or out-of-distribution episodes. TOPReward replaces generated numbers with token probabilities and reports VOC of 0.874 with Qwen3-VL-8B against 0.218 for GVL with the same model on a filtered Open X-Embodiment subset. &lt;strong&gt;Watch out:&lt;/strong&gt; these are model-dependent, and the numbers come from the authors’ own benchmarks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Information-based scoring.&lt;/strong&gt; DemInf (Hejna et al., RSS 2025) estimates each demonstration’s contribution to state-action mutual information with k-nearest neighbor (kNN) estimators over learned embeddings. Its abstract reports a 5-10% improvement on RoboMimic and better performance on real ALOHA and Franka setups. The authors note it works best with relative actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Policy-in-the-loop.&lt;/strong&gt; CUPID uses influence functions to estimate each demonstration’s effect on expected return from evaluation rollouts, and reports that under 33% of curated data yields state-of-the-art RoboMimic diffusion policies. Demo-SCORE trains a classifier on successful versus failed policy rollouts and filters demonstrations that resemble the failures, and its abstract reports over 15-35% higher absolute success. These need a policy and rollouts, so they’re the highest-fidelity and the most expensive tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CUPID finding to remember.&lt;/strong&gt; In CUPID’s results, DemInf curated the “highest overall quality,” yet CUPID-curated policies matched or beat it. The authors write that “human perception of demonstration quality does not necessarily correspond to data that maximizes downstream policy success.” That’s a direct warning about every heuristic score in this guide, including the ones that look best.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dataset-level metrics
&lt;/h3&gt;

&lt;p&gt;Some questions aren’t about any one episode.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task coverage.&lt;/strong&gt; Episodes per canonical task. A dataset with 1,900 episodes of one task and 12 of another isn’t balanced, no matter what the total says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source balance.&lt;/strong&gt; Episodes per source repo when you’ve merged several.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diversity.&lt;/strong&gt; &lt;a href="https://voxel51.com/blog/vla-generalization-data-diversity" rel="noopener noreferrer"&gt;How spread out trajectories are within a task&lt;/a&gt;. Belkhale et al.&amp;nbsp;also found “state diversity is not always beneficial,” so more spread isn’t automatically better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixture weights.&lt;/strong&gt; If you’re combining datasets, the weights matter as much as the filtering. Re-Mix (Hejna et al., CoRL 2024) optimizes domain weights and reports beating uniform weights by 38%. Octo and OpenVLA both curated mixtures from Open X-Embodiment by hand. Octo dropped datasets without images or delta end-effector control, plus ones “too repetitive, low image resolution, or excessively niche.” OpenVLA kept single-arm, third-person-camera, end-effector datasets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The panel reports coverage as a count on the Integrity &amp;amp; Coverage tab. It’s a count, not a pick, since the right balance depends on what you’re training for.&lt;/p&gt;

&lt;h2&gt;
  
  
  From metrics to a ranking
&lt;/h2&gt;

&lt;p&gt;Dozens of metrics don’t help you decide where to look. One ranking does. Getting there takes two decisions: compared to what, and how to combine them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compared to what: normalization
&lt;/h3&gt;

&lt;p&gt;Task strings are canonicalized, then episodes are grouped by task (an episode with several tasks is grouped by its sorted set). Groups with at least 20 episodes are scored against themselves. A smaller group is pooled across views, and a view with fewer than 20 episodes overall is pooled and marked low-confidence in the panel.&lt;/p&gt;

&lt;p&gt;The scoring uses robust z-scores with a floored scale. Zero-inflated metrics (such as &lt;code&gt;idle_lead_s&lt;/code&gt; and &lt;code&gt;frame_gaps&lt;/code&gt;, where most episodes score exactly zero) use percentile ranks or a tail-based scale, because a standard deviation computed over mostly zeros is meaningless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why per task.&lt;/strong&gt; Without it, you rank tasks against each other. When we ran an earlier version of this scoring on a multi-task robot dataset, two tasks topped the ranking as a group. That’s the cross-task effect: a batch that mixes tasks produces misleading outliers, because “normal” differs by task. Score episodes of the same task together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdd0qqwj5esj4avwk31l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdd0qqwj5esj4avwk31l.png" alt="Two histograms comparing pooled and per-task warn lines on the same two tasks. Pooled, the warn line flags 26 of task B's 30 episodes. Per task, it flags 2 of 120 in task A and 1 of 30 in task B." width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 9. A slower, smaller task (B) pooled with a larger one (A). Pooled, the warn line sits near A's distribution and flags 26 of B's 30 episodes for being task B. Per task, each group is flagged against its own distribution: 2 of 120 in A and 1 of 30 in B. Synthetic data, robust z with a median and MAD scale.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Combined how: groups, and the max
&lt;/h3&gt;

&lt;p&gt;Correlated metrics share a group, and a group counts as one vote:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Scored members&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;motion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sparc&lt;/code&gt;, &lt;code&gt;ldlj&lt;/code&gt;, &lt;code&gt;sparc_phase&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;idle_lead_s&lt;/code&gt;, &lt;code&gt;idle_trail_s&lt;/code&gt;, &lt;code&gt;longest_pause_s&lt;/code&gt;, &lt;code&gt;end_motion_ratio&lt;/code&gt;, &lt;code&gt;length_z&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tracking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;track_resid&lt;/code&gt;, &lt;code&gt;accel_spike_frac&lt;/code&gt;, &lt;code&gt;joint_limit_frac&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gripper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gripper_flips_per_s&lt;/code&gt;, &lt;code&gt;missed_grasp_frac&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;consistency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action_divergence&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;camera&lt;/code&gt; (opt-in)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;blur&lt;/code&gt;, &lt;code&gt;exposure_err&lt;/code&gt;, &lt;code&gt;clipped_frac&lt;/code&gt;, &lt;code&gt;frozen_frac&lt;/code&gt;, &lt;code&gt;video_action_lag_ms&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The scored members of each metric group. Each group counts as one vote in the episode score.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Inside a group, the value is the maximum of the weighted, oriented member z-scores. Across groups, the episode’s score is the maximum group value, with the weighted mean as a tie-breaker. Weights are 1.0 except &lt;code&gt;ldlj&lt;/code&gt;, &lt;code&gt;idle_trail_s&lt;/code&gt;, and &lt;code&gt;end_motion_ratio&lt;/code&gt; at 0.5.&lt;/p&gt;

&lt;p&gt;The effect is that five fine ones can’t average away one severe problem in one dimension. An episode with a frozen camera and perfect motion still ranks near the top. Each episode also carries &lt;code&gt;n_flags&lt;/code&gt; (how many groups are flagged) and &lt;code&gt;driver&lt;/code&gt; (which group is behind its score), so you know why it ranks where it does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyi9w2ur88y3zapplnw8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyi9w2ur88y3zapplnw8r.png" alt="Group values for an example episode across the motion, time, tracking, gripper, consistency, and camera groups, with the camera group's frozen_frac setting the episode score of 4.3." width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 10. An example episode with made-up z-scores. Every group is fine except camera, where frozen_frac is at 4.3. The score is the maximum group value, n_flags counts groups at or above the warn line (z = 2), and driver names the group behind the score.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbakemgpff2g5r7bn56e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbakemgpff2g5r7bn56e.gif" alt="The worst-first ranking shows each episode's score, flag count, and the group driving it." width="640" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The worst-first ranking shows each episode's score, flag count, and the group driving it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow, step by step
&lt;/h2&gt;

&lt;p&gt;A metric earns its place by changing what you do next. Here’s the loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Import the dataset
&lt;/h3&gt;

&lt;p&gt;One sample is one episode. Set &lt;code&gt;VFF_MULTIMODAL=1&lt;/code&gt; before importing FiftyOne in every process that touches a LeRobot dataset, since it enables the &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;multimodal episode viewer&lt;/a&gt;. Install the plugin with the &lt;a href="https://docs.voxel51.com/plugins/index.html" rel="noopener noreferrer"&gt;plugin manager&lt;/a&gt; or link a checkout into your plugins folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/lerobot/my_dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dataset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LeRobotDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Merge another source if you want to curate across datasets
&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/lerobot/other_dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dataset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LeRobotDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://docs.voxel51.com/user_guide/app.html" rel="noopener noreferrer"&gt;App guide&lt;/a&gt; for sidebar filters and &lt;a href="https://docs.voxel51.com/user_guide/app.html#spaces" rel="noopener noreferrer"&gt;Spaces&lt;/a&gt;, which is where the curation panel opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Compute quality
&lt;/h3&gt;

&lt;p&gt;Open the &lt;a href="https://docs.voxel51.com/plugins/using_plugins.html#using-operators" rel="noopener noreferrer"&gt;operator browser&lt;/a&gt; and run &lt;strong&gt;LeRobot curation: compute quality&lt;/strong&gt;. It’s &lt;a href="https://docs.voxel51.com/plugins/using_plugins.html#delegated-operations" rel="noopener noreferrer"&gt;delegated&lt;/a&gt; by default, so it runs in the background. The form has four tabs: Data (what you tell the tool), Metrics (a checkbox per metric, grouped by family), Camera, and Normalization (the smallest task group scored on its own, default 20).&lt;/p&gt;

&lt;p&gt;Your picks are remembered, so re-running doesn’t start from scratch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vnms64t7q88q5kjilkn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vnms64t7q88q5kjilkn.gif" alt="Running LeRobot curation: compute quality from the operator browser. It runs delegated, in the background." width="640" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Running LeRobot curation: compute quality from the operator browser. It runs delegated, in the background.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Read the Overview
&lt;/h3&gt;

&lt;p&gt;Open a panel and choose &lt;strong&gt;LeRobot Curation&lt;/strong&gt;. The Overview tab has the score histogram, verdict counts for Integrity and Language, episodes per task, the outlier scatter, and the worst-first ranking. The panel follows the view: filter the grid and it re-ranks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Drill into the family that drives the problem
&lt;/h3&gt;

&lt;p&gt;Use the driver column to pick a tab. Motion &amp;amp; Action shows one histogram per metric. Vision shows the five camera metrics with camera chips. Click any bar and the samples grid filters to those episodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Open the inspector
&lt;/h3&gt;

&lt;p&gt;Click any row. The inspector shows every metric with its per-arm or per-camera breakdown, joint traces (action solid, state dashed), the speed profile with the idle threshold, the gripper timeline, and a few frames from the cameras, picked around the flagged spans.&lt;/p&gt;

&lt;p&gt;Flagged spans (idle stretches, the longest pause, the roughest smoothness window, and acceleration spikes) are also written as &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html#temporal-tags" rel="noopener noreferrer"&gt;temporal tags&lt;/a&gt; on the episode’s &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html#timeline-tracks" rel="noopener noreferrer"&gt;timeline&lt;/a&gt; once their metric reaches warn so that you can scrub straight to them. Regrasp recoveries are tagged too, as information. A re-run replaces the plugin’s tags and leaves any tag you drew yourself alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiabcm3t2ofl065ljo456.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiabcm3t2ofl065ljo456.gif" alt="The inspector shows every metric, the joint traces, and the speed profile, and writes flagged spans as temporal tags on the episode timeline." width="640" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The inspector shows every metric, the joint traces, and the speed profile, and writes flagged spans as temporal tags on the episode timeline.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Tag, don’t delete
&lt;/h3&gt;

&lt;p&gt;The footer has three tag buttons: &lt;code&gt;review&lt;/code&gt;, &lt;code&gt;exclude-candidate&lt;/code&gt;, and &lt;code&gt;relabel&lt;/code&gt;. They tag the selection, or everything in view if nothing is selected. Nothing is deleted or hidden. These are ordinary FiftyOne &lt;a href="https://docs.voxel51.com/user_guide/using_datasets.html#tags" rel="noopener noreferrer"&gt;sample tags&lt;/a&gt;, so you can filter on them with &lt;a href="https://docs.voxel51.com/user_guide/using_views.html#view-stages" rel="noopener noreferrer"&gt;view stages&lt;/a&gt;, save a view, or act on them from code.&lt;/p&gt;

&lt;p&gt;Marking an episode exclude-candidate is always a separate human action. The scores never do it for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Export the kept subset
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exclude-candidate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;export_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/lerobot/my_dataset_curated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dataset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LeRobotDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;export_media&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html#multimodal-lerobot-export" rel="noopener noreferrer"&gt;native exporter&lt;/a&gt; (&lt;a href="https://docs.voxel51.com/api/fiftyone.utils.lerobot_export.html" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;) writes a self-contained LeRobot v3 dataset. &lt;code&gt;export_media=True&lt;/code&gt; is required, episodes are renumbered contiguously, and task and frame indexes are rebuilt. From there, you can push it to the Hub as you would any LeRobot dataset.&lt;/p&gt;

&lt;p&gt;If you’d rather edit in place, send the excluded &lt;code&gt;episode_index&lt;/code&gt; values to &lt;code&gt;lerobot-edit-dataset&lt;/code&gt; with the &lt;code&gt;delete_episodes&lt;/code&gt; operation. Task string rewrites are a separate matter: as of the sources we reviewed, &lt;code&gt;lerobot-edit-dataset&lt;/code&gt; can’t yet edit task strings, so accepted rewrites need to be patched into &lt;code&gt;meta/tasks.parquet&lt;/code&gt; and the per-frame &lt;code&gt;task_index&lt;/code&gt; after export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it honest
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://voxel51.com/blog/robot-episode-quality-triage" rel="noopener noreferrer"&gt;&lt;strong&gt;Scores are triage, not verdicts&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt; Smoothness and timing say nothing about whether the demonstration did the right thing. The plugin’s own documentation puts it this way: use it to decide where to look first, and don’t use it as an automatic accept/reject gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The evidence is genuinely mixed.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smoothness filtering helped in RINSE, but not in behavior cloning with proprioceptive signals on episodes already filtered for success.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://voxel51.com/blog/robot-data-quality-scoring" rel="noopener noreferrer"&gt;curation-metrics audit&lt;/a&gt; found 5 of 7 metrics exploited episode length, and detection accuracy didn’t predict policy success. That’s one preprint on 80 scripted demonstrations. It’s also exactly why outlier metrics aren’t scored and why the length check exists.&lt;/li&gt;
&lt;li&gt;CUPID found that data humans would call high quality can underperform data selected by influence on the policy.&lt;/li&gt;
&lt;li&gt;The gripper and tracking metrics have no published downstream validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The thresholds are starting points.&lt;/strong&gt; Defaults such as the 2% joint-limit margin, the warn line at z ≥ 2, the 20-episode group size, and the camera thresholds were calibrated on a 102-episode development set. They aren’t yet adjustable in the form. Check them on your own data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the metrics are checked.&lt;/strong&gt; Each scored metric has a corruption it must catch: added hesitation, an inserted idle run, a duplicated frame, a blanked task, a stalled joint, and a blurred or frozen video. The harness applies the corruption to a real episode and asserts that the corrupted copy ranks worse than the original. It also re-scores every episode truncated to a common length, to find metrics that only measure duration. On 102 episodes, it takes about 5 minutes with camera metrics included. That tells you a metric detects what it claims to detect. It can’t tell you the detection predicts policy quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So validate.&lt;/strong&gt; Before you drop data, train on the curated subset and the full set, then compare. A curation run you haven’t validated with a training run is a hypothesis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FiftyOne fits
&lt;/h2&gt;

&lt;p&gt;Rerun and Foxglove are excellent at what they’re built for: looking closely at a recording. If you want a synchronized view of cameras, joint traces, and 3D for one episode, they’re good tools.&lt;/p&gt;

&lt;p&gt;Curation asks a different question, which is about a collection. Which of these 10,000 episodes should I look at first? Of those, which ones have a camera problem? Show me only the ones from this session. Tag these 40. Give me everything else as a dataset I can train on.&lt;/p&gt;

&lt;p&gt;That needs a dataset model rather than a viewer: episodes as queryable samples, fields you can filter on, charts that filter the grid when you click them, tags that persist, saved views, and an export that round-trips to the training format. FiftyOne is built around that model, and that’s why the workflow above works the way it does.&lt;/p&gt;

&lt;p&gt;The part that matters most if you build your own is how little of this is fixed. FiftyOne’s &lt;a href="https://docs.voxel51.com/plugins/index.html" rel="noopener noreferrer"&gt;plugin framework&lt;/a&gt; is Python &lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html#operators" rel="noopener noreferrer"&gt;operators&lt;/a&gt; plus React &lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html#panels" rel="noopener noreferrer"&gt;panels&lt;/a&gt;, and you can mix the two in &lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html#hybrid-panels-python-javascript-react" rel="noopener noreferrer"&gt;hybrid panels&lt;/a&gt;. The panel in this guide is a React app backed by operators. One data operator returns the whole payload (rows, histogram bins, thresholds, and verdict counts) and re-fires whenever the view changes. A second operator returns a single episode’s arrays and frames on demand for the inspector. The five tabs are handwritten React. Adding a metric is one Python function and one dictionary entry, and charting it means copying a documented chart template.&lt;/p&gt;

&lt;p&gt;If your rig has a metric nobody else computes, such as force-torque spikes, a custom contact signal, or a per-operator score, you add it, and it gets the same filter, tag, inspector, and export workflow as everything else.&lt;/p&gt;

&lt;p&gt;Nothing in this post requires this exact plugin. It’s &lt;a href="https://voxel51.com/blog/fiftyone-plugins-mcap-lerobot-curation" rel="noopener noreferrer"&gt;one implementation of the pattern&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LeRobot curation plugin: &lt;a href="https://github.com/harpreetsahota204/lerobot_data_curation" rel="noopener noreferrer"&gt;https://github.com/harpreetsahota204/lerobot_data_curation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FiftyOne multimodal and LeRobot docs: &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;https://docs.voxel51.com/user_guide/multimodal.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Build your own plugin: &lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html" rel="noopener noreferrer"&gt;https://docs.voxel51.com/plugins/developing_plugins.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FiftyOne Brain (embeddings, similarity): &lt;a href="https://docs.voxel51.com/brain/index.html" rel="noopener noreferrer"&gt;https://docs.voxel51.com/brain/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Annotating LeRobot subtasks and dialogue in FiftyOne: &lt;a href="https://docs.voxel51.com/plugins/plugins_ecosystem/fo_annotate_lerobot.html" rel="noopener noreferrer"&gt;https://docs.voxel51.com/plugins/plugins_ecosystem/fo_annotate_lerobot.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LeRobot dataset v3 format: &lt;a href="https://huggingface.co/docs/lerobot/en/lerobot-dataset-v3" rel="noopener noreferrer"&gt;https://huggingface.co/docs/lerobot/en/lerobot-dataset-v3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face on community dataset defects: &lt;a href="https://huggingface.co/blog/lerobot-datasets" rel="noopener noreferrer"&gt;https://huggingface.co/blog/lerobot-datasets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RINSE (smoothness metrics): &lt;a href="https://arxiv.org/abs/2604.23000" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2604.23000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Data Quality in Imitation Learning (action divergence): &lt;a href="https://arxiv.org/abs/2306.02437" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2306.02437&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SCIZOR: &lt;a href="https://arxiv.org/abs/2505.22626" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2505.22626&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DemInf: &lt;a href="https://arxiv.org/abs/2502.08623" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2502.08623&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CUPID: &lt;a href="https://arxiv.org/abs/2506.19121" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2506.19121&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What metrics should I compute first on a LeRobot dataset?
&lt;/h3&gt;

&lt;p&gt;Start with the integrity checks (&lt;code&gt;frame_gaps&lt;/code&gt;, &lt;code&gt;length_mismatch&lt;/code&gt;, &lt;code&gt;nonfinite_values&lt;/code&gt;, &lt;code&gt;too_short&lt;/code&gt;), because they’re free and tell you whether the rest is worth running. Then add &lt;code&gt;sparc&lt;/code&gt; for motion, &lt;code&gt;idle_lead_s&lt;/code&gt; and &lt;code&gt;longest_pause_s&lt;/code&gt; for dead time, and &lt;code&gt;frozen_frac&lt;/code&gt; if you can afford video decode.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the difference between SPARC and LDLJ?
&lt;/h3&gt;

&lt;p&gt;Both measure smoothness of the speed profile. SPARC measures how many corrections the motion contains, via its frequency spectrum, and is scale-invariant. It responds to stop-and-go motion but not to additive white noise. LDLJ is log dimensionless jerk. It does respond to white noise, but it’s noisier and duration-sensitive, so it carries half weight in the score.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why are scores compared per task instead of across the whole dataset?
&lt;/h3&gt;

&lt;p&gt;Different tasks have different normal motion and timing. A pooled score ranks the slowest, most correction-heavy task as the problem. Per-task normalization compares each episode to its own peers. It needs about 20 episodes per task. With fewer, the panel pools the view and shows a low-confidence banner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why isn’t track_lag_ms part of the score?
&lt;/h3&gt;

&lt;p&gt;It’s mostly a hardware property. A slow servo loop delays every episode by roughly the same amount. The metric is useful for comparing rigs and sessions, so we summarize it per dataset and flag only episodes far from the median.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a generic task string such as “Hold” hurt a VLA?
&lt;/h3&gt;

&lt;p&gt;Nobody has measured it in anything we reviewed. The &lt;code&gt;task_generic&lt;/code&gt; check rests on the Hugging Face community’s documented list of such strings as real defects. A VLA uses the instruction as conditioning input, so a weak string is a reasonable thing to review. It has its own verdict and never enters the score.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the scores to drop episodes automatically?
&lt;/h3&gt;

&lt;p&gt;We recommend against it. The published evidence is mixed: smoothness filtering helped in one study, while another found most curation metrics exploited episode length and that detection accuracy didn’t predict policy success. Use the ranking to decide what to review, tag &lt;code&gt;exclude-candidate&lt;/code&gt; yourself, and validate with a training run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the plugin use any models?
&lt;/h3&gt;

&lt;p&gt;No.&amp;nbsp;Every metric is signal or pixel arithmetic, so there’s nothing to download. Model-based checks such as VLM instruction verification and embedding-based duplicate detection are deliberately left out. FiftyOne provides the &lt;a href="https://docs.voxel51.com/brain/index.html" rel="noopener noreferrer"&gt;embeddings and similarity&lt;/a&gt; and &lt;a href="https://docs.voxel51.com/model_zoo/index.html" rel="noopener noreferrer"&gt;model zoo&lt;/a&gt; infrastructure to build them as extra &lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html#operators" rel="noopener noreferrer"&gt;operators&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I export only the curated episodes?
&lt;/h3&gt;

&lt;p&gt;Filter out the &lt;code&gt;exclude-candidate&lt;/code&gt; tag with &lt;code&gt;dataset.match_tags("exclude-candidate", bool=False)&lt;/code&gt; and call &lt;code&gt;.export(..., dataset_type=fo.types.LeRobotDataset, export_media=True)&lt;/code&gt;. The exporter writes a self-contained v3 dataset with episodes renumbered contiguously.&lt;/p&gt;

</description>
      <category>lerobot</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>python</category>
    </item>
    <item>
      <title>Your Robot Data Needs Custom Tools. Build Them as FiftyOne Plugins</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:55:51 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/your-robot-data-needs-custom-tools-build-them-as-fiftyone-plugins-387g</link>
      <guid>https://dev.to/harpreetsahota/your-robot-data-needs-custom-tools-build-them-as-fiftyone-plugins-387g</guid>
      <description>&lt;p&gt;&lt;em&gt;How the FiftyOne plugin framework turns your MCAP and&lt;/em&gt; &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;&lt;em&gt;LeRobot&lt;/em&gt;&lt;/a&gt; &lt;em&gt;curation checks into buttons, panels, and background jobs that run on every episode, and why that works across a whole dataset rather than one recording.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have 400 &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;teleoperation&lt;/a&gt; episodes and a policy to train by Friday.&lt;/p&gt;

&lt;p&gt;Some of those episodes are bad. The operator hesitated for eight seconds before the grasp. The wrist camera froze halfway through. The gripper opened and closed three times because the first grasp slipped. The task string says &lt;code&gt;"pick up"&lt;/code&gt; and nothing else.&lt;/p&gt;

&lt;p&gt;A policy will learn from all of it.&lt;/p&gt;

&lt;p&gt;You could open each episode in a log viewer and scrub through it. At two minutes per episode, that's more than 13 hours, and you'll stop paying attention long before the end. What you want is a ranked list, worst first, with every flag one click away from the moment it happened.&lt;/p&gt;

&lt;p&gt;No viewer ships that list, because the checks depend on your robot. A dual-arm rig with multi-joint hands needs different smoothness metrics than a wheeled rover with an inertial measurement unit (IMU) and GPS. The tool that curates your data has to be one you can extend in Python, on your schema, across the whole dataset.&lt;/p&gt;

&lt;p&gt;That's what the &lt;a href="https://docs.voxel51.com/plugins/index.html" rel="noopener noreferrer"&gt;FiftyOne plugin framework&lt;/a&gt; is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A FiftyOne plugin is a folder with two files, &lt;code&gt;fiftyone.yml&lt;/code&gt; and &lt;code&gt;__init__.py&lt;/code&gt;. FiftyOne discovers it at runtime, so you never fork FiftyOne or wait for a release to add a button, form, panel, or background job.&lt;/li&gt;
&lt;li&gt;FiftyOne reads &lt;code&gt;.mcap&lt;/code&gt;, &lt;code&gt;.bag&lt;/code&gt;, and &lt;code&gt;.rrd&lt;/code&gt; files as &lt;code&gt;media_type="multimodal"&lt;/code&gt; episodes and imports LeRobot v3 with &lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt;, so a plugin can score every episode in a dataset with the same code.&lt;/li&gt;
&lt;li&gt;FiftyOne plugin code runs on the FiftyOne server in Python so that it can use NumPy, SciPy, PyTorch, a GPU, or a secret API key. Long jobs run in the background through delegated execution.&lt;/li&gt;
&lt;li&gt;Most viewer extensions run in the browser against one recording, and labeling-platform hooks act on one workflow task. FiftyOne plugins run server-side Python with custom UI across a whole dataset.&lt;/li&gt;
&lt;li&gt;Two open-source plugins show the pattern for physical AI: &lt;a href="https://github.com/harpreetsahota204/lerobot_data_curation" rel="noopener noreferrer"&gt;&lt;code&gt;lerobot_data_curation&lt;/code&gt;&lt;/a&gt; ranks LeRobot episodes worst-first, and &lt;a href="https://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;&lt;code&gt;demo_quality_scorer&lt;/code&gt;&lt;/a&gt; does the same for MCAP recordings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why physical AI data needs custom tooling
&lt;/h2&gt;

&lt;p&gt;Image datasets are forgiving. Every sample is a picture, every label is a box or a mask, and the same quality checks work on most of them.&lt;/p&gt;

&lt;p&gt;Robot data has no such standard shape. One MCAP file from an autonomous vehicle might hold six cameras, two LIDARs, an IMU, GPS fixes, and a &lt;code&gt;/tf&lt;/code&gt; tree. One LeRobot episode from a tabletop arm holds two camera streams, a 14-dimensional &lt;code&gt;observation.state&lt;/code&gt;, a matching &lt;code&gt;action&lt;/code&gt; array, and a task string. An &lt;a href="https://voxel51.com/blog/egosuite-open100k-mcap-fiftyone" rel="noopener noreferrer"&gt;egocentric dataset&lt;/a&gt; adds audio and hand poses.&lt;/p&gt;

&lt;p&gt;The questions you need answered are specific to that shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the motion in this episode smooth, or full of corrections?&lt;/li&gt;
&lt;li&gt;Did any sensor drop frames, drift its clock, or clip its values?&lt;/li&gt;
&lt;li&gt;Does the action track the state, or is there lag between video and action?&lt;/li&gt;
&lt;li&gt;Is the gripper behavior consistent with the other episodes of the same task?&lt;/li&gt;
&lt;li&gt;Is the language instruction usable for fine-tuning a vision-language-action (VLA) model?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generic viewer can't answer those. It can only show you the data and wait for you to notice.&lt;/p&gt;

&lt;p&gt;FiftyOne gives you the dataset layer: episodes as samples, fields you can filter and sort on, views, tags, and an App with a &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;multimodal viewer&lt;/a&gt; that plays MCAP and LeRobot streams in lockstep. &lt;a href="https://voxel51.com/plugins" rel="noopener noreferrer"&gt;Plugins&lt;/a&gt; give you the part that's specific to your robot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov15tetq2zklikv3ob27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov15tetq2zklikv3ob27.png" alt="Diagram of FiftyOne at the center of an ML stack, with plugin operators and panels connecting it to labeling tools, vector search backends, model inference, and automation." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram of FiftyOne at the center of an ML stack, with plugin operators and panels connecting it to labeling tools, vector search backends, model inference, and automation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a FiftyOne plugin?
&lt;/h2&gt;

&lt;p&gt;A plugin is a folder FiftyOne finds. If the folder contains a &lt;code&gt;fiftyone.yml&lt;/code&gt;, FiftyOne loads it at runtime and adds whatever it declares to the App, the SDK, and the CLI.&lt;/p&gt;

&lt;p&gt;Plugins add two kinds of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operators&lt;/strong&gt; are actions. You trigger one, it runs, it finishes. "Compute episode quality" is an operator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Panels&lt;/strong&gt; are UI that stays open next to your data and reacts to what you do. A worst-first ranking that re-sorts when you filter the grid is a panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, a panel is a special kind of operator, so everything below about operators applies to panels too.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Notebook script&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;FiftyOne plugin operator&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who can run it&lt;/td&gt;
&lt;td&gt;You, in your kernel&lt;/td&gt;
&lt;td&gt;Anyone with the App open, no code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inputs&lt;/td&gt;
&lt;td&gt;Variables you edit by hand&lt;/td&gt;
&lt;td&gt;A typed form, validated before anything runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it acts on&lt;/td&gt;
&lt;td&gt;The dataset you loaded&lt;/td&gt;
&lt;td&gt;The view or selection the user has open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long jobs&lt;/td&gt;
&lt;td&gt;Your kernel is busy until it finishes&lt;/td&gt;
&lt;td&gt;Delegated to a worker, so the App stays free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reuse&lt;/td&gt;
&lt;td&gt;Copy and paste&lt;/td&gt;
&lt;td&gt;One URI: buttons, panels, pipelines, notebooks, agents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;What changes when you promote a notebook script to a FiftyOne plugin operator.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. Why not keep it in a notebook?&lt;/strong&gt; Start in a notebook. Promote the code to an operator when someone else needs to run it, or when you catch yourself running it a second time. The figure below shows what changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftickqizl278qf4gaut0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftickqizl278qf4gaut0q.png" alt="Comparison of a notebook script and a FiftyOne plugin operator across who can run it, inputs, what it acts on, long jobs, and reuse." width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comparison of a notebook script and a FiftyOne plugin operator across who can run it, inputs, what it acts on, long jobs, and reuse.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The row that matters most for robot data is "what it acts on". A script scores whatever dataset you loaded. An operator scores the view the user has open right now, so the same code works on one task, one robot, one week of collection, or the episodes someone tagged for &lt;code&gt;review&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a FiftyOne plugin
&lt;/h2&gt;

&lt;p&gt;Here's the folder for a plugin with a Python backend and a custom React panel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy80a4hnd3cvdusil4ibq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy80a4hnd3cvdusil4ibq.png" alt="FiftyOne plugin folder with required fiftyone.yml and _init_.py files and optional requirements.txt and JavaScript files, showing what loads on the Python server and what loads in the App." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne plugin folder with required fiftyone.yml and &lt;em&gt;init&lt;/em&gt;.py files and optional requirements.txt and JavaScript files, showing what loads on the Python server and what loads in the App.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fiftyone.yml&lt;/code&gt; is the manifest. It holds the plugin name in &lt;code&gt;@org/name&lt;/code&gt; form, the operators and panels it exposes, and any secrets it needs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__init__.py&lt;/code&gt; holds your classes and a &lt;code&gt;register(p)&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; lists pip dependencies. For MCAP work, that's usually &lt;code&gt;mcap&lt;/code&gt;, &lt;code&gt;mcap-protobuf-support&lt;/code&gt; and friends.&lt;/li&gt;
&lt;li&gt;Everything below the dashed line is optional JavaScript for custom UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Python-only plugin is two files. Teal in every figure in this post means the FiftyOne server, which runs Python. Orange means the App in your browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; Every operator name listed in &lt;code&gt;fiftyone.yml&lt;/code&gt; must match the name in that operator's config. If you register a class but forget to list it, the operator doesn't show up, and you get no error. It's the most common "where did my operator go" bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete operator in 25 lines
&lt;/h2&gt;

&lt;p&gt;This operator tags samples whose detections fall below a confidence threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.operators&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.operators.types&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ViewField&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TagLowConf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Operator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OperatorConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag_low_conf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tag low-confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_placement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Placement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Places&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SAMPLES_GRID_SECONDARY_ACTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tag low-conf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_field_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedded_doc_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Detections&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Label field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confidence threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target_view&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter_labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;thresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag_samples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low_conf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tagged &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; samples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TagLowConf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's what the user sees:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibmvzh4kiqeti4sr74yq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibmvzh4kiqeti4sr74yq.png" alt="TagLowConf operator code color-coded to the button, input form, and notification it creates in the FiftyOne App." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TagLowConf operator code color-coded to the button, input form, and notification it creates in the FiftyOne App.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Four methods, four jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;config&lt;/code&gt; names the operator. &lt;code&gt;name&lt;/code&gt; is its ID, and &lt;code&gt;label&lt;/code&gt; becomes the form's heading.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolve_placement&lt;/code&gt; puts a "Tag low-conf" button in the grid's action bar.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolve_input&lt;/code&gt; declares a dropdown and a number. The App builds the form, with defaults and validation, from those two lines.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execute&lt;/code&gt; does the work on the target view and sends a notification.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You wrote no UI code. You described the inputs and the App built the interface. Swap the body of &lt;code&gt;execute&lt;/code&gt; for spectral arc length (SPARC) smoothness over an &lt;code&gt;/odom&lt;/code&gt; channel, and you have the skeleton of an MCAP quality scorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How an operator call works
&lt;/h2&gt;

&lt;p&gt;Every operator call is two round trips between the App and the FiftyOne server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedhzgsk1cd5rsw7bamm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedhzgsk1cd5rsw7bamm8.png" alt="Sequence diagram of a FiftyOne operator call, with one round trip that resolves inputs into a form and a second that executes and returns UI operations." width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sequence diagram of a FiftyOne operator call, with one round trip that resolves inputs into a form and a second that executes and returns UI operations.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resolve inputs.&lt;/strong&gt; You click the button. The App sends the current context (dataset, view, selection) to the server, which calls your &lt;code&gt;resolve_input&lt;/code&gt; and returns a schema. The App turns it into a form. If the operator is dynamic, this loop re-runs on every input change, which is how &lt;code&gt;demo_quality_scorer&lt;/code&gt; shows a signal picker only after you choose a telemetry channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute.&lt;/strong&gt; You hit Execute. The server validates the parameters against your schema, then calls &lt;code&gt;execute&lt;/code&gt;. You return a result plus any UI operations, and the App applies them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your code runs only on the server. The browser draws what the server tells it to. That's why an operator can decode protobuf MCAP messages with &lt;code&gt;mcap-protobuf-support&lt;/code&gt;, run SciPy's Welch power spectral density (PSD) on an IMU stream, or call a model on a GPU, without shipping any of that to the browser.&lt;/p&gt;

&lt;p&gt;When something breaks, ask which arrow failed. A wrong form means &lt;code&gt;resolve_input&lt;/code&gt;. A fine form that does nothing means &lt;code&gt;execute&lt;/code&gt;. An &lt;code&gt;execute&lt;/code&gt; that ran but didn't change the App means the UI operations you returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Long jobs run in the background with delegated execution
&lt;/h2&gt;

&lt;p&gt;Scoring 400 episodes is not a click-and-wait job. Decoding video alone costs &lt;code&gt;lerobot_data_curation&lt;/code&gt; about 0.4 seconds per camera per episode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhnfgipi1voln1tc3wrn5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhnfgipi1voln1tc3wrn5.png" alt="Flowchart comparing immediate execution in the FiftyOne server with delegated execution, where jobs queue in MongoDB and an orchestrator runs them." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Flowchart comparing immediate execution in the FiftyOne server with delegated execution, where jobs queue in MongoDB and an orchestrator runs them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;allow_delegated_execution=True&lt;/code&gt; in the operator's config, and the user can choose. Immediate execution runs inside the server while the App waits, with a live progress bar. Delegated execution writes the job to a queue in MongoDB, and a separate worker runs the same execute method. You start a worker with &lt;code&gt;fiftyone delegated launch&lt;/code&gt; and track jobs with &lt;code&gt;fiftyone delegated list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both physical AI plugins below default to delegated execution, because a real corpus takes minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; If no worker is running, delegated jobs sit in the queue forever. And a delegated job has no browser attached, so write results to the dataset rather than trying to change the user's view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Panels: UI that stays open
&lt;/h2&gt;

&lt;p&gt;A curation tool needs more than a form. It needs a place to see the ranking, click into an episode, and tag it. That's a panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qnsfgzhogcbez77ix7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qnsfgzhogcbez77ix7p.png" alt="FiftyOne panel lifecycle from on_load to render to event handlers, next to four places to keep state: panel state, panel data, execution store, and dataset fields." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne panel lifecycle from on_load to render to event handlers, next to four places to keep state: panel state, panel data, execution store, and dataset fields.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A panel's &lt;code&gt;on_load&lt;/code&gt; runs when it opens, render returns a layout, and events call your Python handlers with a fresh context. The event that matters most for curation is &lt;code&gt;on_change_view&lt;/code&gt;: when the user filters the grid, your panel re-computes for the new view.&lt;/p&gt;

&lt;p&gt;Here's a complete panel that shows class counts and follows the grid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClassCounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Panel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PanelConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class_counts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Class counts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_change_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground_truth.detections.label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;counts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())}])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;panel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; classes**&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;counts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small values go in panel state, which your Python handlers can read. Large payloads like chart data go through &lt;code&gt;set_data&lt;/code&gt;, which is write-only from Python. Settings you want to survive a reload go in the execution store, &lt;code&gt;ctx.store&lt;/code&gt;. Results that belong to the data go in dataset fields, where you can filter and sort on them like any other field.&lt;/p&gt;

&lt;p&gt;When the built-in widgets run out, you have two options. A hybrid panel keeps state and compute in Python and renders a React component you write. A JS panel puts React in charge of the UI and calls your Python operators whenever it needs the server. Both plugins below are JS panels backed by Python operators, which suits dense, interactive dashboards. If you build a React UI, &lt;a href="https://voodo.dev.fiftyone.ai" rel="noopener noreferrer"&gt;VOODO&lt;/a&gt;, the component library the FiftyOne App itself uses, makes it look native.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ek718bm4d7epmpawaiy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ek718bm4d7epmpawaiy.png" alt="Comparison of Python, hybrid, and JS FiftyOne panels showing what runs in the browser, what runs on the server, and which need a Node build." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comparison of Python, hybrid, and JS FiftyOne panels showing what runs in the browser, what runs on the server, and which need a Node build.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Operators compose
&lt;/h2&gt;

&lt;p&gt;Every operator has a URI, like &lt;code&gt;@voxel51/brain/compute_similarity&lt;/code&gt;, and a typed input schema. Anything that can call an operator can call yours.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6r5bg9d3m3c8l5asscd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6r5bg9d3m3c8l5asscd.png" alt="One FiftyOne operator called from eight places: an App button, the operator browser, another operator, a pipeline stage, a notebook, AI agents through the MCP server, a background worker, and teammates." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One FiftyOne operator called from eight places: an App button, the operator browser, another operator, a pipeline stage, a notebook, AI agents through the MCP server, a background worker, and teammates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That includes the &lt;a href="https://github.com/voxel51/fiftyone-mcp-server" rel="noopener noreferrer"&gt;FiftyOne Model Context Protocol (MCP) server&lt;/a&gt;, which exposes &lt;code&gt;list_operators&lt;/code&gt;, &lt;code&gt;get_operator_schema&lt;/code&gt;, and &lt;code&gt;execute_operator&lt;/code&gt; to &lt;a href="https://voxel51.com/blog/fiftyone-agent-skills-mcp-tools" rel="noopener noreferrer"&gt;AI agents&lt;/a&gt;. An agent can find your quality scorer, read its inputs, run it on the episodes tagged &lt;code&gt;review&lt;/code&gt;, and report back which ones failed sensor health. The schema validates the agent's JSON the same way it validates a person filling in the form.&lt;/p&gt;

&lt;p&gt;For a physical AI team, that means the curation logic you write once runs from a button during review, from a notebook in your training pipeline, from a background worker overnight, and &lt;a href="https://voxel51.com/blog/fiftyone-agent-skills-gemini-video-plugin" rel="noopener noreferrer"&gt;from an agent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How FiftyOne plugins differ from viewer extensions
&lt;/h2&gt;

&lt;p&gt;Most robotics viewers have extension points, and they're usually scoped to the recording you have open: a custom panel, a message converter, or a new file loader that runs in the browser or desktop app. Labeling platforms add hooks that act on one task in a workflow stage.&lt;/p&gt;

&lt;p&gt;FiftyOne plugins run on the server, in Python, against any view of a dataset. A single plugin can add a form, run a job across every episode in the background, render a custom panel with the results, and expose all of it to agents through the MCP server, without leaving the language your robotics stack is written in.&lt;/p&gt;

&lt;p&gt;For live debugging of a single recording, a dedicated log viewer is the right tool, and FiftyOne doesn't try to replace it. FiftyOne reads &lt;code&gt;.rrd&lt;/code&gt; files and &lt;a href="https://voxel51.com/glossary/mcap" rel="noopener noreferrer"&gt;MCAP files&lt;/a&gt;, so a common setup is to debug individual logs in a viewer and &lt;a href="https://voxel51.com/glossary/physical-ai-data-curation" rel="noopener noreferrer"&gt;curate the dataset&lt;/a&gt; in FiftyOne.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two FiftyOne plugins for MCAP and LeRobot curation
&lt;/h2&gt;

&lt;p&gt;Both plugins below were built using only the pieces covered above. Both follow the same rule, stated in their READMEs: the scores order your review queue, and a person makes the call. A perfectly smooth demonstration can still show the wrong task.&lt;/p&gt;

&lt;h3&gt;
  
  
  LeRobot Data Curation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/harpreetsahota204/lerobot_data_curation" rel="noopener noreferrer"&gt;&lt;code&gt;lerobot_data_curation&lt;/code&gt;&lt;/a&gt; ranks the episodes of a &lt;a href="https://voxel51.com/blog/lerobot-dataset-support-voxel51" rel="noopener noreferrer"&gt;LeRobot v3 dataset&lt;/a&gt; worst-first, so you know which to look at before training a policy or &lt;a href="https://voxel51.com/blog/lerobot-v3-datasets-vla-training-fiftyone" rel="noopener noreferrer"&gt;fine-tuning a VLA&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It scores each episode on motion smoothness, time efficiency, action-state tracking, gripper behavior, consistency, integrity, and language, and optionally on camera quality: blur, exposure, clipping, frozen feeds, and video-action lag. It has no model-based metrics, so there's no model to download.&lt;/p&gt;

&lt;p&gt;How it maps to the framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A typed form with tabs.&lt;/strong&gt; The compute operator's form has Data, Metrics, Camera, and Normalization tabs. You pick the &lt;code&gt;observation.state&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; arrays, and assign arm and gripper joints by name. Nothing is guessed from the data, and your picks are remembered for the next run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegated by default.&lt;/strong&gt; The operator sets &lt;code&gt;allow_delegated_execution=True&lt;/code&gt; and &lt;code&gt;default_choice_to_delegated=True&lt;/code&gt;, so scoring runs in the background. Camera decoding is the slow part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A JS panel backed by Python operators.&lt;/strong&gt; The LeRobot Curation panel is a React component with Overview, Motion &amp;amp; Action, Integrity &amp;amp; Coverage, Vision and Language tabs. Click a histogram bar to filter the grid to those episodes. Click a row to open an inspector with joint traces, the speed profile, the gripper timeline, and frames picked around the flagged spans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It follows the view.&lt;/strong&gt; The panel refreshes when the view changes, so filtering the grid re-ranks it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags on the timeline.&lt;/strong&gt; Flagged spans such as idle stretches, the longest pause, and acceleration spikes are written as temporal tags on each episode so that you can scrub straight to them in the multimodal viewer. Footer buttons apply &lt;code&gt;review&lt;/code&gt;, &lt;code&gt;exclude-candidate&lt;/code&gt;, and &lt;code&gt;relabel&lt;/code&gt; sample tags.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/lerobot/my_dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dataset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LeRobotDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# then run "LeRobot curation: compute quality" from the operator browser
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; Scores are relative to the batch you scored. Per-task normalization needs about 20 episodes per task, and the default thresholds were calibrated on a 102-episode development set. Check them on your own data. Cloud-hosted LeRobot sources aren't supported yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo Quality Scorer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;&lt;code&gt;demo_quality_scorer&lt;/code&gt;&lt;/a&gt; does the same job for &lt;a href="https://voxel51.com/blog/mcap-datasets-multimodal-robotics-fiftyone" rel="noopener noreferrer"&gt;multimodal MCAP datasets&lt;/a&gt;: teleoperations, autonomous vehicles, drones, rovers, and egocentric capture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FaoEvFR3ln0102q6uMLVsta41lR3hVg6PCICgHs4fhRNWg%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FaoEvFR3ln0102q6uMLVsta41lR3hVg6PCICgHs4fhRNWg%2Fanimated.gif%3Fwidth%3D640" alt="Demo Quality Scorer plugin in the FiftyOne App ranking robot episodes from a multimodal MCAP dataset and opening one in the multimodal viewer." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Demo Quality Scorer plugin in the FiftyOne App ranking robot episodes from a multimodal MCAP dataset and opening one in the multimodal viewer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It computes three metric families and ranks episodes worst-first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Motion smoothness:&lt;/strong&gt; SPARC, log dimensionless jerk (LDLJ), jerk RMS, and a low-to-high frequency power ratio, computed over windows of a speed profile you choose from position or velocity signals such as &lt;code&gt;/odom -&amp;gt; twist_linear&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensor health:&lt;/strong&gt; dropout, rate stability, clock drift, clipping, and cross-channel desync, all from message timestamps and raw values, so it works on any channel the plugin can decode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outliers:&lt;/strong&gt; models that flag episodes whose motion and health features don't look like the rest of the batch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How it maps to the framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A dynamic form.&lt;/strong&gt; The form inspects the first sample's channels. If no telemetry channel carries a numeric signal, the motion family switches itself off and says why. If the server lacks a decoder, the form names the package to install.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immediate or delegated, your choice.&lt;/strong&gt; Run immediately with an in-app progress bar, or schedule it in the background (default).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A JS panel.&lt;/strong&gt; The Episode Quality panel is a React component that has Motion, Health, and Outliers tabs. Click a histogram bar to filter the grid. Click a table row to open that episode in the multimodal viewer, with a toast naming the timecode of its worst flagged interval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results as fields.&lt;/strong&gt; Scores land in &lt;code&gt;quality.*&lt;/code&gt; fields such as &lt;code&gt;quality.sparc&lt;/code&gt; and &lt;code&gt;quality.ldlj&lt;/code&gt;, so you can sort and filter on them outside the panel too.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fiftyone[multimodal]&amp;gt;=1.22.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;mcap&lt;/span&gt; &lt;span class="n"&gt;mcap&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;protobuf&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;support&lt;/span&gt; \
    &lt;span class="n"&gt;mcap&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ros1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="n"&gt;mcap&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ros2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="n"&gt;scikit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;learn&lt;/span&gt;
&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;harpreetsahota204&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;demo_quality_scorer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; Structural errors, such as the wrong action at a key moment, are invisible to every metric here. Re-running over a different set of episodes re-fits normalization, so expect scores to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build your own in six steps
&lt;/h2&gt;

&lt;p&gt;Your robot has checks neither plugin covers. Here's the loop for writing them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywgzpf0owb11162owf9k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywgzpf0owb11162owf9k.png" alt="Six steps to build a FiftyOne plugin: scaffold, write, register, run, iterate, and ship, with a loop from iterate back to write." width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Six steps to build a FiftyOne plugin: scaffold, write, register, run, iterate, and ship, with a loop from iterate back to write.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Scaffold the plugin
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;fiftyone plugins create&lt;/code&gt; or make a folder in your plugins directory with a &lt;code&gt;fiftyone.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@your-org/gripper_checks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Flags&lt;/span&gt; &lt;span class="n"&gt;episodes&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;grasp&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;
&lt;span class="n"&gt;operators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;score_grasps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Write the operator
&lt;/h3&gt;

&lt;p&gt;One class per operator or panel, like the examples above. Keep the scoring logic in a plain Python function so a notebook, a test, and the operator can all call it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Register it
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;p.register(ScoreGrasps)&lt;/code&gt; inside &lt;code&gt;register(p)&lt;/code&gt;, and make sure the name matches &lt;code&gt;fiftyone.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Run the App in debug mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;VFF_MULTIMODAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="n"&gt;my_lerobot_dataset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fiftyone app debug&lt;/code&gt; prints server logs and tracebacks to your terminal. &lt;code&gt;VFF_MULTIMODAL=1&lt;/code&gt; turns on the multimodal viewer in the App; set it before &lt;code&gt;fiftyone&lt;/code&gt; is imported in every process that touches a multimodal dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Iterate
&lt;/h3&gt;

&lt;p&gt;Edit, refresh the App, repeat. Python changes are picked up on the next request without a server restart. If you have a React panel, keep &lt;code&gt;npm run dev&lt;/code&gt; rebuilding it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Ship it
&lt;/h3&gt;

&lt;p&gt;Push to GitHub. Anyone installs it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gripper_checks&lt;/span&gt;
&lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt; &lt;span class="nd"&gt;@your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gripper_checks&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good to know. How do I test an operator without the App?&lt;/strong&gt; Operators are callable from Python with &lt;code&gt;foo.execute_operator&lt;/code&gt;, so you can test them headless in pytest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.voxel51.com/plugins/developing_plugins.html" rel="noopener noreferrer"&gt;Plugin developer guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;FiftyOne multimodal user guide&lt;/a&gt; for MCAP and LeRobot&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/voxel51/fiftyone-plugins" rel="noopener noreferrer"&gt;voxel51/fiftyone-plugins&lt;/a&gt;, the official plugins and panel examples&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/harpreetsahota204/lerobot_data_curation" rel="noopener noreferrer"&gt;lerobot_data_curation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;demo_quality_scorer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/voxel51/fiftyone-mcp-server" rel="noopener noreferrer"&gt;FiftyOne MCP server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://voodo.dev.fiftyone.ai" rel="noopener noreferrer"&gt;VOODO&lt;/a&gt;, the React design system for plugin panels&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I curate a LeRobot dataset before training a policy?
&lt;/h3&gt;

&lt;p&gt;Import it with &lt;code&gt;fo.Dataset.from_dir(dataset_dir=..., dataset_type=fo.types.LeRobotDataset)&lt;/code&gt;, where each sample is one episode. Then install &lt;code&gt;lerobot_data_curation&lt;/code&gt; and run its compute operator to rank episodes worst-first on motion, gripper, camera, and language metrics. Review the top of the list and tag episodes as &lt;code&gt;exclude-candidate&lt;/code&gt; or &lt;code&gt;relabel&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can FiftyOne open MCAP files?
&lt;/h3&gt;

&lt;p&gt;Yes. FiftyOne treats &lt;code&gt;.mcap&lt;/code&gt;, &lt;code&gt;.bag&lt;/code&gt;, and &lt;code&gt;.rrd&lt;/code&gt; files as &lt;code&gt;media_type="multimodal"&lt;/code&gt; samples, one episode per file, and plays their streams in a synchronized multimodal viewer. Set &lt;code&gt;VFF_MULTIMODAL=1&lt;/code&gt; before importing &lt;code&gt;fiftyone&lt;/code&gt; to turn the viewer on.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I find bad demonstrations in robot teleoperation data?
&lt;/h3&gt;

&lt;p&gt;Score every episode on motion smoothness (SPARC, LDLJ, jerk) and sensor health (dropout, rate stability, clock drift), then sort worst-first. &lt;a href="https://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;&lt;code&gt;demo_quality_scorer&lt;/code&gt;&lt;/a&gt; does this for MCAP datasets. Treat the scores as a review queue, because a smooth demonstration can still show the wrong task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do FiftyOne plugins need JavaScript?
&lt;/h3&gt;

&lt;p&gt;No. A Python-only plugin is &lt;code&gt;fiftyone.yml&lt;/code&gt; plus &lt;code&gt;__init__.py&lt;/code&gt;, and the App builds forms and panels from Python declarations. Add React only when you need custom interactivity, such as the dashboards in &lt;code&gt;lerobot_data_curation&lt;/code&gt; and &lt;code&gt;demo_quality_scorer&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where does FiftyOne plugin code run?
&lt;/h3&gt;

&lt;p&gt;It runs on the FiftyOne server, in Python, or on a background worker for delegated operations. It never runs in the browser, so it can use any Python library, a GPU, or secrets without exposing them.&lt;/p&gt;

</description>
      <category>fiftyoneplugins</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>python</category>
    </item>
    <item>
      <title>50 Embodiments, 497 Episodes, One Indexed Dataset: Curating the LeRobot Community Dataset with FiftyOne</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:54:50 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/50-embodiments-497-episodes-one-indexed-dataset-curating-the-lerobot-community-dataset-with-3043</link>
      <guid>https://dev.to/harpreetsahota/50-embodiments-497-episodes-one-indexed-dataset-curating-the-lerobot-community-dataset-with-3043</guid>
      <description>&lt;p&gt;&lt;em&gt;A single prompt to an AI agent running&lt;/em&gt; &lt;a href="https://github.com/voxel51/fiftyone-skills/tree/main" rel="noopener noreferrer"&gt;&lt;em&gt;FiftyOne Skills&lt;/em&gt;&lt;/a&gt; &lt;em&gt;pulled 10 episodes from every robot embodiment in the LeRobot community dataset, computed&lt;/em&gt; &lt;a href="https://voxel51.com/video-embeddings" rel="noopener noreferrer"&gt;&lt;em&gt;video embeddings&lt;/em&gt;&lt;/a&gt; &lt;em&gt;with Qwen3-VL, and built a queryable similarity index. Here's the code and the thinking behind it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The LeRobot community dataset is one of the largest community-built pools of &lt;a href="https://voxel51.com/cross-embodiment" rel="noopener noreferrer"&gt;cross-embodiment robot data&lt;/a&gt;: 235 contributors, 50 robot types, ~900 GB. Hugging Face's dataset card lists 46 robot types across 791 cleaned datasets. The 50 here come from scanning &lt;code&gt;robot_type&lt;/code&gt; in all 1,755 contributor folders, so the two counts cover different sets of datasets. It's organized as 1,755 contributor datasets, one per folder, so picking a balanced slice starts with reading each one's metadata. If you're training a policy that has to generalize across robots, you need episodes from every embodiment and a way to see which clips are actually worth keeping.&lt;/p&gt;

&lt;p&gt;Now that &lt;a href="https://voxel51.com/blog/lerobot-dataset-support-voxel51" rel="noopener noreferrer"&gt;FiftyOne natively supports LeRobot&lt;/a&gt;, I figured I’d use &lt;a href="https://github.com/voxel51/fiftyone-skills/blob/main/skills/fiftyone-dataset-import/LEROBOT-IMPORT.md" rel="noopener noreferrer"&gt;the FiftyOne skill to import LeRobot datasets&lt;/a&gt; and check it out. All it took was a one-sentence prompt:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"i want to have a little bit from every embodiment, i have enough disk space for the whole dataset... but let's take 10 episodes from each embodiment to start"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That sentence kicked off a session that read every contributor dataset's metadata, found 50 unique robot types, downloaded the right shard files for each, imported 497 episodes into FiftyOne, embedded every clip with Qwen3-VL, computed a similarity index, and saved 75 queryable views. All without writing a data loader, a Parquet parser, or a video decode loop.&lt;/p&gt;

&lt;p&gt;One sentence was enough because the agent had &lt;a href="https://github.com/voxel51/fiftyone-skills/tree/main" rel="noopener noreferrer"&gt;FiftyOne Skills&lt;/a&gt; installed. Skills are &lt;a href="https://voxel51.com/blog/fiftyone-agent-skills-mcp-tools" rel="noopener noreferrer"&gt;packaged workflows that teach AI assistants&lt;/a&gt; how to do complex FiftyOne tasks: &lt;a href="https://voxel51.com/blog/lerobot-import-fiftyone-agent-skills" rel="noopener noreferrer"&gt;importing LeRobot episodes&lt;/a&gt;, running zoo models, computing embeddings, finding duplicates. The agent didn't have to guess at the LeRobot v3 layout or the FiftyOne API. It followed the skill. Install them in Cursor, Claude Code, Codex, or Gemini CLI with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sL&lt;/span&gt; &lt;span class="n"&gt;skil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;voxel51&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fiftyone&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;skills&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what FiftyOne is for. Not visualization. Dataset curation at the scale that &lt;a href="https://voxel51.com/robotics" rel="noopener noreferrer"&gt;robot learning&lt;/a&gt; actually requires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New here?&lt;/strong&gt; &lt;a href="https://github.com/voxel51/fiftyone" rel="noopener noreferrer"&gt;FiftyOne&lt;/a&gt; is an open-source toolkit for loading, querying, visualizing, and curating ML datasets — images, videos, and multimodal data. &lt;a href="https://github.com/huggingface/lerobot" rel="noopener noreferrer"&gt;LeRobot&lt;/a&gt; is Hugging Face's framework for robot learning: it defines a &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;standard v3 dataset format&lt;/a&gt; (Parquet + MP4 shards with episode metadata) that the community uses to share teleoperation recordings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lerobot/community_dataset_v3&lt;/code&gt; is a collection of 1,755 contributor datasets across 50 robot embodiments, each a &lt;a href="https://voxel51.com/blog/lerobot-v3-datasets-vla-training-fiftyone" rel="noopener noreferrer"&gt;complete LeRobot v3 dataset&lt;/a&gt; with its own &lt;code&gt;meta/info.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Using the &lt;a href="https://github.com/voxel51/fiftyone-skills/tree/main/skills/fiftyone-dataset-import" rel="noopener noreferrer"&gt;FiftyOne Skills dataset-import workflow&lt;/a&gt;, the agent scanned every &lt;code&gt;*/*/meta/info.json&lt;/code&gt;, grouped by &lt;code&gt;robot_type&lt;/code&gt;, picked the smallest source dataset per embodiment with ≥10 episodes, and downloaded only the needed shard-0 files.&lt;/li&gt;
&lt;li&gt;Each FiftyOne sample is one episode — not a frame, not a video file — with fields &lt;code&gt;robot_type&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;fps&lt;/code&gt;, &lt;code&gt;dataset_name&lt;/code&gt;, and a &lt;code&gt;media_reference&lt;/code&gt; pointer into the local LeRobot directory tree.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Qwen3-VL-Embedding-2B&lt;/code&gt; turns each episode (up to 32 frames from its time window) into one 2048-dimensional video embedding. The similarity index, Uniform Manifold Approximation and Projection (UMAP), uniqueness, and representativeness all run from that one field in under a minute.&lt;/li&gt;
&lt;li&gt;The same model embeds text into the same space, so the index answers text queries: type "folding a cloth" and get the &lt;code&gt;same bimanual&lt;/code&gt; cloth-folding episodes back, with no labels involved.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uniqueness&lt;/code&gt; scores range from 0.013 to 1.0 across the 497 episodes. 292 clips score below 0.1 because each embodiment's 10 clips come from one recording session. The two highest scores, the only ones above 0.7, both belong to single-frame episodes (0.02–0.03 s long): uniqueness flags &lt;a href="https://voxel51.com/blog/robot-episode-quality-triage" rel="noopener noreferrer"&gt;broken recordings&lt;/a&gt; as readily as rare behavior.&lt;/li&gt;
&lt;li&gt;75 saved views — 50 by robot type plus analytical cuts by frames per second (FPS), duration, robot category, task keyword, and curation intent — are stored in the dataset and load from the App's Views dropdown with no code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;To load and query the pre-built dataset (no GPU required):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fiftyone&amp;gt;=1.22.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;huggingface_hub[cli]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="n"&gt;qwen&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;vl&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt; &lt;span class="n"&gt;torchvision&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;To re-run UMAP visualization yourself (Step 3):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;umap&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;learn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FiftyOne 1.22.0 is the first release with &lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt;. The dataset loads from the Hub with embeddings, similarity index, UMAP layout, and curation scores already computed. Steps 1–2, Step 4, and episode-to-episode search in Step 5 run entirely on the pre-built data, with no GPU or model weights. Text search in Step 5 downloads &lt;code&gt;Qwen3-VL-Embedding-2B&lt;/code&gt; (about 4 GB) to embed your query; it runs on CPU and is faster on a GPU. Step 3 is for readers building their own dataset who want to replicate the embedding pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the LeRobot community dataset is organized
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FeI01iFQM5P7BZVGgeu3BcVmPSaSwGwEJPNml7bj3BD00Q%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FeI01iFQM5P7BZVGgeu3BcVmPSaSwGwEJPNml7bj3BD00Q%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App grid of LeRobot community dataset episodes with bar charts of robot type and an episode duration histogram, then one episode opened with its camera views and state and action values" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App grid of LeRobot community dataset episodes with bar charts of robot type and an episode duration histogram, then one episode opened with its camera views and state and action values&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lerobot/community_dataset_v3&lt;/code&gt; is a collection of contributor datasets. Each one is a complete LeRobot v3 dataset in its own &lt;code&gt;&amp;lt;contributor&amp;gt;/&amp;lt;dataset&amp;gt;/&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;community_dataset_v3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="n"&gt;kuehnrobin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="n"&gt;g1_stack_cube_left&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
      &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
      &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
      &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="n"&gt;villekuosmanen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="n"&gt;pick_coffee_capsule_under_dome&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
      &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
      &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,755 directories. 17,617 files. 897.5 GB total. Each sub-dataset carries its own &lt;code&gt;meta/info.json&lt;/code&gt; with its robot type, episode count, and FPS.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt; reads one LeRobot v3 dataset at a time, so the agent started by downloading all &lt;code&gt;*/*/meta/info.json&lt;/code&gt; files — a 56-second fetch — and then scanning them to build the plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Counts across all 1,755 sub-datasets
&lt;/span&gt;&lt;span class="n"&gt;Robot&lt;/span&gt; &lt;span class="nf"&gt;types &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;so100&lt;/span&gt;                   &lt;span class="mi"&gt;899&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt;   &lt;span class="mi"&gt;34801&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;
  &lt;span class="n"&gt;so101&lt;/span&gt;                   &lt;span class="mi"&gt;312&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt;    &lt;span class="mi"&gt;7490&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;
  &lt;span class="n"&gt;so101_follower&lt;/span&gt;          &lt;span class="mi"&gt;121&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt;    &lt;span class="mi"&gt;4414&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;
  &lt;span class="n"&gt;so100_follower&lt;/span&gt;          &lt;span class="mi"&gt;120&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt;    &lt;span class="mi"&gt;3065&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;
  &lt;span class="n"&gt;arx5&lt;/span&gt;                     &lt;span class="mi"&gt;43&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt;    &lt;span class="mi"&gt;2108&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;
  &lt;span class="n"&gt;Unitree_G1_inspire&lt;/span&gt;        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;        &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;  &lt;span class="c1"&gt;# only 7 available
&lt;/span&gt;  &lt;span class="n"&gt;so100MovellaDot&lt;/span&gt;          &lt;span class="mi"&gt;75&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt;      &lt;span class="mi"&gt;157&lt;/span&gt; &lt;span class="n"&gt;total_episodes&lt;/span&gt;  &lt;span class="c1"&gt;# max 5 per source
&lt;/span&gt;  &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 1,755 sub-datasets were already at &lt;code&gt;codebase_version: v3.0&lt;/code&gt;. No conversion needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxypoiupfw5nbd8m1prcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxypoiupfw5nbd8m1prcu.png" alt="Diagram of the contributor and dataset folder layout, the four-step plan to scan, group by robot type, pick one source per embodiment, and download, and the shard-0 Parquet and MP4 files fetched from one source" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: the contributor/dataset folder layout, with the per-dataset meta/info.json files the agent scans first. Middle: the four-step plan — scan, group by robot_type, pick one source dataset per embodiment, download. Right: inside one source dataset, only file-000.parquet and file-000.mp4 (the first ~10 episodes) are fetched; the other shards are skipped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good to know. What's a shard-0 set?&lt;/strong&gt; LeRobot v3 packs many episodes into shared MP4 and Parquet shards. &lt;code&gt;file-000.parquet&lt;/code&gt; and &lt;code&gt;file-000.mp4&lt;/code&gt; hold the first batch of episodes for each dataset. Downloading only those files makes a 10-episode subset cheap, even from a 50-GB source dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: load the dataset
&lt;/h2&gt;

&lt;p&gt;The agent session did the heavy lifting — reading every contributor dataset's metadata, picking the right shard files, importing 497 episodes, computing embeddings, and pushing the result to the Hub. The import step is covered in &lt;a href="https://voxel51.com/blog/lerobot-import-fiftyone-agent-skills" rel="noopener noreferrer"&gt;How to Import LeRobot Datasets into FiftyOne Using Agent Skills&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The dataset is at &lt;a href="https://huggingface.co/datasets/Voxel51/community_v3_10per_embodiment" rel="noopener noreferrer"&gt;&lt;code&gt;Voxel51/community_v3_10per_embodiment&lt;/code&gt;&lt;/a&gt;. Load it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.utils.huggingface&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fouh&lt;/span&gt;

&lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fouh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_from_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/community_v3_10per_embodiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                    &lt;span class="c1"&gt;# 497
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;robot_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 50 embodiments
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_field_schema&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;      &lt;span class="c1"&gt;# episode_index, robot_type, task, duration, fps,
&lt;/span&gt;                                  &lt;span class="c1"&gt;# dataset_name, media_reference,
&lt;/span&gt;                                  &lt;span class="c1"&gt;# qwen3vl_embedding, qwen3vl_embedding_camera,
&lt;/span&gt;                                  &lt;span class="c1"&gt;# uniqueness, representativeness, ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here on, the source videos live in the Hub repo alongside the metadata. FiftyOne reads through the &lt;code&gt;media_reference&lt;/code&gt; pointer — nothing to download separately. And because the unit is an episode rather than a file, every query you run is a dataset-level question, not a file-browser search.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "one sample = one episode" actually means
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FmLAub2AoBay9lWFeU00Lz2I13a8IQ7kMq6e2j19dUDTg%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FmLAub2AoBay9lWFeU00Lz2I13a8IQ7kMq6e2j19dUDTg%2Fanimated.gif%3Fwidth%3D640" alt="A single LeRobot episode opened in the FiftyOne App, showing synced top and wrist camera views, a joint state and action table, a joint position plot, and the sample's fields, including media_reference" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A single LeRobot episode opened in the FiftyOne App, showing synced top and wrist camera views, a joint state and action table, a joint position plot, and the sample's fields, including media_reference&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most tools treat robot data as video files or frame sequences. FiftyOne takes a different unit: &lt;a href="https://voxel51.com/episode" rel="noopener noreferrer"&gt;the episode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every sample in this dataset carries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episode_index&lt;/span&gt;   &lt;span class="c1"&gt;# int — 0..9 within the source dataset
&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot_type&lt;/span&gt;      &lt;span class="c1"&gt;# str — e.g. "arx5", "so100", "Unitree_G1_Dex3"
&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset_name&lt;/span&gt;    &lt;span class="c1"&gt;# str — e.g. "villekuosmanen/pick_coffee_capsule_under_dome"
&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;            &lt;span class="c1"&gt;# str — e.g. "Pick up the coffee capsule"
&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;        &lt;span class="c1"&gt;# float — seconds (0.02 to 148.6 in this dataset)
&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fps&lt;/span&gt;             &lt;span class="c1"&gt;# float — 10, 15, 20, 25, 30, or 50
&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;media_reference&lt;/span&gt; &lt;span class="c1"&gt;# LeRobotEpisodeReference — a FiftyOne type with two fields:
&lt;/span&gt;                       &lt;span class="c1"&gt;#   .data   = [chunk_index, file_index, first_row, last_row]  (Parquet)
&lt;/span&gt;                       &lt;span class="c1"&gt;#   .videos = {camera: [chunk_index, file_index, from_ts, to_ts]}  (MP4)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of a FiftyOne sample as a row in a table: the left side has filterable scalar fields (&lt;code&gt;robot_type&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;fps&lt;/code&gt;) and the right side has the &lt;code&gt;media_reference&lt;/code&gt; pointer. The pointer's two values — &lt;code&gt;data[2:4]&lt;/code&gt; and &lt;code&gt;videos[top][2:4]&lt;/code&gt; — map to the actual Parquet rows and MP4 time window in the LeRobot folder on disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fisgdjaquufkqfve05mu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fisgdjaquufkqfve05mu1.png" alt="Diagram of a FiftyOne sample with filterable scalar fields above a media_reference pointer whose data and videos values map to Parquet rows and an MP4 time window on disk" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A FiftyOne sample (left) with filterable scalars above the line and the media_reference pointer below it. Arrows connect the pointer's two values — data[2:4] and videos[top][2:4] — to the actual Parquet rows and MP4 time window in the LeRobot folder on disk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three roads lead from a FiftyOne view back to frame data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7y5v3f8vkhyanucg0t5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7y5v3f8vkhyanucg0t5v.png" alt="Diagram of three ways to get frames from a FiftyOne view: read in place and Torch dataset, which copy no data, and export to a standalone LeRobot v3 dataset, which copies data" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read in place&lt;/strong&gt; via &lt;code&gt;sample.media_reference.data&lt;/code&gt; — no copy, your code, one episode at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Torch dataset&lt;/strong&gt; via &lt;code&gt;view.values("episode_index")&lt;/code&gt; passed to &lt;code&gt;LeRobotDataset(..., episodes=...)&lt;/code&gt; — no copy, for training loops. &lt;code&gt;episode_index&lt;/code&gt; restarts at 0 in every source dataset, so filter the view to one &lt;code&gt;dataset_name&lt;/code&gt; first; for a mix of sources, export instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; via &lt;code&gt;view.export(dataset_type=fo.types.LeRobotDataset)&lt;/code&gt; — copies data, produces a standalone v3 repo. Each export must come from a single source dataset, so filter by &lt;code&gt;dataset_name&lt;/code&gt; first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule: in-process → don't copy. Out-of-process → copy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Path&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Call&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Copies data?&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Use it when&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read in place&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sample.media_reference.data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Your own code needs one episode's rows, such as for a plot, a metric, or a policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Torch dataset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LeRobotDataset(..., episodes=view.values("episode_index"))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;A training loop reads batches from a curated subset filtered to one &lt;code&gt;dataset_name&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export&lt;/td&gt;
&lt;td&gt;&lt;code&gt;view.export(dataset_type=fo.types.LeRobotDataset)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Something outside the Python process needs a standalone LeRobot v3 repo, one source per export&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Three ways to get frames from a FiftyOne view of LeRobot episodes, and when each one copies data&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From inspection to curation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FG7JNF68bXWMIA021fWsoTJ4gUTCneoCMklNXLDyyYofQ%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FG7JNF68bXWMIA021fWsoTJ4gUTCneoCMklNXLDyyYofQ%2Fanimated.gif%3Fwidth%3D640" alt="Filtering LeRobot episodes by task in the FiftyOne App sidebar and lasso-selecting points in the Embeddings panel to show the matching episodes in the grid" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Filtering LeRobot episodes by task in the FiftyOne App sidebar and lasso-selecting points in the Embeddings panel to show the matching episodes in the grid&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Timeline viewers are built for inspecting one recording at a time: open a log, scrub the timeline, and watch camera, inertial measurement unit (IMU), and joint streams in sync. That's the right tool when you're debugging one specific run.&lt;/p&gt;

&lt;p&gt;FiftyOne starts where that ends. Once a run is recorded and you have hundreds of episodes, the question isn't "what happened in this episode?" It's "across all 497 episodes from 50 different robots, which clips are visually unique, which are near-duplicates, which ones actually belong in my training set?"&lt;/p&gt;

&lt;p&gt;A timeline scrubber can't answer that. A queryable, indexed dataset can.&lt;/p&gt;

&lt;p&gt;The rest of this post shows exactly what that looks like — every step from here on is something you can't do in a timeline scrubber.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: what's already on the dataset
&lt;/h2&gt;

&lt;p&gt;The Hub dataset ships with a video embedding for every episode, computed with &lt;code&gt;Qwen/Qwen3-VL-Embedding-2B&lt;/code&gt; from the &lt;a href="https://github.com/harpreetsahota204/qwen3vl_embeddings" rel="noopener noreferrer"&gt;&lt;code&gt;qwen3vl_embeddings&lt;/code&gt;&lt;/a&gt; remote zoo source. Each episode's time window is decoded with PyAV; up to 32 evenly spaced frames go to the model as one video clip, producing a 2048-dimensional &lt;code&gt;qwen3vl_embedding&lt;/code&gt; field. &lt;code&gt;qwen3vl_embedding_camera&lt;/code&gt; records which camera stream the frames came from, since camera names and layouts differ across the 50 embodiments.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Qwen3-VL-Embedding actually produces, and why it matters
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/collections/Qwen/qwen3-vl" rel="noopener noreferrer"&gt;Qwen3-VL&lt;/a&gt; is an open vision-language model family from Qwen/Alibaba. It comes in two variants: &lt;code&gt;Qwen3-VL-Instruct&lt;/code&gt; (generates text — captions, bounding boxes, answers) and &lt;code&gt;Qwen3-VL-Embedding&lt;/code&gt; (trained for retrieval, so it produces dense vectors directly). The embedding model reads the clip, and the hidden state of the last token becomes the vector, L2-normalized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# what produced qwen3vl_embedding (inside the qwen3vl_embeddings zoo model)
&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                        &lt;span class="c1"&gt;# frames + instruction
&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_hidden_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_token_position&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# one 2048-d vector per clip
&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiq8muw8tyzdr292g4ntv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiq8muw8tyzdr292g4ntv.png" alt="Two-lane diagram in which frames decoded with PyAV pass through Qwen3-VL-Embedding into one 2048-dimensional vector, and a text query passes through the same model into the same vector space" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Top lane: an episode's time window is decoded with PyAV, up to 32 evenly spaced frames are tokenized by the vision encoder and run through the language model, and the last token's hidden state becomes one 2048-d vector stored as qwen3vl_embedding. Bottom lane: at query time, a text query goes through the same model and lands in the same space, which is what makes text-to-video search work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This model is useful for robotics data because &lt;strong&gt;video and text map into the same vector space.&lt;/strong&gt; The similarity index &lt;code&gt;qwen3vl_embedding_sim&lt;/code&gt; keeps the model name, so it accepts text queries as well as episodes: &lt;code&gt;ds.sort_by_similarity("folding a cloth", k=10, brain_key="qwen3vl_embedding_sim")&lt;/code&gt; — no labels, no metadata matching, just the model's understanding of what was in the clip.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://voxel51.com/vision-language-action-model-vla" rel="noopener noreferrer"&gt;Vision-Language-Action (VLA) model&lt;/a&gt; development, this is specifically useful at the &lt;a href="https://voxel51.com/blog/fiftyone-supports-multimodal-data" rel="noopener noreferrer"&gt;&lt;strong&gt;data curation stage&lt;/strong&gt;&lt;/a&gt;. A VLA policy needs to generalize across scenes, embodiments, and task variations. But you can only curate what you can measure. Video embeddings give you a numeric handle on task semantics and visual appearance simultaneously, which makes it possible to compute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Similarity&lt;/strong&gt; — which episodes, across all 50 embodiments, are doing visually similar things?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniqueness&lt;/strong&gt; — which demonstrations cover behavior your dataset hasn't seen much of?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Representativeness&lt;/strong&gt; — which episodes best stand in for entire clusters of similar behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't extract these signals from episode metadata alone. They require a model that actually watched the clips.&lt;/p&gt;

&lt;p&gt;You also get &lt;code&gt;uniqueness&lt;/code&gt; and &lt;code&gt;representativeness&lt;/code&gt; scores out of the box. More on those below.&lt;/p&gt;

&lt;p&gt;If you want to try a different model, such as &lt;code&gt;Qwen/Qwen3-VL-Embedding-8B&lt;/code&gt;, write its vectors to a new field; Step 5 shows the full embedding loop for LeRobot episodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Compute similarity, uniqueness, and representativeness
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FrpjHY1Jdxy5I1aZeQsUtyQxjZTxcEgvlNqOwztrtyis%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FrpjHY1Jdxy5I1aZeQsUtyQxjZTxcEgvlNqOwztrtyis%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App grid of LeRobot episodes tagged with uniqueness and representativeness scores, next to the Embeddings panel and a score histogram" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App grid of LeRobot episodes tagged with uniqueness and representativeness scores, next to the Embeddings panel and a score histogram&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip the code if you loaded from the Hub.&lt;/strong&gt; The pre-built dataset already includes &lt;code&gt;qwen3vl_embedding_sim&lt;/code&gt;, &lt;code&gt;qwen3vl_embedding_umap&lt;/code&gt;, &lt;code&gt;uniqueness&lt;/code&gt;, and &lt;code&gt;representativeness&lt;/code&gt;. The code block below is for readers building their own LeRobot dataset — it requires umap-learn and an existing embedding field. The explanations after it apply either way.&lt;/p&gt;

&lt;p&gt;If you're building your own dataset, compute the &lt;code&gt;qwen3vl_embedding&lt;/code&gt; field first (the loop is in Step 5), then run the four calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.brain&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fob&lt;/span&gt;

&lt;span class="n"&gt;EMBED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Similarity index — powers Sort by Similarity in the App. Passing the model name
# lets the index embed text queries (register the qwen3vl_embeddings source first)
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-VL-Embedding-2B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# UMAP — 2D layout for the Embeddings panel
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_visualization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;umap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_umap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Uniqueness — how different each episode is from its nearest neighbors
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_uniqueness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uniqueness_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uniqueness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;similarity_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Representativeness — how close each episode is to a cluster center
&lt;/span&gt;&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_representativeness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;representativeness_field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;representativeness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;similarity_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why UMAP for video embeddings?
&lt;/h3&gt;

&lt;p&gt;Each episode is a 2048-dimensional vector. You can't look at that. Dimensionality reduction maps it to 2D so you can see structure in the Embeddings panel.&lt;/p&gt;

&lt;p&gt;The three common choices are principal component analysis (PCA), t-distributed stochastic neighbor embedding (t-SNE), and UMAP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PCA&lt;/strong&gt; is linear. It finds the axes of maximum variance, which is fast but can't capture the curved manifold structure that emerges when you embed semantically diverse video. A pick-and-place cluster and a folding cluster may sit on a low-variance direction PCA ignores entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;t-SNE&lt;/strong&gt; is non-linear and good at revealing local clusters, but it destroys global structure — the &lt;em&gt;distance between&lt;/em&gt; clusters in the 2D plot is meaningless, and it's slow on datasets larger than a few thousand samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UMAP&lt;/strong&gt; is non-linear, keeps local cluster structure, and usually keeps more of the arrangement &lt;em&gt;between&lt;/em&gt; clusters than t-SNE does. Don't over-read it, though: distances between far-apart clusters are still only roughly meaningful. It runs on 500 episodes in seconds. FiftyOne's UMAP backend uses Euclidean distance with &lt;code&gt;min_dist=0.1&lt;/code&gt; by default, which gives tight clusters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wmtkgkl2rgqd16wjism.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wmtkgkl2rgqd16wjism.png" alt="Three scatter plots of the same 497 Qwen3-VL-Embedding vectors reduced with PCA, t-SNE, and UMAP, colored by robot category" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same 497 Qwen3-VL-Embedding vectors reduced three ways, colored by robot category. PCA's two axes hold 20% of the variance, and the categories overlap; t-SNE separates local knots but inter-knot distances are arbitrary; UMAP is the layout the Embeddings panel shows. In every map, the 10 clips from one source dataset collapse into a single knot—same scene, same camera, same task.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a first look at a cross-embodiment dataset, UMAP is the practical choice: you get clusters you can click into, and their rough arrangement is meaningful.&lt;/p&gt;

&lt;p&gt;The map also shows what the embeddings respond to most: the recording session. For 495 of the 497 episodes, the nearest neighbor comes from the same source dataset. Step past a clip's own siblings, though, and the next matches in the full 2048-d space often share the task on a different robot. The nearest non-sibling to the &lt;code&gt;so100&lt;/code&gt; box-in-bin episodes is a &lt;code&gt;so100MovellaDot&lt;/code&gt; ball-in-box episode (cosine distance 0.157), and the nearest non-sibling to the &lt;code&gt;arx5&lt;/code&gt; dome-lifting episodes is a &lt;code&gt;trossen_ai_stationary&lt;/code&gt; cube-in-box episode (0.170).&lt;/p&gt;

&lt;h3&gt;
  
  
  What uniqueness and representativeness actually compute
&lt;/h3&gt;

&lt;p&gt;Both metrics are grounded in the embedding space. The FiftyOne Brain docs cover &lt;a href="https://docs.voxel51.com/brain/index.html#image-uniqueness" rel="noopener noreferrer"&gt;&lt;code&gt;uniqueness&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.voxel51.com/brain/index.html#image-representativeness" rel="noopener noreferrer"&gt;&lt;code&gt;representativeness&lt;/code&gt;&lt;/a&gt;; here's what each one computes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uniqueness&lt;/strong&gt; — K-nearest-neighbor distance, K=3. For each episode, FiftyOne finds its 3 nearest neighbors in embedding space and computes a weighted mean of those distances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uniqueness = weighted_mean(distances_to_3_nearest_neighbors)
             weights = [0.6, 0.3, 0.1]   # closer neighbors count more
             normalized to [0, 1] by dividing by the dataset maximum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpretation: &lt;code&gt;uniqueness=1.0&lt;/code&gt; means this episode is farther from its nearest neighbors than any other episode in the set — it occupies a region of embedding space that no other clip is near. &lt;code&gt;uniqueness=0.013&lt;/code&gt; means the episode is almost on top of another one — effectively a near-duplicate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fixfxvqdm0e6idrqef5da.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fixfxvqdm0e6idrqef5da.png" alt="Toy embedding space where an isolated episode scores 1.0 and a clustered one scores about 0.01 on uniqueness, beside a log-scale histogram of uniqueness across 497 LeRobot episodes" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: a toy embedding space. Episode A is far from its three nearest neighbors (line width = weight), so it scores 1.0; episode B sits in a knot of near-identical clips and scores ≈ 0.01. Right: the real distribution over all 497 episodes on a log scale — 292 score below 0.1, two score above 0.7, and one arx5 clip (a single-frame episode) sets the maximum at 1.0.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Representativeness&lt;/strong&gt; — K-Means distance to cluster center, K=20 clusters. For each episode, FiftyOne finds the nearest cluster center and scores it as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;representativeness = 1 / (1 + distance_to_nearest_cluster_center)
                     normalized locally within each cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpretation: &lt;code&gt;representativeness=1.0&lt;/code&gt; means this episode is at the heart of its cluster — it's the most "typical" example of that group of behaviors. Low representativeness means the episode sits at the edge of a cluster, more idiosyncratic than central.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkw0oraa436pt2le1ibmb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkw0oraa436pt2le1ibmb.png" alt="K-means cluster centers with equal-distance rings, where a central episode scores near 1.0 and a peripheral one near 0.3, beside the representativeness distribution across 497 episodes" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: K-Means centers (×) with rings of equal distance. Episode P sits on its center and scores ≈ 1.0; episode Q is assigned to the blue cluster but sits far out and scores ≈ 0.3. Right: the real distribution — 198 of 497 episodes score above 0.8, and the floor of 0.33 means no episode is a pure outlier.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these two metrics are the right pair for VLA data curation
&lt;/h3&gt;

&lt;p&gt;A Vision-Language-Action model needs two things from its training data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Coverage&lt;/strong&gt; — enough behavioral diversity for the policy to generalize to new scenes and task variations. Low-uniqueness episodes (near-duplicates) don't add coverage; they just add training compute and risk overfitting to that one scene configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal density&lt;/strong&gt; — enough repetition of each important behavior for the policy to learn it. Keeping only high-uniqueness episodes would discard the repetitions that make a skill learnable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Uniqueness and representativeness together let you navigate that tradeoff explicitly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seed set: diverse AND learnable&lt;/td&gt;
&lt;td&gt;High uniqueness + high representativeness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redundancy audit: what can be cut?&lt;/td&gt;
&lt;td&gt;Low uniqueness floor (near-duplicates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical demos per task&lt;/td&gt;
&lt;td&gt;Sort by representativeness within each task view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rare demonstrations (or broken ones)&lt;/td&gt;
&lt;td&gt;Uniqueness &amp;gt; 0.7, then check duration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Four VLA data curation goals and the uniqueness and representativeness queries that answer each one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fco8vybj7po6sjgkx2t22.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fco8vybj7po6sjgkx2t22.png" alt="Scatter plot of uniqueness against representativeness for every LeRobot episode, colored by robot category, with shaded query regions and 50 ringed points marking curate_diverse_seeds" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every episode is placed by its two scores (uniqueness on a symlog axis, colored by robot category). The shaded regions are the queries from the table: the redundancy audit on the left, canonical demos along the top, high-uniqueness outliers on the right, and the 50 orange-ringed points are curate_diverse_seeds — the top-50 by uniqueness with representativeness above 0.5.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this dataset, &lt;code&gt;uniqueness&lt;/code&gt; ranges from 0.013 to 1.0. The floor shows near-identical clips in the set — 292 of 497 score below 0.1, and the bottom six are four &lt;code&gt;moss&lt;/code&gt; cube-stacking episodes and two &lt;code&gt;sam_bimanual&lt;/code&gt; cloth-folding episodes, each from a single recording session. At the top, only two episodes score above 0.7: an &lt;code&gt;arx5&lt;/code&gt; clip at 1.0 and an &lt;code&gt;xarm6&lt;/code&gt; clip at 0.86. Both are single-frame episodes, 0.02–0.03 seconds long. &lt;code&gt;representativeness&lt;/code&gt; ranges from 0.33 → 1.0 — no episode is a pure outlier; even the most peripheral clips are loosely connected to a cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; Both ends of the uniqueness ranking come from how we built this subset, not from rare behavior or wasted data. Each embodiment contributes 10 consecutive episodes from one recording session, so every clip has nine siblings sitting right next to it in embedding space. On this dataset, every one of the 50 episodes in &lt;code&gt;curate_redundant_candidates&lt;/code&gt; has a sibling as its nearest neighbor, sampled on purpose. The top of the ranking has the opposite story: the five single-frame episodes look unlike everything else because there's almost nothing in them. They take the top two spots, and all five land in &lt;code&gt;curate_diverse_seeds&lt;/code&gt;. Uniqueness did its job by surfacing recordings worth a second look. Those clips belong in the cut pile, though, not the seed set, so pair uniqueness with a duration filter.&lt;/p&gt;

&lt;p&gt;These two numbers let you make dataset decisions — what to keep, what to cut, what's overrepresented — without watching a single video. A log viewer shows you one episode at a time. FiftyOne gives you a signal across all 497 at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Save views that answer real questions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F1KsDYAzEH102JfWRMq5cplQC02MpivSrEZ1lfv7P502WT4%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F1KsDYAzEH102JfWRMq5cplQC02MpivSrEZ1lfv7P502WT4%2Fanimated.gif%3Fwidth%3D640" alt="Switching between the category_single_arm and category_bimanual saved views from the Views dropdown in the FiftyOne App" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Switching between the category_single_arm and category_bimanual saved views from the Views dropdown in the FiftyOne App&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Views in FiftyOne are lazy queries — no data is copied, they resolve on load. Once saved, they appear in the App's Views dropdown.&lt;/p&gt;

&lt;p&gt;The pre-built dataset already has 75 views saved. Check what's there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_saved_views&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;# ['so100', 'so101', ..., 'curate_diverse_seeds', 'curate_redundant_candidates',
#  'category_humanoid', 'task_pick_place', 'fps_30', ...]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To build them yourself on your own dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;

&lt;span class="c1"&gt;# Curation: 50 most visually distinct AND cluster-representative episodes
&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curate_diverse_seeds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uniqueness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;representativeness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Redundancy candidates: bottom 50 by uniqueness
&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curate_redundant_candidates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uniqueness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Robot category views
&lt;/span&gt;&lt;span class="n"&gt;HUMANOID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unitree_G1_Dex3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unitree_G1_Gripper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unitree_G1_Inspire&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unitree_G1_inspire&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;g1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category_humanoid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;robot_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;is_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HUMANOID&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="c1"&gt;# Task keyword views
&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_pick_place&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;re_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pick|place|grasp|grab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="c1"&gt;# FPS views — reveal recording-condition variation across embodiments
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fps_val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;fps_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fps_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fps_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;75 views total. fps_30 covers 387 of the 497 episodes. &lt;code&gt;fps_10&lt;/code&gt; covers 30 — all from three stationary arms: &lt;code&gt;widowx&lt;/code&gt;, &lt;code&gt;ur5e_gello&lt;/code&gt;, and &lt;code&gt;mycobot320&lt;/code&gt;. &lt;code&gt;task_pick_place&lt;/code&gt; returns 367 episodes; &lt;code&gt;task_fold_pour&lt;/code&gt; returns 70.&lt;/p&gt;

&lt;p&gt;Each is a reusable, shareable slice of the dataset that resolves in milliseconds. No script, no re-download, no custom data loader — just a name in a dropdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: similarity search by episode and by text
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FJlCOm00OS6Cz4KipzCrlNuv02lg01RLDJ7sqCEWeNdURxQ%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FJlCOm00OS6Cz4KipzCrlNuv02lg01RLDJ7sqCEWeNdURxQ%2Fanimated.gif%3Fwidth%3D640" alt="Running a text similarity search for &amp;quot;robot arm picking up lego pieces&amp;quot; against the qwen3vl_embedding_sim index in the FiftyOne App's Similarity Search panel" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Running a text similarity search for "robot arm picking up lego pieces" against the qwen3vl_embedding_sim index in the FiftyOne App's Similarity Search panel&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Search by episode
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;qwen3vl_embedding_sim&lt;/code&gt; index powers episode-to-episode search directly from the App or from Python. Pass any sample's ID, and FiftyOne returns the nearest neighbors by video embedding distance. Because every clip's closest matches are its own recording session, filter out the query's &lt;code&gt;dataset_name&lt;/code&gt; to see what other robots are doing something similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ViewField&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="c1"&gt;# one sam_bimanual cloth-folding episode as the query
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;girardijp/sam_fold_cloth_single&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;episode_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# unfiltered: the query and its 9 siblings from the same session
&lt;/span&gt;&lt;span class="n"&gt;same_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# filtered: the 10 nearest episodes from other source datasets
&lt;/span&gt;&lt;span class="n"&gt;others&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;others&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# [so100_bimanual] playing with folding      (all 10 hits)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5bj1k2wm6mijok2i371y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5bj1k2wm6mijok2i371y.png" alt="A sam_bimanual cloth-folding query episode marked on the UMAP, beside the 10 nearest episodes from other source datasets ranked by cosine distance, all so100_bimanual folding clips" width="799" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: the query (star) on the UMAP, with its session's knot and the cross-source neighbors. Right: the 10 nearest episodes from other source datasets, ranked by cosine distance. The query's 9 siblings all sit within 0.015; past them, every hit is a so100_bimanual "playing with folding" episode, 0.19–0.31 away.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FxqVi8dFer9jSn801701OqAwU4YgZE8nZOua9eowBGzGlM%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FxqVi8dFer9jSn801701OqAwU4YgZE8nZOua9eowBGzGlM%2Fanimated.gif%3Fwidth%3D640" alt="Selecting LeRobot episodes in the FiftyOne App and running an image similarity search from the Similarity Search panel" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting LeRobot episodes in the FiftyOne App and running an image similarity search from the Similarity Search panel&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;sam_bimanual&lt;/code&gt; rig folding a cloth and a pair of SO-100 arms folding cloth look nothing alike as robots, and the model still puts them next to each other. That's the cross-embodiment signal. Some sessions pull in a neighboring robot type instead: the 10 &lt;code&gt;Unitree_G1_Dex3&lt;/code&gt; cube-stacking clips sit within 0.030 of each other, and the first other episode, at rank 10, is a &lt;code&gt;Unitree_Z1_Dual&lt;/code&gt; red-cup pick (0.142).&lt;/p&gt;

&lt;h3&gt;
  
  
  Search by text
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FSGFzV6AlZ01rQ00R8ZPDD5uDXe8dYqFVSjT24zSQ1KnPY%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FSGFzV6AlZ01rQ00R8ZPDD5uDXe8dYqFVSjT24zSQ1KnPY%2Fanimated.gif%3Fwidth%3D640" alt="Reopening saved similarity searches in the FiftyOne App, such as &amp;quot;robot arm picking up lego pieces&amp;quot; and &amp;quot;blue robotic arm,&amp;quot; to reload their matching LeRobot episodes" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Reopening saved similarity searches in the FiftyOne App, such as "robot arm picking up lego pieces" and "blue robotic arm," to reload their matching LeRobot episodes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The same index takes text. The dataset stores the episode vectors, but FiftyOne still needs the model to embed your query, so register the &lt;a href="https://github.com/harpreetsahota204/qwen3vl_embeddings" rel="noopener noreferrer"&gt;&lt;code&gt;qwen3vl_embeddings&lt;/code&gt;&lt;/a&gt; remote zoo source first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.zoo&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foz&lt;/span&gt;

&lt;span class="c1"&gt;# declare which models the remote source provides
&lt;/span&gt;&lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_zoo_model_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/harpreetsahota204/qwen3vl_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# optional: the first text query downloads the weights automatically
&lt;/span&gt;&lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_zoo_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-VL-Embedding-2B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folding a cloth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pouring water into a cup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;humanoid robot picking up a can&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# summarised output:
# folding a cloth -&amp;gt; sam_bimanual, "Fold a cloth" (x3)
# pouring water into a cup -&amp;gt; bi_so100_follower, "bi soarm100 pour water" (x3)
# humanoid robot picking up a can -&amp;gt; Unitree_G1_Inspire, "Pick up the cans into the box on the table." (x3)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same text box works in the App's similarity panel once &lt;code&gt;qwen3vl_embedding_sim&lt;/code&gt; is selected. Not every query lands: "robot stacking a cube" returns a &lt;code&gt;Unitree_Z1_Single&lt;/code&gt; red-cup pick rather than the &lt;code&gt;Unitree_G1_Dex3&lt;/code&gt; cube-stacking clips. Treat &lt;a href="https://voxel51.com/blog/fiftyone-search-multimodal-data" rel="noopener noreferrer"&gt;text search&lt;/a&gt; as a fast way to find candidates, then check them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the embedding and index on your own dataset
&lt;/h3&gt;

&lt;p&gt;LeRobot samples have no &lt;code&gt;filepath&lt;/code&gt;, so &lt;code&gt;compute_embeddings&lt;/code&gt; can't read the clips for you: decode each episode's time window with PyAV and pass the frames to the model as one video. This is the code behind &lt;code&gt;qwen3vl_embedding&lt;/code&gt; (set ROOT to the folder that holds your &lt;code&gt;&amp;lt;contributor&amp;gt;/&amp;lt;dataset&amp;gt;/ directories&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;av&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.brain&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fob&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.zoo&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foz&lt;/span&gt;

&lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/path/to/community_dataset_v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;episode_frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_frames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;av&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;fps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;average_rate&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;to_ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;from_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;max_frames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_ts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;any_frame&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backward&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time_base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;from_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to_ts&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_image&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;


&lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_zoo_model_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/harpreetsahota204/qwen3vl_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;foz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_zoo_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-VL-Embedding-2B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# or Qwen/Qwen3-VL-Embedding-8B
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_embedder&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_load_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_samples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autosave&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;media_reference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset_name&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;videos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;episode_frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# model.embed() expects a file path; the underlying embedder accepts a frame list as a video
&lt;/span&gt;    &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;}])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_camera&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;

&lt;span class="n"&gt;fob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-VL-Embedding-2B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing the model name to &lt;code&gt;compute_similarity&lt;/code&gt; lets the index embed text at query time; an index built without it accepts only episodes as queries. On a GPU, the 497 episodes take about 20 minutes. Then run the UMAP, uniqueness, and representativeness calls from Step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together: export a curated LeRobot v3 training split
&lt;/h2&gt;

&lt;p&gt;Once loaded, querying across 50 embodiments looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 50 most unique AND representative episodes, minus the single-frame recordings
&lt;/span&gt;&lt;span class="n"&gt;seed_view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_saved_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curate_diverse_seeds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Episodes from other robots that look like a given clip
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;robot_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sam_bimanual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;similar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Episodes matching a description (register the qwen3vl_embeddings source first)
&lt;/span&gt;&lt;span class="n"&gt;cloth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_by_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folding a cloth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brain_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3vl_embedding_sim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Export the curated subset as standalone LeRobot v3 repos, one per source dataset
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seed_view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seed_view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;export_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my_training_subset/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dataset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LeRobotDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The export loops over &lt;code&gt;dataset_name&lt;/code&gt; because a LeRobot v3 repo has one feature schema, and the 50 embodiments here have different cameras and action dimensions. The 45 seed episodes come from 16 source datasets, so you get 16 repos.&lt;/p&gt;

&lt;p&gt;That last step is the point. You went from a 900 GB community dataset to a curated, re-indexed LeRobot v3 training split — filtered by visual uniqueness, robot category, task type, or any combination — without writing a parser, a data loader, or a custom video decode loop. A viewer shows you what the robot did. A curated dataset decides what goes into the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does fo.types.LeRobotDataset actually store in FiftyOne's database?
&lt;/h3&gt;

&lt;p&gt;Episode-level metadata only: &lt;code&gt;episode_index&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;fps&lt;/code&gt;, &lt;code&gt;robot_type&lt;/code&gt;, and a &lt;code&gt;media_reference&lt;/code&gt; that points into the local LeRobot directory. No pixel data, no Parquet rows. The source files stay where they are; FiftyOne reads through the pointer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use apply_model on a LeRobot dataset in FiftyOne?
&lt;/h3&gt;

&lt;p&gt;Not for a standard zoo model — those expect &lt;code&gt;sample.filepath&lt;/code&gt; pointing to a video or image file, and LeRobot samples use &lt;code&gt;media_reference&lt;/code&gt; instead. The embedding in this post was computed by working around that: PyAV extracts frames from the episode's time window, and the frames go to the model as a list of PIL images. Step 5 shows the full loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does Qwen3-VL-Embedding use last-token pooling?
&lt;/h3&gt;

&lt;p&gt;That's how &lt;code&gt;Qwen3-VL-Embedding&lt;/code&gt; was trained. It's a decoder-only model with no CLS token, and its retrieval training teaches the final token's hidden state to summarize the whole input, whether it's a video clip or a sentence. Using the same pooling for both is what puts episodes and text queries in one space. Mean pooling over an instruction-tuned model's hidden states gives usable vectors for comparing clips, but not for matching them against text.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do saved views work after load_from_hub?
&lt;/h3&gt;

&lt;p&gt;They're stored in the FiftyOne dataset document alongside the samples. &lt;code&gt;ds.list_saved_views()&lt;/code&gt; returns all 75 names; &lt;code&gt;ds.load_saved_view("curate_diverse_seeds")&lt;/code&gt; returns the 50 episodes matching that query, resolved against whatever embeddings and fields exist locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I export a curated FiftyOne view to LeRobot v3 for policy training?
&lt;/h3&gt;

&lt;p&gt;Export a curated view as standalone LeRobot v3 repos, one per source dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ViewField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;export_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my_curated_subset/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dataset_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LeRobotDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exporter refuses to mix episodes from different sources in one repo, since each embodiment has its own cameras and action layout. For each source, it re-indexes episodes from 0, recomputes stats, and writes a self-contained v3 structure that any LeRobot-compatible trainer can read directly.&lt;/p&gt;

</description>
      <category>lerobot</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>python</category>
    </item>
    <item>
      <title>Real vs. Synthetic LeRobot Datasets: 5 Sets and the Sim-to-Real Evidence</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:48:43 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/real-vs-synthetic-lerobot-datasets-5-sets-and-the-sim-to-real-evidence-4jii</link>
      <guid>https://dev.to/harpreetsahota/real-vs-synthetic-lerobot-datasets-5-sets-and-the-sim-to-real-evidence-4jii</guid>
      <description>&lt;p&gt;&lt;em&gt;A field guide to 5 manipulation datasets in&lt;/em&gt; &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;&lt;em&gt;LeRobot v3.0&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, 2 real and 3 synthetic, with their exact fields and three strategies for generating robot data when real teleoperation can't scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Large language models train on trillions of tokens. Vision-language models train on billions of images. The largest dataset in this post has 637,498 trajectories, all synthetic. The two real, teleoperated datasets top out at 4,258 and 1,804 episodes. Every real demonstration requires a human at a controller, a physical robot, and a prepared scene. That collection process doesn't scale like web data.&lt;/p&gt;

&lt;p&gt;We pulled 5 &lt;a href="https://voxel51.com/blog/mcap-datasets-multimodal-robotics-fiftyone" rel="noopener noreferrer"&gt;manipulation datasets&lt;/a&gt; into LeRobot v3.0 and FiftyOne: two real, teleoperated single-arm datasets and three simulated datasets spanning single-arm and bimanual setups. This guide examines the data bottleneck, compares three &lt;a href="https://voxel51.com/blog/synthetic-data-curation-physical-ai" rel="noopener noreferrer"&gt;synthetic-generation strategies&lt;/a&gt;, and links each dataset to its live FiftyOne Space, paper, and license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;5 datasets: 2 real teleoperation (&lt;code&gt;fmb_multi&lt;/code&gt; on a Franka Panda, &lt;code&gt;rh20t_cfg1&lt;/code&gt; on a Flexiv arm), 3 synthetic (&lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;, &lt;code&gt;InternData-A1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Scarcity starts at the source: &lt;code&gt;fmb_multi&lt;/code&gt; has 1,804 episodes and &lt;code&gt;rh20t_cfg1&lt;/code&gt; has 4,258. Their FiftyOne exports contain 302 and 28 episodes, respectively. &lt;code&gt;InternData-A1&lt;/code&gt;'s full synthetic release has 637,498 trajectories, roughly &lt;strong&gt;350×&lt;/strong&gt; the size of &lt;code&gt;fmb_multi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The 3 synthetic datasets use different generation strategies: MimicGen decomposes and re-stitches human demonstrations (&lt;code&gt;robocasa-MG_100&lt;/code&gt;); an LLM code generator and VLM observer create and repair task programs without seed demonstrations (&lt;code&gt;robotwin_unified&lt;/code&gt;); and a compositional pipeline scripts skill policies from an asset library without demonstrations (&lt;code&gt;InternData-A1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Only the &lt;code&gt;InternData-A1&lt;/code&gt; paper reports sim-to-real transfer: zero-shot transfer after pretraining exclusively on synthetic data. The &lt;code&gt;robocasa-MG_100&lt;/code&gt; and &lt;code&gt;robotwin_unified&lt;/code&gt; papers don't make that claim for the exports in this post.&lt;/li&gt;
&lt;li&gt;Both real datasets here report &lt;code&gt;fps&lt;/code&gt; as &lt;strong&gt;nominal, not measured&lt;/strong&gt; — their source .npy/session data carries no per-frame timestamps, so frames map onto a synthetic grid rather than a recorded rate. Worth knowing before treating &lt;code&gt;fps&lt;/code&gt; as ground truth for either.&lt;/li&gt;
&lt;li&gt;Licensing: CC-BY-4.0 (&lt;code&gt;fmb_multi&lt;/code&gt;), mixed CC-BY-SA-4.0/CC-BY-NC-4.0 by episode (&lt;code&gt;rh20t_cfg1&lt;/code&gt;), Apache-2.0 (&lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;), CC-BY-NC-SA-4.0 (&lt;code&gt;InternData-A1&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why real robot data is scarce, concretely
&lt;/h2&gt;

&lt;p&gt;Human &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;teleoperation&lt;/a&gt; requires trained operators, specialized hardware, real objects, and safety controls.&lt;/p&gt;

&lt;p&gt;Architecture can't compensate for narrow data from one robot in one environment. Researchers use simulation, human video, and robot self-exploration to expand the training set beyond what teleoperators can collect.&lt;/p&gt;

&lt;p&gt;Scarcity isn't the only problem.&lt;/p&gt;

&lt;p&gt;Robot data consists of sequences, and a policy must learn how an action at time T changes the state at T+1. When fps is nominal rather than measured from timestamps, as in both real datasets in this post, the timing is an assumption rather than a recorded fact.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2506.01844" rel="noopener noreferrer"&gt;SmolVLA paper&lt;/a&gt; reports state-of-the-art results with an order of magnitude less data than competing approaches and attributes the result to &lt;a href="https://voxel51.com/blog/robot-episode-quality-triage" rel="noopener noreferrer"&gt;curation rather than collection volume&lt;/a&gt; alone. Each synthetic dataset below makes a different choice about how much real data stays in its generation loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to generate synthetic manipulation data
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dataset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Generation strategy&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Seed data&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Scale achieved&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Sim-to-real evidence&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robocasa-MG_100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MimicGen: decompose human demos into object-centric segments, re-stitch into new scene/object configurations, keep only successful rollouts (rejection sampling)&lt;/td&gt;
&lt;td&gt;1,250 human teleop demos (4 operators, 3D SpaceMouse)&lt;/td&gt;
&lt;td&gt;72,000 trajectories in the full Generated-3000 release; this export is the Generated-100 1/30 subsample, 2,400 episodes&lt;/td&gt;
&lt;td&gt;Paper reports BC-Transformer results on the generated data; no sim-to-real transfer claim for this export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robotwin_unified&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An LLM code-generation agent writes task programs from natural-language instructions, executes them in a SAPIEN-based simulator, and a VLM observer inspects execution to detect and localize failures for iterative repair&lt;/td&gt;
&lt;td&gt;None — fully automated, no human demonstrations at any stage&lt;/td&gt;
&lt;td&gt;100,000+ trajectories across 50 tasks and 5 robot embodiments in the source corpus; this export is the full aloha-embodiment slice, 27,500 episodes&lt;/td&gt;
&lt;td&gt;Domain randomization (textures, lighting, clutter) is the stated sim-to-real strategy; the source paper cautions against treating benchmark performance as a real-world deployment proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;InternData-A1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully decoupled compositional pipeline: asset specification, scripted skill policies, task composition, and rendering as independent stages — no demonstrations of any kind&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;637,498 trajectories, 401.4M frames, across 4 embodiments and 70 tasks&lt;/td&gt;
&lt;td&gt;Paper reports &lt;strong&gt;zero-shot sim-to-real transfer&lt;/strong&gt; on several tasks after pretraining a policy exclusively on this synthetic corpus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Three synthetic data generation strategies compared by seed data requirement, scale achieved, and published sim-to-real evidence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;MimicGen augments 1,250 real seed demonstrations. RoboTwin needs no seed demonstrations, but its program-synthesis loop uses a VLM observer to detect failures. InternData-A1 also skips demonstrations and composes hand-scripted skill policies across an asset library. Only InternData-A1 reports zero-shot transfer, but one result across three different task suites can't establish which strategy generalizes best. The sim-to-real claim has not been independently reproduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 LeRobot datasets at a glance
&lt;/h2&gt;

&lt;p&gt;"Episodes" reflects what's in each live FiftyOne export, not the full upstream source. &lt;code&gt;robocasa-MG_100&lt;/code&gt; and &lt;code&gt;robotwin_unified&lt;/code&gt; are complete imports; &lt;code&gt;fmb_multi&lt;/code&gt;, &lt;code&gt;rh20t_cfg1&lt;/code&gt;, and &lt;code&gt;InternData-A1&lt;/code&gt; are disk-budget-constrained subsets (&lt;code&gt;InternData-A1&lt;/code&gt;'s export also covers a single task/embodiment out of 1,488 possible task/embodiment directories in its source repo).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dataset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Real or Sim&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;fmb_multi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contact-rich Franka Panda assembly demonstrations across three boards, phase-labeled by skill primitive&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;rh20t_cfg1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real Flexiv-arm teleoperation with ten synchronized camera views, part of RH20T's one-shot skill-transfer dataset&lt;/td&gt;
&lt;td&gt;Mixed CC-BY-SA-4.0 / CC-BY-NC-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robocasa-MG_100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Synthetic kitchen manipulation generated via MimicGen augmentation of real human demonstrations&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robotwin_unified&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bimanual ALOHA-embodiment simulation generated by an LLM code-gen agent and VLM observer, with heavy domain randomization&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;InternData-A1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A single Franka articulation task ("close the laptop") from a fully synthetic, zero-demonstration compositional simulation pipeline&lt;/td&gt;
&lt;td&gt;CC-BY-NC-SA-4.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The 5 LeRobot v3.0 manipulation datasets in this post, with descriptions, license terms, and whether the data is real or simulated.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 LeRobot datasets in detail
&lt;/h2&gt;

&lt;h3&gt;
  
  
  fmb_multi — Contact-rich Franka assembly, phase-labeled
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FxT3u3IkM00c9fcBCnsb1Z4t01ON456cwNpvSHgcDroqoU%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FxT3u3IkM00c9fcBCnsb1Z4t01ON456cwNpvSHgcDroqoU%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne playing an fmb_multi episode with four synchronized Franka Panda camera streams, joint position and end-effector pose plots, and a 28-dimensional observation state panel." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne playing an fmb_multi episode with four synchronized Franka Panda camera streams, joint position and end-effector pose plots, and a 28-dimensional observation state panel.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/fmb-multi-302ep" rel="noopener noreferrer"&gt;Voxel51/fmb-multi-302ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/fmb-multi-302ep" rel="noopener noreferrer"&gt;harpreetsahota/fmb-multi-302ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/robot-lev/fmb_multi" rel="noopener noreferrer"&gt;robot-lev/fmb_multi&lt;/a&gt; (1,804 episodes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2401.08553" rel="noopener noreferrer"&gt;Paper (arXiv:2401.08553)&lt;/a&gt; · &lt;a href="https://functional-manipulation-benchmark.github.io" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Designed to evaluate generalization in contact-rich assembly tasks, the Functional Manipulation Benchmark focuses on complex insertion-and-regrasp chains where force sensing plays a critical role, rather than basic pick-and-place routines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fmb_multi&lt;/code&gt; serves as its multi-object subset: across three distinct assembly boards, a Franka Panda robot arm performs a structured sequence of skill primitives (approach, grasp, insert, place, regrasp). Throughout these trials, the system collects 6-axis &lt;a href="https://voxel51.com/end-effector" rel="noopener noreferrer"&gt;end-effector&lt;/a&gt; force/torque readings along with visual and &lt;a href="https://voxel51.com/proprioception" rel="noopener noreferrer"&gt;proprioceptive inputs&lt;/a&gt;. The benchmark prioritizes cross-object and board adaptability over massive scale, capping the entire suite at under 2,000 trajectories.&lt;/p&gt;

&lt;p&gt;In this export, 302 demonstrations are captured (distributed almost evenly across the three boards at 100, 100, and 102 episodes), drawn from a full source collection of 1,804 episodes—showing inherent data sparsity before any FiftyOne filtering.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Available fields:&lt;/em&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Camera Streams:&lt;/strong&gt; 4 video feeds (&lt;code&gt;side_1&lt;/code&gt;, &lt;code&gt;side_2&lt;/code&gt;, &lt;code&gt;wrist_1&lt;/code&gt;, &lt;code&gt;wrist_2&lt;/code&gt;; 256×256 AV1 format, converted from original BGR)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proprioception &amp;amp; State:&lt;/strong&gt; &lt;code&gt;observation.state&lt;/code&gt; (28-dimensional vector covering joint position, velocity, end-effector pose, end-effector velocity, and gripper state), alongside dedicated &lt;code&gt;observation.state.joint_position&lt;/code&gt;, &lt;code&gt;.ee_pose&lt;/code&gt;, and .&lt;code&gt;gripper&lt;/code&gt; fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force &amp;amp; Kinematics:&lt;/strong&gt; &lt;code&gt;observation.force&lt;/code&gt; and &lt;code&gt;observation.torque&lt;/code&gt; (measured in the end-effector frame), plus a flattened 6×7 &lt;code&gt;observation.jacobian&lt;/code&gt; matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control Action:&lt;/strong&gt; A 7-dimensional Cartesian action vector&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  rh20t_cfg1 — Flexiv arm, ten camera views, license split by episode
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FQZuIVy02OKOpYQW13JQtAhwRUZ018rVVrSR6xqqlyKUew%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FQZuIVy02OKOpYQW13JQtAhwRUZ018rVVrSR6xqqlyKUew%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne playing an rh20t_cfg1 episode with five of the ten synchronized camera views of a Flexiv arm, a 15-dimensional observation state, and force and torque plots." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne playing an rh20t_cfg1 episode with five of the ten synchronized camera views of a Flexiv arm, a 15-dimensional observation state, and force and torque plots.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/rh20t-cfg1-28ep" rel="noopener noreferrer"&gt;Voxel51/rh20t-cfg1-28ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/rh20t-cfg1-28ep" rel="noopener noreferrer"&gt;harpreetsahota/rh20t-cfg1-28ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/robot-lev/rh20t_cfg1" rel="noopener noreferrer"&gt;robot-lev/rh20t_cfg1&lt;/a&gt; (4,258 episodes; RH20T's full dataset spans 110,000+ sequences across many robot configs, of which &lt;code&gt;cfg1&lt;/code&gt;/Flexiv is one)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2307.00595" rel="noopener noreferrer"&gt;Paper (arXiv:2307.00595)&lt;/a&gt; · &lt;a href="https://rh20t.github.io/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; mixed, 16 episodes CC-BY-SA-4.0 / 12 episodes CC-BY-NC-4.0 in this export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Designed to test one-shot skill transfer—specifically whether a robot can master a new task from a single demonstration—RH20T collected an extensive library of over 110,000 manipulation sequences across various robotic platforms and camera setups.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;rh20t_cfg1&lt;/code&gt; subset focuses on a Flexiv arm captured by ten synchronized camera views per episode. It preserves RH20T's scene-based licensing model, dividing scenes roughly equally between permissive and non-commercial terms.&lt;/p&gt;

&lt;p&gt;You can verify the scene-to-license mapping for each episode via the folder attribute in &lt;code&gt;meta/rh20t_episodes.json&lt;/code&gt;, with the full 28-row breakdown detailed on the dataset card.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Dataset Overview &amp;amp; Key Features&lt;/em&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Streams:&lt;/strong&gt; Includes 10 multi-angle camera feeds (&lt;code&gt;cam_&amp;lt;serial&amp;gt;&lt;/code&gt;, 360×640 AV1) recorded from fixed RealSense-style positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State &amp;amp; Kinematics:&lt;/strong&gt; Features a 15-dimensional &lt;code&gt;observation.state&lt;/code&gt; vector combining end-effector pose (7-dim), joint positions (7-dim), and gripper status (1-dim). Note that joint values are zeroed out if the source &lt;code&gt;has_joint&lt;/code&gt; flag was false.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force/Torque &amp;amp; Actions:&lt;/strong&gt; Provides wrist-based &lt;code&gt;observation.force&lt;/code&gt;/&lt;code&gt;torque&lt;/code&gt; measurements alongside a dedicated 6-axis arm-mounted &lt;code&gt;observation.robot_ft&lt;/code&gt; sensor, an 8-dimensional control &lt;code&gt;action&lt;/code&gt; (target pose + gripper state), and original &lt;code&gt;meta.rating&lt;/code&gt; quality annotations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  robocasa-MG_100 — Full synthetic kitchen dataset via MimicGen
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FCwdDeCkA1ZZzdOwNh01gzZ99OQc8KdSyqQs01pDQ01LhRg%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FCwdDeCkA1ZZzdOwNh01gzZ99OQc8KdSyqQs01pDQ01LhRg%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne playing a robocasa-MG_100 &amp;quot;open the microwave door&amp;quot; episode with one wrist view and two workspace views, a 16-dimensional state, and a 12-dimensional action." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne playing a robocasa-MG_100 "open the microwave door" episode with one wrist view and two workspace views, a 16-dimensional state, and a 12-dimensional action.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/robocasa-MG_100" rel="noopener noreferrer"&gt;Voxel51/robocasa-MG_100&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/robocasa-MG_100" rel="noopener noreferrer"&gt;harpreetsahota/robocasa-MG_100&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/DAVIAN-Robotics/robocasa-MG_100" rel="noopener noreferrer"&gt;DAVIAN-Robotics/robocasa-MG_100&lt;/a&gt; — &lt;strong&gt;all 2,400 episodes, full dataset, no subsetting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2406.02523" rel="noopener noreferrer"&gt;Paper (arXiv:2406.02523)&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2310.17596" rel="noopener noreferrer"&gt;MimicGen paper (arXiv:2310.17596)&lt;/a&gt; · &lt;a href="https://robocasa.ai" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Designed to assess how synthetic data scaling can build upon a limited pool of &lt;a href="https://voxel51.com/blog/libero-dataset-curation-fiftyone" rel="noopener noreferrer"&gt;human demonstrations&lt;/a&gt;, RoboCasa features procedurally generated kitchen settings covering 120 distinct configurations across 10 layouts and 12 visual styles.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MG_100&lt;/code&gt; release aligns with the paper's &lt;code&gt;Generated-100&lt;/code&gt; experimental tier—a 1/30 subset of MimicGen rollouts comprising 24 atomic tasks with 100 trajectories each (totalling 688,750 frames, with episode lengths spanning 89 to 838 frames). MimicGen segments human teleoperation recordings into object-focused primitives, recombines them across varied scene geometries and asset configurations, and validates success through rejection sampling. Because this system multiplies a foundational seed dataset of 1,250 human demonstrations gathered from 4 operators, it relies far more heavily on initial human inputs than the other synthetic benchmarks.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Available Fields:&lt;/em&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 camera streams:&lt;/strong&gt; 1 wrist-mounted view and 2 static workspace angles (formatted as 128×128 AV1 video).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Representation:&lt;/strong&gt; A 16-dimensional &lt;code&gt;observation.state&lt;/code&gt; array capturing gripper joint states, base pose, and base-to-end-effector position/quaternion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Space:&lt;/strong&gt; A 12-dimensional delta-pose &lt;code&gt;action&lt;/code&gt; array incorporating base and gripper commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status Flag:&lt;/strong&gt; A boolean &lt;code&gt;next.done&lt;/code&gt; flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  robotwin_unified — 27,500-episode bimanual simulation with domain randomization
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fah7WZMmRcEPYZ8JZV9Nb01Q7K02fiXUHJ8eUuOcwgQ8Ao%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fah7WZMmRcEPYZ8JZV9Nb01Q7K02fiXUHJ8eUuOcwgQ8Ao%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne grid view of the robotwin_unified dataset filtered to the ALOHA embodiment, with a dashboard charting the 50 most frequent robot tasks and episode duration." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne grid view of the robotwin_unified dataset filtered to the ALOHA embodiment, with a dashboard charting the 50 most frequent robot tasks and episode duration.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/robotwin-unified-27500ep" rel="noopener noreferrer"&gt;Voxel51/robotwin-unified-27500ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/robotwin-unified-27500ep" rel="noopener noreferrer"&gt;harpreetsahota/robotwin-unified-27500ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/lerobot/robotwin_unified" rel="noopener noreferrer"&gt;lerobot/robotwin_unified&lt;/a&gt; — this export is &lt;strong&gt;all 27,500 episodes, full &lt;code&gt;aloha&lt;/code&gt;-embodiment portion, no subsetting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2506.18088" rel="noopener noreferrer"&gt;Paper (arXiv:2506.18088)&lt;/a&gt; · &lt;a href="https://github.com/RoboTwin-Platform/RoboTwin" rel="noopener noreferrer"&gt;Simulator/generator code&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bypassing human demonstration requirements entirely, RoboTwin 2.0 advances data generation beyond traditional MimicGen-style augmentation.&lt;/p&gt;

&lt;p&gt;The process begins with an LLM writing executable task scripts from natural-language instructions, then running them inside a SAPIEN-based simulation environment. A vision-language model evaluates execution frame by frame, pinpointing failures to guide the code-generation agent in self-correcting errors through iterative refinement until high reliability is achieved. &lt;code&gt;robotwin_unified&lt;/code&gt; represents the bimanual ALOHA dataset produced by this automated setup: 27,500 episodes comprising 50 dual-arm tasks and 6,075,103 frames. The corpus features extensive &lt;a href="https://voxel51.com/domain-randomization" rel="noopener noreferrer"&gt;domain randomization&lt;/a&gt;—spanning 11,000 synthetic background textures, lighting, tabletop elevations, and instruction variants across a 731-object asset library.&lt;/p&gt;

&lt;p&gt;With 23,559 unique task strings, the dataset maintains a near 1:1 mapping between instructions and episodes.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Available Fields:&lt;/em&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Feeds:&lt;/strong&gt; 3 camera streams (an overhead &lt;code&gt;cam_high&lt;/code&gt; plus dual wrist views &lt;code&gt;cam_left_wrist&lt;/code&gt; and &lt;code&gt;cam_right_wrist&lt;/code&gt;, 480×640 AV1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Representation:&lt;/strong&gt; A 14-DoF &lt;code&gt;observation.state&lt;/code&gt; capturing waist, shoulder, elbow, forearm_roll, wrist_angle, wrist_rotate, and gripper parameters for both left and right arms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Space:&lt;/strong&gt; A 14-dimensional &lt;code&gt;action&lt;/code&gt; vector aligned with the state dimensions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task strings:&lt;/strong&gt; With 23,559 distinct task strings across 27,500 episodes, grouping by task isn't practical. Free-text search and filtering are the intended way to find episodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  InternData-A1 — One task, one embodiment, out of 1,488 possible exports
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FM900Yoy3ILt01BgqhWlagLe02euBU58ZjJ9EAFxxjoRksY%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FM900Yoy3ILt01BgqhWlagLe02euBU58ZjJ9EAFxxjoRksY%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne playing an InternData-A1 &amp;quot;close the laptop&amp;quot; episode with head and hand camera views, a 49-dimensional observation state, and gripper pose plots." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne playing an InternData-A1 "close the laptop" episode with head and hand camera views, a 49-dimensional observation state, and gripper pose plots.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/interndata-a1-close-the-laptop-369ep" rel="noopener noreferrer"&gt;Voxel51/interndata-a1-close-the-laptop-369ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/interndata-a1-close-the-laptop-369ep" rel="noopener noreferrer"&gt;harpreetsahota/interndata-a1-close-the-laptop-369ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/InternRobotics/InternData-A1" rel="noopener noreferrer"&gt;InternRobotics/InternData-A1&lt;/a&gt; (637,498 trajectories, 401.4M frames, 4 embodiments, 70 tasks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2511.16651" rel="noopener noreferrer"&gt;Paper (arXiv:2511.16651)&lt;/a&gt; · &lt;a href="https://internrobotics.github.io/interndata-a1.github.io/" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-NC-SA-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Among all synthetic datasets highlighted here, InternData-A1 represents the most radical departure from traditional workflows, bypassing human demonstrations entirely—even as an initial seed.&lt;/p&gt;

&lt;p&gt;Instead, it relies on a decoupled simulation architecture that synthesizes environment scenes, object assets, and programmatic skill policies. This approach unlocks massive scaling (401.4M frames across 637,498 trajectories) that would be nearly impossible to gather via manual teleoperation. Beyond sheer volume, its most notable milestone is empirical: a &lt;a href="https://voxel51.com/vision-language-action-model-vla" rel="noopener noreferrer"&gt;VLA policy&lt;/a&gt; pretrained solely on this synthetic collection demonstrates zero-shot transfer across multiple physical tasks—making it the only synthetic dataset in this group with published &lt;a href="https://voxel51.com/sim-to-real-transfer" rel="noopener noreferrer"&gt;sim-to-real transfer&lt;/a&gt; validation.&lt;/p&gt;

&lt;p&gt;The upstream repository hosts &lt;strong&gt;1,488&lt;/strong&gt; &lt;a href="https://voxel51.com/blog/lerobot-dataset-support-voxel51" rel="noopener noreferrer"&gt;&lt;strong&gt;standalone LeRobot v3.0 datasets&lt;/strong&gt;&lt;/a&gt; structured under &lt;code&gt;&amp;lt;embodiment&amp;gt;/&amp;lt;task_category&amp;gt;/&amp;lt;task&amp;gt;&lt;/code&gt; directories, each following an independent schema. This specific export isolates &lt;code&gt;franka-1/articulation_tasks/close_the_laptop&lt;/code&gt;, featuring 369 out of the 578 episodes available for that task, all guided by the uniform prompt "Close the laptop."&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Dataset Features &amp;amp; Structure&lt;/em&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Inputs:&lt;/strong&gt; Two video perspectives (&lt;code&gt;images.rgb.head&lt;/code&gt; at 360×640 and &lt;code&gt;images.rgb.hand&lt;/code&gt; at 480×640) alongside camera intrinsic and extrinsic parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kinematics &amp;amp; Control:&lt;/strong&gt; Granular state and action values, including &lt;code&gt;states.joint.position&lt;/code&gt;, &lt;code&gt;actions.gripper.pose&lt;/code&gt;, and leader-arm teleoperation signals under &lt;code&gt;master_actions.*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived Convenience Aggregations:&lt;/strong&gt; Includes a 49-dimensional &lt;code&gt;observation state&lt;/code&gt; vector and a 43-dimensional &lt;code&gt;action&lt;/code&gt; vector, engineered post-import to align with FiftyOne's &lt;a href="https://voxel51.com/blog/fiftyone-supports-multimodal-data" rel="noopener noreferrer"&gt;State &amp;amp; Action visualizer&lt;/a&gt; natively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;More LeRobot datasets.&lt;/strong&gt; These five come from the &lt;a href="https://voxel51.com/blog/lerobot-v3-datasets-vla-training-fiftyone" rel="noopener noreferrer"&gt;11 LeRobot v3.0 datasets for VLA training&lt;/a&gt; roundup. Five humanoid and bimanual datasets are compared in &lt;a href="https://voxel51.com/blog/humanoid-lerobot-datasets-vla-training" rel="noopener noreferrer"&gt;5 humanoid and bimanual LeRobot datasets&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is real robot data so much scarcer than simulated data?
&lt;/h3&gt;

&lt;p&gt;Every real demonstration requires a human operator, a physical robot, a real environment with real objects, and safety overhead around all of it — none of which parallelizes the way web scraping does. The two real datasets in this post (&lt;code&gt;fmb_multi&lt;/code&gt;, &lt;code&gt;rh20t_cfg1&lt;/code&gt;) top out at 1,804 and 4,258 episodes at the source; the largest synthetic dataset here (&lt;code&gt;InternData-A1&lt;/code&gt;) is 637,498 trajectories, because simulation removes the human-in-the-loop requirement for every single trajectory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which of these synthetic datasets has actual sim-to-real evidence?
&lt;/h3&gt;

&lt;p&gt;Only &lt;code&gt;InternData-A1&lt;/code&gt; — its paper reports zero-shot sim-to-real transfer on several tasks after pretraining a policy exclusively on the synthetic corpus. &lt;code&gt;robocasa-MG_100&lt;/code&gt;'s paper reports simulation-only BC-Transformer results, and &lt;code&gt;robotwin_unified&lt;/code&gt;'s paper explicitly cautions against treating its benchmark performance as a real-world deployment proxy without separate validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does "fps is nominal" mean and why does it matter?
&lt;/h3&gt;

&lt;p&gt;It means the frame rate in the dataset's metadata wasn't measured from actual per-frame timestamps—it's a value assumed for a synthetic, evenly spaced frame grid. Both &lt;code&gt;fmb_multi&lt;/code&gt; (source &lt;code&gt;.npy&lt;/code&gt; files carry no timestamps) and &lt;code&gt;rh20t_cfg1&lt;/code&gt; (cameras were downsampled from variable native capture rates) report nominal fps. If your training pipeline assumes fps reflects real elapsed time between frames, that assumption doesn't hold for either dataset as tightly as it does for datasets with measured timestamps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can synthetic data actually replace real teleoperation data?
&lt;/h3&gt;

&lt;p&gt;The evidence is suggestive but not settled. &lt;code&gt;InternData-A1&lt;/code&gt; reports zero-shot transfer on one task suite, and the result has not been independently reproduced. The SmolVLA result also suggests that data quality and diversity can matter more than raw scale. Treat each sim-to-real claim as one data point, not a general result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which licenses permit commercial use?
&lt;/h3&gt;

&lt;p&gt;Apache-2.0 (&lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;) permits commercial use subject to its terms. CC-BY-4.0 (&lt;code&gt;fmb_multi&lt;/code&gt;) permits commercial use with attribution. CC-BY-NC-SA-4.0 (&lt;code&gt;InternData-A1&lt;/code&gt;) prohibits commercial use. &lt;code&gt;rh20t_cfg1&lt;/code&gt;'s 28-episode export mixes CC-BY-SA-4.0 and CC-BY-NC-4.0, so check the per-episode license table before using it commercially.&lt;/p&gt;

</description>
      <category>lerobot</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>5 Humanoid and Bimanual LeRobot Datasets, Compared by State and Action Space</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:47:42 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/5-humanoid-and-bimanual-lerobot-datasets-compared-by-state-and-action-space-40p1</link>
      <guid>https://dev.to/harpreetsahota/5-humanoid-and-bimanual-lerobot-datasets-compared-by-state-and-action-space-40p1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkncpqrn5wjkpr7d747j4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkncpqrn5wjkpr7d747j4.jpg" alt="Infographic comparing five humanoid and bimanual LeRobot datasets by state vector size, action space, camera count, and robot type." width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: Generated using Nano Banana 2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A field guide to 5 humanoid and bimanual manipulation datasets in LeRobot v3.0 — with the exact state/action dimensionality, fields, and research behind each one, and why "humanoid dataset" hides a 7x spread in how these robots actually represent themselves.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two robots doing near-identical tasks—reach into clutter, pick something up, place it somewhere—can record their own state as a 29-dimensional vector or a 216-dimensional one, depending entirely on how many joints, cameras, and collection-process signals the platform records. Neither number is wrong. They're just different robots, and a policy trained on one has no native way to use data from the other.&lt;/p&gt;

&lt;p&gt;That's &lt;a href="https://voxel51.com/cross-embodiment" rel="noopener noreferrer"&gt;&lt;strong&gt;embodiment heterogeneity&lt;/strong&gt;&lt;/a&gt;, one of the five reasons robot data is fundamentally harder to organize than the image and text data that trains typical vision-language models.&lt;/p&gt;

&lt;p&gt;We pulled 5 humanoid and bimanual manipulation datasets — real teleoperation and &lt;a href="https://voxel51.com/blog/libero-dataset-curation-fiftyone" rel="noopener noreferrer"&gt;pure simulation&lt;/a&gt;, a Unitree G1 to a pair of matched humanoid torsos — into &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;LeRobot v3.0&lt;/a&gt; and &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;FiftyOne&lt;/a&gt;, and the spread in how they represent the same broad category of task is the throughline of this post. Below: what embodiment heterogeneity actually costs in practice, a side-by-side of state/action dimensionality across all 5, and a tour of each with links to the live FiftyOne Space, the paper, and the license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;5 datasets spanning a dual-arm mobile manipulator (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;), a full humanoid on legs (&lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, Unitree G1), two matched simulated humanoid torsos (&lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;), and a stationary bimanual dexterous-hand rig (&lt;code&gt;trex_dataset&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;observation.state&lt;/code&gt; dimensionality ranges from 29 (&lt;code&gt;HIW-500-LeRobot&lt;/code&gt;'s joint vector, which sits alongside a separate 23-dim whole-body-control vector) up to 216 (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;, which also carries RL collection signals), roughly a &lt;strong&gt;7x spread&lt;/strong&gt; across datasets that all fall under "humanoid/bimanual manipulation."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tavis-head-gr1t2&lt;/code&gt; and &lt;code&gt;tavis-head-reachy2&lt;/code&gt; run the &lt;strong&gt;same 5-task benchmark&lt;/strong&gt; on two different humanoid torsos — the cleanest pair in this post for seeing embodiment heterogeneity in isolation, since the task and camera setup are held constant and only the robot's own DoF count changes (44-dim vs. 36-dim state).&lt;/li&gt;
&lt;li&gt;3 of 5 are real-world teleoperation (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;, &lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, &lt;code&gt;trex_dataset&lt;/code&gt;); 2 are simulation (&lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;, both built in NVIDIA IsaacLab).&lt;/li&gt;
&lt;li&gt;Licensing: MIT (&lt;code&gt;trex_dataset&lt;/code&gt;), CC-BY-4.0 (&lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, both &lt;code&gt;tavis-head-*&lt;/code&gt;), CC-BY-NC-SA-4.0 (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;, non-commercial only).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is embodiment heterogeneity?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvelfzblarvaum44oxv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvelfzblarvaum44oxv6.png" alt="Diagram showing robotic arms, legged robots, and wheeled robots surrounding a foundation VLA core, illustrating the range of embodiments a single policy must serve." width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: arXiv 2510.24795&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Different robots have different action spaces (a 7-DoF single arm vs. a 14-DoF bimanual rig vs. a humanoid's whole-body state), different proprioceptive observation spaces (varying joint counts and sensor configurations), and different sensor modalities (RGB, depth, multiple viewpoints, tactile).&lt;/p&gt;

&lt;p&gt;The same action vector induces different physical motion on different robots because end-effector coordinates translate differently depending on kinematic structure—and camera observations vary by mount position, resolution, and field of view on top of that. Coordinate frames aren't even guaranteed to agree: "forward" for one robot's base frame can be "left" for another's.&lt;/p&gt;

&lt;p&gt;The field's general answer is coarse alignment.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://robotics-transformer-x.github.io/" rel="noopener noreferrer"&gt;Open X-Embodiment&lt;/a&gt; normalizes everything to a unified 7-DoF end-effector representation (position x/y/z, orientation roll/pitch/yaw, gripper state), then de-normalizes it per embodiment at training time. That's a real compromise: the same action values might mean an absolute position for one robot and a relative velocity for another, and camera observations still don't line up. But that's what makes cross-embodiment learning possible at all, and models trained on that &lt;a href="https://voxel51.com/blog/vla-generalization-data-diversity" rel="noopener noreferrer"&gt;coarsely aligned diversity&lt;/a&gt; have been shown to outperform narrowly targeted single-embodiment counterparts, even when evaluated on a single embodiment.&lt;/p&gt;

&lt;p&gt;None of the 5 datasets below have had that coarse alignment applied.&lt;/p&gt;

&lt;p&gt;They're raw, as each source team designed them. That's deliberate for this post: seeing the &lt;a href="https://voxel51.com/blog/av-playbook-robotics-transfer" rel="noopener noreferrer"&gt;unaligned heterogeneity&lt;/a&gt; first is what makes clear why &lt;a href="https://voxel51.com/open-x-embodiment" rel="noopener noreferrer"&gt;Open X-Embodiment&lt;/a&gt;-style alignment work exists in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The embodiment spread
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dataset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Robot&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;observation.state dim&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;action dim&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Camera streams&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Distinguishing sensing&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HIW-500-LeRobot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unitree G1 (real)&lt;/td&gt;
&lt;td&gt;29 (+ separate 23-dim observation.state.wbc)&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Whole-body-control vector (pivot velocity/pose + dual end-effector pose)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tavis-head-reachy2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reachy2 (sim)&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Active-vision head (pan/tilt neck)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tavis-head-gr1t2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GR1T2 (sim)&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Active-vision head (pan/tilt neck)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trex_dataset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dexmate Vega-1 + Sharpa Wave hands (real)&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;3 RGB + 20 tactile&lt;/td&gt;
&lt;td&gt;Per-fingertip 6-axis tactile wrench&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AgiBotWorld2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AgiBot G2 (real)&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;RL collection signals (reward/done/succeed/intervened)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;State and action dimensionality, camera count, and distinguishing sensing across all 5 humanoid LeRobot datasets, ordered by observation.state size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reading this table left to right isn't just "more DoF, bigger number."&lt;/p&gt;

&lt;p&gt;Each jump reflects a different design decision about what the platform needs to represent: &lt;code&gt;HIW-500-LeRobot&lt;/code&gt;'s vector is almost entirely joint angles because it's a legged, whole-body-controlled humanoid where balance and locomotion state matter as much as arm position. &lt;code&gt;trex_dataset&lt;/code&gt;'s state and action vectors are identical in size and layout (&lt;code&gt;[L_arm 7 | L_hand 22 | R_arm 7 | R_hand 22]&lt;/code&gt;) because every one of its 58 dimensions is a commanded joint position — dexterous hands alone eat 44 of those 58 dimensions. &lt;code&gt;AgiBotWorld2026&lt;/code&gt;'s 216-dim vector is the largest here not because the robot has more joints than the others, but because it's the only dataset in this post collected via reinforcement-learning-style rollout, and RL collection signals get concatenated in alongside camera extrinsics and full-body state.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;/&lt;code&gt;tavis-head-reachy2&lt;/code&gt; pair is the cleanest illustration of pure embodiment heterogeneity in this table, because it's the only case here where the task, camera rig, and collection method are held constant and &lt;em&gt;only the robot changes&lt;/em&gt;. Same 5 tasks (&lt;code&gt;ClutterPickCubeTask&lt;/code&gt;, &lt;code&gt;ClutterPickLiftTask&lt;/code&gt;, &lt;code&gt;ConditionalPickTask&lt;/code&gt;, &lt;code&gt;MultiShelfScanTask&lt;/code&gt;, &lt;code&gt;WaitThenActTask&lt;/code&gt;), same 4-camera setup, same 60fps, same &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;VR teleoperation&lt;/a&gt; pipeline in IsaacLab — and the state vector still differs by 8 dimensions (44 vs. 36) because GR1T2 and Reachy2 simply have different kinematic structures.&lt;/p&gt;

&lt;p&gt;If you want one policy that works on both, you'd need exactly the kind of per-embodiment de-normalization Open X-Embodiment does at the field level, in miniature, for these two robots alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 humanoid LeRobot datasets at a glance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; "Episodes" reflects what's in each live &lt;a href="https://voxel51.com/blog/lerobot-dataset-support-voxel51" rel="noopener noreferrer"&gt;FiftyOne export&lt;/a&gt;, not the full upstream source. &lt;code&gt;tavis-head-gr1t2&lt;/code&gt; and &lt;code&gt;tavis-head-reachy2&lt;/code&gt; are complete (800/800 each); the other 3 are disk-budget-constrained subsets of much larger real-world sources.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dataset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Real or Sim&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AgiBotWorld2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-world dual-arm humanoid manipulation collected via RL-style human-intervention rollout on the AgiBot G2 platform&lt;/td&gt;
&lt;td&gt;CC-BY-NC-SA-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HIW-500-LeRobot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whole-body Unitree G1 humanoid teleoperation performing household chores across real homes in Southeast Asia&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tavis-head-gr1t2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simulated active-vision humanoid benchmark (GR1T2) comparing head-mounted vs. fixed cameras on identical manipulation tasks&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tavis-head-reachy2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The same active-vision benchmark as tavis-head-gr1t2, run on a Reachy2 humanoid instead&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trex_dataset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bimanual dexterous manipulation with real per-fingertip tactile sensing on a Dexmate Vega-1 robot&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The 5 datasets with a short description, license, and whether the data is real or simulated.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 humanoid LeRobot datasets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AgiBotWorld2026 — Real-world dual-arm manipulation with RL collection signals
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FC4kSyap1mKvxP02bCFsuX7Bseg00aUGCv01dz7HxkWJgEI%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FC4kSyap1mKvxP02bCFsuX7Bseg00aUGCv01dz7HxkWJgEI%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App playing an AgiBotWorld2026 episode with head fisheye, wrist, and top head camera panels alongside a reward stream and a 216-dimensional observation state panel." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App playing an AgiBotWorld2026 episode with head fisheye, wrist, and top head camera panels alongside a reward stream and a 216-dimensional observation state panel.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/AgiBotWorld2026-sample-10ep" rel="noopener noreferrer"&gt;Voxel51/AgiBotWorld2026-sample-10ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/AgiBotWorld2026-sample-10ep" rel="noopener noreferrer"&gt;harpreetsahota/AgiBotWorld2026-sample-10ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/agibot-world/AgiBotWorld2026" rel="noopener noreferrer"&gt;agibot-world/AgiBotWorld2026&lt;/a&gt; (102,078 episodes, 13.6TB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; Project homepage: &lt;a href="https://agibot-world.com" rel="noopener noreferrer"&gt;agibot-world.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-NC-SA-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As one of the most expansive public initiatives in real-world humanoid manipulation, AgiBot World uses the dual-arm AgiBot G2 platform to collect long-horizon tasks in real-world offices, homes, and retail environments rather than controlled laboratory environments. The 2026 dataset release structures this data into three primary collection modalities: standard imitation learning, RL-based rollout featuring human intervention (HG-DAgger), and multi-signal interaction logs. This FiftyOne export focuses exclusively on the HG-DAgger reinforcement learning split, capturing a policy mid-training alongside &lt;a href="https://voxel51.com/blog/motion-smoothness-task-success-robolab-egox" rel="noopener noreferrer"&gt;reward metrics&lt;/a&gt; and human-correction markers rather than polished expert trajectories. All 10 exported episodes are derived from a single task archive (&lt;code&gt;ReinforcementLearning/Home/task_12192/HG-DAgger&lt;/code&gt;, "Insert the loose change into the coin slot of the piggy bank dagger").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Breakdown &amp;amp; Features&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RL Collection Signals:&lt;/strong&gt; Unique to the RL framework, each frame includes &lt;code&gt;reward&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;succeed&lt;/code&gt;, &lt;code&gt;intervened&lt;/code&gt; (indicating active human correction), &lt;code&gt;truncated&lt;/code&gt;, and &lt;code&gt;action_prob&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera Streams:&lt;/strong&gt; 7 visual channels including &lt;code&gt;top_head&lt;/code&gt;, &lt;code&gt;hand_left&lt;/code&gt;, &lt;code&gt;hand_right&lt;/code&gt;, two head fisheyes, a rear fisheye, and a &lt;code&gt;head_depth&lt;/code&gt; stream (configured with &lt;code&gt;dtype: video&lt;/code&gt; and &lt;code&gt;gray16be&lt;/code&gt; pixel format, allowing depth information to be ingested like standard RGB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State &amp;amp; Action Vectors:&lt;/strong&gt; A 216-dimensional &lt;code&gt;observation.state&lt;/code&gt; vector and a 44-dimensional &lt;code&gt;action&lt;/code&gt; vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HIW-500-LeRobot — Humanoid teleoperation in real homes, sampled across all 11 tasks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F02tlcy022ajMaHWEvBeRyCiMV9cqVRLl3bUodJqhX40001Y%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F02tlcy022ajMaHWEvBeRyCiMV9cqVRLl3bUodJqhX40001Y%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App playing a HIW-500-LeRobot kitchen organization episode from a Unitree G1, with head camera video, pivot velocity plots, and the 29-dimensional observation state next to the 23-dimensional action vector." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App playing a HIW-500-LeRobot kitchen organization episode from a Unitree G1, with head camera video, pivot velocity plots, and the 29-dimensional observation state next to the 23-dimensional action vector.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/HIW-500-LeRobot-48ep" rel="noopener noreferrer"&gt;Voxel51/HIW-500-LeRobot-48ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/HIW-500-LeRobot-48ep" rel="noopener noreferrer"&gt;harpreetsahota/HIW-500-LeRobot-48ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/BitRobot/HIW-500-LeRobot" rel="noopener noreferrer"&gt;BitRobot/HIW-500-LeRobot&lt;/a&gt; (23,743 episodes, ~2TB); raw ROS bag / MCAP recordings of this same data are separately published at &lt;code&gt;BitRobot/HIW-500&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://bitrobot-foundation.github.io/humanoids-in-the-wild-500-hours/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Humanoids In-the-Wild dataset delivers precisely what its title promises: hundreds of hours capturing a Unitree G1 humanoid carrying out everyday domestic chores across authentic Southeast Asian residences. It avoids controlled motion-capture setups and uniform apartment stages; instead, each episode introduces shifting floor plans, variable lighting conditions, physical clutter, and distinct operator teleoperation techniques. To ensure broad coverage rather than pulling an isolated single-task shard, we gathered 48 episodes across 6 distinct shard groups, capturing &lt;strong&gt;all 11 task categories&lt;/strong&gt;—ranging from "setting the table" (15 episodes) to "hang hanger" (1 episode).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Available fields:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Video Streams:&lt;/strong&gt; 3 streams in AV1 format (480×1280 &lt;code&gt;head&lt;/code&gt; camera, 480×640 &lt;code&gt;left_wrist&lt;/code&gt;, and 480×640 &lt;code&gt;right_wrist&lt;/code&gt; views).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observation State:&lt;/strong&gt; A 29-DoF &lt;code&gt;observation.state&lt;/code&gt; covering hips, knees, ankles, waist, shoulders, elbows, and wrists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whole-Body Control:&lt;/strong&gt; A 23-dimensional &lt;code&gt;observation.state.wbc&lt;/code&gt; vector covering pivot pose/velocity, dual end-effector poses, and trigger/squeeze inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Vector:&lt;/strong&gt; A corresponding 23-dimensional &lt;code&gt;action&lt;/code&gt; vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Unique Structure:&lt;/strong&gt; The only dataset among the five highlighted where locomotion/stability controls (hip, knee, and ankle joint data) share a vector with manipulation attributes due to its full legged humanoid design.&lt;/p&gt;

&lt;h3&gt;
  
  
  tavis-head-gr1t2 and tavis-head-reachy2 — Same benchmark, two robots, different state size
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FNnUgNbqAWoTT00y01Ma3hr3G3mVjvSSsY1pAwlGIyW9u4%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FNnUgNbqAWoTT00y01Ma3hr3G3mVjvSSsY1pAwlGIyW9u4%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App playing a tavis-head-reachy2 ClutterPickCubeTask episode in simulation with head, fixed, and both wrist camera views beside the 36-dimensional state and 19-dimensional action panels." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App playing a tavis-head-reachy2 ClutterPickCubeTask episode in simulation with head, fixed, and both wrist camera views beside the 36-dimensional state and 19-dimensional action panels.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/tavis-head-gr1t2-800ep" rel="noopener noreferrer"&gt;Voxel51/tavis-head-gr1t2-800ep&lt;/a&gt; · &lt;a href="https://huggingface.co/datasets/Voxel51/tavis-head-reachy2-800ep" rel="noopener noreferrer"&gt;Voxel51/tavis-head-reachy2-800ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/tavis-head-gr1t2-800ep" rel="noopener noreferrer"&gt;harpreetsahota/tavis-head-gr1t2-800ep&lt;/a&gt; · &lt;a href="https://huggingface.co/spaces/harpreetsahota/tavis-head-reachy2-800ep" rel="noopener noreferrer"&gt;harpreetsahota/tavis-head-reachy2-800ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/tavis-benchmark" rel="noopener noreferrer"&gt;tavis-benchmark&lt;/a&gt; org (both are complete, 800/800 episodes each)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2605.07943" rel="noopener noreferrer"&gt;Paper (arXiv:2605.07943)&lt;/a&gt; · &lt;a href="https://github.com/spiglerg/tavis" rel="noopener noreferrer"&gt;Code&lt;/a&gt; · Paired π₀ checkpoints for head-camera vs. fixed-camera comparison, published under the same &lt;code&gt;tavis-benchmark&lt;/code&gt; org&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The TAVIS benchmark isolates a critical question often overlooked by robot-learning datasets: does active head and camera movement directly improve manipulation performance, and can this advantage be quantified? To evaluate this, the benchmark records identical tasks using matching four-camera setups across two distinct humanoid torsos: Fourier's GR1T2 and Pollen's Reachy2. Every episode captures parallel streams from both a head-mounted active vision camera and a fixed baseline camera, enabling direct policy comparisons with head-camera integration enabled or disabled using the same demonstration data.&lt;/p&gt;

&lt;p&gt;Both platforms evaluate five shared task classes (split into 250/250/100/100/100 episodes):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClutterPickCubeTask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClutterPickLiftTask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConditionalPickTask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MultiShelfScanTask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WaitThenActTask&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All demonstrations were gathered in &lt;a href="https://voxel51.com/blog/synthetic-data-curation-physical-ai" rel="noopener noreferrer"&gt;NVIDIA IsaacLab&lt;/a&gt; using VR teleoperation recorded at 60fps across identical four-camera configurations, making this dataset the premier benchmark for studying embodiment heterogeneity under controlled conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset Specifications &amp;amp; Structure&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Observations:&lt;/strong&gt; Four 480×640 AV1 camera streams captured at 60fps (&lt;code&gt;OBS_HEAD&lt;/code&gt;, &lt;code&gt;OBS_FIXED&lt;/code&gt;, &lt;code&gt;OBS_WRIST_LEFT&lt;/code&gt;, and &lt;code&gt;OBS_WRIST_RIGHT&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Space:&lt;/strong&gt; A unified 19-dimensional action vector governing bimanual arm motion, neck movement, and gripper control across both robots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Representation:&lt;/strong&gt; Proprioceptive state (&lt;code&gt;observation.state&lt;/code&gt;) varies by kinematic structure—&lt;strong&gt;44 dimensions for GR1T2 versus 36 dimensions for Reachy2&lt;/strong&gt;—meaning these vectors cannot be used interchangeably despite the shared action space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pose &amp;amp; Language Annotations:&lt;/strong&gt; Includes dedicated left/right end-effector position and orientation attributes, alongside per-frame &lt;code&gt;language_instruction&lt;/code&gt; prompts (featuring 33 distinct instruction strings for Reachy2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To support head-camera versus fixed-camera evaluation, the official &lt;code&gt;tavis-benchmark&lt;/code&gt; organization provides paired π₀ baseline checkpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  trex_dataset — Bimanual dexterous manipulation with fingertip tactile sensing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fq3qUVaAvkVuEGRGhBy75NiBGzJx1uxEf5CZp6wwoLYk%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fq3qUVaAvkVuEGRGhBy75NiBGzJx1uxEf5CZp6wwoLYk%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App playing a trex_dataset episode from a Dexmate Vega-1 rig, showing head and wrist cameras, a tactile sensor stream, and per-fingertip tactile force plots next to the 58-dimensional state and action panels." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App playing a trex_dataset episode from a Dexmate Vega-1 rig, showing head and wrist cameras, a tactile sensor stream, and per-fingertip tactile force plots next to the 58-dimensional state and action panels.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/trex-dataset-23ep" rel="noopener noreferrer"&gt;Voxel51/trex-dataset-23ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/trex-dataset-23ep" rel="noopener noreferrer"&gt;harpreetsahota/trex-dataset-23ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/zekaiwang/trex_dataset" rel="noopener noreferrer"&gt;zekaiwang/trex_dataset&lt;/a&gt; (5,464 episodes, 1.5TB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2606.17055" rel="noopener noreferrer"&gt;Paper (arXiv:2606.17055)&lt;/a&gt; · &lt;a href="https://tactile-rex.github.io/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The T-Rex dataset highlights an exceptionally dense sensing modality rarely captured in robotic manipulation: authentic per-fingertip tactile feedback. Built using a stationary bimanual Dexmate Vega-1 setup equipped with Sharpa Wave dexterous hands, each hand features five dedicated tactile sensors. Every one of its 58 state values corresponds strictly to arm or hand joint positions, as the robot's base, torso, and head remained stationary during collection—distinguishing it from the mobile or legged systems featured elsewhere. Teleoperated with Manus gloves and VIVE trackers across 207 distinct objects and 22 motor primitives, this 23-episode slice represents the initial contiguous release of the larger dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Data Fields:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Streams:&lt;/strong&gt; 3 RGB feeds (&lt;code&gt;head_left&lt;/code&gt;, &lt;code&gt;left_wrist&lt;/code&gt;, &lt;code&gt;right_wrist&lt;/code&gt; at 640×360).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tactile Video Feeds:&lt;/strong&gt; 20 distinct streams—providing a raw sensor image alongside an estimated deformation map for every fingertip (2 hands × 5 fingers × 2 stream types).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kinematic Vectors:&lt;/strong&gt; 58-dimensional observation.state and matching action vectors structured as &lt;code&gt;[L_arm 7 | L_hand 22 | R_arm 7 | R_hand 22]&lt;/code&gt; (both share identical dimensions because both record joint positions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tactile Force Measurements:&lt;/strong&gt; A 60-dimensional &lt;code&gt;observation.tactile_force&lt;/code&gt; vector capturing a 6-axis wrench per fingertip—a rich modality unique to this dataset among all five showcased.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Decoding Technicalities:&lt;/strong&gt; To preserve exact physical measurements, the 20 tactile video channels are compressed losslessly via &lt;code&gt;libx264 -qp 0&lt;/code&gt;. This enforces the H.264 &lt;strong&gt;High 4:4:4 Predictive&lt;/strong&gt; profile, which standard web browsers cannot natively render in an HTML &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag, so they lack video thumbnails in standard web interfaces. Standard decoding workflows in FiftyOne, ffmpeg, PyAV, or torchcodec handle these streams smoothly, while the 3 primary RGB channels use conventional yuv420p encoding and preview seamlessly everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More LeRobot datasets.&lt;/strong&gt; These five come from the &lt;a href="https://voxel51.com/blog/lerobot-v3-datasets-vla-training-fiftyone" rel="noopener noreferrer"&gt;11 LeRobot v3.0 datasets for VLA training&lt;/a&gt; roundup. Five more are compared in &lt;a href="https://voxel51.com/blog/lerobot-datasets-vla-training-real-synthetic" rel="noopener noreferrer"&gt;real vs. synthetic LeRobot datasets&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is embodiment heterogeneity and why does it matter for VLA training?
&lt;/h3&gt;

&lt;p&gt;It's the fact that different robots have different action spaces, &lt;a href="https://voxel51.com/proprioception" rel="noopener noreferrer"&gt;proprioceptive observation spaces&lt;/a&gt;, and sensor modalities — a 7-DoF arm, a 14-DoF bimanual rig, and a legged humanoid's whole-body state simply don't share a representation. It matters because you can't concatenate datasets across embodiments without some form of alignment; a policy trained on one robot's 216-dim state vector has no native way to consume another robot's 36-dim vector, even if both robots are "doing the same task."&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I combine these 5 datasets into one training set?
&lt;/h3&gt;

&lt;p&gt;Not directly — none of them share a common action or state representation. &lt;code&gt;tavis-head-gr1t2&lt;/code&gt; and &lt;code&gt;tavis-head-reachy2&lt;/code&gt; are the closest thing to compatible here (identical 19-dim action space, same task definitions), but even they differ in &lt;code&gt;observation.state&lt;/code&gt; size (44 vs. 36). Combining any of these 5 for cross-embodiment training would require Open X-Embodiment-style coarse alignment — normalizing to a shared representation (e.g. a unified end-effector pose vector) and accepting that the same action values mean physically different things per robot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do tavis-head-gr1t2 and tavis-head-reachy2 have different state dimensions for the same tasks?
&lt;/h3&gt;

&lt;p&gt;Because GR1T2 and Reachy2 are physically different robots with different &lt;a href="https://voxel51.com/degrees-of-freedom-dof" rel="noopener noreferrer"&gt;degrees of freedom&lt;/a&gt;, even though the TAVIS benchmark holds the task, camera rig, fps, and collection method constant across both. The 44-vs-36 dimension gap in &lt;code&gt;observation.state&lt;/code&gt; is purely a function of each robot's kinematic structure—nothing about the task changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which of these could I combine most easily?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tavis-head-gr1t2&lt;/code&gt; and &lt;code&gt;tavis-head-reachy2&lt;/code&gt; share an identical 19-dim &lt;code&gt;action&lt;/code&gt; space and the same 5 task definitions, so a policy that only consumes actions (not full proprioceptive state) could plausibly train across both with minimal remapping. Every other pair in this post differs in both state and action dimensionality, camera count, and often collection method.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which of these datasets are real vs. simulated?
&lt;/h3&gt;

&lt;p&gt;Real: &lt;code&gt;AgiBotWorld2026&lt;/code&gt;, &lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, &lt;code&gt;trex_dataset&lt;/code&gt;. Simulated (NVIDIA IsaacLab): &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which datasets are safe for commercial use?
&lt;/h3&gt;

&lt;p&gt;MIT (&lt;code&gt;trex_dataset&lt;/code&gt;) is unrestricted. CC-BY-4.0 (&lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;) requires attribution but allows commercial use. CC-BY-NC-SA-4.0 (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;) is non-commercial only.&lt;/p&gt;

</description>
      <category>lerobot</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How to Import LeRobot Datasets into FiftyOne Using Agent Skills</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:41:35 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/how-to-import-lerobot-datasets-into-fiftyone-using-agent-skills-egp</link>
      <guid>https://dev.to/harpreetsahota/how-to-import-lerobot-datasets-into-fiftyone-using-agent-skills-egp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FIHIqY927o8I4OTQAbtBWblk5qu02ANUuVWWhhhVbBA4k%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FIHIqY927o8I4OTQAbtBWblk5qu02ANUuVWWhhhVbBA4k%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne App showing a LeRobot dataset of robot arm episodes, with the sidebar filtering 800 samples down to 100 by task." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne App showing a LeRobot dataset of robot arm episodes, with the sidebar filtering 800 samples down to 100 by task.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A standardized dataset format solved robot learning’s data fragmentation problem. Here’s how to get one of those datasets into FiftyOne without writing a parser, and what happens when the version doesn’t match what you expect.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Robot learning needs &lt;a href="https://voxel51.com/blog/libero-dataset-curation-fiftyone" rel="noopener noreferrer"&gt;demonstration data&lt;/a&gt; the way language models need text, but until recently every lab and vendor recorded it differently: proprietary formats per robot, inconsistent schemas, no shared way to index or search across datasets. Hugging Face built &lt;code&gt;LeRobotDataset&lt;/code&gt; to fix that: one standardized format (Parquet for state and action, MP4 for camera feeds) across radically different &lt;a href="https://voxel51.com/embodiment" rel="noopener noreferrer"&gt;embodiments&lt;/a&gt;, from SO-100 arms to bimanual ALOHA rigs to industrial Kuka arms.&lt;/p&gt;

&lt;p&gt;The standardization paid off measurably. SmolVLA, a 450M-parameter &lt;a href="https://voxel51.com/policy" rel="noopener noreferrer"&gt;policy&lt;/a&gt;, was pretrained only on community-uploaded &lt;code&gt;lerobot&lt;/code&gt;-tagged datasets on the Hub, and that alone lifted SO-100 task success from 51.7% to 78.3% (&lt;a href="https://huggingface.co/blog/lerobot-datasets-v3" rel="noopener noreferrer"&gt;source&lt;/a&gt;). The format itself had to evolve under that success: the original v2.x layout stored one Parquet file and one MP4 per &lt;a href="https://voxel51.com/episode" rel="noopener noreferrer"&gt;episode&lt;/a&gt;, which started hitting filesystem limits once &lt;a href="https://voxel51.com/blog/open-x-embodiment-fiftyone-data-curation" rel="noopener noreferrer"&gt;community datasets&lt;/a&gt; reached millions of episodes. &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;&lt;code&gt;lerobot&lt;/code&gt;&lt;/a&gt; v0.4.0 shipped v3.0 in response, packing many episodes into shared chunked files and supporting streaming without a full download first.&lt;/p&gt;

&lt;p&gt;FiftyOne ships &lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt; as a first-class import path, built around one idea: a sample is an episode, not a file. The &lt;code&gt;fiftyone-lerobot-import&lt;/code&gt; &lt;a href="https://voxel51.com/blog/fiftyone-agent-skills-mcp-tools" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; wraps that native support with the parts a script usually skips: detecting whether a Hub dataset is still on v2.x, running the documented conversion before import, and refusing to create a dataset or install a package without asking first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The fiftyone-lerobot-import agent skill imports LeRobotDataset v3.0 recordings into FiftyOne, where one sample is one episode rather than one file.&lt;/li&gt;
&lt;li&gt;The skill auto-detects LeRobot's older v2.x layout (&lt;code&gt;meta/episodes.jsonl&lt;/code&gt;, &lt;code&gt;meta/tasks.jsonl&lt;/code&gt;) and runs the documented conversion to v3.0 before importing, instead of failing on an unsupported-version error.&lt;/li&gt;
&lt;li&gt;Two explicit stop gates hold the run, one before installing any package (pyarrow, huggingface_hub, lerobot) and one before creating the FiftyOne dataset.&lt;/li&gt;
&lt;li&gt;Tested end to end against &lt;code&gt;lerobot/aloha_sim_insertion_human&lt;/code&gt;, a real v2.1-tagged Hub dataset, which converted to v3.0 and imported as 50 episodes of a bimanual ALOHA arm with zero episodes skipped.&lt;/li&gt;
&lt;li&gt;The skill is a thin layer over the open-source FiftyOne SDK (&lt;code&gt;fo.Dataset.from_dir&lt;/code&gt;, &lt;code&gt;dataset.add_dir&lt;/code&gt;), so the same import runs identically from a chat prompt or a checked-in script.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part one: what is an agent skill, actually?
&lt;/h2&gt;

&lt;p&gt;Strip away the buzzwords and a skill is a folder. Inside it is a markdown file that tells an AI coding agent, step by step, how an expert would do a specific job: which files to check first, which command to run, which traps to avoid, and how to tell success from failure. The agent reads that folder like a new hire reading a training guide, then does the work itself in your terminal, on your data.&lt;/p&gt;

&lt;p&gt;Voxel51 packages this kind of runbook for FiftyOne into an &lt;a href="https://voxel51.com/blog/antigravity-cli-fiftyone-skills" rel="noopener noreferrer"&gt;open-source repository&lt;/a&gt;, installed with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -sL skil.sh | sh -s -- voxel51/fiftyone-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer clones the repo, discovers every skill inside it, and lets you pick which ones your agent should have access to going forward.&lt;/p&gt;

&lt;p&gt;This post is about &lt;code&gt;fiftyone-lerobot-import&lt;/code&gt;. Point it at a local LeRobot directory or a Hugging Face Hub repo id, and it detects the version, converts if needed, and builds the FiftyOne dataset. That’s a different problem from the one &lt;a href="https://voxel51.com/blog/mcap-import-fiftyone-agent-skills" rel="noopener noreferrer"&gt;&lt;code&gt;fiftyone-dataset-import&lt;/code&gt;&lt;/a&gt;’s &lt;a href="https://voxel51.com/mcap" rel="noopener noreferrer"&gt;MCAP&lt;/a&gt; path solves. MCAP’s question is a schema scan: what kind of sensor data is this? LeRobot’s question is about versioning: what version of this format is this, and does it need conversion first?&lt;/p&gt;

&lt;h2&gt;
  
  
  Part Two: A Real LeRobot Dataset, End to End
&lt;/h2&gt;

&lt;p&gt;A skill only proves itself on data that actually exercises its hardest path. A dataset already sitting on v3.0 would import cleanly and tell you nothing about the conversion logic, so we looked for a dataset still tagged v2.1 on the Hub.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dataset: lerobot/aloha_sim_insertion_human
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lerobot/aloha_sim_insertion_human&lt;/code&gt; is one of LeRobot’s official example repos: a bimanual ALOHA arm running a simulated peg-insertion task, one task label (“Insert the peg into the socket.”), 50 episodes, 50 fps, 25,000 total frames.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repo&lt;/td&gt;
&lt;td&gt;lerobot/aloha_sim_insertion_human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revision pulled&lt;/td&gt;
&lt;td&gt;v2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episodes&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FPS&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;robot_type&lt;/td&gt;
&lt;td&gt;aloha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;observation.images.top (video, av1 codec, 480×640)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;observation.state / action dims&lt;/td&gt;
&lt;td&gt;14 / 14 (bimanual: waist, shoulder, elbow, forearm roll, wrist angle, wrist rotate, gripper, same layout on both arms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;td&gt;“Insert the peg into the socket.”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;What lerobot/aloha_sim_insertion_human contains, pulled at the v2.1 tag.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We pulled the Hub’s &lt;code&gt;v2.1&lt;/code&gt; tag specifically, not &lt;code&gt;main&lt;/code&gt; (already v3.0), so the conversion path would get exercised for real instead of simulated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The prompt and the version-detection moment
&lt;/h3&gt;

&lt;p&gt;The prompt to the agent was one sentence: import &lt;code&gt;lerobot/aloha_sim_insertion_human&lt;/code&gt; into FiftyOne.&lt;/p&gt;

&lt;p&gt;The skill’s inspection step reads &lt;code&gt;meta/info.json&lt;/code&gt; and finds &lt;code&gt;codebase_version&lt;/code&gt;: v2.1, along with layout markers like &lt;code&gt;meta/episodes.jsonl&lt;/code&gt; and &lt;code&gt;meta/tasks.jsonl&lt;/code&gt;. Instead of failing on unsupported versions, the skill detects the older format, hits the packages stop gate, and runs the conversion script (python-m lerobot. scripts. convert_dataset_v21_to_v30) to update the dataset to v3.0, ensuring reliable import even with legacy data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hits the packages stop gate and asks before installing &lt;code&gt;lerobot&lt;/code&gt; (needed only for the conversion).&lt;/li&gt;
&lt;li&gt;Runs the conversion: &lt;code&gt;python -m lerobot.scripts.convert_dataset_v21_to_v30 --repo-id lerobot/aloha_sim_insertion_human --root &amp;lt;path&amp;gt; --push-to-hub=false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it honest.&lt;/strong&gt; The installed lerobot release (v0.6.2) didn’t expose the converter at the documented &lt;code&gt;lerobot.datasets.v30.*&lt;/code&gt; module path; it was still at the older &lt;code&gt;lerobot.scripts.*&lt;/code&gt; path. The skill’s own troubleshooting notes already anticipate this exact fallback, and it’s what happened on the first real run, not a hypothetical edge case.&lt;/li&gt;
&lt;li&gt;Re-checks &lt;code&gt;meta/info.json&lt;/code&gt;, confirms &lt;code&gt;codebase_version: v3.0&lt;/code&gt;, and the new layout: &lt;code&gt;meta/episodes/chunk-000&lt;/code&gt;, &lt;code&gt;meta/tasks.parquet&lt;/code&gt;, &lt;code&gt;meta/stats.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Hits the creation stop gate and presents the full import plan before touching anything:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LeRobot Import Plan for lerobot/aloha_sim_insertion_human:

Source:
  - codebase_version: v3.0 (converted from v2.1: yes)
  - 50 episodes, 50 fps, robot_type: aloha
  - camera: observation.images.top (video, av1)
  - observation.state: 14 dims, action: 14 dims
  - tasks: 1 ("Insert the peg into the socket.")

Import:
  - Dataset name: lerobot-aloha-sim-insertion-human
  - Episodes: all
  - Persistent: yes

Proceed with import? (yes/no)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The payoff: 50 episodes, zero skipped
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;dataset.media_type&lt;/code&gt; comes back &lt;code&gt;"multimodal"&lt;/code&gt;. All 50 episodes import; &lt;code&gt;dataset.info["lerobot"]["skipped_episodes"]&lt;/code&gt; is an empty list: a clean import, worth stating plainly rather than manufacturing a gap that isn’t there. Every episode runs exactly 10 seconds (&lt;code&gt;dataset.bounds("duration")&lt;/code&gt; returns &lt;code&gt;(10.0, 10.0)&lt;/code&gt;), which checks out at 50 fps × 500 frames/episode.&lt;/p&gt;

&lt;p&gt;Opening episode 0 in the App shows the &lt;code&gt;observation.images.top&lt;/code&gt; tile: two robot arms working over a blue and red object, with the episode header reading “Insert the peg into the socket. · 10s.” That’s what “one sample equals one episode” looks like in the viewer, not an abstraction: a single click surfaces the whole 10-second demonstration, not one video frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part Three: The Same LeRobot Import, in Code
&lt;/h2&gt;

&lt;p&gt;A chat prompt is a good way to explore a dataset you haven’t seen before, but a pipeline that runs on every new upload needs code, not a conversation. Everything the skill did above sits on top of the open-source FiftyOne SDK. Here’s the full pipeline, runnable from a fresh environment against the same Hub dataset used in Part Two.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install "pyarrow&amp;gt;=10.0.0" huggingface_hub lerobot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lerobot&lt;/code&gt; is only needed for the v2.1 → v3.0 conversion below. A dataset already on &lt;code&gt;codebase_version: v3.0&lt;/code&gt; only needs &lt;code&gt;pyarrow&lt;/code&gt; and &lt;code&gt;huggingface_hub&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Download from the Hub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fo.Dataset.from_dir reads from a local directory, not directly from the Hub, so pull the files down first:
from huggingface_hub import snapshot_download

local_dir = snapshot_download(
    repo_id="lerobot/aloha_sim_insertion_human",
    repo_type="dataset",
    revision="v2.1",
    local_dir="/abs/path/lerobot/aloha_sim_insertion_human",
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Convert v2.1 to v3.0
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m lerobot.scripts.convert_dataset_v21_to_v30 \
    --repo-id lerobot/aloha_sim_insertion_human \
    --root /abs/path/lerobot/aloha_sim_insertion_human \
    --push-to-hub=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip this step for any dataset already tagged &lt;code&gt;v3.0&lt;/code&gt; on the Hub; for example, pull &lt;code&gt;revision="v3.0"&lt;/code&gt; in Step 2 instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Import into FiftyOne
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fiftyone as fo

dataset = fo.Dataset.from_dir(
    dataset_dir=local_dir,
    dataset_type=fo.types.LeRobotDataset,
    name="lerobot-aloha-sim-insertion-human",
    persistent=True,
)

assert dataset.media_type == "multimodal"
fo.launch_app(dataset)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a second recording session to the same dataset is the same four steps repeated against the new source directory, then &lt;code&gt;dataset.add_dir(dataset_dir=..., dataset_type=fo.types.LeRobotDataset)&lt;/code&gt; instead of &lt;code&gt;from_dir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No Parquet reader, no video decode loop, no custom viewer. The importer reads &lt;code&gt;meta/info.json&lt;/code&gt; and &lt;code&gt;meta/episodes/*.parquet&lt;/code&gt;, nothing in the video files, so it stays fast even on large sources. The skill’s stop gates are a conversation; the SDK calls underneath are the same ones either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Leaves You
&lt;/h2&gt;

&lt;p&gt;Every team re-solves “is my data in the format my tools expect” by hand, one script at a time, and LeRobot is just the latest format to need that check. &lt;code&gt;fiftyone-lerobot-import&lt;/code&gt; turns that check into something an agent already knows how to run correctly against your files, every time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dataset: &lt;a href="https://huggingface.co/datasets/lerobot/aloha_sim_insertion_human" rel="noopener noreferrer"&gt;&lt;code&gt;lerobot/aloha_sim_insertion_human&lt;/code&gt;&lt;/a&gt;, part of LeRobot’s official example datasets, Hugging Face&lt;/li&gt;
&lt;li&gt;Skill repo: &lt;a href="https://github.com/voxel51/fiftyone-skills" rel="noopener noreferrer"&gt;&lt;code&gt;voxel51/fiftyone-skills&lt;/code&gt;&lt;/a&gt;, installed the same one-line way as Part One&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you import a LeRobot dataset into FiftyOne?
&lt;/h3&gt;

&lt;p&gt;Two ways: ask an agent with the &lt;code&gt;fiftyone-lerobot-import&lt;/code&gt; skill, or call &lt;code&gt;fo.Dataset.from_dir(..., dataset_type=fo.types.LeRobotDataset)&lt;/code&gt; directly. Both require the dataset to be on &lt;code&gt;codebase_version: v3.0&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the difference between LeRobot v2.x and v3.0?
&lt;/h3&gt;

&lt;p&gt;v2.x stores one Parquet file and one MP4 per episode. v3.0 packs many episodes into shared chunked files and supports streaming. &lt;code&gt;fo.types.LeRobotDataset&lt;/code&gt; only supports v3.0. v2.x sources need conversion first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need the lerobot package to import into FiftyOne?
&lt;/h3&gt;

&lt;p&gt;Only for converting a v2.x source to v3.0. A straight v3.0 import needs only &lt;code&gt;pyarrow&amp;gt;=10.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does FiftyOne represent a LeRobot dataset?
&lt;/h3&gt;

&lt;p&gt;One sample per episode, not per file, with &lt;code&gt;media_type == "multimodal"&lt;/code&gt; and fields like &lt;code&gt;episode_index&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;duration&lt;/code&gt;, and &lt;code&gt;fps&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if some episodes in the source are malformed?
&lt;/h3&gt;

&lt;p&gt;They’re reported under &lt;code&gt;dataset.info["lerobot"]["skipped_episodes"]&lt;/code&gt; instead of being silently dropped or silently accepted.&lt;/p&gt;

</description>
      <category>lerobot</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>From Franka Arms to Humanoids: 11 LeRobot v3.0 Datasets for VLA Training in 2026</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:40:28 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/from-franka-arms-to-humanoids-11-lerobot-v30-datasets-for-vla-training-in-2026-126j</link>
      <guid>https://dev.to/harpreetsahota/from-franka-arms-to-humanoids-11-lerobot-v30-datasets-for-vla-training-in-2026-126j</guid>
      <description>&lt;p&gt;&lt;em&gt;A field guide to 11 robot-learning datasets, from Franka arms to full humanoids and real teleoperation to simulation, reformatted into LeRobotDataset v3.0 and loaded into FiftyOne with their exact fields and per-frame streams.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A vision-language-action (VLA) model doesn't learn from a folder of images.&lt;/p&gt;

&lt;p&gt;It learns from episodes: a camera stream, a robot's joint state, and the action it took, all stamped to the same clock and lined up frame by frame. Get that alignment wrong, and the model can't tell which arm motion caused which visual change. The training signal disappears.&lt;/p&gt;

&lt;p&gt;This makes &lt;a href="https://voxel51.com/blog/vla-models-data-centric-ai-robotics" rel="noopener noreferrer"&gt;data engineering&lt;/a&gt; a prerequisite for modeling.&lt;/p&gt;

&lt;p&gt;We pulled together 11 datasets in &lt;a href="https://voxel51.com/lerobot" rel="noopener noreferrer"&gt;LeRobot v3.0&lt;/a&gt;, from a Franka arm to a Unitree G1 humanoid and a fully synthetic manipulation suite. We reformatted or subsetted each dataset and loaded it into FiftyOne, where you can inspect video streams, state/action vectors, and task labels before downloading.&lt;/p&gt;

&lt;p&gt;This guide explains the data problem, compares LeRobot with the raw-sensor-log alternative MCAP, breaks down the FiftyOne schema, and links each dataset to its live FiftyOne Space, paper, and license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;11 datasets, all loadable today with &lt;code&gt;fiftyone.utils.huggingface.load_from_hub&lt;/code&gt;, spanning single Franka arms to full humanoids (&lt;code&gt;HIW-500-LeRobot&lt;/code&gt; on Unitree G1, &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;) to a bimanual dexterous-hand platform (&lt;code&gt;trex_dataset&lt;/code&gt; on Dexmate Vega-1).&lt;/li&gt;
&lt;li&gt;4 of the 11 FiftyOne exports carry the &lt;strong&gt;complete&lt;/strong&gt; source episode set (&lt;code&gt;robocasa-MG_100&lt;/code&gt;: 2,400/2,400; &lt;code&gt;robotwin_unified&lt;/code&gt;: 27,500/27,500; &lt;code&gt;tavis-head-gr1t2&lt;/code&gt; and &lt;code&gt;tavis-head-reachy2&lt;/code&gt;: 800/800 each). The other 7 are disk-budget-constrained subsets of much larger sources — as small as 10 of AgiBotWorld2026's 102,078 episodes and 23 of trex_dataset's 5,464.&lt;/li&gt;
&lt;li&gt;6 contain real-world teleoperation or human-collected data; 5 are simulated (&lt;code&gt;InternData-A1&lt;/code&gt;, &lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;, &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;). &lt;code&gt;InternData-A1&lt;/code&gt;'s preview clips look photorealistic despite being 100% simulator output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;droid_3d&lt;/code&gt;'s live FiftyOne export is RGB-only — the full source dataset's depth video and point-cloud streams use dtype values (&lt;code&gt;depth_video&lt;/code&gt;, &lt;code&gt;pointcloud&lt;/code&gt;) that &lt;a href="https://docs.voxel51.com/api/fiftyone.utils.lerobot.html" rel="noopener noreferrer"&gt;FiftyOne's LeRobot importer&lt;/a&gt; doesn't recognize, so they're absent from the FiftyOne dataset even though they exist upstream.&lt;/li&gt;
&lt;li&gt;Licenses vary, sometimes within one export: MIT (&lt;code&gt;droid_3d&lt;/code&gt;, &lt;code&gt;trex_dataset&lt;/code&gt;), Apache-2.0 (&lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;), CC-BY-4.0 (&lt;code&gt;fmb_multi&lt;/code&gt;, &lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, &lt;code&gt;tavis-head-*&lt;/code&gt;), and CC-BY-NC-SA-4.0 (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;, &lt;code&gt;InternData-A1&lt;/code&gt;). The 28-episode &lt;code&gt;rh20t_cfg1&lt;/code&gt; export mixes CC-BY-SA-4.0 and CC-BY-NC-4.0 by episode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why data organization matters more than architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmh22bp42y12bmggfaca3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmh22bp42y12bmggfaca3.png" alt="Flowchart of a vision-language-action model pipeline, running from multimodal vision, language, and state input through tokenization and multimodal fusion to action prediction and an execution loop, with a robotic hand picking apples at the center." width="800" height="922"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: arXiv 2505.04769&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://voxel51.com/vision-language-action-model-vla" rel="noopener noreferrer"&gt;vision-language-action (VLA) model&lt;/a&gt; fuses three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a vision encoder that turns camera frames into tokens&lt;/li&gt;
&lt;li&gt;a language encoder that turns instructions into tokens&lt;/li&gt;
&lt;li&gt;and an action decoder that turns the fused representation into joint commands or end-effector deltas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most VLAs bolt this onto a pretrained vision-language backbone, because CLIP- or SigLIP-scale visual and linguistic priors are cheap to get from the internet.&lt;/p&gt;

&lt;p&gt;Action grounding is expensive. A video of a person picking up a mug has no joint angles, end-effector pose, or gripper command attached. A robot policy needs that information to reproduce the motion. Vision-language models (VLMs) can draw on web-scale data; VLAs don't have equivalent action data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19ilpdiayuik0i9fct13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19ilpdiayuik0i9fct13.png" alt="Infographic titled The Action Gap, showing why action grounding is expensive and laying out five ways robot training data differs from vision-language data: temporal structure, proprioception, action grounding, embodiment heterogeneity, and data scarcity." width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: Generated using NanoBanana 2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That gap creates five differences from typical vision-language training data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporal structure.&lt;/strong&gt; Episodes are ordered sequences, not shuffleable independent samples — a policy has to model how actions at time T affect the state at T+1.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://voxel51.com/proprioception" rel="noopener noreferrer"&gt;&lt;strong&gt;Proprioception&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt; Joint positions, velocities, and end-effector poses are low-dimensional (10-50 values) but essential — vision shows where the object is, not where the robot's own arm is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action grounding.&lt;/strong&gt; Understanding "pick up the cup" doesn't provide the motor command sequence needed to grasp that cup in that configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embodiment heterogeneity.&lt;/strong&gt; A 7-DoF single arm, a 14-DoF bimanual rig, and a humanoid's 29-DoF whole-body state don't share an action space, so the datasets require remapping before concatenation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data scarcity.&lt;/strong&gt; Teleoperation is slow and expensive to collect, so robot datasets run to thousands or millions of trajectories — not the billions of tokens or images that make LLM/VLM pretraining work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a common structure, these differences block dataset combination.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://voxel51.com/rlds" rel="noopener noreferrer"&gt;RLDS&lt;/a&gt; provides a lossless episode/step hierarchy, &lt;a href="https://voxel51.com/open-x-embodiment" rel="noopener noreferrer"&gt;Open X-Embodiment&lt;/a&gt; coarsely aligns actions into 7-DoF vectors across 22 embodiments, and LeRobot packages synchronized episodes in a compressed format. The 11 datasets below share that organization, which exposes the remaining differences in embodiments, sensors, state, and action.&lt;/p&gt;

&lt;h2&gt;
  
  
  LeRobot v3.0 vs. MCAP: two different jobs
&lt;/h2&gt;

&lt;p&gt;If you've read our companion piece, &lt;a href="https://voxel51.com/blog/mcap-datasets-multimodal-robotics-fiftyone" rel="noopener noreferrer"&gt;10 Multimodal MCAP Datasets in 2026&lt;/a&gt;, you've seen the other half of this problem. &lt;a href="https://voxel51.com/mcap" rel="noopener noreferrer"&gt;MCAP&lt;/a&gt; and LeRobot both solve robot-data organization, but for opposite inputs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;.&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;MCAP&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;LeRobot v3.0&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it stores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Continuous, multi-rate sensor log (camera, LiDAR, IMU, GPS, all on their own clocks)&lt;/td&gt;
&lt;td&gt;Discrete, synchronized episodes (state/action/vision aligned to one frame grid)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lossless (LZ4/Zstandard at the chunk level)&lt;/td&gt;
&lt;td&gt;Lossy video compression (AV1/H.264), trades fidelity for size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SLAM, sensor fusion, perception benchmarking, raw log archival&lt;/td&gt;
&lt;td&gt;Imitation learning, VLA pretraining and fine-tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One sample is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One continuous recording (minutes long), decoded live&lt;/td&gt;
&lt;td&gt;One episode (seconds to a couple minutes), pre-chunked into fixed frame steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chunk-indexed seeking within a file&lt;/td&gt;
&lt;td&gt;StreamingLeRobotDataset streams directly from the Hub, no local download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In FiftyOne&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;dataset.media_type == "multimodal", one sample = one continuous recording&lt;/td&gt;
&lt;td&gt;dataset.media_type == "multimodal", one sample = one episode, per-frame data resolved through media_reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;How MCAP and LeRobot v3.0 differ across storage, compression, and typical use.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you have 20 minutes of unsynchronized LiDAR, camera, and IMU data and need to seek to timestamp 4:32, use MCAP. If you have labeled robot demonstrations and need synchronized state/action/vision triples for training, use LeRobot. The formats cover different parts of the robot-data pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1r319l4lsuitu2t2nejt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1r319l4lsuitu2t2nejt.png" alt="Comparison infographic of LeRobot v3.0 and MCAP across what each format stores, compression, typical use, what one sample is, streaming, and how each appears in FiftyOne." width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: Generated using NanoBanana 2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;LeRobot v3.0 changes how it packages episodes. Version 2.1 used one Parquet file and one MP4 per episode, which strains filesystem limits at millions of episodes. Version 3.0 packs many episodes into shared Parquet and MP4 files and resolves them through relational metadata. That structure makes a 102,078-episode, 13.6TB release such as AgiBotWorld2026 practical to package.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FiftyOne LeRobot schema: what's a sample, what's a stream
&lt;/h2&gt;

&lt;p&gt;Every one of the 11 datasets below is &lt;a href="https://voxel51.com/blog/lerobot-dataset-support-voxel51" rel="noopener noreferrer"&gt;loaded into FiftyOne&lt;/a&gt; the same way, so the field schema is consistent across all of them. Each is a &lt;a href="https://voxel51.com/blog/fiftyone-supports-multimodal-data" rel="noopener noreferrer"&gt;multimodal FiftyOne dataset&lt;/a&gt; where &lt;strong&gt;one sample equals one episode&lt;/strong&gt;. The sample-level fields you can query, filter, and sort on in the App are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Field&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;FiftyOne type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What it holds&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;id&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ObjectIdField&lt;/td&gt;
&lt;td&gt;FiftyOne sample id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;media_reference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MediaReferenceField&lt;/td&gt;
&lt;td&gt;Pointer into the LeRobot source's data/&lt;em&gt;.parquet, videos/&lt;/em&gt;/*.mp4, and meta/ files for this episode — resolved on demand, not duplicated per sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tags&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ListField(StringField)&lt;/td&gt;
&lt;td&gt;FiftyOne sample tags (empty by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;metadata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;td&gt;Standard FiftyOne sample metadata (size_bytes, mime_type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;created_at / last_modified_at&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DateTimeField&lt;/td&gt;
&lt;td&gt;FiftyOne bookkeeping timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;episode_index&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IntField&lt;/td&gt;
&lt;td&gt;Episode index within the export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;task&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;StringField&lt;/td&gt;
&lt;td&gt;Primary task label for the episode (free text, not fo.Classification — see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tasks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ListField(StringField)&lt;/td&gt;
&lt;td&gt;Full list of task label(s) for the episode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;length&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IntField&lt;/td&gt;
&lt;td&gt;Number of frames in the episode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FloatField&lt;/td&gt;
&lt;td&gt;Episode duration in seconds (length / fps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robot_type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;StringField&lt;/td&gt;
&lt;td&gt;Robot platform identifier (empty/null for several of these 11 — see per-dataset notes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;fps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FloatField&lt;/td&gt;
&lt;td&gt;Recording frame rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Sample-level fields in a FiftyOne LeRobot dataset, and what each one holds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is &lt;code&gt;task&lt;/code&gt; a string instead of a &lt;code&gt;Classification&lt;/code&gt;?&lt;/strong&gt; These datasets store task identity as free text: an instruction, a skill-primitive name, or a placeholder such as &lt;code&gt;"task 1"&lt;/code&gt;. A &lt;code&gt;fo.Classification&lt;/code&gt; field would imply a closed label set with confidence scores. &lt;code&gt;robotwin_unified&lt;/code&gt; alone has 23,559 distinct task strings across 27,500 episodes. Use task and tasks for text search and grouping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does per-frame data live?&lt;/strong&gt; Video streams (&lt;code&gt;observation.images.*&lt;/code&gt;), proprioceptive state (&lt;code&gt;observation.state&lt;/code&gt;, &lt;code&gt;observation.state.*&lt;/code&gt;), actions, and per-frame sensor data stay in the referenced LeRobot &lt;code&gt;data/*.parquet&lt;/code&gt; and &lt;code&gt;videos/*/*.mp4&lt;/code&gt; files. FiftyOne surfaces them in the &lt;strong&gt;State &amp;amp; Action&lt;/strong&gt;, &lt;strong&gt;Streams&lt;/strong&gt;, and &lt;strong&gt;Statistics&lt;/strong&gt; tabs as you scrub through an episode. A 60fps humanoid episode can carry a 44-dimensional state vector and 4 video streams per frame, so flattening these values into top-level sample fields would not scale. &lt;code&gt;media_reference&lt;/code&gt; avoids that duplication.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 11 LeRobot datasets at a glance
&lt;/h2&gt;

&lt;p&gt;"Episodes" below reflects what's actually in each live FiftyOne export, not the full upstream source. 4 of 11 are complete imports; the other 7 are disk-budget-constrained subsets (as small as 10-68 episodes) selected for shard contiguity, not statistical representativeness — each entry states the selection method and the full source size for comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dataset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Sensors&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;License (this export)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Real or Sim&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AgiBotWorld2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-world dual-arm humanoid manipulation collected via RL-style human-intervention rollout on the AgiBot G2 platform&lt;/td&gt;
&lt;td&gt;RGB + depth, 7 cams, RL signals&lt;/td&gt;
&lt;td&gt;CC-BY-NC-SA-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;droid_3d&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RGB-only slice of a large real-world DROID manipulation dataset whose full release adds depth and point clouds for 3D-aware policies&lt;/td&gt;
&lt;td&gt;RGB only, 3 cams&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;fmb_multi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contact-rich Franka Panda assembly demonstrations across three boards, phase-labeled by skill primitive&lt;/td&gt;
&lt;td&gt;RGB + force/torque, 4 cams&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HIW-500-LeRobot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whole-body Unitree G1 humanoid teleoperation performing household chores across real homes in Southeast Asia&lt;/td&gt;
&lt;td&gt;RGB stereo, 3 cams&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;InternData-A1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A single Franka articulation task ("close the laptop") from a fully synthetic, zero-demonstration compositional simulation pipeline&lt;/td&gt;
&lt;td&gt;RGB, 2 cams&lt;/td&gt;
&lt;td&gt;CC-BY-NC-SA-4.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sim&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;rh20t_cfg1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real Flexiv-arm teleoperation with ten synchronized camera views, part of RH20T's one-shot skill-transfer dataset&lt;/td&gt;
&lt;td&gt;RGB, 10 cams&lt;/td&gt;
&lt;td&gt;Mixed CC-BY-SA-4.0 / CC-BY-NC-4.0&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robocasa-MG_100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Synthetic kitchen manipulation generated via MimicGen augmentation of real human demonstrations&lt;/td&gt;
&lt;td&gt;RGB, 3 cams&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robotwin_unified&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bimanual ALOHA-embodiment simulation generated by an LLM code-gen agent and VLM observer, with heavy domain randomization&lt;/td&gt;
&lt;td&gt;RGB, 3 cams&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tavis-head-gr1t2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simulated active-vision humanoid benchmark (GR1T2) comparing head-mounted vs. fixed cameras on identical manipulation tasks&lt;/td&gt;
&lt;td&gt;RGB, active-vision head + fixed + 2 wrist cams&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sim&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tavis-head-reachy2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The same active-vision benchmark as tavis-head-gr1t2, run on a Reachy2 humanoid instead&lt;/td&gt;
&lt;td&gt;RGB, active-vision head + fixed + 2 wrist cams&lt;/td&gt;
&lt;td&gt;CC-BY-4.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sim&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trex_dataset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bimanual dexterous manipulation with real per-fingertip tactile sensing on a Dexmate Vega-1 robot&lt;/td&gt;
&lt;td&gt;RGB + tactile, 3 RGB + 20 tactile streams&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Real&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The 11 LeRobot v3.0 datasets, with sensors, license, and whether the data is real or simulated.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 11 LeRobot datasets in detail
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AgiBotWorld2026: real-world dual-arm manipulation with RL collection signals
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Ff4MD6Svd86T02yv00XLCY02Vx7sWULxECCjgikCIgoPGFE%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Ff4MD6Svd86T02yv00XLCY02Vx7sWULxECCjgikCIgoPGFE%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing an AgiBotWorld2026 episode, with seven camera panels including head fisheyes, a reward signal plot, and the 216-dimension observation state vector." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing an AgiBotWorld2026 episode, with seven camera panels including head fisheyes, a reward signal plot, and the 216-dimension observation state vector.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/AgiBotWorld2026-sample-10ep" rel="noopener noreferrer"&gt;Voxel51/AgiBotWorld2026-sample-10ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/AgiBotWorld2026-sample-10ep" rel="noopener noreferrer"&gt;harpreetsahota/AgiBotWorld2026-sample-10ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/agibot-world/AgiBotWorld2026" rel="noopener noreferrer"&gt;agibot-world/AgiBotWorld2026&lt;/a&gt; (102,078 episodes, 13.6TB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; Project homepage: &lt;a href="https://agibot-world.com" rel="noopener noreferrer"&gt;agibot-world.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-NC-SA-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgiBot World is one of the largest real-world humanoid manipulation datasets released to date.&lt;/p&gt;

&lt;p&gt;A team collected long-horizon household and commercial tasks using the AgiBot G2 dual-arm platform. They deployed it in real homes, retail spaces, and offices—not just a single controlled lab.&lt;/p&gt;

&lt;p&gt;The 2026 release has three collection modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure imitation learning&lt;/li&gt;
&lt;li&gt;RL-style rollout with human intervention (HG-DAgger)&lt;/li&gt;
&lt;li&gt;Richer multi-signal interaction logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And three annotation layers on top of the raw episodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subtask segmentation&lt;/li&gt;
&lt;li&gt;2D bounding boxes&lt;/li&gt;
&lt;li&gt;Step-level language instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This FiftyOne export pulls from the HG-DAgger reinforcement-learning slice specifically, so you're looking at a robot mid-training, complete with reward signals and human-correction flags, not a polished expert demonstration. All 10 episodes come from a single &lt;code&gt;ReinforcementLearning/Home/task_12192/HG-DAgger&lt;/code&gt; archive, one task ("Insert the loose change into the coin slot of the piggy bank dagger").&lt;/p&gt;

&lt;p&gt;Because this is the RL collection category, every frame carries collection-process signals most imitation-learning datasets don't: &lt;code&gt;reward&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;succeed&lt;/code&gt;, &lt;code&gt;intervened&lt;/code&gt; (whether a human corrected the policy at that frame), &lt;code&gt;truncated&lt;/code&gt;, and &lt;code&gt;action_prob&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 7 camera streams (&lt;code&gt;top_head&lt;/code&gt;, &lt;code&gt;hand_left&lt;/code&gt;, &lt;code&gt;hand_right&lt;/code&gt;, two head fisheyes, a rear fisheye, and a &lt;code&gt;head_depth&lt;/code&gt; stream — declared &lt;code&gt;dtype&lt;/code&gt;: video with &lt;code&gt;gray16be&lt;/code&gt; pixel format, so it imports like RGB despite being depth data), a 216-dim &lt;code&gt;observation.state&lt;/code&gt; vector, and a 44-dim &lt;code&gt;action&lt;/code&gt; vector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; The 13.6TB source repo isn't a standard flat LeRobot layout — it ships as per-task &lt;code&gt;.tar.gz&lt;/code&gt; archives under &lt;code&gt;ImitationLearning/&lt;/code&gt;, &lt;code&gt;ReinforcementLearning/&lt;/code&gt;, and &lt;code&gt;RichInteraction/&lt;/code&gt;, and these three categories declare &lt;strong&gt;incompatible&lt;/strong&gt; &lt;code&gt;observation.state&lt;/code&gt; shapes (193 / 216 / 169 respectively), so a single LeRobot v3 export can only cover one category at a time. This export stays inside &lt;code&gt;ReinforcementLearning&lt;/code&gt;. The source ships LeRobot v2.1 and was converted with &lt;code&gt;lerobot.scripts.convert_dataset_v21_to_v30&lt;/code&gt;; AgiBot's three custom annotation layers (&lt;code&gt;key_frame&lt;/code&gt;, &lt;code&gt;instruction_segments&lt;/code&gt;, subtask/bounding-box labels) are dropped by that converter and aren't present here.&lt;/p&gt;

&lt;h3&gt;
  
  
  HIW-500-LeRobot — Humanoid teleoperation in real homes, sampled across all 11 tasks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FKORNVDr00IjhbmDb02HgtSgnHh6bvLtUu02ZHSWV7bgl8w%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FKORNVDr00IjhbmDb02HgtSgnHh6bvLtUu02ZHSWV7bgl8w%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing an HIW-500-LeRobot kitchen organization episode from a Unitree G1 humanoid, with head and wrist camera streams above a whole-body-control action plot." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing an HIW-500-LeRobot kitchen organization episode from a Unitree G1 humanoid, with head and wrist camera streams above a whole-body-control action plot.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/HIW-500-LeRobot-48ep" rel="noopener noreferrer"&gt;Voxel51/HIW-500-LeRobot-48ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/HIW-500-LeRobot-48ep" rel="noopener noreferrer"&gt;harpreetsahota/HIW-500-LeRobot-48ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/BitRobot/HIW-500-LeRobot" rel="noopener noreferrer"&gt;BitRobot/HIW-500-LeRobot&lt;/a&gt; (23,743 episodes, ~2TB); raw ROS bag / MCAP recordings of this same data are separately published at BitRobot/HIW-500&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://bitrobot-foundation.github.io/humanoids-in-the-wild-500-hours/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Humanoids In-the-Wild is exactly what it sounds like.&lt;/p&gt;

&lt;p&gt;The dataset captures hundreds of hours showing a Unitree G1 humanoid handling domestic tasks throughout actual residences in Southeast Asia. Environments, illumination, object arrangements, and individual operator control patterns naturally fluctuate between demonstrations—by design, offering raw, uncurated field recordings rather than sanitized laboratory trials. This curated selection spans all 11 task classes from the source collection (ranging from table preparation to hanging keys on pegs), delivering a representative window into the complete 23,743-trajectory dataset without requiring a multi-terabyte retrieval.&lt;/p&gt;

&lt;p&gt;Rather than extracting the minimal contiguous block (which would limit coverage to a single activity), we sampled 48 demonstrations across 6 distinct file groups to ensure representation across &lt;strong&gt;every task class&lt;/strong&gt;—from "setting the table" (15 demonstrations) to "hang hanger" (a single demonstration).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 camera streams (&lt;code&gt;head&lt;/code&gt; at 480×1280, &lt;code&gt;left_wrist&lt;/code&gt;/&lt;code&gt;right_wrist&lt;/code&gt; at 480×640, all AV1), a 29-DoF &lt;code&gt;observation.state&lt;/code&gt; (hips, knees, ankles, waist, shoulders, elbows, wrists), a 23-dim &lt;code&gt;observation.state.wbc&lt;/code&gt; whole-body-control vector (pivot velocity/pose + both end-effector poses + trigger/squeeze), and a matching 23-dim action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; The source's &lt;code&gt;language_persistent&lt;/code&gt; and &lt;code&gt;language_events&lt;/code&gt; fields carried the dataset's 161 fine-grained subtask labels and 148K+ annotations; this export includes only the 11 coarse task categories, not the fine-grained subtask text. Camera intrinsics/extrinsics mentioned in the source docs also aren't exposed as FiftyOne fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  tavis-head-gr1t2 and tavis-head-reachy2 — Active-vision humanoids, full datasets, in simulation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FEHlNRSBN02b5XMKsLUjLgGA79uE00hvK01qgU2WDE2MV8k%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FEHlNRSBN02b5XMKsLUjLgGA79uE00hvK01qgU2WDE2MV8k%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing a simulated GR1T2 humanoid ClutterPickCubeTask episode from tavis-head-gr1t2, with head, fixed, and wrist camera views and the 44-dimension state vector." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing a simulated GR1T2 humanoid ClutterPickCubeTask episode from tavis-head-gr1t2, with head, fixed, and wrist camera views and the 44-dimension state vector.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/tavis-head-gr1t2-800ep" rel="noopener noreferrer"&gt;Voxel51/tavis-head-gr1t2-800ep&lt;/a&gt; · &lt;a href="https://huggingface.co/datasets/Voxel51/tavis-head-reachy2-800ep" rel="noopener noreferrer"&gt;Voxel51/tavis-head-reachy2-800ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/tavis-head-gr1t2-800ep" rel="noopener noreferrer"&gt;harpreetsahota/tavis-head-gr1t2-800ep&lt;/a&gt; · &lt;a href="https://huggingface.co/spaces/harpreetsahota/tavis-head-reachy2-800ep" rel="noopener noreferrer"&gt;harpreetsahota/tavis-head-reachy2-800ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/tavis-benchmark" rel="noopener noreferrer"&gt;tavis-benchmark&lt;/a&gt; org (both are complete, 800/800 episodes each)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2605.07943" rel="noopener noreferrer"&gt;Paper (arXiv:2605.07943)&lt;/a&gt; · &lt;a href="https://github.com/spiglerg/tavis" rel="noopener noreferrer"&gt;Code&lt;/a&gt; · Paired π₀ checkpoints for head-camera vs. fixed-camera comparison, published under the same tavis-benchmark org&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TAVIS asks a specific, narrow question that most robot-learning datasets don't bother to isolate: &lt;em&gt;does letting a humanoid move its own head/camera during manipulation actually help, and can you measure that benefit directly?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The benchmark pairs identical tasks and identical camera rigs across two different humanoid torsos — Fourier's GR1T2 and Pollen's Reachy2 — and records both a head-mounted "active vision" stream and a fixed-camera view for every episode, so you can train and compare policies with the head-camera on or off using the same demonstrations. It's simulation-only (built in NVIDIA IsaacLab), VR-teleoperated, and small (800 episodes per robot) by design, since the goal is controlled comparison, not scale.&lt;/p&gt;

&lt;p&gt;Both robots share 5 task classes: &lt;code&gt;ClutterPickCubeTask&lt;/code&gt;, &lt;code&gt;ClutterPickLiftTask&lt;/code&gt;, &lt;code&gt;ConditionalPickTask&lt;/code&gt;, &lt;code&gt;MultiShelfScanTask&lt;/code&gt;, &lt;code&gt;WaitThenActTask&lt;/code&gt; (250/250/100/100/100 episode split).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 4 camera streams (&lt;code&gt;OBS_HEAD&lt;/code&gt;, &lt;code&gt;OBS_FIXED&lt;/code&gt;, &lt;code&gt;OBS_WRIST_LEFT&lt;/code&gt;, &lt;code&gt;OBS_WRIST_RIGHT&lt;/code&gt;, all 480×640 AV1 at 60fps), a 19-dim &lt;code&gt;action&lt;/code&gt; (bimanual arm targets + neck + grippers), &lt;code&gt;observation.state&lt;/code&gt; (44-dim for GR1T2, 36-dim for Reachy2 — the two robots have different DoF counts, so don't assume the vectors are interchangeable), separate left/right end-effector position and orientation fields, and a per-frame &lt;code&gt;language_instruction&lt;/code&gt; string (33 distinct values for Reachy2).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FuZO00yD6aPOYPXY75IjPPqWzGzRzkOW2eNLNslWICYT4%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FuZO00yD6aPOYPXY75IjPPqWzGzRzkOW2eNLNslWICYT4%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing the same active-vision ClutterPickCubeTask on a simulated Reachy2 humanoid, with four camera panels and per-frame state and action plots." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing the same active-vision ClutterPickCubeTask on a simulated Reachy2 humanoid, with four camera panels and per-frame state and action plots.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; &lt;code&gt;robot_type&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; in both sources despite the org page naming the robots explicitly. A separate &lt;code&gt;tavis-hands-*&lt;/code&gt; pair (300 episodes each, testing local-occlusion perception via wrist cameras rather than head search) exists in the same org but isn't part of this roundup.&lt;/p&gt;

&lt;h3&gt;
  
  
  trex_dataset — Bimanual dexterous manipulation with fingertip tactile sensing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FGG4lanSIvTTBUSKOD5RPeRJLAw02SE027ZCTphVjeu6jo%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FGG4lanSIvTTBUSKOD5RPeRJLAw02SE027ZCTphVjeu6jo%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing a T-Rex bimanual episode on a Dexmate Vega-1, showing an RGB camera view beside a grayscale fingertip tactile stream and per-fingertip tactile force readings." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing a T-Rex bimanual episode on a Dexmate Vega-1, showing an RGB camera view beside a grayscale fingertip tactile stream and per-fingertip tactile force readings.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/trex-dataset-23ep" rel="noopener noreferrer"&gt;Voxel51/trex-dataset-23ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/trex-dataset-23ep" rel="noopener noreferrer"&gt;harpreetsahota/trex-dataset-23ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/zekaiwang/trex_dataset" rel="noopener noreferrer"&gt;zekaiwang/trex_dataset&lt;/a&gt; (5,464 episodes, 1.5TB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2606.17055" rel="noopener noreferrer"&gt;Paper (arXiv:2606.17055)&lt;/a&gt; · &lt;a href="https://tactile-rex.github.io/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;T-Rex is built around a sensing modality almost no other manipulation dataset bothers with at this density: real per-fingertip tactile sensing.&lt;/p&gt;

&lt;p&gt;This dataset captures five sensors per hand, on a bimanual Dexmate Vega-1 robot fitted with Sharpa Wave dexterous hands. Each episode includes not just RGB video but 20 additional tactile video streams (a raw sensor image and a deformation map per fingertip), plus 6-axis force/torque readings at each fingertip, collected via Manus glove and VIVE tracker teleoperation across 207 objects and 22 distinct motor primitives.&lt;/p&gt;

&lt;p&gt;If you care about contact-rich, touch-conditioned manipulation rather than vision-only policies, this is one of the only public datasets built specifically to support it.&lt;/p&gt;

&lt;p&gt;The 23-episode export here is the first contiguous shard of the full 5,464-episode dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 RGB camera streams (&lt;code&gt;head_left&lt;/code&gt;, &lt;code&gt;left_wrist&lt;/code&gt;, &lt;code&gt;right_wrist&lt;/code&gt;, 640×360) plus &lt;strong&gt;20 tactile video streams&lt;/strong&gt; — a raw sensor image and an estimated deformation map per fingertip, 2 hands × 5 fingers × 2 stream types — a 58-dim &lt;code&gt;observation.state&lt;/code&gt; and matching &lt;code&gt;action&lt;/code&gt; (&lt;code&gt;[L_arm 7 | L_hand 22 | R_arm 7 | R_hand 22]&lt;/code&gt; joint layout), and a 60-dim &lt;code&gt;observation.tactile_force&lt;/code&gt; (per-fingertip 6-axis wrench).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; The 20 tactile streams are encoded losslessly (&lt;code&gt;libx264 -qp 0&lt;/code&gt;, since pixel values are physically meaningful sensor readings), which forces the H.264 &lt;strong&gt;High 4:4:4 Predictive&lt;/strong&gt; profile — most browsers can't decode this in a plain &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element. As a result, these streams show no thumbnail in a generic viewer. FiftyOne, ffmpeg, PyAV, or torchcodec decode them fine; the 3 RGB streams use standard &lt;code&gt;yuv420p&lt;/code&gt; and preview everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  fmb_multi — Contact-rich Franka assembly, phase-labeled
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FZWFNhwA4N1B6MWkQNgNQcZZKBX00HdtqliSOlXEQhbRs%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FZWFNhwA4N1B6MWkQNgNQcZZKBX00HdtqliSOlXEQhbRs%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing an fmb_multi Franka Panda assembly episode, with four 256 by 256 camera views of a colored assembly board and end-effector force and torque plots." width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing an fmb_multi Franka Panda assembly episode, with four 256 by 256 camera views of a colored assembly board and end-effector force and torque plots.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/fmb-multi-302ep" rel="noopener noreferrer"&gt;Voxel51/fmb-multi-302ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/fmb-multi-302ep" rel="noopener noreferrer"&gt;harpreetsahota/fmb-multi-302ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/robot-lev/fmb_multi" rel="noopener noreferrer"&gt;robot-lev/fmb_multi&lt;/a&gt; (1,804 episodes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2401.08553" rel="noopener noreferrer"&gt;Paper (arXiv:2401.08553)&lt;/a&gt; · &lt;a href="https://functional-manipulation-benchmark.github.io" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Functional Manipulation Benchmark was built to stress-test generalization in contact-rich assembly.&lt;/p&gt;

&lt;p&gt;Moving beyond basic pick-and-place actions, this benchmark evaluates complex insertion and regrasping sequences where force feedback plays a critical role. The &lt;code&gt;fmb_multi&lt;/code&gt; subset focuses on multi-object manipulation across three distinct assembly boards. Each trajectory features a Franka Panda robotic arm executing a structured skill workflow—approach, grasp, insert, place, and regrasp—while capturing 6-axis end-effector force/torque dynamics, along with visual and proprioceptive signals. Rather than aiming for sheer volume, the benchmark remains intentionally compact (under 2,000 total episodes) to prioritize testing policy generalization across diverse objects and board configurations.&lt;/p&gt;

&lt;p&gt;The exported 302-episode subset is distributed almost uniformly across the three boards, containing 100, 100, and 102 episodes, respectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 4 camera streams (&lt;code&gt;side_1&lt;/code&gt;, &lt;code&gt;side_2&lt;/code&gt;, &lt;code&gt;wrist_1&lt;/code&gt;, &lt;code&gt;wrist_2&lt;/code&gt;, 256×256 AV1, converted from source BGR), &lt;code&gt;observation.state&lt;/code&gt; (28-dim: joint position + velocity + ee_pose + ee velocity + gripper), separate &lt;code&gt;observation.state.joint_position&lt;/code&gt;/&lt;code&gt;.ee_pose&lt;/code&gt;/&lt;code&gt;.gripper fields&lt;/code&gt;, &lt;code&gt;observation.force&lt;/code&gt;/&lt;code&gt;observation.torque&lt;/code&gt; (end-effector frame), a flattened 6×7 &lt;code&gt;observation.jacobian&lt;/code&gt;, and a 7-dim cartesian &lt;code&gt;action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; &lt;code&gt;task&lt;/code&gt; here is the skill primitive active at &lt;strong&gt;frame 0 only&lt;/strong&gt; — not an episode summary. Use &lt;code&gt;tasks&lt;/code&gt; for the full ordered sequence an episode passes through. Depth was dropped by the upstream port (not this export); the source FMB release includes 4 depth maps per frame that never made it into &lt;code&gt;fmb_multi&lt;/code&gt;. &lt;code&gt;fps&lt;/code&gt;: 10 is nominal — the original &lt;code&gt;.npy&lt;/code&gt; files carry no real timestamps, so frames map 1:1 onto a synthetic grid rather than a measured rate. Per-episode &lt;code&gt;board&lt;/code&gt;/&lt;code&gt;object_id&lt;/code&gt; metadata lives in the source's &lt;code&gt;meta/fmb_episodes.json&lt;/code&gt;, not as a FiftyOne field — join back to it by &lt;code&gt;episode_index&lt;/code&gt; if you need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  rh20t_cfg1 — Flexiv arm, ten camera views, license split by episode
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fw2PBoZ01Ez2USLLwZWwu79k02PJ4W651r3vVhVnev4e6c%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fw2PBoZ01Ez2USLLwZWwu79k02PJ4W651r3vVhVnev4e6c%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing an rh20t_cfg1 episode from a Flexiv arm, with ten synchronized camera views and a force and torque plot beneath the state and action panel." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing an rh20t_cfg1 episode from a Flexiv arm, with ten synchronized camera views and a force and torque plot beneath the state and action panel.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/rh20t-cfg1-28ep" rel="noopener noreferrer"&gt;Voxel51/rh20t-cfg1-28ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/rh20t-cfg1-28ep" rel="noopener noreferrer"&gt;harpreetsahota/rh20t-cfg1-28ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/robot-lev/rh20t_cfg1" rel="noopener noreferrer"&gt;robot-lev/rh20t_cfg1&lt;/a&gt; (4,258 episodes; RH20T's full dataset spans 110,000+ sequences across many robot configs, of which &lt;code&gt;cfg1&lt;/code&gt;/Flexiv is one)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2307.00595" rel="noopener noreferrer"&gt;Paper (arXiv:2307.00595)&lt;/a&gt; · &lt;a href="https://rh20t.github.io/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; mixed, 16 episodes CC-BY-SA-4.0 / 12 episodes CC-BY-NC-4.0 in this export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RH20T tests one-shot skill transfer: can a robot learn a new task from just a single demonstration?&lt;/p&gt;

&lt;p&gt;To answer this, it collects an enormous spread of over 110,000 manipulation sequences across diverse robot platforms and camera setups. The &lt;code&gt;rh20t_cfg1&lt;/code&gt; subset focuses on a single Flexiv arm, paired with ten synchronized camera views per episode. RH20T licenses its data per scene, and this port preserves that original split rather than applying a blanket license. Roughly half the scenes permit commercial use, while the rest are non-commercial only.&lt;/p&gt;

&lt;p&gt;Check &lt;code&gt;meta/rh20t_episodes.json&lt;/code&gt;'s folder field to identify the scene and licensing terms for each episode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 10 camera streams (&lt;code&gt;cam_&amp;lt;serial&amp;gt;&lt;/code&gt;, 360×640 AV1, one per fixed RealSense-style camera), &lt;code&gt;observation.state&lt;/code&gt; (15-dim: ee_pose 7 + joint 7 + gripper 1 — note joint is all-zero for episodes where the source's &lt;code&gt;has_joint&lt;/code&gt; flag was false), &lt;code&gt;observation.force&lt;/code&gt;/&lt;code&gt;torque&lt;/code&gt; plus a separate robot-mounted &lt;code&gt;observation.robot_ft&lt;/code&gt; (6-axis), an 8-dim &lt;code&gt;action&lt;/code&gt; (next ee_pose + gripper target), and a per-episode &lt;code&gt;meta.rating&lt;/code&gt; &lt;a href="https://voxel51.com/blog/robot-data-quality-scoring" rel="noopener noreferrer"&gt;quality score&lt;/a&gt; from the original RH20T annotation pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; &lt;code&gt;task&lt;/code&gt; values here are literally &lt;code&gt;"task 1"&lt;/code&gt; through &lt;code&gt;"task 6"&lt;/code&gt; — the LeRobot port never carried over RH20T's original natural-language task descriptions, only a numeric cluster id. Treat &lt;code&gt;task&lt;/code&gt; as an opaque grouping key, not a description. Audio sidecars from the source (one &lt;code&gt;.wav&lt;/code&gt; per episode) are deliberately excluded — they sit outside the declared LeRobot schema entirely, and the source data includes volunteer-recorded human-robot interaction with faces/voices that warrants care regardless.&lt;/p&gt;

&lt;h3&gt;
  
  
  robocasa-MG_100 — Full synthetic kitchen dataset via MimicGen
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FqjqFv9SrOm1Tbru7O9Ks02J6ihuIElN7fULgFtBoJ012k%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FqjqFv9SrOm1Tbru7O9Ks02J6ihuIElN7fULgFtBoJ012k%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app grid view of the robocasa-MG_100 dataset, showing dozens of synthetic kitchen episode thumbnails beside a dashboard of task counts and episode duration." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app grid view of the robocasa-MG_100 dataset, showing dozens of synthetic kitchen episode thumbnails beside a dashboard of task counts and episode duration.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/robocasa-MG_100" rel="noopener noreferrer"&gt;Voxel51/robocasa-MG_100&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/robocasa-MG_100" rel="noopener noreferrer"&gt;harpreetsahota/robocasa-MG_100&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/DAVIAN-Robotics/robocasa-MG_100" rel="noopener noreferrer"&gt;DAVIAN-Robotics/robocasa-MG_100&lt;/a&gt; — &lt;strong&gt;all 2,400 episodes, full dataset, no subsetting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2406.02523" rel="noopener noreferrer"&gt;Paper (arXiv:2406.02523)&lt;/a&gt; · &lt;a href="https://arxiv.org/abs/2310.17596" rel="noopener noreferrer"&gt;MimicGen paper (arXiv:2310.17596)&lt;/a&gt; · &lt;a href="https://robocasa.ai" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RoboCasa constructs 120 fully procedurally generated kitchen environments across ten distinct layouts and a dozen aesthetic styles to test how far synthetic augmentation can stretch a limited collection of human demonstrations.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MG_100&lt;/code&gt; dataset shows the MimicGen pipeline in action: rather than logging thousands of additional manual rollouts, researchers broke down 1,250 human teleoperation demonstrations into modular, object-centric primitives. They then synthesized 100 new rollouts for every task by stitching those components back together inside unseen spatial setups, keeping only successful executions. While synthetic, this approach directly addresses a key robotics bottleneck: evaluating how effectively automated generation can replace labour-intensive physical data collection. Specifically, &lt;code&gt;MG_100&lt;/code&gt; mirrors the paper's &lt;code&gt;Generated-100&lt;/code&gt; setup, taking a 1/30 random slice from the broader MimicGen output to yield 100 demonstrations across 24 atomic tasks (spanning 688,750 total frames, with trajectory lengths between 89 and 838 frames).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 camera streams (2 static workspace views + 1 wrist-mounted eye-in-hand, all 128×128 AV1), a 16-dim &lt;code&gt;observation.state&lt;/code&gt; (base-to-eef position/quaternion + gripper qpos + base position/quaternion), a 12-dim delta-pose+gripper+base action, and a &lt;code&gt;next.done&lt;/code&gt; termination flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; Task episode counts are imbalanced by design — fixed-object tasks (e.g. "turn on the sink faucet") have exactly 100 episodes, while pick-and-place tasks split across many object-category variants, as few as 1 episode for some combinations, because MimicGen samples object identity per generation attempt. &lt;code&gt;NavigateKitchen&lt;/code&gt;, the paper's 25th atomic task, is absent from every MimicGen-generated release (including this one) because MimicGen can't synthesize mobile-navigation trajectories — that's a property of the source data, not something excluded on import.&lt;/p&gt;

&lt;h3&gt;
  
  
  robotwin_unified — 27,500-episode bimanual simulation with domain randomization
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F4CzivTXOuxEAve9qLV5US4cxfA7mAhl6RPtL028bO00tE%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F4CzivTXOuxEAve9qLV5US4cxfA7mAhl6RPtL028bO00tE%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app playing a robotwin_unified bimanual ALOHA episode with the instruction to lift a plastic bottle, showing the 14-degree-of-freedom state and action vectors." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app playing a robotwin_unified bimanual ALOHA episode with the instruction to lift a plastic bottle, showing the 14-degree-of-freedom state and action vectors.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/robotwin-unified-27500ep" rel="noopener noreferrer"&gt;Voxel51/robotwin-unified-27500ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/robotwin-unified-27500ep" rel="noopener noreferrer"&gt;harpreetsahota/robotwin-unified-27500ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/lerobot/robotwin_unified" rel="noopener noreferrer"&gt;lerobot/robotwin_unified&lt;/a&gt; — this export is &lt;strong&gt;all 27,500 episodes, full &lt;code&gt;aloha&lt;/code&gt;-embodiment portion, no subsetting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2506.18088" rel="noopener noreferrer"&gt;Paper (arXiv:2506.18088)&lt;/a&gt; · &lt;a href="https://github.com/RoboTwin-Platform/RoboTwin" rel="noopener noreferrer"&gt;Simulator/generator code&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RoboTwin 2.0 takes data generation a step beyond MimicGen-style augmentation—it removes the human demonstration requirement entirely.&lt;/p&gt;

&lt;p&gt;An LLM writes executable task programs from a natural-language instruction, a SAPIEN-based simulator runs them, and a vision-language model watches the execution frame-by-frame to catch failures and drive the code-generation agent to fix its own mistakes, iterating until the task succeeds reliably. &lt;code&gt;robotwin_unified&lt;/code&gt; is the bimanual ALOHA slice of that pipeline's output: 50 dual-arm tasks, heavy domain randomization across textures, lighting, and clutter, and a task-instruction vocabulary so varied that nearly every one of its 27,500 episodes has a unique description (23,559 distinct task strings, near one-to-one).&lt;/p&gt;

&lt;p&gt;Domain randomization spans scene clutter (731-object asset library), background textures (11,000 AI-generated textures), lighting, tabletop height, and instruction phrasing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 camera streams (&lt;code&gt;cam_high&lt;/code&gt; overhead + &lt;code&gt;cam_left_wrist&lt;/code&gt;/&lt;code&gt;cam_right_wrist&lt;/code&gt;, 480×640 AV1), a 14-DoF &lt;code&gt;observation.state&lt;/code&gt; (left/right waist, shoulder, elbow, forearm_roll, wrist_angle, wrist_rotate, gripper), and a matching 14-dim action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; Entirely simulated (SAPIEN-based) — the source paper itself cautions against treating performance here as a proxy for real-world deployment readiness without separate sim-to-real validation. Because near every episode has a unique task string, don't try to build a &lt;code&gt;Classification&lt;/code&gt; schema over task — text search/filtering is the right access pattern here, not label grouping.&lt;/p&gt;

&lt;h3&gt;
  
  
  droid_3d — RGB-only in FiftyOne; depth and point clouds need a different loader
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FZKu01mnX1CS02RCrULKBQNONrQk6XfOgwed9orF448i8o%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FZKu01mnX1CS02RCrULKBQNONrQk6XfOgwed9orF448i8o%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app grid view of the droid_3d RGB export, with episode thumbnails each captioned by its natural-language task description." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app grid view of the droid_3d RGB export, with episode thumbnails each captioned by its natural-language task description.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/droid-3d-rgb-68ep" rel="noopener noreferrer"&gt;Voxel51/droid-3d-rgb-68ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/droid-3d-rgb-68ep" rel="noopener noreferrer"&gt;harpreetsahota/droid-3d-rgb-68ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/ZibinDong/droid_3d" rel="noopener noreferrer"&gt;ZibinDong/droid_3d&lt;/a&gt; (58,201 episodes, ~1.3TB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2505.10105" rel="noopener noreferrer"&gt;Paper (arXiv:2505.10105)&lt;/a&gt; — the paper this depth/point-cloud extension supports · &lt;a href="https://github.com/ZibinDong/lerobotdataset3d" rel="noopener noreferrer"&gt;Reader package for the full 3D data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DROID is one of the most widely used real-robot manipulation datasets in the field.&lt;/p&gt;

&lt;p&gt;This dataset is assembled by pooling data collection across many labs on a standardized rig, and &lt;code&gt;droid_3d&lt;/code&gt; builds on that foundation by adding a genuinely useful extension: synchronized depth video and quantized point clouds alongside the original RGB, purpose-built to support 3D-aware manipulation and representation-learning research (EmbodiedMAE). The catch for anyone working purely in FiftyOne or stock LeRobot is that those depth and point-cloud streams use dtypes neither tool currently decodes, so you get the RGB-and-language backbone of a dataset whose real distinguishing feature lives just outside this export.&lt;/p&gt;

&lt;p&gt;The importer recognizes &lt;code&gt;video&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; dtypes, while the source declares depth and point clouds as &lt;code&gt;depth_video&lt;/code&gt; and &lt;code&gt;pointcloud&lt;/code&gt;, which have no built-in FiftyOne import path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 3 RGB camera streams (&lt;code&gt;wrist&lt;/code&gt;, &lt;code&gt;external_0&lt;/code&gt;, &lt;code&gt;external_1&lt;/code&gt;, 224×398 AV1), an 8-dim &lt;code&gt;action&lt;/code&gt;, and up to 3 independent natural-language task descriptions per episode (&lt;code&gt;language_1&lt;/code&gt;/&lt;code&gt;_2&lt;/code&gt;/&lt;code&gt;_3&lt;/code&gt; — different phrasings/emphasis of the same action, not paraphrases; only &lt;code&gt;language_1&lt;/code&gt; is surfaced as the sample-level &lt;code&gt;task&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; If you need the depth or point-cloud streams, this FiftyOne export won't have them at any episode count — install &lt;code&gt;lerobotdataset3d&lt;/code&gt; (&lt;code&gt;pip install git+https://github.com/ZibinDong/lerobotdataset3d.git&lt;/code&gt;) and load the full &lt;code&gt;ZibinDong/droid_3d&lt;/code&gt; repo directly with &lt;code&gt;LeRobotDatasetDepthPointcloud&lt;/code&gt; instead. &lt;code&gt;robot_type&lt;/code&gt; is empty here because DROID spans multiple robot platforms, with no single embodiment tag recorded at the dataset level.&lt;/p&gt;

&lt;h3&gt;
  
  
  InternData-A1 — One task, one embodiment, out of 1,488 possible exports
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FhIykbjUczCLIm4fc83GzGuCAv1zfNTgnlgCBz4NQPMc%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FhIykbjUczCLIm4fc83GzGuCAv1zfNTgnlgCBz4NQPMc%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne app grid view of the InternData-A1 close-the-laptop export, showing synthetic Franka episodes rendered across many different floor and surface textures." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne app grid view of the InternData-A1 close-the-laptop export, showing synthetic Franka episodes rendered across many different floor and surface textures.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FiftyOne Format Dataset on Hugging Face:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/Voxel51/interndata-a1-close-the-laptop-369ep" rel="noopener noreferrer"&gt;Voxel51/interndata-a1-close-the-laptop-369ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it in a Hugging Face Space:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/harpreetsahota/interndata-a1-close-the-laptop-369ep" rel="noopener noreferrer"&gt;harpreetsahota/interndata-a1-close-the-laptop-369ep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/InternRobotics/InternData-A1" rel="noopener noreferrer"&gt;InternRobotics/InternData-A1&lt;/a&gt; (637,498 trajectories, 401.4M frames, 4 embodiments, 70 tasks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Resources:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2511.16651" rel="noopener noreferrer"&gt;Paper (arXiv:2511.16651)&lt;/a&gt; · &lt;a href="https://internrobotics.github.io/interndata-a1.github.io/" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; CC-BY-NC-SA-4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;InternData-A1 pushes the synthetic-data bet furthest of any dataset in this series: no human demonstrations anywhere in its pipeline, not even as a seed.&lt;/p&gt;

&lt;p&gt;Instead, a fully decoupled simulation stack composes scenes, objects, and scripted skill policies from an asset library. It renders the results at a scale (637,498 trajectories, 401.4M frames) that would be genuinely difficult to reach through teleoperation. The paper's headline result is what makes it worth paying attention to beyond the scale: a policy pretrained exclusively on this &lt;a href="https://voxel51.com/blog/synthetic-data-curation-physical-ai" rel="noopener noreferrer"&gt;synthetic data&lt;/a&gt; reportedly transfers zero-shot to several real-world tasks, which is real (if early) evidence that scale and &lt;a href="https://voxel51.com/blog/vla-generalization-data-diversity" rel="noopener noreferrer"&gt;diversity in simulation&lt;/a&gt; can substitute for at least some real-world collection. The source repo contains &lt;strong&gt;1,488 independent LeRobot v3.0 datasets&lt;/strong&gt;, one per &lt;code&gt;&amp;lt;embodiment&amp;gt;/&amp;lt;task_category&amp;gt;/&amp;lt;task&amp;gt;&lt;/code&gt; directory, each with its own schema.&lt;/p&gt;

&lt;p&gt;This export covers &lt;code&gt;franka-1/articulation_tasks/close_the_laptop&lt;/code&gt;: 369 of that task's 578 episodes, the largest complete shard-0 set obtainable without downloading every shard. Every episode shares the instruction "Close the laptop."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields you get:&lt;/strong&gt; 2 camera streams (&lt;code&gt;images.rgb.head&lt;/code&gt; 360×640, &lt;code&gt;images.rgb.hand&lt;/code&gt; 480×640), camera intrinsics/extrinsics vectors, and a long list of per-component state/action fields (&lt;code&gt;states.joint.position&lt;/code&gt;, &lt;code&gt;actions.gripper.pose&lt;/code&gt;, &lt;code&gt;master_actions.*&lt;/code&gt; for the teleop leader-arm commands, etc.) — plus two &lt;strong&gt;derived&lt;/strong&gt; convenience fields, a 49-dim &lt;code&gt;observation.state&lt;/code&gt; and 43-dim action, packed post-import specifically so FiftyOne's State &amp;amp; Action viewer tile (which only renders features literally named &lt;code&gt;observation.state&lt;/code&gt;/&lt;code&gt;action&lt;/code&gt;) can display this dataset's proprioception at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known quirks.&lt;/strong&gt; 100% synthetic — generated through a compositional simulation pipeline (asset specification, skill policies, task composition, and rendering as decoupled stages), not collected on real hardware, despite photorealistic preview clips. &lt;code&gt;franka-1&lt;/code&gt; and &lt;code&gt;franka-2&lt;/code&gt; are two &lt;em&gt;different&lt;/em&gt; feature spaces in the source (differing image shape and gripper value range) — this export uses only &lt;code&gt;franka-1&lt;/code&gt;; don't merge episodes across the two without checking &lt;code&gt;meta/info.json&lt;/code&gt; on each. Because the source's 1,488 task directories span embodiments with genuinely incompatible schemas (single-arm Franka vs. dual-arm Genie-1/Lift-2/Split-Aloha), this export covers exactly one task/embodiment pair rather than a cross-embodiment sample.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go deeper on these datasets.&lt;/strong&gt; &lt;a href="https://voxel51.com/blog/humanoid-lerobot-datasets-vla-training" rel="noopener noreferrer"&gt;5 humanoid and bimanual LeRobot datasets&lt;/a&gt; compares state and action spaces across five of these embodiments, and &lt;a href="https://voxel51.com/blog/lerobot-datasets-vla-training-real-synthetic" rel="noopener noreferrer"&gt;real vs. synthetic LeRobot datasets&lt;/a&gt; compares synthetic generation strategies and the published sim-to-real evidence. To bring your own LeRobot data into FiftyOne, see &lt;a href="https://voxel51.com/blog/lerobot-import-fiftyone-agent-skills" rel="noopener noreferrer"&gt;how to import LeRobot datasets&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's the difference between MCAP and LeRobot format?
&lt;/h3&gt;

&lt;p&gt;MCAP stores continuous, multi-rate sensor logs (camera, LiDAR, IMU, all on independent clocks) losslessly, built for SLAM and sensor-fusion work where you need to seek to an arbitrary timestamp. LeRobot stores discrete, pre-synchronized episodes — state, action, and vision aligned to one frame grid — compressed for training imitation-learning and VLA policies. See our &lt;a href="https://voxel51.com/blog/mcap-datasets-multimodal-robotics-fiftyone" rel="noopener noreferrer"&gt;MCAP dataset roundup&lt;/a&gt; for the raw-sensor-log side of this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why are some of these FiftyOne datasets subsets instead of the full source?
&lt;/h3&gt;

&lt;p&gt;Disk budget. 7 of the 11 sources range from several hundred GB to multiple TB — &lt;code&gt;AgiBotWorld2026&lt;/code&gt; alone is 13.6TB. For those, we imported a shard-contiguous subset (typically the smallest set of Parquet/MP4 files that produces a complete, non-truncated set of episodes). The 4 exceptions (&lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;, &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;) are complete because their full sources are small enough (under ~15GB each) to import entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does media_reference mean in a FiftyOne LeRobot dataset?
&lt;/h3&gt;

&lt;p&gt;It's the field that points a FiftyOne sample to its episode's data without copying that data into the sample document itself—chunk/file indexes, frame ranges, and per-video timestamp ranges in the LeRobot &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;videos/&lt;/code&gt;, and &lt;code&gt;meta/&lt;/code&gt; files. FiftyOne's App resolves it live, rendering video and state/action streams in the Streams and State &amp;amp; Action tabs as you scrub through an episode, rather than loading everything into memory up front.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is task a string field instead of a classification label?
&lt;/h3&gt;

&lt;p&gt;These 11 datasets don't store task identity as a fixed, closed set of labels. Values range from free-form natural language (&lt;code&gt;droid_3d&lt;/code&gt; has 23,858 distinct task strings across its full source) to skill-primitive phase names (&lt;code&gt;fmb_multi&lt;/code&gt;) and numeric placeholders (&lt;code&gt;rh20t_cfg1's "task 1"-"task 6"&lt;/code&gt;). &lt;code&gt;fo.Classification&lt;/code&gt; implies a taxonomy with confidence scores; a plain &lt;code&gt;StringField&lt;/code&gt; matches the source data without inventing structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use droid_3d's FiftyOne export for depth or point-cloud work?
&lt;/h3&gt;

&lt;p&gt;No — this export is RGB-only at any episode count, because FiftyOne's LeRobot importer doesn't support the &lt;code&gt;depth_video&lt;/code&gt;/&lt;code&gt;pointcloud&lt;/code&gt; dtypes the full source uses for those modalities. For depth and point clouds, install the community &lt;a href="https://github.com/ZibinDong/lerobotdataset3d" rel="noopener noreferrer"&gt;&lt;code&gt;lerobotdataset3d&lt;/code&gt;&lt;/a&gt; package and load &lt;code&gt;ZibinDong/droid_3d&lt;/code&gt; directly with its &lt;code&gt;LeRobotDatasetDepthPointcloud&lt;/code&gt; reader.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which of these are humanoid vs. single-arm?
&lt;/h3&gt;

&lt;p&gt;Humanoid: &lt;code&gt;HIW-500-LeRobot&lt;/code&gt; (Unitree G1), &lt;code&gt;tavis-head-gr1t2&lt;/code&gt; (GR1T2), &lt;code&gt;tavis-head-reachy2&lt;/code&gt; (Reachy2). Single- or dual-arm: &lt;code&gt;fmb_multi&lt;/code&gt; (Franka), &lt;code&gt;rh20t_cfg1&lt;/code&gt; (Flexiv), &lt;code&gt;droid_3d&lt;/code&gt;, &lt;code&gt;AgiBotWorld2026&lt;/code&gt; (dual-arm), &lt;code&gt;InternData-A1&lt;/code&gt; (Franka, in this export), &lt;code&gt;robocasa-MG_100&lt;/code&gt; (Franka on a mobile base), &lt;code&gt;robotwin_unified&lt;/code&gt; (ALOHA bimanual), &lt;code&gt;trex_dataset&lt;/code&gt; (bimanual dexterous hands, not a full humanoid).&lt;/p&gt;

&lt;h3&gt;
  
  
  Which licenses permit commercial use?
&lt;/h3&gt;

&lt;p&gt;MIT (&lt;code&gt;droid_3d&lt;/code&gt;, &lt;code&gt;trex_dataset&lt;/code&gt;) and Apache-2.0 (&lt;code&gt;robocasa-MG_100&lt;/code&gt;, &lt;code&gt;robotwin_unified&lt;/code&gt;) permit commercial use subject to their license terms. CC-BY-4.0 (&lt;code&gt;fmb_multi&lt;/code&gt;, &lt;code&gt;HIW-500-LeRobot&lt;/code&gt;, &lt;code&gt;tavis-head-gr1t2&lt;/code&gt;, &lt;code&gt;tavis-head-reachy2&lt;/code&gt;) also permits commercial use with attribution. CC-BY-NC-SA-4.0 (&lt;code&gt;AgiBotWorld2026&lt;/code&gt;, &lt;code&gt;InternData-A1&lt;/code&gt;) prohibits commercial use. &lt;code&gt;rh20t_cfg1&lt;/code&gt;'s 28-episode export mixes CC-BY-SA-4.0 and CC-BY-NC-4.0, so check the per-episode license table before using it commercially.&lt;/p&gt;

</description>
      <category>lerobotv30</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>The Smoothest Episode in the Batch Was Also a Failure: Motion Smoothness vs. Task Success in RoboLab-EgoX Policy Rollouts</title>
      <dc:creator>Harpreet Sahota</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:34:22 +0000</pubDate>
      <link>https://dev.to/harpreetsahota/the-smoothest-episode-in-the-batch-was-also-a-failure-motion-smoothness-vs-task-success-in-1o1o</link>
      <guid>https://dev.to/harpreetsahota/the-smoothest-episode-in-the-batch-was-also-a-failure-motion-smoothness-vs-task-success-in-1o1o</guid>
      <description>&lt;p&gt;&lt;em&gt;A policy rollout scored smoother than 119 of its 127 peers on every motion metric a quality scorer knows how to compute, and it still failed the task. Here’s how that episode was found, and what it says about scoring robot data in general.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you work with robot data, you’ve probably done some version of quality scoring without calling it that.&lt;/p&gt;

&lt;p&gt;You watch a recording, and your eye picks out the obvious problems: a shaky reach, a hand that overshoots and corrects itself, a pause that goes on a beat too long. Over the last couple of years, that intuition has started getting turned into actual software. A handful of metrics, with names like &lt;em&gt;spectral arc length&lt;/em&gt; and &lt;em&gt;log dimensionless jerk&lt;/em&gt;, read a robot’s motion data and reduce it to a number per recording, so you can sort a folder of a hundred episodes by how rough or smooth each one looks instead of sitting through all of them yourself.&lt;/p&gt;

&lt;p&gt;A real-world example clearly illustrates the limitations of these metrics. After scoring 127 attempts at a single manipulation task, an episode ranking among the top ten smoothest in the entire dataset turned out to be a total task failure. Despite failing its objective—placing a cube into a bowl—the recording triggered zero warning flags and showed no statistical anomalies. Because smoothness and task completion represent two distinct aspects of a recording, looking solely at arm trajectory smoothness reveals nothing about whether the goal was achieved.&lt;/p&gt;

&lt;p&gt;This post walks through how that episode turned up: what dataset it’s in, how an open-source FiftyOne plugin scored it, and what the result does and doesn’t prove about scoring robot data more broadly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scoring all 127 episodes of &lt;code&gt;RubiksCubeTask&lt;/code&gt; from &lt;code&gt;Voxel51/RoboLab-EgoX&lt;/code&gt; with the &lt;code&gt;demo_quality_scorer&lt;/code&gt; FiftyOne plugin shows that motion smoothness does not predict task success. The task split 77 successes to 50 labeled failures, the best-balanced task in a 4,000-episode, 28-task corpus.&lt;/li&gt;
&lt;li&gt;One episode, &lt;code&gt;RubiksCubeTask_cape_hill_sf0&lt;/code&gt;, instruction “Put the cube in the bowl,” labeled &lt;code&gt;success: False&lt;/code&gt;, ranks 8th-smoothest of all 127 episodes on the plugin’s &lt;code&gt;overall_score&lt;/code&gt;. It trips zero flag thresholds and doesn’t register as a statistical outlier either. Every check the scorer runs comes back clean.&lt;/li&gt;
&lt;li&gt;Smoothness correlates with success across the batch as a whole: the median &lt;code&gt;overall_score&lt;/code&gt; is 0.17 for successful episodes and 0.38 for failed ones, with lower values indicating smoother. This one episode is the exception, not proof the whole approach is broken. It’s rare enough that scoring stays useful and common enough that you can’t skip watching the episode yourself.&lt;/li&gt;
&lt;li&gt;These episodes run for only 5.4 seconds at 15 frames per second, too short for the scorer’s automatic windowing, which is designed to target 100 samples per window. A manual override, down to a 2-second window, still produces a usable ranking, and the walkthrough below covers exactly where that workaround stops being trustworthy.&lt;/li&gt;
&lt;li&gt;The single smoothest episode in the entire batch, ranked first of 127, is also a labeled failure. But unlike &lt;code&gt;cape_hill_sf0&lt;/code&gt;, it does register on the scorer’s outlier tab, which makes it a weaker “this would have slipped through” story than an episode that clears every check, including outlier detection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What motion smoothness metrics measure, and what they can't
&lt;/h2&gt;

&lt;p&gt;A few years ago, robots that recorded their own sensor data mostly produced a pile of loosely related files: a video here, a CSV of joint angles there, maybe a separate GPS log if the robot moved outdoors.&lt;/p&gt;

&lt;p&gt;Increasingly, that data gets recorded instead as a single file per attempt at a task, called an &lt;a href="https://voxel51.com/episode" rel="noopener noreferrer"&gt;episode&lt;/a&gt;, which bundles every sensor stream together: cameras, LiDAR, joint encoders, the robot’s own estimate of its &lt;a href="https://voxel51.com/end-effector" rel="noopener noreferrer"&gt;end-effector&lt;/a&gt; position, all ticking along at their own independent rates for as long as the task takes. The standard container for one of these files is &lt;a href="https://voxel51.com/mcap" rel="noopener noreferrer"&gt;MCAP&lt;/a&gt;, developed by Foxglove and &lt;a href="https://voxel51.com/blog/mcap-datasets-multimodal-robotics-fiftyone" rel="noopener noreferrer"&gt;now the default recording format for ROS 2&lt;/a&gt;. Because an MCAP file describes its own contents (what channels it has, what each one’s messages look like), any tool that understands the format can open a recording it’s never seen before without needing the original robot’s software.&lt;/p&gt;

&lt;p&gt;FiftyOne is an open source tool for organizing, visualizing, and curating machine learning datasets, and as of version 1.19 it &lt;a href="https://docs.voxel51.com/user_guide/multimodal.html" rel="noopener noreferrer"&gt;opens MCAP files natively&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F02Fjr1DvQ00jYoy5e01LsJb6pXZGqztjLbqcg8S7owIpAo%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F02Fjr1DvQ00jYoy5e01LsJb6pXZGqztjLbqcg8S7owIpAo%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne multimodal viewer playing an MCAP file, with a LiDAR point cloud in a 3D panel and three synced camera feeds below." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne multimodal viewer playing an MCAP file, with a LiDAR point cloud in a 3D panel and three synced camera feeds below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Point a sample at a &lt;code&gt;.mcap&lt;/code&gt; file, and FiftyOne decodes whatever cameras, point clouds, or robot-state channels it finds and lays them out in a synced, scrubbable viewer, just as it would show you images or video. That matters here because it’s what makes it practical to treat a folder of a hundred robot recordings as a dataset you can query, rather than a hundred files you’d otherwise have to open one at a time.&lt;/p&gt;

&lt;p&gt;After converting episodes into a searchable dataset, the next challenge is deciding which ones merit a human's time to inspect. That’s the job of a quality scorer: software that reads each episode’s sensor data and produces a handful of numbers meant to flag which recordings look rough, broken, or otherwise unfit to train on. This post uses one such tool, an open-source &lt;a href="http://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;FiftyOne plugin called&lt;/a&gt; &lt;a href="http://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;&lt;code&gt;demo_quality_scorer&lt;/code&gt;&lt;/a&gt;, and our analysis zeroes in on the specific metrics that practitioners typically rely on above all others: the measures of motion smoothness.&lt;/p&gt;

&lt;p&gt;Every smoothness metric relies on a speed profile as its foundational baseline, tracking the instantaneous velocity of a specific component—whether an individual joint or the end-effector. From that baseline, several distinct mathematical formulas evaluate specific facets of motion cleanliness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spectral Arc Length (SPARC):&lt;/strong&gt; Analyzes the frequency content of a speed profile and favors motion with low, concentrated energy, indicating a single deliberate movement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log Dimensionless Jerk (LDLJ):&lt;/strong&gt; Integrates the square of jerk over a recording and normalizes it for fair comparison across different window lengths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jerk RMS:&lt;/strong&gt; Provides a unit-based measurement of total jerk, making it effective for identifying localized rough spots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low/High-Frequency Power Ratio:&lt;/strong&gt; Evaluates whether motion energy stems from deliberate, slow movement versus rapid vibration or continuous correction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Shared Limitation:&lt;/strong&gt; All four metrics evaluate the movement signal itself rather than the task's actual success or outcome.&lt;/p&gt;

&lt;p&gt;That core limitation encapsulates the central message of this article. When a robot fluidly and purposefully transports a cube to an incorrect location or releases it a millimeter shy of its target, its resulting velocity profile appears indistinguishable from a flawless execution. While the trajectory remains pristine, the end result falls short.&lt;/p&gt;

&lt;p&gt;This exact issue motivated the design of &lt;code&gt;demo_quality_scorer&lt;/code&gt;: it serves as a &lt;a href="https://voxel51.com/data-triage" rel="noopener noreferrer"&gt;triage tool&lt;/a&gt; rather than an automated filter. Scores dictate which recordings warrant human inspection first, rather than dictating automatic deletion. Comprehensive details on all plugin metrics—including &lt;a href="https://voxel51.com/outlier-detection" rel="noopener noreferrer"&gt;outlier detection&lt;/a&gt; and sensor health checks—are available in two companion articles: &lt;a href="https://voxel51.com/blog/robot-data-quality-scoring" rel="noopener noreferrer"&gt;Everyone Is Scoring Robot Data, Nobody Is Looking at It&lt;/a&gt; and &lt;a href="https://voxel51.com/blog/robot-episode-quality-triage" rel="noopener noreferrer"&gt;Smooth Isn’t the Same as Correct&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Below, we demonstrate a concrete, verifiable example of this vulnerability in action, identified by evaluating the scorer against an independent dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pick one task without downloading 21.8 GB
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F024zE2HAKDfypOclbq02KljpHX00J6WcR42TzBBLy01Yb4Y%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2F024zE2HAKDfypOclbq02KljpHX00J6WcR42TzBBLy01Yb4Y%2Fanimated.gif%3Fwidth%3D640" alt="Browsing the Voxel51 organization on Hugging Face to the RoboLab-EgoX dataset card and its FiftyOne loading instructions." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Browsing the Voxel51 organization on Hugging Face to the RoboLab-EgoX dataset card and its FiftyOne loading instructions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dataset used here, &lt;code&gt;Voxel51/RoboLab-EgoX&lt;/code&gt;, contains 4,000 &lt;code&gt;.mcap&lt;/code&gt; episodes across 28 manipulation tasks, totalling 21.8 GB. Finding one interesting episode doesn’t require all of it. Alongside the actual recordings, the Hugging Face repo includes a &lt;code&gt;samples.json&lt;/code&gt; manifest with every episode’s metadata (which task it belongs to, whether it succeeded, and how long it ran), a couple of megabytes with no video or telemetry attached. Reading that file first tells you exactly which 127 files, out of 4,000, are worth downloading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hf_hub_download&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;manifest_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/RoboLab-EgoX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest_path&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;by_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# Sort for the best success/fail balance, not just the most episodes
&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# ('RubiksCubeTask', {'success': 77, 'fail': 50})
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good to know. Why optimize for balance instead of raw episode count?&lt;/strong&gt; &lt;code&gt;HammersInLeftBinTask&lt;/code&gt; has more episodes than any other task, 227 of them, but only 11 are successes. A task that’s almost entirely failures doesn’t give you a meaningful “typical success” to compare a “typical failure” against. &lt;code&gt;RubiksCubeTask&lt;/code&gt;’s 77/50 split does, which is why it’s the one used for the rest of this post.&lt;/p&gt;

&lt;p&gt;With the task picked, downloading just its 127 files comes out to about 570 MB, a small fraction of the full dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RubiksCubeTask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voxel51/RoboLab-EgoX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filepath&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repo_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Build the dataset and pick your motion signals
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FKVzX8HAMJ21GOutGgDaKIaGvllnt3sTSctVEbSH3iw8%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FKVzX8HAMJ21GOutGgDaKIaGvllnt3sTSctVEbSH3iw8%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne sample grid of RubiksCubeTask episodes, opening one episode's synced camera views and plotting its joint and action channels over time." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne sample grid of RubiksCubeTask episodes, opening one episode's synced camera views and plotting its joint and action channels over time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the files on disk, &lt;a href="https://voxel51.com/blog/mcap-import-fiftyone-agent-skills" rel="noopener noreferrer"&gt;building a FiftyOne dataset out of them&lt;/a&gt; is a matter of pointing each sample at its file and copying over whatever metadata is useful to query later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VFF_MULTIMODAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# set before fiftyone is imported, in every process
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RoboLab-EgoX-RubiksCube&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_samples&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;fo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;take_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;take_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instruction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# multimodal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FiftyOne infers &lt;code&gt;media_type&lt;/code&gt; from the file extension, the same way it would recognize a &lt;code&gt;.jpg&lt;/code&gt; as an image, so pointing a sample at an &lt;code&gt;.mcap&lt;/code&gt; file is the entire import step. No importer class, no schema mapping.&lt;/p&gt;

&lt;p&gt;Before scoring anything, the scorer needs to know which of an episode’s channels actually represent the robot’s motion. This dataset ships a &lt;code&gt;/joint-positions&lt;/code&gt; channel (13 joint angles) and an &lt;code&gt;/end-effector-pose&lt;/code&gt; channel, both readings of where the robot’s own body actually was. It also ships an &lt;code&gt;/actions&lt;/code&gt; channel, which is the policy’s commanded output rather than a sensor reading: what the policy told the robot to do, not what the robot’s state ended up being. Scoring &lt;code&gt;/actions&lt;/code&gt; would measure the policy’s intent instead of its execution, and those two can diverge exactly in the cases worth catching, so &lt;code&gt;/joint-positions&lt;/code&gt; and &lt;code&gt;/end-effector-pose&lt;/code&gt; are the two signals used here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Score it, and work around a 5.4-second episode
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FyLvKKWL01osLSylXNetW5BS9bY1U4jqrcmpGs7eBCsl00%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FyLvKKWL01osLSylXNetW5BS9bY1U4jqrcmpGs7eBCsl00%2Fanimated.gif%3Fwidth%3D640" alt="The demo_quality_scorer compute episode quality form in FiftyOne, with motion smoothness metrics, position signal pickers, and manual window settings." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The demo_quality_scorer compute episode quality form in FiftyOne, with motion smoothness metrics, position signal pickers, and manual window settings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;demo_quality_scorer&lt;/code&gt; computes SPARC, LDLJ, jerk RMS, and the PSD ratio for each time window, then summarizes each episode in two ways: the median across all its windows and the worst window. Automatic windowing is built to target 100 samples per window, with a 2-second floor, which works well for episodes running tens of seconds to several minutes. It doesn’t work here. At 15 frames per second for 5.4 seconds, a &lt;code&gt;RubiksCubeTask&lt;/code&gt; episode has 81 samples total, fewer than automatic windowing would even ask for in a single window.&lt;/p&gt;

&lt;p&gt;Install the plugin the same way you’d &lt;a href="https://docs.voxel51.com/plugins/index.html" rel="noopener noreferrer"&gt;install any FiftyOne plugin&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fiftyone.operators&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;

&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_operator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo-quality-scorer/compute_episode_quality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;motion_enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;motion_cfg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position_signals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/joint-positions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/end-effector-pose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pose_position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto_window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# the plugin's own floor, and the shortest usable window
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overlap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;health_enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;health_cfg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;health_channels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/joint-positions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/end-effector-pose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outliers_enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; A 2-second window on a 5.4-second episode, with 50% overlap between consecutive windows, works out to roughly four windows per episode. That’s enough for the plugin’s median-versus-worst-window summary to mean something, but nowhere close to the roughly 48 windows a longer, 87-second episode from a different corpus (&lt;code&gt;ABC-130k&lt;/code&gt;, covered in the companion posts above) would produce. Trust the overall ranking here; don’t lean too hard on any single window’s flag the way you could with a longer recording. As a sanity check, this episode was first scored within a 30-episode slice of the same task, then again across the full 127 episodes. Its rank held steady at 8th-smoothest both times, but the underlying z-scores shifted between runs, because they’re fit fresh against whatever batch you score. That’s a property of the method, not a bug: a score always means “relative to this batch,” never “relative to some universal scale.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Read the ranking against the success label
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fic4lAaaVgPbQK7nC3wG9K4KWJATEsyfWwCYWPXh7y9w%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2Fic4lAaaVgPbQK7nC3wG9K4KWJATEsyfWwCYWPXh7y9w%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne Episode Quality panel showing motion smoothness histograms and a worst-first ranking table for 127 RubiksCubeTask episodes, then filtered by success label." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne Episode Quality panel showing motion smoothness histograms and a worst-first ranking table for 127 RubiksCubeTask episodes, then filtered by success label.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once scoring finishes, each sample includes a &lt;code&gt;quality&lt;/code&gt; field containing all computed metrics. Sorting by &lt;code&gt;overall_score&lt;/code&gt; and printing the &lt;code&gt;success&lt;/code&gt; label next to it is what actually surfaces the interesting cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;take&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overall_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_flags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outlier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_outlier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that, and the ten smoothest episodes in the dataset print out worst-to-best by score, with &lt;code&gt;success&lt;/code&gt;: False sitting a few rows down from the very top instead of clustered at the bottom where you’d expect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RoboLab-EgoX episode that broke the story
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FG6ajbsZMQbCli1QLlzwendOlxXF8FT8QjQy9pZbd8xg%2Fanimated.gif%3Fwidth%3D640" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fimage.mux.com%2FG6ajbsZMQbCli1QLlzwendOlxXF8FT8QjQy9pZbd8xg%2Fanimated.gif%3Fwidth%3D640" alt="FiftyOne multimodal viewer of a RubiksCubeTask episode with ego and wrist cameras showing a robot arm above a cube and bowl, alongside the episode's channel list.&amp;quot; Confirm this is actually cape_hill_sf0 before naming it so, since the post says neither episode has been watched." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FiftyOne multimodal viewer of a RubiksCubeTask episode with ego and wrist cameras showing a robot arm above a cube and bowl, alongside the episode's channel list." Confirm this is actually cape_hill_sf0 before naming it so, since the post says neither episode has been watched.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RubiksCubeTask_cape_hill_sf0&lt;/code&gt;, instruction “Put the cube in the bowl,” &lt;code&gt;success: False&lt;/code&gt;, has an &lt;code&gt;overall_score&lt;/code&gt; of -0.30, which makes it the 8th-smoothest episode out of 127. Its SPARC (-2.69) and LDLJ (-8.76) both sit near the clean end of the batch’s own distribution, and n_flags reads 0, meaning nothing crossed a warn threshold on the episode’s median. It doesn’t show up on the outlier tab either; &lt;code&gt;is_outlier: False&lt;/code&gt;. Every mechanism the plugin runs, scored the same way described above, comes back and says this episode is fine.&lt;/p&gt;

&lt;p&gt;For comparison, &lt;code&gt;RubiksCubeTask_photo_studio_01_sf0&lt;/code&gt; is the single smoothest episode in the entire batch, ranked first of 127 with an &lt;code&gt;overall_score&lt;/code&gt; of -0.85, and it’s also labeled &lt;code&gt;success: False&lt;/code&gt;. That’s the more dramatic number of the two, but it does register as a statistical outlier (&lt;code&gt;is_outlier: True&lt;/code&gt;): an isolation-forest and nearest-neighbor check, fit across the whole batch, flags it as unusual, even though unusual doesn’t automatically mean bad. &lt;code&gt;cape_hill_sf0&lt;/code&gt; also clears that check, which is why it's the cleaner example of the two. Nothing about it, not its smoothness scores and not its statistical profile, would have made anyone look twice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Episode&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Success label&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Rank (of 127)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;overall_score&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;is_outlier&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RubiksCubeTask_cape_hill_sf0&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;8th smoothest&lt;/td&gt;
&lt;td&gt;-0.30&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RubiksCubeTask_photo_studio_01_sf0&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;1st smoothest&lt;/td&gt;
&lt;td&gt;-0.85&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Two labeled failures in the RubiksCubeTask batch rank among the smoothest episodes by every motion metric, but only cape_hill_sf0 also clears outlier detection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest.&lt;/strong&gt; &lt;code&gt;RoboLab-EgoX&lt;/code&gt; episodes are &lt;a href="https://voxel51.com/blog/synthetic-data-curation-physical-ai" rel="noopener noreferrer"&gt;simulated policy rollouts&lt;/a&gt; recorded on NVIDIA’s RoboLab benchmark, not &lt;a href="https://voxel51.com/teleoperation" rel="noopener noreferrer"&gt;human teleoperation&lt;/a&gt;, so it’s worth saying plainly that this isn’t real-world hardware. The &lt;code&gt;success&lt;/code&gt; field is the benchmark’s own ground-truth task-completion label, decided by the simulator, not a model’s prediction layered on after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for triage, not gates
&lt;/h2&gt;

&lt;p&gt;Run this same scorer over a different corpus, &lt;code&gt;ABC-130k&lt;/code&gt;, 40 teleoperated manipulation episodes spanning 40 different tasks, and its single lowest-quality episode by verdict turns out to be a sensor dropout: a channel that simply stopped reporting partway through the recording. That’s a real, common failure mode, and the scorer catches it well, because dropout is fundamentally a timestamp problem and sensor-health metrics read exactly that. But it’s a different failure mode from a wrong-but-smooth action, and finding a real example of the second kind took scoring a second corpus, one with genuine success and failure labels to check the ranking against, rather than assuming ABC-130k would eventually produce one.&lt;/p&gt;

&lt;p&gt;That’s the actual argument for scoring episodes at all. Not that it replaces a person watching the recording, but that it tells that person which four episodes out of forty, or which eight out of a hundred and twenty-seven, are worth the ninety seconds it takes to look. &lt;code&gt;cape_hill_sf0&lt;/code&gt; would never have made that list on smoothness alone; smoothness is exactly what says it’s fine. It made the list only because someone thought to sort by score and then cross-reference against a label the scorer itself never sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/datasets/Voxel51/RoboLab-EgoX" rel="noopener noreferrer"&gt;Voxel51/RoboLab-EgoX on Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/harpreetsahota204/demo_quality_scorer" rel="noopener noreferrer"&gt;demo_quality_scorer on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://voxel51.com/blog/robot-data-quality-scoring" rel="noopener noreferrer"&gt;Everyone Is Scoring Robot Data, Nobody Is Looking at It&lt;/a&gt;, the full quality-metric stack, including sensor health and outliers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://voxel51.com/blog/robot-episode-quality-triage" rel="noopener noreferrer"&gt;Smooth Isn’t the Same as Correct&lt;/a&gt;, SPARC, LDLJ, jerk RMS, and the PSD ratio worked through in detail&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://voxel51.com/blog/view-mcap-files-fiftyone" rel="noopener noreferrer"&gt;Open MCAP Robotics Logs in FiftyOne, No ROS Needed&lt;/a&gt;, what the multimodal viewer actually does with a file like this&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an “episode” in robot data, exactly?
&lt;/h3&gt;

&lt;p&gt;One continuous recording of a robot attempting a task, start to finish, with every sensor it carries (cameras, joint encoders, whatever else is onboard) logged together on a shared timeline rather than split into separate files. It’s the natural unit for this kind of data because the sensors are all ticking at their own independent rates for the same stretch of time, and splitting that into individual frames or per-sensor files throws away the relationships between streams that actually matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just widen the window instead of shrinking it to 2 seconds?
&lt;/h3&gt;

&lt;p&gt;Because 2 seconds is already the plugin’s floor, not an arbitrary choice made for this post. &lt;code&gt;AUTO_MIN_WINDOW_S&lt;/code&gt; in &lt;code&gt;demo_quality_scorer&lt;/code&gt;’s own windowing code is set to 2.0 regardless of what automatic windowing would otherwise compute for a given signal. There’s no smaller manual value that stays meaningful for a duration-sensitive metric like LDLJ, whose value scales roughly with window length to the fourth power, which means comparing windows of different lengths to each other isn’t valid in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is RoboLab-EgoX real robot data?
&lt;/h3&gt;

&lt;p&gt;No, it’s simulated. The episodes are policy rollouts recorded on NVIDIA’s RoboLab manipulation benchmark and converted to MCAP by Voxel51, not real-world teleoperated demonstrations collected on physical hardware. The finding here, that smoothness and task success are different properties a scorer can’t conflate, generalizes well beyond this one dataset. The specific numbers and the specific episode do not represent physical hardware, and that distinction is worth repeating whenever this example comes up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a low overall_score mean an episode is good?
&lt;/h3&gt;

&lt;p&gt;Only relative to whatever batch it was scored against. &lt;code&gt;overall_score&lt;/code&gt; is a weighted mean of z-scores, fit fresh every time the scorer runs, oriented so that a higher number always means worse. That makes it useful for ranking episodes within a single run, but it’s not comparable across different scoring runs, different signal choices, or different datasets. A score of -0.30 in this batch says nothing about what -0.30 would mean in a different one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does n_flags: 0 actually rule out?
&lt;/h3&gt;

&lt;p&gt;Only that no individual metric’s episode-level median crossed the warn threshold (a z-score of 2 or higher) against this batch’s own distribution. It doesn’t rule out one rough window inside an otherwise clean episode, which &lt;code&gt;demo_quality_scorer&lt;/code&gt; tracks separately as a flagged interval, and it says nothing at all about whether the task was actually completed, which is not something any motion metric here is designed to measure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did the smoothest RoboLab-EgoX episode fail the task?
&lt;/h3&gt;

&lt;p&gt;Because smoothness metrics measure how the arm moved, not whether the cube landed in the bowl, so a fluid trajectory to the wrong place scores as clean.&lt;/p&gt;

</description>
      <category>robolabegox</category>
      <category>robotics</category>
      <category>computervision</category>
      <category>python</category>
    </item>
  </channel>
</rss>
