Spaces:
Running
Running
darren.zhang
Claude Opus 5 (1M context)
teleopSample: cut what a pick-and-place page loads by ~60%
350de76 Download teleopSample/lib/steps-ui.js from faradayfuture/SampleViewer: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/spaces/faradayfuture/SampleViewer/resolve/main/teleopSample/lib/steps-ui.js
- Command line
-
hf download hf://spaces/faradayfuture/SampleViewer/teleopSample/lib/steps-ui.js
-
curl -L -o steps-ui.js https://huggingface.co/spaces/faradayfuture/SampleViewer/resolve/main/teleopSample/lib/steps-ui.js
12.1 kB
| /* Step UI shared by every sample viewer on this Space. | |
| * | |
| * Builds the three blocks that describe a teleop episode's steps — the instruction / | |
| * object / target / scene header, the segmented step timeline over the scrubber, and the | |
| * current-step annotation panel — plus the per-step measurements behind them. | |
| * | |
| * The pick-and-place pages hand it their own containers ('inline' mode) and pre-computed | |
| * per-step measurements; the older pages let it insert the blocks itself ('inject' mode) | |
| * and it derives the measurements from the page's series. Both get the same layout. | |
| * | |
| * StepsUI.mount({series, steps, task, mode, hosts, numbering}) -> {update(t), stepAt(t)} | |
| */ | |
| (function (global) { | |
| const COLORS = ["#8a8f98", "#4f8ff7", "#41c76b", "#e0a53e", "#b46fe0", "#e0616f", | |
| "#e08f5f", "#5fc9e0"]; | |
| const TOKC = { | |
| idle: '#8a8f98', reach_to_object: '#4f8ff7', fine_align: '#6f9fd8', grasp_close: '#41c76b', | |
| lift: '#e0a53e', transport: '#b46fe0', align_over_target: '#e0d15f', | |
| lower_onto_target: '#e0955f', release: '#e0616f', retreat: '#5fe0c0', | |
| carry_away: '#b46fe0', hold: '#8a8f98', | |
| }; | |
| const esc = s => String(s == null ? '' : s) | |
| .replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); | |
| const CSS = ` | |
| .su-instr{margin:10px 22px 0;padding:11px 15px;background:#12151c;border:1px solid #232733;border-radius:8px;font-size:13px;line-height:1.65} | |
| .su-instr .k{font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;color:#9aa1ad;margin-right:8px} | |
| .su-instr .obj{color:#7fb2ff}.su-instr .tgt{color:#7fe0a0} | |
| .su-tlwrap{margin:12px 22px 0;padding:9px 14px;background:#12151c;border:1px solid #232733;border-radius:8px} | |
| .su-tl{position:relative;display:flex;height:26px;border-radius:5px;overflow:hidden;cursor:pointer} | |
| .su-tl .seg{position:relative;opacity:.85;transition:opacity .12s} | |
| .su-tl .seg.on{opacity:1;box-shadow:inset 0 0 0 2px #fff} | |
| .su-tl .seg span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10.5px;color:#0b0d12;font-weight:700} | |
| .su-tl .cur{position:absolute;top:0;bottom:0;width:2px;background:#fff;pointer-events:none} | |
| .su-tlnums{display:flex;justify-content:space-between;font-size:10.5px;color:#6b7686;margin-top:4px;font-variant-numeric:tabular-nums} | |
| .su-annot{margin:12px 22px 0;padding:14px 18px;background:#12151c;border:1px solid #232733;border-radius:8px} | |
| .su-annot .t{font-size:11px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase;margin-bottom:10px} | |
| .su-annot h3{margin:0 0 2px;font-size:16px} | |
| .su-annot h3 .tok{font-size:11.5px;color:#6f92c8;font-family:ui-monospace,monospace;margin-left:8px} | |
| .su-annot .when{font-size:12px;color:#8890a0;font-variant-numeric:tabular-nums;margin-bottom:8px} | |
| .su-annot p{margin:0 0 6px;line-height:1.6;font-size:13.5px;color:#dfe4ec} | |
| .su-sgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:7px 26px;margin-top:10px} | |
| .su-sg{display:flex;justify-content:space-between;border-bottom:1px dashed #232733;padding-bottom:3px;font-size:12.5px} | |
| .su-sg .k{color:#9aa1ad}.su-sg .v{font-variant-numeric:tabular-nums} | |
| .su-item .t{font-size:11px;color:#8890a0;font-variant-numeric:tabular-nums} | |
| .su-item .title{font-size:13.5px;margin:2px 0;line-height:1.35} | |
| .su-item .tok{font-size:10.5px;color:#6f92c8;font-family:ui-monospace,monospace}`; | |
| function styles() { | |
| if (document.getElementById('su-css')) return; | |
| const e = document.createElement('style'); | |
| e.id = 'su-css'; e.textContent = CSS; | |
| document.head.appendChild(e); | |
| } | |
| /** End-effector / gripper measurements over a frame range, from the page's own series. */ | |
| function measure(S, a, b) { | |
| const END = S.end || S.end_pos; // [n][6] = left xyz, right xyz | |
| const LR = S.end_l && S.end_r; // the newer pages ship them apart | |
| a = Math.max(0, a); b = Math.min(S.n - 1, b); | |
| const of = (i, o) => (LR ? (o ? S.end_r[i][o - 3] : S.end_l[i][o]) : END[i][o]); | |
| const arm = o => { | |
| let path = 0, vmax = 0; | |
| for (let i = a + 1; i <= b; i++) { | |
| const d = Math.hypot(of(i, o) - of(i - 1, o), of(i, o + 1) - of(i - 1, o + 1), | |
| of(i, o + 2) - of(i - 1, o + 2)); | |
| path += d; vmax = Math.max(vmax, d * S.fps); | |
| } | |
| return { | |
| path, vmax, dz: of(b, o + 2) - of(a, o + 2), | |
| dxy: Math.hypot(of(b, o) - of(a, o), of(b, o + 1) - of(a, o + 1)), | |
| vmean: path / Math.max(1e-6, (b - a) / S.fps), | |
| }; | |
| }; | |
| const L = arm(0), R = arm(3); | |
| return { L, R, act: L.path >= R.path ? L : R, side: L.path >= R.path ? 'left' : 'right' }; | |
| } | |
| function rows(S, st, a, b) { | |
| const out = []; | |
| const f = (v, n) => (v == null ? '–' : Number(v).toFixed(n == null ? 2 : n)); | |
| if (st.gripper) { | |
| out.push(['gripper open', `${f(st.gripper.start)} → ${f(st.gripper.end)} (min ${f(st.gripper.min)})`]); | |
| } else if (S.grip_l) { | |
| out.push(['gripper L', `${f(S.grip_l[a])} → ${f(S.grip_l[b])}`]); | |
| out.push(['gripper R', `${f(S.grip_r[a])} → ${f(S.grip_r[b])}`]); | |
| } | |
| const ee = st.ee || measure(S, a, b).act; | |
| out.push(['ee path', f(ee.travel_m != null ? ee.travel_m : ee.path, 3) + ' m']); | |
| const dz = ee.dz_m != null ? ee.dz_m : ee.dz; | |
| out.push(['ee Δz', (dz >= 0 ? '+' : '') + f(dz, 3) + ' m']); | |
| out.push(['ee Δxy', f(ee.dxy_m != null ? ee.dxy_m : ee.dxy, 3) + ' m']); | |
| out.push(['speed mean / max', | |
| `${f(ee.v_mean != null ? ee.v_mean : ee.vmean)} / ${f(ee.v_max != null ? ee.v_max : ee.vmax)} m/s`]); | |
| if (st.contact) out.push(['contact |F| peak', f(st.contact.force_peak_N, 1) + ' N']); | |
| if (st.effort) out.push(['peak |τ|', `${st.effort.peak_joint} ${f(st.effort.peak_Nm, 1)} Nm`]); | |
| if (st.tracking) out.push(['cmd err mean / max', | |
| `${f(st.tracking.cmd_err_mean_rad, 3)} / ${f(st.tracking.cmd_err_max_rad, 3)} rad`]); | |
| if (!st.ee && S.intervened) { | |
| const n = S.intervened.slice(a, b + 1).reduce((p, c) => p + (c ? 1 : 0), 0); | |
| out.push(['operator-sourced frames', Math.round(100 * n / (b - a + 1)) + '%']); | |
| } | |
| return out; | |
| } | |
| function mount(cfg) { | |
| styles(); | |
| const S = cfg.series, STEPS = cfg.steps || [], task = cfg.task || {}; | |
| if (!S || !STEPS.length) return null; | |
| const DUR = S.dur != null ? S.dur : (S.t ? S.t[S.n - 1] : (S.n - 1) / S.fps); | |
| const frameAt = t => Math.max(0, Math.min(S.n - 1, Math.round(t * S.fps))); | |
| const num = (st, i) => (cfg.numbering === 'id' && st.id != null ? st.id : i + 1); | |
| const col = (st, i) => (cfg.colorBy === 'token' && TOKC[st.token]) || COLORS[i % COLORS.length]; | |
| const seek = cfg.seek || (t => document.querySelectorAll('video') | |
| .forEach(v => { try { v.currentTime = t; } catch (e) {} })); | |
| // ---- instruction header | |
| const instr = document.createElement('div'); | |
| instr.className = 'su-instr'; | |
| instr.innerHTML = | |
| `<div><span class="k">instruction</span>${esc(task.instruction)}</div>` + | |
| (task.note ? `<div class="su-note">${esc(task.note)}</div>` : '') + | |
| `<div style="margin-top:6px"><span class="k">object</span><span class="obj">${esc(task.object) || '—'}</span>` + | |
| `<span class="k" style="margin-left:14px">target</span><span class="tgt">${esc(task.target) || '—'}</span>` + | |
| `<span class="k" style="margin-left:14px">scene</span>${esc(task.scene)}</div>`; | |
| // ---- timeline | |
| const tlw = document.createElement('div'); | |
| tlw.className = 'su-tlwrap'; | |
| tlw.innerHTML = `<div class="su-tl"><div class="cur"></div></div>` + | |
| `<div class="su-tlnums"><span>0.00s</span><span>${DUR.toFixed(2)}s</span></div>`; | |
| const tl = tlw.querySelector('.su-tl'), cur = tlw.querySelector('.cur'); | |
| STEPS.forEach((st, i) => { | |
| const sg = document.createElement('div'); | |
| sg.className = 'seg'; | |
| sg.style.background = col(st, i); | |
| sg.style.width = (100 * (st.end_s - st.start_s) / DUR) + '%'; | |
| sg.title = `${num(st, i)}. ${st.title || st.label} ${st.start_s.toFixed(2)}–${st.end_s.toFixed(2)}s`; | |
| sg.innerHTML = `<span>${num(st, i)}</span>`; | |
| sg.onclick = e => { e.stopPropagation(); seek(st.start_s + 0.01); }; | |
| tl.insertBefore(sg, cur); | |
| }); | |
| tl.onclick = e => { const r = tl.getBoundingClientRect(); seek((e.clientX - r.left) / r.width * DUR); }; | |
| // ---- current-step panel | |
| const an = document.createElement('div'); | |
| an.className = 'su-annot'; | |
| an.innerHTML = `<div class="t">step annotation · current step</div><h3 class="lbl">–</h3>` + | |
| `<div class="when"></div><p class="desc"></p><div class="su-sgrid sig"></div>`; | |
| // ---- place them | |
| const H = cfg.hosts || {}; | |
| const put = (node, host, how) => { | |
| if (!host) return; | |
| if (how === 'fill') { host.innerHTML = ''; host.appendChild(node); } | |
| else if (how === 'after') host.parentNode.insertBefore(node, host.nextSibling); | |
| else host.parentNode.insertBefore(node, host); | |
| }; | |
| put(instr, H.instr || H.instrAfter, H.instr ? 'fill' : 'after'); | |
| put(tlw, H.timeline || H.timelineBefore, H.timeline ? 'fill' : 'before'); | |
| put(an, H.panel || H.panelAfter, H.panel ? 'fill' : 'after'); | |
| // ---- step list rows (same format on every page) | |
| const list = cfg.listItems ? [...cfg.listItems] : []; | |
| list.forEach((el, i) => { | |
| const st = STEPS[i]; | |
| if (!st) return; | |
| const a = frameAt(st.start_s), b = frameAt(st.end_s); | |
| el.classList.add('su-item'); | |
| el.style.borderLeftColor = col(st, i); | |
| el.innerHTML = | |
| `<div class="t">${st.start_s.toFixed(2)}s – ${st.end_s.toFixed(2)}s · ` + | |
| `${(st.end_s - st.start_s).toFixed(2)}s · ${b - a + 1}f` + | |
| (st.success_s != null ? ` · ✓ ${st.success_s.toFixed(2)}s` : '') + `</div>` + | |
| `<div class="title">${num(st, i)}. ${esc(st.title || st.label)}</div>` + | |
| (st.token ? `<div class="tok">${esc(st.token)}</div>` : ''); | |
| el.onclick = () => seek(st.start_s + 0.01); | |
| }); | |
| const lbl = an.querySelector('.lbl'), when = an.querySelector('.when'); | |
| const desc = an.querySelector('.desc'), sig = an.querySelector('.sig'); | |
| let curIdx = -1; | |
| function stepAt(t) { | |
| for (let i = 0; i < STEPS.length; i++) if (t >= STEPS[i].start_s && t < STEPS[i].end_s) return i; | |
| return t < STEPS[0].start_s ? 0 : STEPS.length - 1; | |
| } | |
| function update(t) { | |
| const i = stepAt(t), st = STEPS[i]; | |
| cur.style.left = (100 * t / DUR) + '%'; | |
| if (i === curIdx) return i; | |
| curIdx = i; | |
| const a = st.start_frame != null ? st.start_frame : frameAt(st.start_s); | |
| const b = st.end_frame != null ? st.end_frame : frameAt(st.end_s); | |
| lbl.innerHTML = `${num(st, i)}. ${esc(st.title || st.label)}` + | |
| (st.token ? `<span class="tok">${esc(st.token)}</span>` : ''); | |
| when.textContent = | |
| `${st.start_s.toFixed(2)}s – ${st.end_s.toFixed(2)}s · ${(st.end_s - st.start_s).toFixed(2)}s · ` + | |
| `frames ${a}–${b} (${b - a + 1})` + | |
| (st.success_s != null ? ` · success at ${st.success_s.toFixed(2)}s` : ''); | |
| let text = st.description || (cfg.describe && cfg.describe(st, i)); | |
| if (!text) { | |
| const m = measure(S, a, b); | |
| text = `${m.side} arm · ${m.act.path.toFixed(3)} m of end-effector travel in ` + | |
| `${(st.end_s - st.start_s).toFixed(2)}s (mean ${m.act.vmean.toFixed(2)}, peak ` + | |
| `${m.act.vmax.toFixed(2)} m/s), height change ${m.act.dz >= 0 ? '+' : ''}${m.act.dz.toFixed(2)} m.`; | |
| } | |
| desc.textContent = text; | |
| sig.innerHTML = rows(S, st, a, b).map(r => | |
| `<div class="su-sg"><span class="k">${r[0]}</span><span class="v">${r[1]}</span></div>`).join(''); | |
| tl.querySelectorAll('.seg').forEach((e, k) => e.classList.toggle('on', k === i)); | |
| list.forEach((e, k) => e.classList.toggle('active', k === i)); | |
| return i; | |
| } | |
| return { update, stepAt, measure: (a, b) => measure(S, a, b), colorOf: col, DUR, frameAt }; | |
| } | |
| global.StepsUI = { mount, measure, COLORS, TOKC }; | |
| })(window); | |