diff --git a/scripts/barrier_picker_app/app.py b/scripts/barrier_picker_app/app.py index 17490fe..a2d3c02 100644 --- a/scripts/barrier_picker_app/app.py +++ b/scripts/barrier_picker_app/app.py @@ -73,6 +73,7 @@ class QueueItem: duration_s: float | None done: bool metadata: dict # experimental fields aggregated from the merged TSV + existing: dict | None # current barrier_opening.csv row if already picked # ─── Queue building ───────────────────────────────────────────────────── @@ -142,13 +143,19 @@ def _build_queue() -> list[QueueItem]: "duration_s": float(r.duration_s) if pd.notna(r.duration_s) else None, } + existing_by_key: dict[tuple[str, str, str], dict] = {} if OUTPUT_CSV.exists(): out = pd.read_csv(OUTPUT_CSV) - done_keys = set(zip(out["machine_name"], - out["session_date"], - out["session_time"])) - else: - done_keys = set() + for _, r in out.iterrows(): + key = (r["machine_name"], r["session_date"], r["session_time"]) + existing_by_key[key] = { + "opening_s": float(r["opening_s"]) if pd.notna(r["opening_s"]) else None, + "trim_first_s": int(r["trim_first_s"]) if pd.notna(r.get("trim_first_s")) else 0, + "bad_rois": str(r.get("bad_rois", "") or "") if pd.notna(r.get("bad_rois")) else "", + "analyst": str(r.get("analyst", "") or "") if pd.notna(r.get("analyst")) else "", + "notes": str(r.get("notes", "") or "") if pd.notna(r.get("notes")) else "", + } + done_keys = set(existing_by_key) seen: set[tuple[str, str, str]] = set() items: list[QueueItem] = [] @@ -189,6 +196,7 @@ def _build_queue() -> list[QueueItem]: duration_s=inv_row["duration_s"], done=key in done_keys, metadata=metadata, + existing=existing_by_key.get(key), )) return items @@ -217,6 +225,7 @@ async def get_queue() -> JSONResponse: "duration_s": q.duration_s, "done": q.done, "metadata": q.metadata, + "existing": q.existing, } for q in queue ]) diff --git a/scripts/barrier_picker_app/static/index.html b/scripts/barrier_picker_app/static/index.html index 9245077..81ca80a 100644 --- a/scripts/barrier_picker_app/static/index.html +++ b/scripts/barrier_picker_app/static/index.html @@ -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; } @@ -178,6 +196,7 @@
+
@@ -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(); }