File size: 15,763 Bytes
a4d1b37
 
 
 
 
 
 
 
 
 
 
b8d0bff
a4d1b37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b8d0bff
a4d1b37
 
 
b8d0bff
a4d1b37
 
 
 
 
 
 
b8d0bff
a4d1b37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
'use strict';
const $=id=>document.getElementById(id);
const COLORS=['#2563eb','#db2777','#16a34a','#ea580c','#7c3aed','#0891b2','#ca8a04','#dc2626'];
const LABELS={matched:'模型对应候选',missing_mask:'缺 mask · 待补',not_visible_in_reference:'参考帧未能定位',mask_quality_issue:'mask 质量待审',ambiguous:'身份对应含糊',requires_mask_union:'多 mask 分组待审'};
const REVIEW={unreviewed:'尚未审核',reviewed:'已审核并记录',needs_review:'需修改',reject:'不采用'};
const KEY='videoglamm-activitynet-object20-review-v1';
const DATA_SHA='6e112bf37251619cc39a6ca58897f1c2abe85c486daef1a4dd345d91f1f8e208';
const state={rows:[],filtered:[],cur:null,group:null,mask:null,frame:0,reviews:{},timer:null};
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const src=file=>`${state.cur.asset_dir}/${file}`;
const currentGroup=()=>state.cur.groups.find(g=>g.group_id===state.group);
const groupColor=g=>state.cur.source_masks.find(o=>o.mask_id===g.mask_id)?.color||'#a16207';
function stop(){if(state.timer)clearInterval(state.timer);state.timer=null;$('play').textContent='播放采样帧';}
function reviewOf(id){return state.reviews[id]||{status:'unreviewed',note:''};}
function filterRows(keep=true){
 const q=$('search').value.trim().toLowerCase(),f=$('filter').value,id=keep?state.cur?.sample_id:null;
 state.filtered=state.rows.filter(r=>(f==='all'||reviewOf(r.sample_id).status===f)&&(!q||`${r.sample_id} ${r.caption} ${r.groups.map(g=>g.entity_label).join(' ')}`.toLowerCase().includes(q)));
 $('list-count').textContent=`${state.filtered.length} / ${state.rows.length} samples`;
 $('review-count').textContent=`本浏览器:${Object.values(state.reviews).filter(r=>r.status!=='unreviewed').length} / 20 已记录`;
 renderList();
 if(!state.filtered.length){stop();state.cur=null;$('entry').hidden=true;$('loading').hidden=false;$('loading').textContent='没有符合筛选的样本。';$('prev').disabled=$('next').disabled=true;return;}
 if(!id||!state.filtered.some(r=>r.sample_id===id))load(state.filtered[0].sample_id);else renderList();
}
function renderList(){
 $('entry-list').innerHTML=state.filtered.map(r=>`<button class="entry-item ${state.cur?.sample_id===r.sample_id?'active':''}" data-id="${r.sample_id}" aria-label="样本 ${r.selection_index} ${r.video_id}"><strong>${String(r.selection_index).padStart(2,'0')} · ${esc(r.video_id)}</strong><small>${r.source_masks.length} masks · ${r.groups.length} 实体 · ${REVIEW[reviewOf(r.sample_id).status]}</small></button>`).join('');
 $('entry-list').querySelectorAll('button').forEach(b=>b.onclick=()=>load(b.dataset.id));
 const i=state.filtered.indexOf(state.cur);$('prev').disabled=i<=0;$('next').disabled=i<0||i>=state.filtered.length-1;
}
function load(id){
 stop();state.cur=state.rows.find(r=>r.sample_id===id);const r=state.cur;state.group=null;state.frame=r.reference_frame_id;state.mask=r.source_masks[0]?.mask_id||null;
 $('mode').value='all';$('group-filter').value='all';$('loading').hidden=true;$('entry').hidden=false;
 $('title').textContent=`${String(r.selection_index).padStart(2,'0')} · ${r.sample_id}`;
 $('meta').textContent=`10 张采样 RGB · ${r.source_masks.length} 个源 mask · ${r.groups.length} 个实体组 · 片段 ${r.source_timestamps.map(t=>t.toFixed(2)).join('–')} s`;
 $('badges').innerHTML='<span class="badge">字符 span 校验通过</span><span class="badge warn">部分对应 · 待人工审</span>';
 $('mask').innerHTML=r.source_masks.map(o=>`<option value="${o.mask_id}">${o.mask_id} · ${esc(o.source_classes.join(' / '))}</option>`).join('');
 $('objects').innerHTML=r.source_masks.map((o,i)=>`<button class="obj-button" style="--c:${o.color}" data-mask="${o.mask_id}"><strong>${o.mask_id} · ${esc(o.source_classes.join(' / '))}</strong><small>${o.source_seg_token} · 仅帧 ${String(r.reference_frame_id).padStart(2,'0')} · ${o.mask_foreground_pixels.toLocaleString()} pixels<br>源 bbox [${o.source_bbox.join(', ')}]</small></button>`).join('');
 $('objects').querySelectorAll('button').forEach(b=>b.onclick=()=>inspectMask(b.dataset.mask));
 $('frames').innerHTML=Array.from({length:10},(_,i)=>`<button class="frame-thumb ${i===r.reference_frame_id?'reference':''}" data-frame="${i}" aria-label="采样帧 ${i}${i===r.reference_frame_id?' mask 参考帧':''}"><img src="${src(String(i).padStart(2,'0')+'.jpg')}" alt="采样帧 ${i}"><span>${String(i).padStart(2,'0')} ${i===r.reference_frame_id?'· mask':'· RGB'}</span></button>`).join('');
 $('frames').querySelectorAll('button').forEach(b=>b.onclick=()=>{stop();state.frame=Number(b.dataset.frame);renderMedia();});
 const duration=r.source_timestamps[1]-r.source_timestamps[0];$('timing').textContent=`按源采样公式推算:间隔约 ${(duration/10).toFixed(3)} s(约 ${(10/duration).toFixed(3)} 张/秒);时间标签是估计值。`;
 $('issues').innerHTML=[...r.issues.map(i=>`<p><strong>${esc(i.type)}</strong>${i.text?' · '+esc(i.text):''}<br>${esc(i.description)}</p>`),...r.model_review_reasons.map(t=>`<p>${esc(t)}</p>`),...r.validation.warnings.map(t=>`<p>本地检查:${esc(typeof t==='string'?t:JSON.stringify(t))}</p>`),...r.local_adjustments.map(t=>`<p>确定性整理(原模型输出保留):${esc(JSON.stringify(t))}</p>`)].join('')||'<p>没有额外问题记录;仍需人工复核实体完整性及对应。</p>';
 const rev=reviewOf(id);$('review-status').value=rev.status;$('note').value=rev.note;$('saved').textContent=rev.updated_at?'本浏览器保存于 '+new Date(rev.updated_at).toLocaleString():'';
 $('original-refined').textContent=r.refined_caption_original;$('original-new').textContent=r.new_caption_original||'原字段不存在';
 $('raw-json').textContent=JSON.stringify(r,null,2);
 $('links').innerHTML=`<a href="${src('original_annotation.json')}" target="_blank">源完整 JSON</a><a href="${src('model_annotation.json')}" target="_blank">未改动的 GPT 输出</a>`;
 $('gpt-overlay').src=src('mask_overlay.jpg');$('gpt-identities').src=src('identity_sheet.jpg');$('gpt-binary').src=src('binary_mask_sheet.png');
 renderList();renderCaption();renderGroups();renderDetail();renderMedia();window.scrollTo(0,0);
}
function renderCaption(){
 const r=state.cur,chars=Array.from(r.caption),spans=[];
 r.groups.forEach((g,gi)=>g.mentions.forEach(m=>{if(m.span)spans.push({s:m.span[0],e:m.span[1],g,gi});}));
 const pts=[...new Set([0,chars.length,...spans.flatMap(m=>[m.s,m.e])])].sort((a,b)=>a-b);$('caption').replaceChildren();
 for(let i=0;i<pts.length-1;i++){
  const s=pts[i],e=pts[i+1],active=spans.filter(m=>m.s<=s&&e<=m.e),t=chars.slice(s,e).join('');
  if(!active.length){$('caption').append(document.createTextNode(t));continue;}
  const selected=active.find(m=>m.g.group_id===state.group),a=selected||active[0],wrap=document.createElement('span');
  wrap.className='mention'+(selected?' selected':'');wrap.style.setProperty('--c',groupColor(a.g));wrap.title=active.map(m=>`${m.g.group_id} · ${m.g.entity_label} · ${LABELS[m.g.grounding_status]}`).join('\n');
  wrap.append(document.createTextNode(t));const sup=document.createElement('sup');sup.textContent=[...new Set(active.map(m=>m.g.group_id))].join(',');wrap.append(sup);
  wrap.onclick=()=>{const ids=[...new Set(active.map(m=>m.g.group_id))];selectGroup(ids[(ids.indexOf(state.group)+1)%ids.length]);};$('caption').append(wrap);
 }
 const g=currentGroup();$('selected-summary').textContent=g?`选中 ${g.group_id} · ${g.entity_label} · ${LABELS[g.grounding_status]};高亮包括该组全部提及。共享 / 重叠 span 可反复点击切换,或在下方直接选择组。`:'候选对应的文字与源 mask 同色;棕色为待补 / 待审。跨组 span 允许重叠,角标列出全部组。';
}
function selectGroup(id){
 stop();state.group=id;const g=currentGroup();state.frame=state.cur.reference_frame_id;state.mask=g.mask_id||null;$('mode').value=g.mask_id?'selected':'rgb';
 renderCaption();renderGroups();renderDetail();renderMedia();
}
function renderGroups(){
 const filter=$('group-filter').value;
 $('groups').innerHTML=state.cur.groups.map((g,i)=>({g,i})).filter(({g})=>filter==='all'||(filter==='matched'?g.grounding_status==='matched':g.grounding_status!=='matched')).map(({g,i})=>`<button class="group ${state.group===g.group_id?'active':''}" style="--c:${groupColor(g)}" data-group="${g.group_id}"><div class="group-title">${g.group_id} · ${esc(g.entity_label)}</div><span class="status ${g.grounding_status}">${LABELS[g.grounding_status]}</span><span class="group-objects"> ${esc(g.entity_kind)}${g.mask_id?' · '+g.mask_id:''}</span>${g.mentions.map(m=>`<div class="group-mention"><code>${esc(m.text)}</code> [${m.span?m.span.join(', '):'待确定'})</div>`).join('')}<div class="group-objects">${g.candidate_mask_ids.length?'待审 mask:'+g.candidate_mask_ids.join(', '):g.mask_id?'候选对应,未经人工确认':'没有已对应的源 mask'}</div></button>`).join('');
 $('groups').querySelectorAll('button').forEach(b=>b.onclick=()=>selectGroup(b.dataset.group));
}
function renderDetail(){
 const g=currentGroup();if(!g){$('group-detail').innerHTML='<p>点击 caption 或下方实体组查看对应。原始 mask 清单包含全部源 mask,可逐个检查。</p>';$('mask-preview').replaceChildren();return;}
 $('group-detail').innerHTML=`<strong>${esc(g.group_id+' · '+g.entity_label)}</strong><p><span class="status ${g.grounding_status}">${LABELS[g.grounding_status]}</span> · ${esc(g.visibility)} · 模型信心 ${esc(g.confidence)}</p><p>${esc(g.reason)}</p>${g.mentions.map(m=>`<div><code>${esc(m.text)}</code> · [${m.span?m.span.join(', '):'待确定'})</div>`).join('')}`;
 $('mask-preview').innerHTML=g.mask_id?`<div>该组的模型候选对应:${g.mask_id}(非人工认证)</div><img src="${src(g.mask_id+'_mask.png')}" alt="实体组 ${g.group_id} 的候选源 mask ${g.mask_id}">`:`<div class="pending-box">此实体没有已对应的源 mask。text 已保留,待补 / 待审。${g.candidate_mask_ids.length?' 候选 ID:'+g.candidate_mask_ids.join(', ')+';可在上方原始 mask 清单中独立检查。':''}</div>`;
}
function inspectMask(id){stop();state.mask=id;state.frame=state.cur.reference_frame_id;$('mode').value='selected';renderMedia();}
function renderMedia(){
 const r=state.cur,ref=state.frame===r.reference_frame_id,mode=$('mode').value,mid=state.mask;
 let file=String(state.frame).padStart(2,'0')+'.jpg',message='原始 RGB';
 if(ref&&mode==='all'){file='all_overlay.jpg';message=`全部 ${r.source_masks.length} 个源 mask 叠加(颜色见清单)`;}
 if(ref&&mode==='selected'){if(mid){file=mid+'_overlay.jpg';message=`独立检查源 mask ${mid} · 叠加显示`;}else message='当前实体无对应 mask · 显示参考 RGB';}
 if(ref&&mode==='binary'){if(mid){file=mid+'_mask.png';message=`独立检查源 mask ${mid} · 二值图`;}else message='当前实体无对应 mask · 显示参考 RGB';}
 if(!ref)message='此采样帧无源 mask / bbox · 仅显示 RGB';
 $('big-image').src=src(file);$('big-image').alt=`${r.sample_id} 帧 ${state.frame} · ${message}`;
 $('frame-state').textContent=`帧 ${String(state.frame).padStart(2,'0')} / 09 · 估计 ${r.estimated_frame_times_s[state.frame].toFixed(3)} s · ${ref?'MASK 参考帧 · ':''}${message}`;
 $('mask').value=mid||'';
 $('frames').querySelectorAll('button').forEach(b=>{b.classList.toggle('active',Number(b.dataset.frame)===state.frame);b.setAttribute('aria-pressed',String(Number(b.dataset.frame)===state.frame));});
 $('objects').querySelectorAll('button').forEach(b=>b.classList.toggle('active',ref&&mode!=='all'&&mode!=='rgb'&&mid===b.dataset.mask));
}
function persistReview(){
 if(!state.cur)return;state.reviews[state.cur.sample_id]={status:$('review-status').value,note:$('note').value,updated_at:new Date().toISOString()};
 try{localStorage.setItem(KEY,JSON.stringify({dataset_sha256:DATA_SHA,reviews:state.reviews}));$('saved').textContent='已保存在当前浏览器。请导出备份或汇总。';}catch(e){$('saved').textContent='浏览器保存失败;请立即导出审核备份。';}
 renderList();$('review-count').textContent=`本浏览器:${Object.values(state.reviews).filter(r=>r.status!=='unreviewed').length} / 20 已记录`;
}
$('search').oninput=()=>filterRows();$('filter').onchange=()=>filterRows();
$('prev').onclick=()=>load(state.filtered[Math.max(0,state.filtered.indexOf(state.cur)-1)].sample_id);
$('next').onclick=()=>load(state.filtered[Math.min(state.filtered.length-1,state.filtered.indexOf(state.cur)+1)].sample_id);
$('all-groups').onclick=()=>{state.group=null;renderCaption();renderGroups();renderDetail();};
$('group-filter').onchange=renderGroups;
$('reference').onclick=()=>{stop();state.frame=state.cur.reference_frame_id;renderMedia();};
$('mode').onchange=()=>{stop();if($('mode').value!=='rgb')state.frame=state.cur.reference_frame_id;renderMedia();};
$('mask').onchange=()=>inspectMask($('mask').value);
$('play').onclick=()=>{if(state.timer){stop();return;}state.frame=0;$('mode').value='rgb';renderMedia();$('play').textContent='暂停播放';state.timer=setInterval(()=>{if(state.frame>=9){stop();return;}state.frame++;renderMedia();},500);};
$('review-status').onchange=persistReview;$('note').oninput=persistReview;
$('export').onclick=()=>{const x={schema:'videoglamm-activitynet-object20-review-v1',dataset_sha256:DATA_SHA,exported_at:new Date().toISOString(),storage:'browser-local',source_annotations_unchanged:true,reviews:state.reviews};const u=URL.createObjectURL(new Blob([JSON.stringify(x,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=u;a.download='videoglamm-activitynet-object20-review.json';a.click();setTimeout(()=>URL.revokeObjectURL(u),2000);};
$('import').onclick=()=>$('import-file').click();
$('import-file').onchange=async()=>{try{const f=$('import-file').files[0];if(!f)return;const x=JSON.parse(await f.text());if(x.dataset_sha256!==DATA_SHA||!x.reviews||typeof x.reviews!=='object')throw new Error('数据版本或文件格式不匹配');const accepted={};for(const [id,r]of Object.entries(x.reviews)){if(!state.rows.some(a=>a.sample_id===id)||!r||!Object.hasOwn(REVIEW,r.status)||typeof r.note!=='string')throw new Error('存在无效样本或审核字段');accepted[id]={status:r.status,note:r.note,updated_at:r.updated_at||new Date().toISOString()};}for(const [id,r]of Object.entries(accepted)){const old=state.reviews[id];if(!old||Date.parse(r.updated_at)>=Date.parse(old.updated_at))state.reviews[id]=r;}localStorage.setItem(KEY,JSON.stringify({dataset_sha256:DATA_SHA,reviews:state.reviews}));filterRows();if(state.cur)load(state.cur.sample_id);alert('已合并审核记录;相同样本保留更新时间较新的记录。');}catch(e){alert('导入失败:'+e.message);}finally{$('import-file').value='';}};
async function init(){
 const r=await fetch('data/annotations.json');if(!r.ok)throw new Error(`标注载入失败:${r.status}`);state.rows=await r.json();
 try{const saved=JSON.parse(localStorage.getItem(KEY)||'null');if(saved?.dataset_sha256===DATA_SHA)state.reviews=Object.fromEntries(Object.entries(saved.reviews||{}).filter(([id,v])=>state.rows.some(r=>r.sample_id===id)&&v&&Object.hasOwn(REVIEW,v.status)&&typeof v.note==='string'));}catch(e){$('processing').textContent='浏览器存储不可用,请导出审核。';}
 filterRows(false);
 const receipt=await fetch('data/processing_receipt.json').then(r=>r.json());$('processing').textContent=`处理状态:${receipt.status} · ${receipt.request_counts.completed}/20 完成 · ${receipt.request_counts.failed} 失败。`;
}
init().catch(e=>{$('loading').hidden=false;$('loading').textContent=e.message;});