Picker: identify the analyst (initials) per pick
Each annotation row now carries an `analyst` column. On first visit the web picker shows a small login modal asking for initials, persists them in localStorage, and shows the badge in the top-right. Click the badge to change identities. Submissions without initials are rejected by the backend (HTTP 400). Skip remains analyst-free. Backfill: every existing barrier_opening.csv row marked as `GG` since all current picks were done by Giorgio. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
12568b82cc
commit
2623df4172
3 changed files with 145 additions and 14 deletions
|
|
@ -1,7 +1,28 @@
|
||||||
machine_name,session_date,session_time,opening_s,trim_first_s,notes
|
machine_name,session_date,session_time,opening_s,trim_first_s,bad_rois,analyst,notes
|
||||||
ETHOSCOPE_076,2025-07-15,16-03-10,52.0,0,hand-annotated 2025-07-15 batch
|
ETHOSCOPE_076,2025-07-15,16-03-10,52.0,0,,GG,hand-annotated 2025-07-15 batch
|
||||||
ETHOSCOPE_076,2025-07-15,16-31-34,94.0,69,first ~66s misframed (arena partly out of frame)
|
ETHOSCOPE_076,2025-07-15,16-31-34,94.0,69,,GG,first ~66s misframed (arena partly out of frame)
|
||||||
ETHOSCOPE_145,2025-07-15,16-03-27,42.0,0,hand-annotated 2025-07-15 batch
|
ETHOSCOPE_145,2025-07-15,16-03-27,42.0,0,,GG,hand-annotated 2025-07-15 batch
|
||||||
ETHOSCOPE_145,2025-07-15,16-31-41,89.0,69,first ~60s misframed (arena partly out of frame)
|
ETHOSCOPE_145,2025-07-15,16-31-41,89.0,69,,GG,first ~60s misframed (arena partly out of frame)
|
||||||
ETHOSCOPE_268,2025-07-15,16-32-05,75.0,0,hand-annotated 2025-07-15 batch
|
ETHOSCOPE_268,2025-07-15,16-32-05,75.0,0,,GG,hand-annotated 2025-07-15 batch
|
||||||
ETHOSCOPE_076,2024-10-21,11-07-54,346.8,0,
|
ETHOSCOPE_076,2024-10-21,11-07-54,346.8,0,,GG,
|
||||||
|
ETHOSCOPE_181,2024-10-21,11-08-57,287.3,0,,GG,
|
||||||
|
ETHOSCOPE_225,2024-10-21,11-09-12,277.9,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_082,2024-10-21,11-07-46,365.3,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_140,2024-10-21,11-06-58,423.9,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_083,2024-10-21,11-09-07,306.4,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_145,2024-10-21,11-08-35,341.0,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_076,2024-09-17,10-32-10,1875.8,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_082,2024-09-17,10-53-16,646.8,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_140,2024-09-17,11-03-05,86.2,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_181,2024-09-17,10-33-12,1824.3,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_225,2024-09-17,10-53-57,588.3,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_082,2024-09-18,10-15-49,277.4,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_140,2024-09-18,10-15-39,246.1,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_181,2024-09-18,10-15-56,194.7,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_225,2024-09-18,10-15-45,225.4,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_076,2024-10-01,11-04-03,731.1,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_082,2024-10-01,11-03-52,762.4,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_083,2024-10-01,11-07-41,560.3,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_113,2024-10-01,11-07-48,565.7,0,"2,4,6",GG,
|
||||||
|
ETHOSCOPE_140,2024-10-01,11-04-07,755.0,0,"1,3,5",GG,
|
||||||
|
ETHOSCOPE_167,2024-10-01,11-07-55,564.5,0,"1,3,5",GG,
|
||||||
|
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ DB_NAME_RE = re.compile(
|
||||||
)
|
)
|
||||||
|
|
||||||
OUT_COLS = ["machine_name", "session_date", "session_time",
|
OUT_COLS = ["machine_name", "session_date", "session_time",
|
||||||
"opening_s", "trim_first_s", "bad_rois", "notes"]
|
"opening_s", "trim_first_s", "bad_rois", "analyst", "notes"]
|
||||||
|
|
||||||
# ROI numbering in the HD mating arena (verified via tracking_geometry):
|
# ROI numbering in the HD mating arena (verified via tracking_geometry):
|
||||||
# upper row = ROIs 1, 3, 5 (y ≈ 0.125)
|
# upper row = ROIs 1, 3, 5 (y ≈ 0.125)
|
||||||
|
|
@ -275,6 +275,7 @@ class Submission(BaseModel):
|
||||||
idx: int
|
idx: int
|
||||||
time_s: float | None # None when marking unusable
|
time_s: float | None # None when marking unusable
|
||||||
mode: str # "all" | "upper" | "lower" | "unusable" | "skip"
|
mode: str # "all" | "upper" | "lower" | "unusable" | "skip"
|
||||||
|
analyst: str = "" # initials of the human picker (required, non-skip)
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -288,6 +289,10 @@ async def submit(payload: Submission) -> dict:
|
||||||
if payload.mode == "skip":
|
if payload.mode == "skip":
|
||||||
return {"status": "skipped"}
|
return {"status": "skipped"}
|
||||||
|
|
||||||
|
analyst = payload.analyst.strip().upper()
|
||||||
|
if not analyst:
|
||||||
|
raise HTTPException(status_code=400, detail="analyst initials required")
|
||||||
|
|
||||||
if payload.mode == "unusable":
|
if payload.mode == "unusable":
|
||||||
row = {
|
row = {
|
||||||
"machine_name": item.machine_name,
|
"machine_name": item.machine_name,
|
||||||
|
|
@ -296,6 +301,7 @@ async def submit(payload: Submission) -> dict:
|
||||||
"opening_s": float("nan"),
|
"opening_s": float("nan"),
|
||||||
"trim_first_s": 0,
|
"trim_first_s": 0,
|
||||||
"bad_rois": "",
|
"bad_rois": "",
|
||||||
|
"analyst": analyst,
|
||||||
"notes": payload.notes or "unusable",
|
"notes": payload.notes or "unusable",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
|
@ -315,6 +321,7 @@ async def submit(payload: Submission) -> dict:
|
||||||
"opening_s": round(payload.time_s, 1),
|
"opening_s": round(payload.time_s, 1),
|
||||||
"trim_first_s": 0,
|
"trim_first_s": 0,
|
||||||
"bad_rois": bad_rois,
|
"bad_rois": bad_rois,
|
||||||
|
"analyst": analyst,
|
||||||
"notes": payload.notes,
|
"notes": payload.notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,37 @@
|
||||||
border-radius: 4px; cursor: pointer; border: 1px solid #1a4;
|
border-radius: 4px; cursor: pointer; border: 1px solid #1a4;
|
||||||
font-size: 0.95rem; }
|
font-size: 0.95rem; }
|
||||||
#modal-close:hover { background: #3e6; }
|
#modal-close:hover { background: #3e6; }
|
||||||
|
|
||||||
|
/* User badge in header */
|
||||||
|
#user-badge { background: #2a3; color: #042; font-weight: 700;
|
||||||
|
padding: 0.2rem 0.6rem; border-radius: 12px;
|
||||||
|
font-family: ui-monospace, monospace; font-size: 0.85rem;
|
||||||
|
cursor: pointer; user-select: none; }
|
||||||
|
#user-badge:hover { background: #3b4; }
|
||||||
|
#user-badge.empty { background: #d84; color: #311; }
|
||||||
|
|
||||||
|
/* Login modal — narrower than the welcome modal */
|
||||||
|
#login-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.85);
|
||||||
|
display: none; align-items: center; justify-content: center;
|
||||||
|
z-index: 200; }
|
||||||
|
#login-backdrop.show { display: flex; }
|
||||||
|
#login-box { background: #222; border: 1px solid #444; border-radius: 6px;
|
||||||
|
padding: 1.6rem 2rem; max-width: 380px; width: 90%;
|
||||||
|
color: #ddd; }
|
||||||
|
#login-box h2 { margin: 0 0 0.6rem; color: #fff; font-size: 1.1rem; }
|
||||||
|
#login-box p { margin: 0.4rem 0 1rem; color: #aab; font-size: 0.9rem; }
|
||||||
|
#login-input { width: 100%; padding: 0.6rem 0.8rem; font-size: 1.1rem;
|
||||||
|
background: #111; color: #fff; border: 1px solid #444;
|
||||||
|
border-radius: 4px; text-align: center;
|
||||||
|
font-family: ui-monospace, monospace; letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase; }
|
||||||
|
#login-input:focus { outline: none; border-color: #6c5; }
|
||||||
|
#login-submit { width: 100%; margin-top: 1rem; padding: 0.7rem;
|
||||||
|
background: #2d5; color: #053; font-weight: 600;
|
||||||
|
border-radius: 4px; cursor: pointer; border: 1px solid #1a4;
|
||||||
|
font-size: 0.95rem; }
|
||||||
|
#login-submit:hover { background: #3e6; }
|
||||||
|
#login-submit:disabled { background: #444; color: #888; cursor: not-allowed; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -83,9 +114,20 @@
|
||||||
<h1>Cupido — barrier picker</h1>
|
<h1>Cupido — barrier picker</h1>
|
||||||
<span id="info">loading…</span>
|
<span id="info">loading…</span>
|
||||||
<span id="progress"></span>
|
<span id="progress"></span>
|
||||||
|
<span id="user-badge" title="click to change initials">…</span>
|
||||||
<button id="help-btn" title="show the help modal">?</button>
|
<button id="help-btn" title="show the help modal">?</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div id="login-backdrop">
|
||||||
|
<div id="login-box">
|
||||||
|
<h2>Who are you?</h2>
|
||||||
|
<p>Enter your initials so we can record who annotated each video.
|
||||||
|
(Just letters, e.g. <code>GG</code>.)</p>
|
||||||
|
<input id="login-input" maxlength="4" autocomplete="off" autofocus />
|
||||||
|
<button id="login-submit" disabled>Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="modal-backdrop">
|
<div id="modal-backdrop">
|
||||||
<div id="modal">
|
<div id="modal">
|
||||||
<h2>Welcome to the Cupido barrier picker</h2>
|
<h2>Welcome to the Cupido barrier picker</h2>
|
||||||
|
|
@ -162,6 +204,55 @@
|
||||||
const meta = document.getElementById('meta');
|
const meta = document.getElementById('meta');
|
||||||
const progress = document.getElementById('progress');
|
const progress = document.getElementById('progress');
|
||||||
const flash = document.getElementById('flash');
|
const flash = document.getElementById('flash');
|
||||||
|
const userBadge = document.getElementById('user-badge');
|
||||||
|
const loginBackdrop = document.getElementById('login-backdrop');
|
||||||
|
const loginInput = document.getElementById('login-input');
|
||||||
|
const loginSubmit = document.getElementById('login-submit');
|
||||||
|
|
||||||
|
// ─── Analyst identity (persisted in localStorage) ───────────────────
|
||||||
|
function getAnalyst() {
|
||||||
|
try { return localStorage.getItem('cupido.analyst') || ''; }
|
||||||
|
catch (e) { return ''; }
|
||||||
|
}
|
||||||
|
function setAnalyst(s) {
|
||||||
|
try { localStorage.setItem('cupido.analyst', s); } catch (e) {}
|
||||||
|
renderUserBadge();
|
||||||
|
}
|
||||||
|
function renderUserBadge() {
|
||||||
|
const a = getAnalyst();
|
||||||
|
userBadge.textContent = a || 'sign in';
|
||||||
|
userBadge.classList.toggle('empty', !a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLogin() {
|
||||||
|
loginBackdrop.classList.add('show');
|
||||||
|
loginInput.value = getAnalyst();
|
||||||
|
loginSubmit.disabled = !loginInput.value.trim();
|
||||||
|
setTimeout(() => loginInput.focus(), 50);
|
||||||
|
}
|
||||||
|
function hideLogin() { loginBackdrop.classList.remove('show'); }
|
||||||
|
|
||||||
|
function submitLogin() {
|
||||||
|
const v = loginInput.value.trim().toUpperCase();
|
||||||
|
if (!v) return;
|
||||||
|
setAnalyst(v);
|
||||||
|
hideLogin();
|
||||||
|
// After first login, show the welcome modal if not yet seen.
|
||||||
|
try {
|
||||||
|
if (localStorage.getItem('cupido.welcomed') !== '1') {
|
||||||
|
showModal(); // welcome modal — defined below
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
loginInput.addEventListener('input', () => {
|
||||||
|
loginSubmit.disabled = !loginInput.value.trim();
|
||||||
|
});
|
||||||
|
loginInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && loginInput.value.trim()) submitLogin();
|
||||||
|
});
|
||||||
|
loginSubmit.addEventListener('click', submitLogin);
|
||||||
|
userBadge.addEventListener('click', showLogin);
|
||||||
|
|
||||||
let queue = [];
|
let queue = [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
|
|
@ -232,11 +323,17 @@
|
||||||
|
|
||||||
async function submit(mode) {
|
async function submit(mode) {
|
||||||
if (queue.length === 0) return;
|
if (queue.length === 0) return;
|
||||||
|
// Require initials before any picking action (skip is OK without).
|
||||||
|
if (mode !== 'skip' && !getAnalyst()) {
|
||||||
|
showLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const item = queue[cursor];
|
const item = queue[cursor];
|
||||||
const payload = {
|
const payload = {
|
||||||
idx: item.idx,
|
idx: item.idx,
|
||||||
time_s: (mode === 'skip' || mode === 'unusable') ? null : player.currentTime,
|
time_s: (mode === 'skip' || mode === 'unusable') ? null : player.currentTime,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
|
analyst: getAnalyst(),
|
||||||
notes: '',
|
notes: '',
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|
@ -287,8 +384,9 @@
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
// Don't react to picker shortcuts while the modal is open.
|
// Don't react to picker shortcuts while either modal is open.
|
||||||
if (modalBackdrop.classList.contains('show')) return;
|
if (modalBackdrop.classList.contains('show')) return;
|
||||||
|
if (loginBackdrop.classList.contains('show')) return;
|
||||||
// Prevent the browser default (e.g. video focus side effects on space).
|
// Prevent the browser default (e.g. video focus side effects on space).
|
||||||
const stop = () => { e.preventDefault(); e.stopPropagation(); };
|
const stop = () => { e.preventDefault(); e.stopPropagation(); };
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
|
@ -324,19 +422,24 @@
|
||||||
}
|
}
|
||||||
modalClose.addEventListener('click', hideModal);
|
modalClose.addEventListener('click', hideModal);
|
||||||
helpBtn.addEventListener('click', showModal);
|
helpBtn.addEventListener('click', showModal);
|
||||||
// Allow click-on-backdrop to dismiss (but not click-inside-modal)
|
|
||||||
modalBackdrop.addEventListener('click', (e) => {
|
modalBackdrop.addEventListener('click', (e) => {
|
||||||
if (e.target === modalBackdrop) hideModal();
|
if (e.target === modalBackdrop) hideModal();
|
||||||
});
|
});
|
||||||
// Escape closes the modal too.
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && modalBackdrop.classList.contains('show')) {
|
if (e.key === 'Escape' && modalBackdrop.classList.contains('show')) {
|
||||||
e.stopPropagation(); hideModal();
|
e.stopPropagation(); hideModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// On first visit, login first (mandatory) then welcome modal.
|
||||||
|
renderUserBadge();
|
||||||
|
if (!getAnalyst()) {
|
||||||
|
showLogin();
|
||||||
|
} else {
|
||||||
let welcomed = false;
|
let welcomed = false;
|
||||||
try { welcomed = localStorage.getItem('cupido.welcomed') === '1'; } catch (e) {}
|
try { welcomed = localStorage.getItem('cupido.welcomed') === '1'; } catch (e) {}
|
||||||
if (!welcomed) showModal();
|
if (!welcomed) showModal();
|
||||||
|
}
|
||||||
|
|
||||||
fetchQueue();
|
fetchQueue();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue