Picker: render existing annotation as a clickable mark below the video
When a video that already has a barrier_opening row is loaded, a thin bar now appears below the player with a red vertical line at the marked opening_s. A tooltip shows the timestamp, the analyst's initials, and the bad_rois flag. Clicking the line snaps the player to the exact opening; clicking elsewhere on the bar seeks proportionally. Bar is hidden for un-annotated videos. API change: each queue item now carries an `existing` field with the opening_s, bad_rois, analyst, etc. from barrier_opening.csv when available, so the frontend can paint the mark without extra requests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2623df4172
commit
d0f0e2d443
2 changed files with 83 additions and 5 deletions
|
|
@ -107,6 +107,24 @@
|
|||
font-size: 0.95rem; }
|
||||
#login-submit:hover { background: #3e6; }
|
||||
#login-submit:disabled { background: #444; color: #888; cursor: not-allowed; }
|
||||
|
||||
/* Annotation bar (existing-mark indicator below the video) */
|
||||
#annotation-bar { position: relative; width: 100%; max-width: 1400px;
|
||||
height: 18px; background: #181818; border: 1px solid #333;
|
||||
border-radius: 3px; margin-top: 0.5rem; cursor: pointer; }
|
||||
#annotation-bar.empty { background: transparent; border-color: transparent;
|
||||
cursor: default; height: 0.6rem; }
|
||||
#annotation-bar .mark-line { position: absolute; top: -2px; bottom: -2px;
|
||||
width: 3px; background: #f44;
|
||||
box-shadow: 0 0 4px rgba(255,80,80,0.7); }
|
||||
#annotation-bar .mark-line:hover { background: #f88; }
|
||||
#annotation-bar .mark-tooltip { position: absolute; top: 100%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 4px; background: #f44;
|
||||
color: white; padding: 2px 6px;
|
||||
font-size: 0.75rem; border-radius: 3px;
|
||||
font-family: ui-monospace, monospace;
|
||||
white-space: nowrap; pointer-events: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -178,6 +196,7 @@
|
|||
<main>
|
||||
<div id="meta"></div>
|
||||
<video id="player" controls preload="auto"></video>
|
||||
<div id="annotation-bar" class="empty"></div>
|
||||
<div id="controls">
|
||||
<button class="primary" data-mode="all">All barriers open <kbd>1</kbd></button>
|
||||
<button class="primary" data-mode="upper">Upper barrier opens <kbd>2</kbd></button>
|
||||
|
|
@ -268,6 +287,55 @@
|
|||
progress.textContent = `${done}/${queue.length} done`;
|
||||
}
|
||||
|
||||
function renderAnnotationBar(item) {
|
||||
const bar = document.getElementById('annotation-bar');
|
||||
bar.innerHTML = '';
|
||||
const dur = item.duration_s;
|
||||
const ex = item.existing;
|
||||
// Always allow click-to-seek if we have a duration; only show as a real
|
||||
// bar (with marks) when there's an existing annotation to display.
|
||||
if (!dur) {
|
||||
bar.classList.add('empty');
|
||||
bar.onclick = null;
|
||||
return;
|
||||
}
|
||||
if (!ex || ex.opening_s == null || isNaN(ex.opening_s)) {
|
||||
bar.classList.add('empty');
|
||||
bar.onclick = null;
|
||||
return;
|
||||
}
|
||||
bar.classList.remove('empty');
|
||||
// Click anywhere on the bar = seek to that fractional position.
|
||||
bar.onclick = (e) => {
|
||||
const rect = bar.getBoundingClientRect();
|
||||
const t = (e.clientX - rect.left) / rect.width * dur;
|
||||
player.currentTime = Math.max(0, Math.min(dur, t));
|
||||
};
|
||||
// Mark line at opening_s
|
||||
const pct = (ex.opening_s / dur) * 100;
|
||||
const line = document.createElement('div');
|
||||
line.className = 'mark-line';
|
||||
line.style.left = `calc(${pct}% - 1px)`;
|
||||
// Click on the line specifically = snap to exact opening_s
|
||||
line.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
player.currentTime = Math.max(0, Math.min(dur, ex.opening_s));
|
||||
};
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'mark-tooltip';
|
||||
const m = Math.floor(ex.opening_s / 60);
|
||||
const s = (ex.opening_s % 60).toFixed(1);
|
||||
const tag = ex.bad_rois ? ` · bad ROIs ${ex.bad_rois}` : '';
|
||||
const who = ex.analyst ? ` · ${ex.analyst}` : '';
|
||||
tooltip.textContent = `${m}:${s.padStart(4,'0')}${who}${tag}`;
|
||||
// Position tooltip; if mark is near right edge, anchor right.
|
||||
tooltip.style.left = pct < 90 ? `${pct}%` : `${pct}%`;
|
||||
if (pct < 8) tooltip.style.transform = 'translateX(0)';
|
||||
else if (pct > 92) tooltip.style.transform = 'translateX(-100%)';
|
||||
bar.appendChild(line);
|
||||
bar.appendChild(tooltip);
|
||||
}
|
||||
|
||||
function renderMeta(m) {
|
||||
meta.innerHTML = '';
|
||||
if (!m) return;
|
||||
|
|
@ -307,6 +375,7 @@
|
|||
(item.duration_s ? `(${(item.duration_s/60).toFixed(1)} min)` : '') +
|
||||
(item.done ? ' — already done' : '');
|
||||
renderMeta(item.metadata);
|
||||
renderAnnotationBar(item);
|
||||
player.src = `/api/video/${item.idx}`;
|
||||
player.load();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue