SampleViewer / egoSample /faberu.html
faradayfuture's picture
egoSample/faberu: split point cloud into map (0.5 MB, first) / scans (1.8 MB) / depth-hover grid (0.9 MB, last); delta-encoded
d01ec1a verified
Raw History Blame Contribute Delete
39.2 kB
<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Faber U Teleop Viewer</title>
<style>
:root{color-scheme:dark}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{background:#07090d;color:#e6e8ee;font:14px/1.5 -apple-system,"Helvetica Neue",Arial,sans-serif}
a{color:#6f92c8}
.main{height:100vh;overflow-y:auto}
header{padding:14px 20px 10px}
header h1{margin:0 0 3px;font-size:19px}
.sub{color:#8890a0;font-size:12.5px}.sub code{color:#c7ccd6;background:#12151c;padding:1px 5px;border-radius:4px}
.badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}
.bdg{background:#12151c;border:1px solid #1c2028;border-radius:999px;padding:2px 9px;font-size:11px;color:#9aa1ad}
.bdg b{color:#e6e8ee}
.bdg.tag{color:#07090d;background:#6f9fe0;border-color:#6f9fe0;font-weight:700}
.layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:14px;padding:0 20px}
@media(max-width:1360px){.layout{grid-template-columns:1fr}}
.panel{background:#0e1118;border:1px solid #1c2028;border-radius:8px}
.vgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;padding:8px}
@media(max-width:700px){.vgrid{grid-template-columns:1fr}}
.vp{position:relative;background:#000;border-radius:4px;overflow:hidden;aspect-ratio:5/4;min-width:0}
.vp.big{grid-column:span 2;grid-row:span 2}
.vp video{width:100%;height:100%;object-fit:contain;display:block}
.vp .lbl{position:absolute;top:5px;left:6px;font:10.5px ui-monospace,monospace;color:#bcd2e6;background:rgba(0,0,0,.5);padding:1px 5px;border-radius:3px;pointer-events:none}
.vp .rd{position:absolute;bottom:5px;right:6px;font:11px ui-monospace,monospace;color:#fff;background:rgba(0,0,0,.6);padding:1px 6px;border-radius:3px;pointer-events:none}
.three{position:relative;height:clamp(260px,38vh,420px);margin:0 8px 8px;background:#0b0d12;border:1px solid #1c2028;border-radius:6px;overflow:hidden}
.three canvas{display:block}
.three .lbl{position:absolute;top:6px;left:9px;font:12px ui-monospace,monospace;color:#9aa1ad;pointer-events:none;z-index:1}
.three .hint{position:absolute;bottom:6px;right:9px;font-size:10px;color:#556;pointer-events:none;z-index:1}
.chk{display:flex;flex-wrap:wrap;gap:4px 14px;padding:0 12px 8px;font-size:11.5px;color:#9aa1ad}
.chk label{cursor:pointer;display:flex;gap:4px;align-items:center}
.spans-head{padding:12px 14px 8px;font-size:12px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase}
.spans-head b{color:#6f9fe0}
.tl{height:9px;border-radius:4px;margin:0 14px 6px;display:flex;overflow:hidden;cursor:pointer;background:#12151c}
.tl i{height:100%}
.chips{display:flex;flex-wrap:wrap;gap:5px;padding:0 14px 10px}
.chip{min-width:24px;height:21px;padding:0 5px;display:flex;align-items:center;justify-content:center;border-radius:5px;font-size:10.5px;font-weight:700;cursor:pointer;color:#07090d}
.chip.on{outline:2px solid #fff}
.slist{max-height:560px;overflow-y:auto}
.si{border-left:3px solid transparent;border-radius:4px;padding:7px 10px;margin:0 10px 5px;cursor:pointer;background:#12151c}
.si:hover{background:#171b24}.si.on{background:#182339;border-left-color:#6f9fe0}
.si .n{display:inline-flex;min-width:17px;height:17px;padding:0 3px;border-radius:9px;color:#07090d;font-size:10px;font-weight:700;align-items:center;justify-content:center;margin-right:6px}
.si .k{font-size:13px}.si .m{font-size:10.5px;color:#6f92c8;font-family:ui-monospace,monospace;margin-top:2px}
.si .lb{font-size:11px;color:#8890a0}
.legend{display:flex;flex-wrap:wrap;gap:6px 12px;padding:10px 20px 0;color:#8890a0;font-size:11.5px}
.legend .it{display:flex;align-items:center;gap:5px}
.sw{width:11px;height:11px;border-radius:3px;display:inline-block}
.controls{display:flex;align-items:center;gap:12px;padding:9px 16px;margin:14px 20px 0}
.controls button{background:#1b2740;color:#cfe0ff;border:1px solid #2b3654;border-radius:6px;padding:5px 13px;cursor:pointer;font-size:15px}
.controls input[type=range]{flex:1;min-width:0}
.controls .t{font-variant-numeric:tabular-nums;color:#c7ccd6;font-size:13px;white-space:nowrap}
.controls select{background:#12151c;color:#c7ccd6;border:1px solid #2b3654;border-radius:5px;font-size:12px;padding:3px 4px}
.note{margin:10px 20px 0;padding:9px 14px;font-size:12px;color:#9aa1ad;border-left:3px solid #b8860b;background:#14130c;border-radius:4px}
.note b{color:#d9c48a}
.readout{margin:14px 20px 0;padding:13px 18px}
.rt{font-size:11px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase;margin-bottom:9px}
.rg{display:grid;grid-template-columns:repeat(3,1fr);gap:8px 26px}
@media(max-width:900px){.rg{grid-template-columns:1fr 1fr}}
.ro{display:flex;justify-content:space-between;border-bottom:1px dashed #1c2028;padding-bottom:4px;gap:8px}
.ro .k{color:#9aa1ad}.ro .v{font-variant-numeric:tabular-nums;text-align:right}.ro .v.b{color:#7fb2ff}.ro .v.hi{color:#6fe08a}
.charts{margin:12px 20px 0;display:grid;grid-template-columns:1fr 1fr;gap:8px}
@media(max-width:1100px){.charts{grid-template-columns:1fr}}
.chp{padding:6px 14px 8px}
.chp .l{font-size:11px;color:#9aa1ad}.chp .u{color:#5a6373;font-size:10px;margin-left:6px}
.chp canvas{width:100%;height:84px;display:block;cursor:pointer}
.leg{display:inline-flex;flex-wrap:wrap;gap:4px 9px;margin-left:8px;font-size:10.5px}
.leg i{width:9px;height:9px;border-radius:2px;display:inline-block;margin-right:3px;vertical-align:-1px}
.files{display:flex;flex-wrap:wrap;gap:7px}
.fc{background:#12151c;border:1px solid #1c2028;border-radius:5px;padding:4px 9px;font:11.5px ui-monospace,monospace;color:#c7ccd6;text-decoration:none;display:inline-block}
.fc.dl{color:#7fe0a0;border-color:#24402f}.fc.dl:hover{background:#16211a;border-color:#3d9970}
.cpy{background:#12151c;border:1px solid #2b3654;color:#9fc0ff;border-radius:5px;padding:4px 9px;font:11.5px ui-monospace,monospace;cursor:pointer}
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"}}</script>
</head>
<body>
<div class="main" id="main"></div>
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"><link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js">
<script id="D" type="application/json">{"eps": [{"slug": "restock_20260916_ep000", "dataset_dir": "faberu_restock_20260916_ep000", "task": "restock_20260916", "leaf": "Restocking a retail shelf", "leaf_instr": "Fetch a bottle from the table and restock the shelf", "mode": "VR teleoperation, mobile manipulation", "perspective": "Robot egocentric", "planned": true, "rationale": "We can collect data from robots deployed in real-world working scenes. This episode is included in Ego Sample to showcase that sensor capability: a deployed robot captures rich, synchronized multi-sensor data (stereo, fisheye and color cameras, metric depth, dual lidar, IMU, odometry, joint and teleop streams). Robot-egocentric capture is a planned data line.", "ep": 0, "task_name": "Retail shelf restocking (mobile manipulation)", "instruction": "Drive from the shelf to the table, pick up the bottle with the right gripper, carry it back and place it on the shelf", "duration": 80.4, "nframes": 2412, "fps": 30, "hw": "Faber U", "vendor": "FF", "region": "US", "tags": ["US"], "taxo": {"environment_l1": "Retail and Consumer Goods", "environment_l2": "Grocery Store", "environment_l3": "Shelf Aisle", "naics_primary_code": "445110", "naics_primary_title": "Supermarkets and Other Grocery Retailers", "taxonomy_source": "OpenAI Robotics Data Requirements 08-21-2026 (Environment/Skill Group/Jobs taxonomies)", "collection_setup": "staged retail shelf in a lab space", "skill_group": "Inventory & Stock Management", "job_family": "Stock clerk", "task_difficulty": "Medium"}, "n_segments": 22, "skills": {"Navigate": 7, "Wait": 3, "Rotate": 3, "Open": 2, "Retract": 2, "Close": 1, "Pick": 1, "Lift": 1, "Reach": 1, "Place": 1}, "skill_list": ["Close", "Lift", "Navigate", "Open", "Pick", "Place", "Reach", "Retract", "Rotate", "Wait"], "qc": {"status": "pass", "total": 10, "passed": 8, "warnings": 2, "failures": 0}, "state_dim": 27, "action_dim": 19, "depth_valid": 0.847, "lidar_scans": [810, 810], "segments": [{"skill": "Wait", "label": "The robot stands in front of the retail shelf while the teleoperator gets ready", "ts": 0.0, "te": 13.0, "mistake": false}, {"skill": "Close", "label": "The right gripper closes to its travel position", "ts": 13.0, "te": 14.25, "mistake": false}, {"skill": "Navigate", "label": "The robot backs away from the shelf", "ts": 14.25, "te": 16.75, "mistake": false}, {"skill": "Rotate", "label": "The robot turns about 95 degrees right to face the work table", "ts": 16.75, "te": 20.0, "mistake": false}, {"skill": "Wait", "label": "The robot pauses facing the table", "ts": 20.0, "te": 21.25, "mistake": false}, {"skill": "Navigate", "label": "The robot drives forward toward the table", "ts": 21.25, "te": 23.25, "mistake": false}, {"skill": "Rotate", "label": "The robot corrects its heading toward the table", "ts": 23.25, "te": 25.25, "mistake": false}, {"skill": "Navigate", "label": "The robot drives up to the table edge and stops", "ts": 25.25, "te": 32.0, "mistake": false}, {"skill": "Open", "label": "The right gripper opens in front of the bottle on the table", "ts": 32.0, "te": 34.0, "mistake": false}, {"skill": "Pick", "label": "The right arm reaches the bottle on the table and the gripper closes on it", "ts": 34.0, "te": 40.25, "mistake": false}, {"skill": "Lift", "label": "The right arm lifts the bottle and retracts it close to the body", "ts": 40.25, "te": 44.25, "mistake": false}, {"skill": "Navigate", "label": "The robot drives back toward the shelf while holding the bottle", "ts": 44.25, "te": 48.5, "mistake": false}, {"skill": "Rotate", "label": "The robot turns about 95 degrees left to face the shelf", "ts": 48.5, "te": 52.0, "mistake": false}, {"skill": "Navigate", "label": "The robot aligns itself with the shelf", "ts": 52.0, "te": 54.75, "mistake": false}, {"skill": "Reach", "label": "The right arm raises the bottle toward the middle shelf", "ts": 54.75, "te": 61.0, "mistake": false}, {"skill": "Navigate", "label": "The robot repositions sideways along the shelf to reach a free slot", "ts": 61.0, "te": 65.75, "mistake": false}, {"skill": "Place", "label": "The right arm moves the bottle onto the middle shelf", "ts": 65.75, "te": 70.25, "mistake": false}, {"skill": "Open", "label": "The right gripper opens to release the bottle on the shelf", "ts": 70.25, "te": 71.5, "mistake": false}, {"skill": "Retract", "label": "The right arm withdraws from the shelf", "ts": 71.5, "te": 74.0, "mistake": false}, {"skill": "Navigate", "label": "The robot backs away from the shelf and straightens up", "ts": 74.0, "te": 76.75, "mistake": false}, {"skill": "Retract", "label": "Both arms return to the home pose", "ts": 76.75, "te": 78.5, "mistake": false}, {"skill": "Wait", "label": "The robot stops; episode ends", "ts": 78.5, "te": 80.4, "mistake": false}]}], "skc": {"Clip": "#4f83cc", "Close": "#e0913a", "CloseBox": "#5aa469", "Cut": "#c0504d", "Dip": "#8064a2", "Drop": "#4bacc6", "Flip": "#d98594", "Fold": "#9b8f2e", "HandOver": "#7f8fa6", "Hang": "#674ea7", "Insert": "#3d9970", "Knead": "#b8860b", "Open": "#a0522d", "Paint": "#2e8b8b", "Pick": "#b5651d", "Place": "#6a5acd", "Plug": "#556b2f", "Pour": "#8b3a62", "Press": "#487eb0", "PressButton": "#cc8e35", "Pull": "#6b8e23", "Rinse": "#9370db", "Rotate": "#20b2aa", "Scan": "#cd853f", "Scoop": "#4f83cc", "Screw (tighten)": "#e0913a", "Smear": "#5aa469", "Stack": "#c0504d", "Stir": "#8064a2", "Straighten": "#4bacc6", "Takedown": "#d98594", "Takeout (remove)": "#9b8f2e", "Tear": "#7f8fa6", "Wipe": "#674ea7", "dip": "#3d9970", "plug": "#b8860b", "Wait": "#5d6574", "Navigate": "#3fa7d6", "Lift": "#e07a5f", "Reach": "#81b29a", "Retract": "#b388eb"}, "hf": "https://huggingface.co/datasets/faradayfuture/egoSample"}</script>
<script>
const D=JSON.parse(document.getElementById('D').textContent);
const EPS=D.eps, SKC=D.skc, HF=D.hf;
const CAMS=['head_stereo_left','head_color','head_stereo_right','head_left_fisheye','head_depth','head_right_fisheye'];
const AX={x:'#e0616f',y:'#41c76b',z:'#4f8ff7'};
const PAL=['#e0616f','#41c76b','#4f8ff7','#e0a33a','#b57be0','#3fc1c9','#d98594'];
let EP=null, SIG=null, vids=[], master=null, playing=false, raf=0, syncT=0, _lastCi=-2, _toggle=null, setCloudT=null;
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const fmt=t=>{t=Math.max(0,t);const m=Math.floor(t/60),s=t%60;return m+':'+s.toFixed(2).padStart(5,'0');};
const skc=s=>SKC[s]||'#666';
function select(i){ EP=EPS[i]; SIG=null; cancelAnimationFrame(raf); clearInterval(syncT); playing=false; render(); loadSig(EP.slug); }
function loadSig(slug){ window.EPDATA=window.EPDATA||{};
// start every download at once; core first-priority, map next, scans next, depth-hover grid last
const P=n=>'epdata/'+slug+'.'+n;
const mapP=fetchBin(P('map'),'map'), scansP=fetchBin(P('scans'),'scans');
const merge=(k,fn)=>b=>{ if(!b||!SIG) return; fn(parseFUP(b)); if(window._addClouds) window._addClouds(); };
const done=()=>{ SIG=window.EPDATA[slug]; if(!SIG) return; sizeCharts(); drawCharts(); init3D();
mapP.then(merge('map',F=>{ SIG.map=decPts(F,0,F.hdr.n); }));
scansP.then(merge('scans',F=>{ let c=0; SIG.clouds={}; Object.entries(F.hdr.clouds).forEach(([k,fr])=>{ SIG.clouds[k]=fr.map(f=>Object.assign({t:f.t},decPts(F,c++,f.n))); }); }))
.then(()=>fetchBin(P('dgrid'),null,'low')).then(b=>{ if(!b||!SIG) return; const F=parseFUP(b), n=F.hdr.w*F.hdr.h; SIG.depthGrid=F.hdr.offs.map((o,k)=>new Uint16Array(F.buf.buffer,F.base+o,n)); });
updateUI(master.currentTime||0);
const go=()=>{ if(_toggle&&!playing) _toggle(); }; if(master.readyState>=3) go(); else master.addEventListener('canplay',go,{once:true}); };
if(window.EPDATA[slug]){ done(); return; }
fetchBin(P('core'),null).then(b=>{ window.EPDATA[slug]=JSON.parse(new TextDecoder().decode(b)); done(); })
.catch(()=>{ const s=document.createElement('script'); s.src='epdata/'+slug+'.js'; s.onload=done; document.body.appendChild(s); }); }
const _prog={};
function showProg(){ const el=document.getElementById('cloudStat'); if(!el) return;
const a=Object.entries(_prog).filter(([k,v])=>!v.done); el.textContent=a.length?'loading '+a.map(([k,v])=>`${k} ${(v.got/1e6).toFixed(1)}${v.total?'/'+(v.total/1e6).toFixed(1):''} MB`).join(' · '):''; }
async function fetchBin(url,label,prio){
try{
const r=await fetch(url,{priority:prio||'high'}); if(!r.ok) throw new Error('HTTP '+r.status);
let b;
if(label){ const total=+r.headers.get('content-length')||0, rd=r.body.getReader(), parts=[]; let got=0; _prog[label]={got:0,total};
for(;;){ const {done,value}=await rd.read(); if(done) break; parts.push(value); got+=value.length; _prog[label].got=got; showProg(); }
b=new Uint8Array(got); let o=0; for(const p of parts){ b.set(p,o); o+=p.length; } _prog[label].done=1; showProg(); }
else b=new Uint8Array(await r.arrayBuffer());
if(b[0]===0x1f&&b[1]===0x8b) b=new Uint8Array(await new Response(new Blob([b]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer());
return b;
}catch(e){ if(label){ const el=document.getElementById('cloudStat'); if(el) el.textContent=label+' failed to load: '+e.message; } return null; }
}
function parseFUP(buf){ const dv=new DataView(buf.buffer,buf.byteOffset); if(String.fromCharCode(...buf.slice(0,4))!=='FUP2') throw new Error('bad pack');
const hl=dv.getUint32(4,true), hdr=JSON.parse(new TextDecoder().decode(buf.slice(8,8+hl))); let base=8+hl; base+=(4-base%4)%4; return {buf,hdr,base}; }
/* planar per-axis deltas (int16 cm) + intensity -> interleaved Int16 xyz (cm) */
function decPts(F,k,n){ const o=F.base+F.hdr.offs[k], d=new Int16Array(F.buf.buffer,F.buf.byteOffset+o,n*3), q=new Int16Array(n*3);
for(let a=0;a<3;a++){ let acc=0; for(let m=0;m<n;m++){ acc+=d[a*n+m]; q[3*m+a]=acc; } }
return {q, I:new Uint8Array(F.buf.buffer,F.buf.byteOffset+o+n*6,n)}; }
function render(){
const e=EP, dur=e.duration, segs=e.segments, T=e.taxo;
const tile=(c,big)=>`<div class="vp${big?' big':''}" ><video data-c="${c}" src="media/${e.slug}/${c}.mp4" muted playsinline preload="auto"></video><span class="lbl">${c}${c==='head_depth'?' · filtered preview · raw 16-bit mm in dataset':''}</span>${c==='head_depth'?'<span class="rd" id="drd">hover for depth</span>':''}</div>`;
const chips=segs.map((s,i)=>`<div class="chip" data-i="${i}" style="background:${skc(s.skill)}" title="${esc(s.skill)} — ${esc(s.label)}">${i+1}</div>`).join('');
const slist=segs.map((s,i)=>`<div class="si" data-i="${i}"><div><span class="n" style="background:${skc(s.skill)}">${i+1}</span><span class="k">${esc(s.skill)}</span><span class="lb"> · ${s.ts.toFixed(2)}–${s.te.toFixed(2)}s</span></div><div class="m">${esc(s.label)}</div></div>`).join('');
const legend=Object.keys(e.skills).sort().map(s=>`<span class="it"><span class="sw" style="background:${skc(s)}"></span>${esc(s)} <b style="color:#e6e8ee">${e.skills[s]}</b></span>`).join('');
const HFDIR=HF+'/tree/main/'+e.dataset_dir, raw=f=>HF+'/resolve/main/'+e.dataset_dir+'/'+f;
const camDl=CAMS.map(c=>`<a class="fc dl" href="media/${e.slug}/${c}.mp4" download="${e.slug}_${c}.mp4">${c}.mp4</a>`).join('');
const refs=['data/file-000.parquet','meta/info.json','meta/ff/segments.parquet','meta/ff/episode_meta.parquet','meta/ff/quality.json','meta/ff/camera_params.json','depth/head_depth/file-000.mkv','pointclouds/lidar_front/file-000.parquet','pointclouds/lidar_back/file-000.parquet','sensors/imu_chassis.parquet','sensors/odom.parquet','sensors/vr_teleop_device.parquet']
.map(f=>`<a class="fc dl" href="${raw(f)}" target="_blank" rel="noopener">${f}</a>`).join('');
document.getElementById('main').innerHTML=`
<header>
<h1>${esc(e.task_name)}</h1>
<div class="sub"><code>${esc(e.task)}</code> · ${esc(e.vendor)} ${esc(e.hw)} · <b style="color:#e0a33a">robot egocentric</b> · ${esc(e.mode)} · episode ${e.ep} · ${e.duration}s @ ${e.fps}fps</div>
<div class="badges">${e.tags.map(t=>`<span class="bdg tag">${esc(t)}</span>`).join('')}<span class="bdg tag" style="background:#e0a33a;border-color:#e0a33a">Robot egocentric</span>${e.planned?'<span class="bdg tag" style="background:#b388eb;border-color:#b388eb">Planned</span>':''}<span class="bdg">instruction: <b>${esc(e.instruction)}</b></span></div>
<div class="badges"><span class="bdg">environment <b>${esc(T.environment_l1)} › ${esc(T.environment_l2)} › ${esc(T.environment_l3)}</b></span><span class="bdg">skill group <b>${esc(T.skill_group)}</b></span><span class="bdg">job <b>${esc(T.job_family)}</b></span><span class="bdg">difficulty <b>${esc(T.task_difficulty)}</b></span><span class="bdg">NAICS <b>${esc(T.naics_primary_code)}</b></span></div>
</header>
<div class="layout">
<div class="panel">
<div class="vgrid">${CAMS.map(c=>tile(c)).join('')}</div>
<div class="chk" id="chk"></div>
<div class="three" id="three"><span class="lbl">world (odom) · robot links · map (all scans, dim) · current scan front <span style="color:#ff8a70">■</span> back <span style="color:#6ea8ff">■</span> · base path · ceiling (&gt;2.3 m) and &gt;10 m hidden</span><span class="hint">drag to orbit · scroll to zoom</span><span id="cloudStat" style="position:absolute;top:26px;left:9px;font:11px ui-monospace,monospace;color:#e0a33a;z-index:1">loading map…</span></div>
</div>
<div class="panel">
<div class="spans-head">temporal spans (${e.n_segments}) · <b>${esc(e.task_name)}</b></div>
<div class="tl" id="tl"></div><div class="chips" id="chips">${chips}</div><div class="slist" id="slist">${slist}</div>
</div>
</div>
<div class="legend">${legend}</div>
<div class="panel controls"><button id="play">&#9654;</button><input type="range" id="seek" min="0" max="10000" value="0"><span class="t" id="tlab">0:00.00 / ${fmt(dur)}</span>
<select id="rate"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option></select></div>
<div class="badges" style="padding:0 20px;margin-top:8px">
<span class="bdg"><b>${e.nframes}</b> frames</span><span class="bdg"><b>${e.n_segments}</b> skill spans</span><span class="bdg">state dim <b>${e.state_dim}</b> · action dim <b>${e.action_dim}</b></span>
<span class="bdg">5 RGB cams + depth</span><span class="bdg">2 lidars · ${e.lidar_scans[0]}+${e.lidar_scans[1]} scans</span><span class="bdg">IMU 200 Hz</span>
<span class="bdg">depth valid <b>${(e.depth_valid*100).toFixed(0)}%</b></span>
<span class="bdg">QC <b style="color:${e.qc.failures?'#ff5c5c':'#6fe08a'}">${e.qc.passed}/${e.qc.total}</b>${e.qc.warnings?` · ${e.qc.warnings} warn`:''}</span></div>
<div class="note"><b>Why a robot episode in Ego Sample.</b> ${esc(e.rationale||'')}</div>
<div class="panel readout"><div class="rt">current-time readout</div><div class="rg">
<div class="ro"><span class="k">time</span><span class="v" id="roT">–</span></div>
<div class="ro"><span class="k">current skill</span><span class="v b" id="roSk">–</span></div>
<div class="ro"><span class="k">span</span><span class="v" id="roSp">–</span></div>
<div class="ro"><span class="k">base x / y / yaw</span><span class="v" id="roB">–</span></div>
<div class="ro"><span class="k">base speed (odom)</span><span class="v" id="roV">–</span></div>
<div class="ro"><span class="k">gripper L / R (rad)</span><span class="v" id="roG">–</span></div>
<div class="ro"><span class="k">teleop active</span><span class="v hi" id="roA">–</span></div>
<div class="ro"><span class="k">right end xyz (base_link)</span><span class="v" id="roE">–</span></div>
<div class="ro"><span class="k">max |target − measured| arm</span><span class="v" id="roTr">–</span></div></div></div>
<div class="charts">
<div class="panel chp"><div class="l">left arm joints <span class="u">rad · solid measured, dim target</span></div><canvas id="chL"></canvas></div>
<div class="panel chp"><div class="l">right arm joints <span class="u">rad · solid measured, dim target</span></div><canvas id="chR"></canvas></div>
<div class="panel chp"><div class="l">grippers <span class="u">rad</span><span class="leg"><span><i style="background:#e0616f"></i>L pos</span><span><i style="background:#41c76b"></i>R pos</span><span><i style="background:#7a3a42"></i>L cmd</span><span><i style="background:#2a6b3e"></i>R cmd</span></span></div><canvas id="chG"></canvas></div>
<div class="panel chp"><div class="l">base velocity <span class="u">m/s, rad/s · solid odom, dim teleop cmd</span><span class="leg"><span><i style="background:${AX.x}"></i>vx</span><span><i style="background:${AX.y}"></i>vy</span><span><i style="background:${AX.z}"></i>wz</span></span></div><canvas id="chV"></canvas></div>
<div class="panel chp"><div class="l">chassis IMU · gyro <span class="u">rad/s</span><span class="leg"><span><i style="background:${AX.x}"></i>x</span><span><i style="background:${AX.y}"></i>y</span><span><i style="background:${AX.z}"></i>z</span></span></div><canvas id="chI"></canvas></div>
<div class="panel chp"><div class="l">end-effector wrench · force <span class="u">N</span><span class="leg"><span><i style="background:#e0616f"></i>L fx fy fz</span><span><i style="background:#41c76b"></i>R fx fy fz</span></span></div><canvas id="chW"></canvas></div>
</div>
<div class="panel" style="margin:14px 20px 30px;padding:11px 16px">
<div class="rt">episode clips &mdash; click to download (preview encodes)</div><div class="files" style="padding:0 0 6px">${camDl}</div>
<div class="rt" style="margin-top:8px">dataset files &middot; <a href="${HFDIR}" target="_blank" rel="noopener" style="text-transform:none;color:#7fe0a0">full episode on HuggingFace &rarr;</a>
<button class="cpy" id="cpy" style="margin-left:8px">📋 copy: hf download (all files)</button></div>
<div class="files">${refs}</div></div>`;
wire();
}
function wire(){
const e=EP, dur=e.duration, segs=e.segments;
vids=[...document.querySelectorAll('.vp video')]; master=vids[0];
const playBtn=document.getElementById('play'), seek=document.getElementById('seek'), rate=document.getElementById('rate');
function toggle(){ playing=!playing; playBtn.innerHTML=playing?'&#10073;&#10073;':'&#9654;';
if(playing){ vids.forEach(v=>{ v.playbackRate=+rate.value; v.play().catch(()=>{}); }); tick(); clearInterval(syncT); syncT=setInterval(()=>syncSlaves(false),250); }
else { vids.forEach(v=>v.pause()); clearInterval(syncT); syncSlaves(true); } }
playBtn.onclick=toggle; _toggle=toggle;
rate.onchange=()=>vids.forEach(v=>v.playbackRate=+rate.value);
seek.oninput=()=>setTime(seek.value/10000*dur);
const tl=document.getElementById('tl'); tl.innerHTML='';
segs.forEach(s=>{ const b=document.createElement('i'); b.style.width=((s.te-s.ts)/dur*100)+'%'; b.style.background=skc(s.skill); b.title=`${s.skill} — ${s.label}`;
b.onclick=ev=>{ ev.stopPropagation(); setTime(s.ts+0.01); }; tl.appendChild(b); });
document.querySelectorAll('#chips .chip,#slist .si').forEach(c=>c.onclick=()=>setTime(segs[+c.dataset.i].ts+0.01));
['chL','chR','chG','chV','chI','chW'].forEach(id=>{ const c=document.getElementById(id); c.onclick=ev=>{ const r=c.getBoundingClientRect(); setTime((ev.clientX-r.left)/r.width*dur); }; });
master.addEventListener('seeked',()=>syncSlaves(true));
document.getElementById('cpy').onclick=()=>{ const cmd=`hf download ${HF.replace('https://huggingface.co/datasets/','')} --repo-type dataset --include "${e.dataset_dir}/*" --local-dir .`;
navigator.clipboard.writeText(cmd).then(()=>{ document.getElementById('cpy').textContent='✓ copied'; setTimeout(()=>document.getElementById('cpy').textContent='📋 copy: hf download (all files)',1500); }); };
document.onkeydown=ev=>{ if(ev.target.tagName==='SELECT') return;
if(ev.code==='Space'){ ev.preventDefault(); toggle(); }
else if(ev.code==='ArrowRight'){ ev.preventDefault(); const n=segs.find(s=>s.ts>master.currentTime+0.05); if(n) setTime(n.ts+0.01); }
else if(ev.code==='ArrowLeft'){ ev.preventDefault(); const p=[...segs].reverse().find(s=>s.ts<master.currentTime-0.3); setTime(p?p.ts+0.01:0); } };
// depth hover readout
const dv=vids.find(v=>v.dataset.c==='head_depth'), rd=document.getElementById('drd'), cache={};
dv.parentElement.addEventListener('mousemove',ev=>{ if(!SIG) return; const r=dv.getBoundingClientRect(), ar=640/400; let w=r.width,h=w/ar; if(h>r.height){h=r.height;w=h*ar;}
const x=(ev.clientX-r.left-(r.width-w)/2)/w, y=(ev.clientY-r.top-(r.height-h)/2)/h; if(x<0||x>1||y<0||y>1){ rd.textContent='—'; return; }
if(!SIG.depthGrid){ rd.textContent='loading…'; return; }
const [gw,gh]=SIG.depth_grid_wh, fi=Math.min(SIG.depthGrid.length-1,Math.round(dv.currentTime*30/SIG.depth_grid_every));
const g=SIG.depthGrid[fi]; const v=g[Math.floor(y*gh)*gw+Math.floor(x*gw)];
rd.textContent=v?`${(v/100).toFixed(2)} m`:'invalid'; });
dv.parentElement.addEventListener('mouseleave',()=>rd.textContent='hover for depth');
setTime(0);
}
function inBuf(v,t){ const b=v.buffered; for(let i=0;i<b.length;i++) if(t>=b.start(i)-0.2&&t<=b.end(i)+0.2) return true; return false; }
function syncSlaves(hard){ const t=master.currentTime, base=+document.getElementById('rate').value;
for(const v of vids){ if(v===master||v.readyState<1) continue;
if(hard){ v.playbackRate=base; if(!v.seeking&&Math.abs(v.currentTime-t)>0.04){ try{v.currentTime=t;}catch(e){} } continue; }
if(!playing) continue; if(v.paused&&!v.ended) v.play().catch(()=>{});
const d=v.currentTime-t, ad=Math.abs(d);
if(ad>0.75){ if(!v.seeking&&inBuf(v,t)){ try{v.currentTime=t;}catch(e){} } v.playbackRate=base; }
else v.playbackRate=ad>0.035?base*Math.max(.5,Math.min(1.6,1-d*3)):base; } }
function setTime(t){ t=Math.max(0,Math.min(EP.duration-0.01,t)); try{ if(Math.abs(master.currentTime-t)>0.03) master.currentTime=t; }catch(e){} syncSlaves(true); updateUI(t); }
function tick(){ if(!playing) return; const t=master.currentTime; updateUI(t);
if(t>=EP.duration-0.05){ try{master.currentTime=0;}catch(e){} syncSlaves(true); vids.forEach(v=>v.play().catch(()=>{})); }
raf=requestAnimationFrame(tick); }
function curSpan(t){ const s=EP.segments; for(let i=0;i<s.length;i++) if(t>=s[i].ts&&t<s[i].te) return i; return -1; }
function updateUI(t){
const dur=EP.duration, segs=EP.segments;
document.getElementById('seek').value=t/dur*10000; document.getElementById('tlab').textContent=fmt(t)+' / '+fmt(dur);
document.getElementById('roT').textContent=t.toFixed(2)+'s';
const ci=curSpan(t); document.getElementById('roSk').textContent=ci>=0?segs[ci].skill:'–'; document.getElementById('roSp').textContent=ci>=0?`${ci+1} / ${segs.length}`:'–';
if(ci!==_lastCi){ const sl=document.querySelectorAll('#slist .si'); sl.forEach((el,k)=>el.classList.toggle('on',k===ci));
document.querySelectorAll('#chips .chip').forEach((el,k)=>el.classList.toggle('on',k===ci));
if(ci>=0){ const box=document.getElementById('slist'), el=sl[ci], top=el.offsetTop-box.offsetTop; if(top<box.scrollTop||top+el.offsetHeight>box.scrollTop+box.clientHeight) box.scrollTop=top-box.clientHeight/2; }
_lastCi=ci; }
if(!SIG) return;
const j=Math.min(SIG.n-1,Math.round(t*SIG.fps)), b=SIG.base[j], bv=SIG.base_vel[j], g=SIG.grip[j], ee=SIG.ee[j];
document.getElementById('roB').textContent=`${b[0].toFixed(2)} / ${b[1].toFixed(2)} m / ${(b[2]*180/Math.PI).toFixed(0)}°`;
document.getElementById('roV').textContent=`${Math.hypot(bv[0],bv[1]).toFixed(2)} m/s · ${(bv[2]*180/Math.PI).toFixed(0)}°/s`;
document.getElementById('roG').textContent=`${g[0].toFixed(2)} / ${g[1].toFixed(2)}`;
document.getElementById('roA').textContent=SIG.active[j]?'yes':'no';
document.getElementById('roE').textContent=ee.slice(7,10).map(v=>v.toFixed(2)).join(', ');
document.getElementById('roTr').textContent=Math.max(...SIG.arm[j].map((v,k)=>Math.abs(v-SIG.arm_target[j][k]))).toFixed(3)+' rad';
drawPlayheads(t); if(setCloudT) setCloudT(t);
}
/* ---------- charts ---------- */
const CH={};
function sizeCharts(){ document.querySelectorAll('.chp canvas').forEach(c=>{ c.width=c.clientWidth*devicePixelRatio; c.height=84*devicePixelRatio; }); }
function plot(id,series){ const c=document.getElementById(id); if(!c) return; const g=c.getContext('2d'), w=c.width, h=c.height, dpr=devicePixelRatio;
g.fillStyle='#0b0e13'; g.fillRect(0,0,w,h); let lo=Infinity,hi=-Infinity; series.forEach(s=>s.d.forEach(v=>{ if(v<lo)lo=v; if(v>hi)hi=v; })); if(lo===hi){lo-=1;hi+=1;}
const n=series[0].d.length, Y=v=>h-4*dpr-(v-lo)/(hi-lo)*(h-8*dpr);
if(lo<0&&hi>0){ g.strokeStyle='#1c2028'; g.beginPath(); g.moveTo(0,Y(0)); g.lineTo(w,Y(0)); g.stroke(); }
series.forEach(s=>{ g.strokeStyle=s.c; g.globalAlpha=s.a||1; g.lineWidth=dpr; g.beginPath(); s.d.forEach((v,i)=>{ const x=i/(n-1)*w; i?g.lineTo(x,Y(v)):g.moveTo(x,Y(v)); }); g.stroke(); });
g.globalAlpha=1; g.fillStyle='#5a6373'; g.font=`${10*dpr}px ui-monospace`; g.fillText(hi.toFixed(2),4*dpr,11*dpr); g.fillText(lo.toFixed(2),4*dpr,h-3*dpr);
CH[id]=g.getImageData(0,0,w,h); }
const colOf=(a,k)=>a.map(r=>r[k]);
function drawCharts(){ if(!SIG) return; sizeCharts();
const arm=(off)=>{ const s=[]; for(let k=0;k<7;k++){ s.push({d:colOf(SIG.arm_target,off+k),c:PAL[k],a:.3}); s.push({d:colOf(SIG.arm,off+k),c:PAL[k]}); } return s; };
plot('chL',arm(0)); plot('chR',arm(7));
plot('chG',[{d:colOf(SIG.grip_cmd,0),c:'#7a3a42'},{d:colOf(SIG.grip_cmd,1),c:'#2a6b3e'},{d:colOf(SIG.grip,0),c:'#e0616f'},{d:colOf(SIG.grip,1),c:'#41c76b'}]);
plot('chV',[{d:colOf(SIG.base_cmd,0),c:AX.x,a:.35},{d:colOf(SIG.base_cmd,1),c:AX.y,a:.35},{d:colOf(SIG.base_cmd,2),c:AX.z,a:.35},{d:colOf(SIG.base_vel,0),c:AX.x},{d:colOf(SIG.base_vel,1),c:AX.y},{d:colOf(SIG.base_vel,2),c:AX.z}]);
plot('chI',[3,4,5].map((k,i)=>({d:colOf(SIG.imu,k),c:[AX.x,AX.y,AX.z][i]})));
plot('chW',[0,1,2].map(k=>({d:colOf(SIG.wrench,k),c:'#e0616f',a:.5+k*.25})).concat([6,7,8].map(k=>({d:colOf(SIG.wrench,k),c:'#41c76b',a:.5+(k-6)*.25}))));
drawPlayheads(master?master.currentTime:0); }
function drawPlayheads(t){ const f=t/EP.duration; Object.entries(CH).forEach(([id,img])=>{ const c=document.getElementById(id); if(!c) return; const g=c.getContext('2d'); g.putImageData(img,0,0);
g.strokeStyle='rgba(255,255,255,.6)'; g.lineWidth=devicePixelRatio; g.beginPath(); g.moveTo(f*c.width,0); g.lineTo(f*c.width,c.height); g.stroke(); }); }
window.addEventListener('resize',()=>{ clearTimeout(window._rz); window._rz=setTimeout(drawCharts,150); });
/* ---------- 3D world view ---------- */
async function init3D(){
const THREE=await import('three'); const {OrbitControls}=await import('three/addons/controls/OrbitControls.js');
const el=document.getElementById('three'); el.querySelectorAll('canvas').forEach(c=>c.remove());
const R=new THREE.WebGLRenderer({antialias:true}); R.setPixelRatio(devicePixelRatio); el.appendChild(R.domElement);
const scene=new THREE.Scene(); scene.background=new THREE.Color(0x0b0d12);
const cam=new THREE.PerspectiveCamera(50,1,.05,200); cam.up.set(0,0,1);
const B0=SIG.base; let cx=0,cy=0; B0.forEach(b=>{cx+=b[0];cy+=b[1];}); cx/=B0.length; cy/=B0.length;
let x0=1e9,x1=-1e9,y0=1e9,y1=-1e9; B0.forEach(b=>{x0=Math.min(x0,b[0]);x1=Math.max(x1,b[0]);y0=Math.min(y0,b[1]);y1=Math.max(y1,b[1]);}); const span=Math.max(x1-x0,y1-y0,2)+3;
cam.position.set(cx-span*.75,cy-span*.75,span*.7); const ctl=new OrbitControls(cam,R.domElement); ctl.target.set(cx,cy,.6); ctl.update();
const grid=new THREE.GridHelper(16,32,0x2a3a5a,0x1a2030); grid.rotation.x=Math.PI/2; grid.position.set(cx,cy,0); scene.add(grid);
// base path
const path=new THREE.Line(new THREE.BufferGeometry().setFromPoints(B0.map(b=>new THREE.Vector3(b[0],b[1],.01))),new THREE.LineBasicMaterial({color:0x6ea8ff})); scene.add(path);
const robot=new THREE.Group(); scene.add(robot);
const body=new THREE.LineSegments(new THREE.BufferGeometry(),new THREE.LineBasicMaterial({color:0xd6dbe6})); robot.add(body);
const joints=new THREE.Points(new THREE.BufferGeometry(),new THREE.PointsMaterial({color:0xffc857,size:.045})); robot.add(joints);
const foot=new THREE.Mesh(new THREE.BoxGeometry(.62,.52,.25),new THREE.MeshBasicMaterial({color:0x4a5570,wireframe:true})); foot.position.set(.1,0,.14); robot.add(foot);
const ax=new THREE.AxesHelper(.35); robot.add(ax);
const L=SIG.links, idx=n=>L.indexOf(n);
const bones=[['chassis_link','body_link1'],['body_link1','body_link2'],['body_link2','body_link3'],['body_link3','body_link5'],['body_link5','head_link3'],['body_link5','arm_base_link']];
['l','r'].forEach(s=>{ bones.push(['arm_base_link',`arm_${s}_link1`]); for(let i=1;i<7;i++) bones.push([`arm_${s}_link${i}`,`arm_${s}_link${i+1}`]); bones.push([`arm_${s}_link7`,`arm_${s}_end_link`],[`arm_${s}_end_link`,`gripper_${s}_center_link`]); });
const clouds={}; const CC={lidar_front:[1,.54,.44],lidar_back:[.43,.66,1]};
let follow=false;
const buildChk=()=>{ document.getElementById('chk').innerHTML=Object.keys(clouds).map(k=>`<label><input type="checkbox" data-k="${k}" checked>${k==='__map'?'environment map (all scans)':k+' (current scan)'}</label>`).join('')+`<label><input type="checkbox" data-k="__path" checked>base path</label><label><input type="checkbox" data-k="__follow">camera follows robot</label>`;
document.querySelectorAll('#chk input').forEach(i=>i.onchange=()=>{ const k=i.dataset.k; if(k==='__path') path.visible=i.checked; else if(k==='__follow') follow=i.checked; else clouds[k].p.visible=i.checked; }); };
let mapPts=null;
window._addClouds=()=>{
if(SIG.map&&!mapPts){ const q=SIG.map.q, I=SIG.map.I, n=I.length, pos=new Float32Array(n*3), col=new Float32Array(n*3);
for(let m=0;m<n;m++){ pos[3*m]=q[3*m]/100; pos[3*m+1]=q[3*m+1]/100; const z=q[3*m+2]/100; pos[3*m+2]=z;
const h=Math.max(0,Math.min(1,z/2.3)), s=.30+.35*I[m]/255; col[3*m]=(.35+.25*h)*s*1.6; col[3*m+1]=(.45+.2*h)*s*1.6; col[3*m+2]=(.6+.15*h)*s*1.6; }
const g=new THREE.BufferGeometry(); g.setAttribute('position',new THREE.BufferAttribute(pos,3)); g.setAttribute('color',new THREE.BufferAttribute(col,3));
mapPts=new THREE.Points(g,new THREE.PointsMaterial({size:.03,vertexColors:true,transparent:true,opacity:.35})); scene.add(mapPts); clouds.__map={p:mapPts,cur:0,static:true}; } if(SIG.clouds) Object.keys(SIG.clouds).forEach(k=>{ if(clouds[k]) return; const p=new THREE.Points(new THREE.BufferGeometry(),new THREE.PointsMaterial({size:.05,vertexColors:true})); scene.add(p); clouds[k]={p,cur:-1}; });
buildChk(); if(setCloudT) setCloudT(master.currentTime||0); };
buildChk();
let last=null;
setCloudT=t=>{ const j=Math.min(SIG.n-1,Math.round(t*SIG.fps)), b=SIG.base[j];
robot.position.set(b[0],b[1],0); robot.rotation.set(0,0,b[2]);
if(follow&&last){ const dx=b[0]-last[0], dy=b[1]-last[1]; cam.position.x+=dx; cam.position.y+=dy; ctl.target.x+=dx; ctl.target.y+=dy; } last=b;
const lt=SIG.link_t; let li=0; while(li<lt.length-1&&lt[li+1]<=t) li++; const P=SIG.link_p[li];
const seg=[]; bones.forEach(([a,c])=>{ const ia=idx(a), ic=idx(c); if(ia>=0&&ic>=0) seg.push(...P[ia],...P[ic]); });
body.geometry.setAttribute('position',new THREE.Float32BufferAttribute(seg,3)); joints.geometry.setAttribute('position',new THREE.Float32BufferAttribute(P.flat(),3));
Object.entries(clouds).forEach(([k,C])=>{ if(C.static) return; const fr=SIG.clouds&&SIG.clouds[k]; if(!fr) return; let i=0; while(i<fr.length-1&&fr[i+1].t<=t) i++; if(i===C.cur) return; C.cur=i;
const f=fr[i], q=f.q, I=f.I, n=I.length, bj=Math.max(0,Math.min(SIG.n-1,Math.round(f.t*SIG.fps))), bb=SIG.base[bj], cs=Math.cos(bb[2]), sn=Math.sin(bb[2]);
const pos=new Float32Array(n*3), col=new Float32Array(n*3), base=CC[k]||[.8,.8,.8];
for(let m=0;m<n;m++){ const x=q[3*m]/100,y=q[3*m+1]/100,z=q[3*m+2]/100; pos[3*m]=bb[0]+cs*x-sn*y; pos[3*m+1]=bb[1]+sn*x+cs*y; pos[3*m+2]=z;
const s=.35+.65*I[m]/255; col[3*m]=base[0]*s; col[3*m+1]=base[1]*s; col[3*m+2]=base[2]*s; }
C.p.geometry.setAttribute('position',new THREE.BufferAttribute(pos,3)); C.p.geometry.setAttribute('color',new THREE.BufferAttribute(col,3)); C.p.geometry.computeBoundingSphere(); }); };
const resize=()=>{ const w=el.clientWidth,h=el.clientHeight; if(!w) return; R.setSize(w,h); cam.aspect=w/h; cam.updateProjectionMatrix(); };
new ResizeObserver(resize).observe(el); resize();
(function loop(){ requestAnimationFrame(loop); ctl.update(); R.render(scene,cam); })();
setCloudT(master.currentTime||0); window._addClouds();
}
function fromHash(){ const s=decodeURIComponent(location.hash.slice(1)); const i=EPS.findIndex(e=>e.slug===s); select(i>=0?i:0); }
window.addEventListener('hashchange',fromHash); fromHash();
</script>
</body></html>