Spaces:
Running
Running
Add top jerky episodes
Browse files- README.md +1 -1
- src/app/[org]/[dataset]/[episode]/fetch-data.ts +19 -1
- src/app/layout.tsx +2 -2
- src/app/page.tsx +1 -1
- src/components/action-insights-panel.tsx +43 -3
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# LeRobot Dataset Visualizer
|
| 2 |
|
| 3 |
-
LeRobot Dataset Visualizer is a web application for interactive exploration and visualization of robotics datasets, particularly those in the LeRobot format. It enables users to browse, view, and analyze episodes from large-scale robotics datasets, combining synchronized video playback with rich, interactive data graphs.
|
| 4 |
|
| 5 |
## Project Overview
|
| 6 |
|
|
|
|
| 1 |
# LeRobot Dataset Visualizer
|
| 2 |
|
| 3 |
+
LeRobot Dataset Tool and Visualizer is a web application for interactive exploration and visualization of robotics datasets, particularly those in the LeRobot format. It enables users to browse, view, and analyze episodes from large-scale robotics datasets, combining synchronized video playback with rich, interactive data graphs.
|
| 4 |
|
| 5 |
## Project Overview
|
| 6 |
|
src/app/[org]/[dataset]/[episode]/fetch-data.ts
CHANGED
|
@@ -1391,6 +1391,11 @@ export type AggAlignment = {
|
|
| 1391 |
numPairs: number;
|
| 1392 |
};
|
| 1393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1394 |
export type CrossEpisodeVarianceData = {
|
| 1395 |
actionNames: string[];
|
| 1396 |
timeBins: number[];
|
|
@@ -1400,6 +1405,7 @@ export type CrossEpisodeVarianceData = {
|
|
| 1400 |
aggVelocity: AggVelocityStat[];
|
| 1401 |
aggAutocorrelation: AggAutocorrelation | null;
|
| 1402 |
speedDistribution: SpeedDistEntry[];
|
|
|
|
| 1403 |
multimodality: number[][] | null;
|
| 1404 |
trajectoryClustering: TrajectoryClustering | null;
|
| 1405 |
aggAlignment: AggAlignment | null;
|
|
@@ -1692,6 +1698,18 @@ export async function loadCrossEpisodeActionVariance(
|
|
| 1692 |
return { chartData, suggestedChunk, shortKeys };
|
| 1693 |
})();
|
| 1694 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1695 |
// Speed distribution: all episode movement scores (not just lowest 10)
|
| 1696 |
const speedDistribution: SpeedDistEntry[] = movementScores.map(s => ({
|
| 1697 |
episodeIndex: s.episodeIndex,
|
|
@@ -1967,7 +1985,7 @@ export async function loadCrossEpisodeActionVariance(
|
|
| 1967 |
return {
|
| 1968 |
actionNames, timeBins, variance, numEpisodes: episodeActions.length,
|
| 1969 |
lowMovementEpisodes, aggVelocity, aggAutocorrelation,
|
| 1970 |
-
speedDistribution, multimodality, trajectoryClustering, aggAlignment,
|
| 1971 |
};
|
| 1972 |
}
|
| 1973 |
|
|
|
|
| 1391 |
numPairs: number;
|
| 1392 |
};
|
| 1393 |
|
| 1394 |
+
export type JerkyEpisode = {
|
| 1395 |
+
episodeIndex: number;
|
| 1396 |
+
meanAbsDelta: number;
|
| 1397 |
+
};
|
| 1398 |
+
|
| 1399 |
export type CrossEpisodeVarianceData = {
|
| 1400 |
actionNames: string[];
|
| 1401 |
timeBins: number[];
|
|
|
|
| 1405 |
aggVelocity: AggVelocityStat[];
|
| 1406 |
aggAutocorrelation: AggAutocorrelation | null;
|
| 1407 |
speedDistribution: SpeedDistEntry[];
|
| 1408 |
+
jerkyEpisodes: JerkyEpisode[];
|
| 1409 |
multimodality: number[][] | null;
|
| 1410 |
trajectoryClustering: TrajectoryClustering | null;
|
| 1411 |
aggAlignment: AggAlignment | null;
|
|
|
|
| 1698 |
return { chartData, suggestedChunk, shortKeys };
|
| 1699 |
})();
|
| 1700 |
|
| 1701 |
+
// Per-episode jerkiness: mean |Ξa| across all dimensions
|
| 1702 |
+
const jerkyEpisodes: JerkyEpisode[] = episodeActions.map(({ index, actions: ep }) => {
|
| 1703 |
+
let sum = 0, count = 0;
|
| 1704 |
+
for (let t = 1; t < ep.length; t++) {
|
| 1705 |
+
for (let d = 0; d < actionDim; d++) {
|
| 1706 |
+
sum += Math.abs((ep[t][d] ?? 0) - (ep[t - 1][d] ?? 0));
|
| 1707 |
+
count++;
|
| 1708 |
+
}
|
| 1709 |
+
}
|
| 1710 |
+
return { episodeIndex: index, meanAbsDelta: count > 0 ? sum / count : 0 };
|
| 1711 |
+
}).sort((a, b) => b.meanAbsDelta - a.meanAbsDelta);
|
| 1712 |
+
|
| 1713 |
// Speed distribution: all episode movement scores (not just lowest 10)
|
| 1714 |
const speedDistribution: SpeedDistEntry[] = movementScores.map(s => ({
|
| 1715 |
episodeIndex: s.episodeIndex,
|
|
|
|
| 1985 |
return {
|
| 1986 |
actionNames, timeBins, variance, numEpisodes: episodeActions.length,
|
| 1987 |
lowMovementEpisodes, aggVelocity, aggAutocorrelation,
|
| 1988 |
+
speedDistribution, jerkyEpisodes, multimodality, trajectoryClustering, aggAlignment,
|
| 1989 |
};
|
| 1990 |
}
|
| 1991 |
|
src/app/layout.tsx
CHANGED
|
@@ -5,8 +5,8 @@ import "./globals.css";
|
|
| 5 |
const inter = Inter({ subsets: ["latin"] });
|
| 6 |
|
| 7 |
export const metadata: Metadata = {
|
| 8 |
-
title: "LeRobot Dataset Visualizer",
|
| 9 |
-
description: "
|
| 10 |
};
|
| 11 |
|
| 12 |
export default function RootLayout({
|
|
|
|
| 5 |
const inter = Inter({ subsets: ["latin"] });
|
| 6 |
|
| 7 |
export const metadata: Metadata = {
|
| 8 |
+
title: "LeRobot Dataset Tool and Visualizer",
|
| 9 |
+
description: "Tool and Visualizer for LeRobot Datasets",
|
| 10 |
};
|
| 11 |
|
| 12 |
export default function RootLayout({
|
src/app/page.tsx
CHANGED
|
@@ -128,7 +128,7 @@ function HomeInner() {
|
|
| 128 |
{/* Centered Content */}
|
| 129 |
<div className="relative z-10 h-screen flex flex-col items-center justify-center text-white text-center">
|
| 130 |
<h1 className="text-4xl md:text-5xl font-bold mb-6 drop-shadow-lg">
|
| 131 |
-
LeRobot Dataset Visualizer
|
| 132 |
</h1>
|
| 133 |
<form onSubmit={handleGo} className="flex gap-2 justify-center mt-6">
|
| 134 |
<input
|
|
|
|
| 128 |
{/* Centered Content */}
|
| 129 |
<div className="relative z-10 h-screen flex flex-col items-center justify-center text-white text-center">
|
| 130 |
<h1 className="text-4xl md:text-5xl font-bold mb-6 drop-shadow-lg">
|
| 131 |
+
LeRobot Dataset Tool and Visualizer
|
| 132 |
</h1>
|
| 133 |
<form onSubmit={handleGo} className="flex gap-2 justify-center mt-6">
|
| 134 |
<input
|
src/components/action-insights-panel.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
| 10 |
ResponsiveContainer,
|
| 11 |
Tooltip,
|
| 12 |
} from "recharts";
|
| 13 |
-
import type { CrossEpisodeVarianceData, LowMovementEpisode, AggVelocityStat, AggAutocorrelation, SpeedDistEntry, TrajectoryClustering, AggAlignment } from "@/app/[org]/[dataset]/[episode]/fetch-data";
|
| 14 |
|
| 15 |
const DELIMITER = " | ";
|
| 16 |
const COLORS = [
|
|
@@ -171,7 +171,7 @@ function AutocorrelationSection({ data, fps, agg, numEpisodes }: { data: Record<
|
|
| 171 |
|
| 172 |
// βββ Action Velocity βββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
|
| 174 |
-
function ActionVelocitySection({ data, agg, numEpisodes }: { data: Record<string, number>[]; agg?: AggVelocityStat[]; numEpisodes?: number }) {
|
| 175 |
const actionKeys = useMemo(() => (data.length > 0 ? getActionKeys(data[0]) : []), [data]);
|
| 176 |
|
| 177 |
const fallbackStats = useMemo(() => {
|
|
@@ -299,6 +299,46 @@ function ActionVelocitySection({ data, agg, numEpisodes }: { data: Record<string
|
|
| 299 |
</ul>
|
| 300 |
<p className="text-xs text-slate-500 pt-1">{insight.tip}</p>
|
| 301 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
</div>
|
| 303 |
);
|
| 304 |
}
|
|
@@ -1135,7 +1175,7 @@ const ActionInsightsPanel: React.FC<ActionInsightsPanelProps> = ({
|
|
| 1135 |
</div>
|
| 1136 |
|
| 1137 |
<AutocorrelationSection data={flatChartData} fps={fps} agg={showAgg ? crossEpisodeData?.aggAutocorrelation : null} numEpisodes={crossEpisodeData?.numEpisodes} />
|
| 1138 |
-
<ActionVelocitySection data={flatChartData} agg={showAgg ? crossEpisodeData?.aggVelocity : undefined} numEpisodes={crossEpisodeData?.numEpisodes} />
|
| 1139 |
|
| 1140 |
{crossEpisodeData?.speedDistribution && crossEpisodeData.speedDistribution.length > 2 && (
|
| 1141 |
<SpeedVarianceSection distribution={crossEpisodeData.speedDistribution} numEpisodes={crossEpisodeData.numEpisodes} />
|
|
|
|
| 10 |
ResponsiveContainer,
|
| 11 |
Tooltip,
|
| 12 |
} from "recharts";
|
| 13 |
+
import type { CrossEpisodeVarianceData, LowMovementEpisode, AggVelocityStat, AggAutocorrelation, SpeedDistEntry, JerkyEpisode, TrajectoryClustering, AggAlignment } from "@/app/[org]/[dataset]/[episode]/fetch-data";
|
| 14 |
|
| 15 |
const DELIMITER = " | ";
|
| 16 |
const COLORS = [
|
|
|
|
| 171 |
|
| 172 |
// βββ Action Velocity βββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
|
| 174 |
+
function ActionVelocitySection({ data, agg, numEpisodes, jerkyEpisodes }: { data: Record<string, number>[]; agg?: AggVelocityStat[]; numEpisodes?: number; jerkyEpisodes?: JerkyEpisode[] }) {
|
| 175 |
const actionKeys = useMemo(() => (data.length > 0 ? getActionKeys(data[0]) : []), [data]);
|
| 176 |
|
| 177 |
const fallbackStats = useMemo(() => {
|
|
|
|
| 299 |
</ul>
|
| 300 |
<p className="text-xs text-slate-500 pt-1">{insight.tip}</p>
|
| 301 |
</div>
|
| 302 |
+
|
| 303 |
+
{jerkyEpisodes && jerkyEpisodes.length > 0 && <JerkyEpisodesList episodes={jerkyEpisodes} />}
|
| 304 |
+
</div>
|
| 305 |
+
);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function JerkyEpisodesList({ episodes }: { episodes: JerkyEpisode[] }) {
|
| 309 |
+
const [showAll, setShowAll] = useState(false);
|
| 310 |
+
const display = showAll ? episodes : episodes.slice(0, 15);
|
| 311 |
+
|
| 312 |
+
return (
|
| 313 |
+
<div className="bg-slate-900/60 rounded-md px-4 py-3 border border-slate-700/60 space-y-2">
|
| 314 |
+
<div className="flex items-center justify-between">
|
| 315 |
+
<p className="text-sm font-medium text-slate-200">
|
| 316 |
+
Most Jerky Episodes <span className="text-xs text-slate-500 font-normal">sorted by mean |Ξa|</span>
|
| 317 |
+
</p>
|
| 318 |
+
{episodes.length > 15 && (
|
| 319 |
+
<button onClick={() => setShowAll(v => !v)} className="text-xs text-slate-400 hover:text-slate-200 transition-colors">
|
| 320 |
+
{showAll ? "Show top 15" : `Show all ${episodes.length}`}
|
| 321 |
+
</button>
|
| 322 |
+
)}
|
| 323 |
+
</div>
|
| 324 |
+
<div className="max-h-48 overflow-y-auto">
|
| 325 |
+
<table className="w-full text-xs">
|
| 326 |
+
<thead>
|
| 327 |
+
<tr className="text-slate-500 border-b border-slate-700">
|
| 328 |
+
<th className="text-left py-1 pr-3">Episode</th>
|
| 329 |
+
<th className="text-right py-1">Mean |Ξa|</th>
|
| 330 |
+
</tr>
|
| 331 |
+
</thead>
|
| 332 |
+
<tbody>
|
| 333 |
+
{display.map(e => (
|
| 334 |
+
<tr key={e.episodeIndex} className="border-b border-slate-800/40 text-slate-300">
|
| 335 |
+
<td className="py-1 pr-3">ep {e.episodeIndex}</td>
|
| 336 |
+
<td className="py-1 text-right tabular-nums">{e.meanAbsDelta.toFixed(4)}</td>
|
| 337 |
+
</tr>
|
| 338 |
+
))}
|
| 339 |
+
</tbody>
|
| 340 |
+
</table>
|
| 341 |
+
</div>
|
| 342 |
</div>
|
| 343 |
);
|
| 344 |
}
|
|
|
|
| 1175 |
</div>
|
| 1176 |
|
| 1177 |
<AutocorrelationSection data={flatChartData} fps={fps} agg={showAgg ? crossEpisodeData?.aggAutocorrelation : null} numEpisodes={crossEpisodeData?.numEpisodes} />
|
| 1178 |
+
<ActionVelocitySection data={flatChartData} agg={showAgg ? crossEpisodeData?.aggVelocity : undefined} numEpisodes={crossEpisodeData?.numEpisodes} jerkyEpisodes={showAgg ? crossEpisodeData?.jerkyEpisodes : undefined} />
|
| 1179 |
|
| 1180 |
{crossEpisodeData?.speedDistribution && crossEpisodeData.speedDistribution.length > 2 && (
|
| 1181 |
<SpeedVarianceSection distribution={crossEpisodeData.speedDistribution} numEpisodes={crossEpisodeData.numEpisodes} />
|