Buckets:
| #!/usr/bin/env python3 | |
| """tools/review_tool/build_relabel_pass4.py — diagnostic frame-click pass for the ENGAGED hand_return events (after pass 3 collapsed to 0.03): | |
| instead of correct / wrong, the owner clicks the FIRST frame (dt = -1.0 … +1.0 s, 0.1 s step, same img_r3 frames) in which a hand is on the wheel, | |
| or chooses 'hand never on the wheel in the window' / 'hand on the wheel in every frame (never left)' / 'unsure'. Labels are stored as | |
| ret_dt=<dt> | never_on | always_on | unsure under ids <orig_id>__r4 (same 8791 service). Read-only on data; no frame extraction. | |
| """ | |
| from __future__ import annotations | |
| import argparse, json, sys, time | |
| from pathlib import Path | |
| CWM3 = Path(__file__).resolve().parents[2] | |
| SUFFIX = "__r4" | |
| HTML = r"""<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>CockpitWM 诊断:hand_return 回盘时刻</title> | |
| <style> | |
| body{font-family:system-ui,Segoe UI,Arial,'Noto Sans CJK SC',sans-serif;margin:0;background:#f5f4f0;color:#111} | |
| header{position:sticky;top:0;background:#fff;border-bottom:1px solid #ccc;padding:8px 16px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;z-index:5} | |
| button{padding:6px 12px;border:1px solid #888;border-radius:6px;background:#fff;cursor:pointer}button.sel{background:#1f5fbf;color:#fff;border-color:#1f5fbf} | |
| main{padding:12px 16px;max-width:1500px;margin:0 auto} | |
| .item{background:#fff;border:1px solid #ddd;border-radius:8px;padding:10px 12px;margin:10px 0}.item.done{border-color:#7ab27a;background:#f4fbf4}.item.cur{outline:3px solid #1f5fbf} | |
| .frames{display:flex;gap:4px;flex-wrap:wrap}.frames img{width:min(12.5vw,180px);border:2px solid #ccc;border-radius:3px;cursor:pointer}.frames img.sel{border-color:#d33;box-shadow:0 0 0 2px #d33} | |
| .player{display:flex;gap:12px;align-items:flex-start;margin:6px 0}.player img{width:min(40vw,560px);border:2px solid #1f5fbf;border-radius:4px} | |
| .q{margin:8px 0 4px;font-weight:600}.meta{font-size:12px;color:#555;word-break:break-all}.ans{margin:6px 0;font-size:14px}.hint{font-size:12px;color:#666}.prog{font-size:13px} | |
| .def{margin:12px 16px;max-width:1500px;background:#fff8e1;border:1px solid #e0b400;border-radius:8px;padding:10px 12px} | |
| input[type=text]{width:60%;padding:4px} | |
| </style></head><body> | |
| <header><b>CockpitWM 诊断:engaged hand_return 的回盘时刻</b><span class="prog" id="prog"></span> | |
| <button onclick="jump(-1)">◀ 上一条 (p)</button><button onclick="jump(1)">下一条 ▶ (n)</button><span class="hint" id="sync">离线模式</span> | |
| <span class="hint">快捷键:, / . 前后一帧,空格播放,Enter = 把当前播放帧选为"首次回盘帧",n/p 翻页。</span></header> | |
| <div class="def"><b>请点选:第一张“至少一只手握在/放在方向盘上”的帧</b>(在 21 帧 −1.0 … +1.0 s 中;EVENT = dt 0)。若 21 帧中手<b>从未</b>放到盘上,选“从未回盘”;若<b>每一帧</b>手都在盘上(根本没离开过),选“一直在盘上”。只看手,不看头。备注可写 phone 等。</div> | |
| <main id="main"></main> | |
| <script> | |
| const ITEMS = __ITEMS__; | |
| const KEY = 'cwm3_review_relabel_hr_engaged_r4_v1'; | |
| let store = JSON.parse(localStorage.getItem(KEY) || '{}'); let cur = 0; let _pl=null; | |
| const SERVER = location.protocol.startsWith('http'); | |
| function setSync(msg, ok){ const e=document.getElementById('sync'); e.textContent=msg; e.style.color= ok?'#2a7a2a':'#b00020'; } | |
| async function pushLabel(id){ if(!SERVER) return; const r=store[id]||{}; try{ const res=await fetch('/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id, label:r.label, note:r.note, rater:'', ts:r.ts})}); const o=await res.json(); setSync(`已保存到服务器(${o.n_labelled} 条)`, true);}catch(e){ setSync('服务器保存失败,仅本地保存', false);} } | |
| async function pullLabels(){ if(!SERVER) return; try{ const res=await fetch('/labels',{cache:'no-store'}); const o=await res.json(); let n=0; ITEMS.forEach(it=>{ const r=o[it.id]; if(r&&r.label&&!(store[it.id]&&store[it.id].label)){ store[it.id]={label:r.label,note:r.note,ts:r.ts}; n++; } }); save(); setSync('已从服务器同步', true); if(n) render(); }catch(e){ setSync('无法连接服务器(离线模式)', false);} } | |
| function save(){ localStorage.setItem(KEY, JSON.stringify(store)); const done=ITEMS.filter(i=>store[i.id]&&store[i.id].label).length; document.getElementById('prog').textContent=`${done}/${ITEMS.length} 已标`; } | |
| function lab(id){ return (store[id]||{}).label||''; } | |
| function ansText(l){ if(!l) return '(未选)'; if(l==='never_on') return '从未回盘'; if(l==='always_on') return '一直在盘上(没离开)'; if(l==='unsure') return '不确定'; const dt=parseFloat(l.split('=')[1]); return `首次回盘帧 dt = ${dt>=0?'+':''}${dt.toFixed(1)} s`; } | |
| function setLabel(id,v,idx){ store[id]=Object.assign(store[id]||{}, {label:v, ts:new Date().toISOString()}); save(); pushLabel(id); renderOne(idx); } | |
| function setNote(id,v){ store[id]=Object.assign(store[id]||{}, {note:v}); save(); clearTimeout(window._nt); window._nt=setTimeout(()=>pushLabel(id), 800); } | |
| function plSet(idx,k){ const it=ITEMS[idx]; k=Math.min(Math.max(parseInt(k),0),it.frames.length-1); const f=it.frames[k]; document.getElementById('pl_'+idx).src=f.file; document.getElementById('rg_'+idx).value=k; document.getElementById('pt_'+idx).textContent=`dt=${f.dt>=0?'+':''}${f.dt.toFixed(1)} s${f.dt===0?' (EVENT)':''}`; } | |
| function plPlay(idx){ if(_pl){ clearInterval(_pl); _pl=null; return; } let k=parseInt(document.getElementById('rg_'+idx).value); _pl=setInterval(()=>{ k=(k+1)%ITEMS[idx].frames.length; plSet(idx,k); },150); } | |
| function plStep(d){ const rg=document.getElementById('rg_'+cur); if(rg) plSet(cur, parseInt(rg.value)+d); } | |
| function pickCur(){ const rg=document.getElementById('rg_'+cur); if(!rg) return; const it=ITEMS[cur]; setLabel(it.id, 'ret_dt='+it.frames[parseInt(rg.value)].dt.toFixed(1), cur); } | |
| function renderOne(idx){ const it=ITEMS[idx]; const d=document.getElementById('it_'+idx); const l=lab(it.id); d.className='item'+(l?' done':'')+(idx===cur?' cur':''); | |
| const k0=Math.max(0,it.frames.findIndex(f=>f.dt===0)); | |
| const frames=`<div class="player"><img id="pl_${idx}" src="${it.frames[k0].file}"><div><input type="range" id="rg_${idx}" min="0" max="${it.frames.length-1}" value="${k0}" style="width:320px" oninput="plSet(${idx},this.value)"> <span id="pt_${idx}">dt=+0.0 s (EVENT)</span> <button onclick="plPlay(${idx})">▶ / ⏸</button> <button onclick="cur=${idx};pickCur()">把当前帧选为首次回盘帧 (Enter)</button></div></div>` | |
| + `<div class="frames">` + it.frames.map((f,k)=>`<img loading="lazy" class="${l==='ret_dt='+f.dt.toFixed(1)?'sel':''}" src="${f.file}" title="dt=${f.dt.toFixed(1)}s(点击 = 选为首次回盘帧)" onclick="cur=${idx};plSet(${idx},${k});setLabel('${it.id}','ret_dt=${f.dt.toFixed(1)}',${idx})">`).join('') + `</div>`; | |
| d.innerHTML = `<div class="meta">${it.meta.orig_id} · ${it.meta.uid} · 自动标签 hand_return 于 t=${it.meta.t_event.toFixed(1)} s(dt=0)</div>${frames} | |
| <div class="q">第 ${idx+1}/${ITEMS.length} 条 · 第一张手在方向盘上的帧是?(点小图)</div> | |
| <div><button class="${l==='never_on'?'sel':''}" onclick="setLabel('${it.id}','never_on',${idx})">21 帧中从未回盘</button> <button class="${l==='always_on'?'sel':''}" onclick="setLabel('${it.id}','always_on',${idx})">一直在盘上(没离开过)</button> <button class="${l==='unsure'?'sel':''}" onclick="setLabel('${it.id}','unsure',${idx})">不确定</button></div> | |
| <div class="ans">当前答案:<b>${ansText(l)}</b></div> | |
| <div><input type="text" placeholder="备注(可选,如 phone)" value="${(store[it.id]||{}).note||''}" oninput="setNote('${it.id}',this.value)"></div>`; | |
| d.onclick=(e)=>{ cur=idx; highlight(); }; } | |
| function render(){ const m=document.getElementById('main'); m.innerHTML=''; ITEMS.forEach((it,idx)=>{ const d=document.createElement('div'); d.id='it_'+idx; m.appendChild(d); renderOne(idx); }); save(); highlight(); } | |
| function highlight(){ document.querySelectorAll('.item').forEach((e,i)=>e.classList.toggle('cur',i===cur)); } | |
| function jump(d){ cur=Math.min(Math.max(cur+d,0),ITEMS.length-1); highlight(); const el=document.getElementById('it_'+cur); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); } | |
| document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT'&&e.target.type==='text') return; | |
| if(e.key==='n') jump(1); else if(e.key==='p') jump(-1); else if(e.key===',') plStep(-1); else if(e.key==='.') plStep(1); | |
| else if(e.key===' '){ e.preventDefault(); plPlay(cur); } else if(e.key==='Enter'){ pickCur(); } }); | |
| render(); pullLabels(); | |
| </script></body></html> | |
| """ | |
| def main(argv=None) -> int: | |
| ap = argparse.ArgumentParser(); ap.add_argument("--out", default=str(CWM3 / "results/review_tool")); a = ap.parse_args(argv) | |
| out = Path(a.out) | |
| P3 = json.load(open(out / "items_relabel_hr_engaged_01s.json", encoding="utf-8")) | |
| items = [] | |
| for it in P3["items"]: | |
| m = dict(it["meta"]); m["pass"] = 4; m["orig_id"] = it["meta"]["orig_id"] | |
| items.append({"id": m["orig_id"] + SUFFIX, "task": "B4", "meta": m, "frames": it["frames"]}) | |
| ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) | |
| json.dump({"built_utc": ts, "pass": 4, "kind": "first_frame_hand_on_wheel", "labels": "ret_dt=<dt> | never_on | always_on | unsure", "n_items": len(items), "items": items}, | |
| open(out / "items_relabel_hr_engaged_r4.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False) | |
| (out / "relabel_hr_engaged_r4.html").write_text(HTML.replace("__ITEMS__", json.dumps(items, ensure_ascii=False)), encoding="utf-8") | |
| print(f"pass-4 diagnostic page built: {len(items)} items -> {out / 'relabel_hr_engaged_r4.html'}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |
Xet Storage Details
- Size:
- 9.88 kB
- Xet hash:
- 37b5b0d6c4663e8cda5c0a52112dc310e4665777517a6183a2d3688fda6f12a3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.