AsianPlayer/TriDrive / code /tools /review_tool /build_relabel_pass4.py
AsianPlayer's picture
download
raw
9.88 kB
#!/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.