Add manuscript-aligned EasyCom EOT INT review workflow
This commit is contained in:
303
floor_event_review_web/app.css
Normal file
303
floor_event_review_web/app.css
Normal file
@@ -0,0 +1,303 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #07101d;
|
||||
--panel: #0d1728;
|
||||
--panel-2: #111d30;
|
||||
--line: #24334b;
|
||||
--line-strong: #344865;
|
||||
--text: #edf5ff;
|
||||
--muted: #98a8bd;
|
||||
--accent: #5eead4;
|
||||
--accent-strong: #2dd4bf;
|
||||
--accent-dim: rgba(94, 234, 212, 0.12);
|
||||
--amber: #fbbf24;
|
||||
--danger: #fb7185;
|
||||
--success: #34d399;
|
||||
--radius: 12px;
|
||||
--shadow: 0 18px 44px rgba(0, 0, 0, 0.24);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
html, body { min-height: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at 45% -20%, rgba(30, 64, 175, 0.18), transparent 42%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button, input, select, textarea { font: inherit; }
|
||||
button, a, select, input, textarea { outline-offset: 3px; }
|
||||
:focus-visible { outline: 2px solid var(--accent); }
|
||||
|
||||
.topbar {
|
||||
min-height: 72px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(280px, 440px) minmax(220px, 1fr);
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
background: rgba(7, 16, 29, 0.92);
|
||||
backdrop-filter: blur(14px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.brand-mark {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid var(--accent);
|
||||
border-radius: 9px;
|
||||
position: relative;
|
||||
box-shadow: inset 0 0 0 5px var(--accent-dim);
|
||||
}
|
||||
.brand-mark::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
top: 14px;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
box-shadow: 5px -6px 0 -0.5px var(--accent), 11px 5px 0 -0.5px var(--accent);
|
||||
transform: skewY(-35deg);
|
||||
}
|
||||
.brand h1 { font-size: 1rem; margin: 0; letter-spacing: 0.01em; }
|
||||
.brand p { color: var(--muted); font-size: 0.8rem; margin: 2px 0 0; }
|
||||
|
||||
.progress-block { display: grid; gap: 7px; }
|
||||
.progress-copy { display: flex; justify-content: space-between; gap: 20px; font-size: 0.86rem; }
|
||||
.progress-copy span { color: var(--muted); }
|
||||
.progress-track { height: 5px; border-radius: 8px; background: #17243a; overflow: hidden; }
|
||||
.progress-track span { display: block; height: 100%; width: 0; background: var(--accent); transition: width 180ms ease; }
|
||||
|
||||
.export-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.export-actions a {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 8px 11px;
|
||||
font-size: 0.84rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
.export-actions a:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
.filters {
|
||||
padding: 12px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(130px, 175px)) minmax(190px, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #091321;
|
||||
}
|
||||
|
||||
.filters label, .inline-fields label, .text-field { display: grid; gap: 6px; }
|
||||
.filters label > span, .inline-fields label > span, .text-field > span {
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
select, input[type="search"], input[type="text"], .text-field input, textarea {
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
background: #0a1525;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 9px 10px;
|
||||
min-height: 39px;
|
||||
}
|
||||
textarea { resize: vertical; line-height: 1.45; }
|
||||
::placeholder { color: #65758d; }
|
||||
|
||||
.button {
|
||||
min-height: 39px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
background: var(--panel-2);
|
||||
}
|
||||
.button:hover { border-color: var(--accent); }
|
||||
.button.primary { background: var(--accent-strong); border-color: var(--accent-strong); color: #04221f; font-weight: 800; }
|
||||
.button.primary:hover { background: var(--accent); }
|
||||
.button:disabled { cursor: wait; opacity: 0.55; }
|
||||
|
||||
.workspace {
|
||||
min-height: calc(100vh - 142px);
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 0.66fr) minmax(460px, 1.65fr) minmax(365px, 1fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: linear-gradient(180deg, rgba(17, 29, 48, 0.96), rgba(10, 20, 35, 0.96));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
.queue-panel, .review-panel { position: sticky; top: 152px; max-height: calc(100vh - 166px); }
|
||||
.queue-panel { display: flex; flex-direction: column; }
|
||||
.review-panel { overflow-y: auto; }
|
||||
|
||||
.panel-heading, .event-header, .section-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.panel-heading { padding: 15px 16px 12px; border-bottom: 1px solid var(--line); }
|
||||
.panel-heading h2, .section-heading h3 { margin: 2px 0 0; font-size: 1rem; }
|
||||
.eyebrow { color: var(--accent); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.12em; }
|
||||
.count-pill, .badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 25px;
|
||||
border-radius: 999px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid var(--line-strong);
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge.accent { color: var(--accent); border-color: rgba(94, 234, 212, 0.45); background: var(--accent-dim); }
|
||||
|
||||
.queue-list { overflow-y: auto; padding: 7px; }
|
||||
.queue-item {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.queue-item:hover { background: rgba(255,255,255,0.035); }
|
||||
.queue-item.active { border-color: var(--accent); background: var(--accent-dim); }
|
||||
.queue-item.reviewed .queue-status { background: var(--success); }
|
||||
.queue-line { display: flex; align-items: center; gap: 7px; }
|
||||
.queue-line strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.83rem; }
|
||||
.queue-status { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: #516078; }
|
||||
.queue-meta { color: var(--muted); font-size: 0.74rem; display: flex; gap: 8px; }
|
||||
.queue-label { font-size: 0.73rem; color: #cbd7e7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.queue-limit { margin: 0; padding: 10px 14px; color: var(--muted); font-size: 0.75rem; border-top: 1px solid var(--line); }
|
||||
|
||||
.event-header { padding: 16px 18px; }
|
||||
.event-header h2 { margin: 8px 0 3px; font-size: 1.2rem; }
|
||||
.event-header p { margin: 0; color: var(--muted); font-size: 0.82rem; }
|
||||
.event-badges { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.llm-suggestion {
|
||||
margin: 0 18px 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(94, 234, 212, 0.3);
|
||||
border-radius: 10px;
|
||||
background: rgba(94, 234, 212, 0.07);
|
||||
color: #c9f8ef;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.llm-suggestion strong { display: block; color: var(--accent); margin-bottom: 3px; }
|
||||
.event-position { color: var(--muted); font-size: 0.8rem; }
|
||||
|
||||
.video-shell { background: #02050a; aspect-ratio: 16 / 9; position: relative; border-block: 1px solid var(--line); }
|
||||
.video-shell video { width: 100%; height: 100%; display: block; object-fit: contain; }
|
||||
.empty-state { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: 7px; color: var(--muted); }
|
||||
.empty-state strong { color: var(--text); }
|
||||
.time-marker { position: absolute; pointer-events: none; left: 50%; top: 10px; bottom: 44px; border-left: 1px dashed rgba(251,191,36,.7); opacity: 0; }
|
||||
.video-shell:hover .time-marker { opacity: .55; }
|
||||
|
||||
.transport { padding: 10px 14px; display: flex; gap: 7px; align-items: center; border-bottom: 1px solid var(--line); }
|
||||
.transport button { color: var(--muted); border: 1px solid var(--line); background: #0a1525; border-radius: 7px; padding: 6px 9px; cursor: pointer; }
|
||||
.transport button:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.transport .jump-event { color: var(--amber); }
|
||||
#timeReadout { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; font-size: 0.8rem; }
|
||||
|
||||
.transcript-section { padding: 16px 18px 8px; }
|
||||
.section-heading { margin-bottom: 10px; }
|
||||
.section-heading > span { color: var(--muted); font-size: .78rem; }
|
||||
.transcript-list { display: grid; gap: 7px; max-height: 270px; overflow-y: auto; padding-right: 3px; }
|
||||
.transcript-row { display: grid; grid-template-columns: 72px minmax(0,1fr); gap: 10px; padding: 9px 10px; border: 1px solid var(--line); border-radius: 8px; background: rgba(2, 8, 18, 0.36); }
|
||||
.transcript-row.anchor { border-color: rgba(251,191,36,.65); background: rgba(251,191,36,.08); }
|
||||
.speaker { color: var(--accent); font-size: .75rem; font-weight: 800; }
|
||||
.speaker small { color: var(--muted); display: block; margin-top: 3px; font-weight: 500; }
|
||||
.utterance { line-height: 1.46; font-size: .9rem; }
|
||||
.utterance small { display: block; color: var(--muted); margin-top: 4px; font-size: .72rem; }
|
||||
.empty-copy { color: var(--muted); padding: 22px; text-align: center; border: 1px dashed var(--line-strong); border-radius: 8px; }
|
||||
|
||||
.raw-details { margin: 10px 18px 18px; border-top: 1px solid var(--line); padding-top: 12px; }
|
||||
.raw-details summary { color: var(--muted); cursor: pointer; font-size: .82rem; }
|
||||
.raw-details pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 260px; overflow-y: auto; color: #bfd0e7; font-size: .74rem; line-height: 1.5; }
|
||||
|
||||
.review-heading { background: rgba(94,234,212,.035); }
|
||||
.save-state { color: var(--muted); font-size: .75rem; }
|
||||
.save-state.saved { color: var(--success); }
|
||||
#reviewForm { padding: 13px 15px 16px; display: grid; gap: 14px; }
|
||||
fieldset { margin: 0; padding: 0; border: 0; display: grid; gap: 8px; }
|
||||
legend { font-size: .84rem; font-weight: 750; margin-bottom: 8px; }
|
||||
.required { color: var(--muted); font-size: .66rem; font-weight: 500; }
|
||||
.choice-grid { display: grid; gap: 7px; }
|
||||
.choice-grid.three { grid-template-columns: repeat(3, 1fr); }
|
||||
.choice-grid.two { grid-template-columns: repeat(2, 1fr); }
|
||||
.choice-grid label { cursor: pointer; min-width: 0; }
|
||||
.choice-grid input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.choice-grid label span { min-height: 52px; border: 1px solid var(--line); border-radius: 8px; padding: 8px; display: grid; align-content: center; gap: 2px; background: rgba(2,8,18,.28); }
|
||||
.choice-grid.compact label span { min-height: 40px; }
|
||||
.choice-grid label:hover span { border-color: var(--line-strong); }
|
||||
.choice-grid input:checked + span { border-color: var(--accent); background: var(--accent-dim); box-shadow: inset 0 0 0 1px rgba(94,234,212,.18); }
|
||||
.choice-grid input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.choice-grid b { font-size: .8rem; overflow: hidden; text-overflow: ellipsis; }
|
||||
.choice-grid small { color: var(--muted); font-size: .67rem; line-height: 1.25; }
|
||||
.inline-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
|
||||
.inline-fields select { font-size: .82rem; }
|
||||
.text-field input, .text-field textarea { font-size: .84rem; }
|
||||
.form-error { color: #fecdd3; background: rgba(251,113,133,.1); border: 1px solid rgba(251,113,133,.4); border-radius: 8px; padding: 9px; font-size: .8rem; }
|
||||
.review-actions { display: grid; grid-template-columns: .9fr 1.4fr; gap: 8px; }
|
||||
kbd { border: 1px solid rgba(3,24,22,.35); border-bottom-width: 2px; border-radius: 4px; padding: 1px 4px; font-size: .68rem; font-family: inherit; }
|
||||
.shortcuts { border-top: 1px solid var(--line); padding: 11px 15px 14px; display: flex; flex-wrap: wrap; gap: 8px 14px; color: var(--muted); font-size: .71rem; }
|
||||
.shortcuts kbd { color: #cbd7e7; border-color: var(--line-strong); margin-right: 2px; }
|
||||
|
||||
.toast { position: fixed; left: 50%; bottom: 22px; transform: translate(-50%, 20px); background: #d9fff9; color: #05251f; border-radius: 9px; padding: 10px 15px; font-weight: 750; font-size: .84rem; box-shadow: var(--shadow); opacity: 0; pointer-events: none; transition: 160ms ease; z-index: 40; }
|
||||
.toast.visible { opacity: 1; transform: translate(-50%, 0); }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.topbar { grid-template-columns: 1fr minmax(250px, 380px); }
|
||||
.export-actions { display: none; }
|
||||
.workspace { grid-template-columns: 220px minmax(430px, 1fr); }
|
||||
.review-panel { position: static; max-height: none; grid-column: 1 / -1; }
|
||||
.queue-panel { top: 152px; }
|
||||
.filters { grid-template-columns: repeat(3, 1fr); }
|
||||
.search-field { grid-column: span 2; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.topbar { position: static; grid-template-columns: 1fr; gap: 12px; }
|
||||
.filters { grid-template-columns: 1fr 1fr; padding: 10px; }
|
||||
.search-field { grid-column: 1 / -1; }
|
||||
.workspace { grid-template-columns: 1fr; padding: 10px; }
|
||||
.queue-panel, .review-panel { position: static; max-height: none; }
|
||||
.queue-panel { max-height: 340px; }
|
||||
.transport { flex-wrap: wrap; }
|
||||
#timeReadout { width: 100%; margin: 2px 0 0; }
|
||||
.choice-grid.three { grid-template-columns: 1fr; }
|
||||
}
|
||||
433
floor_event_review_web/app.js
Normal file
433
floor_event_review_web/app.js
Normal file
@@ -0,0 +1,433 @@
|
||||
"use strict";
|
||||
|
||||
const state = {
|
||||
candidates: [],
|
||||
filtered: [],
|
||||
reviews: {},
|
||||
currentId: null,
|
||||
currentDetail: null,
|
||||
saving: false,
|
||||
};
|
||||
|
||||
const elements = Object.fromEntries(
|
||||
[
|
||||
"progressText", "progressMeta", "progressBar", "sessionFilter", "splitFilter",
|
||||
"familyFilter", "statusFilter", "searchFilter", "clearFilters", "queueTitle",
|
||||
"queueCount", "queueList", "queueLimit", "familyBadge", "splitBadge",
|
||||
"autoConfidenceBadge", "eventTitle", "eventSubtitle", "eventPosition",
|
||||
"llmSuggestion", "llmSuggestionText",
|
||||
"eventVideo", "mediaEmpty", "jumpEvent", "timeReadout", "clipLabel",
|
||||
"transcriptList", "rawCandidate", "reviewForm", "reviewerInput", "formError",
|
||||
"saveButton", "uncertainButton", "saveState", "toast",
|
||||
].map((id) => [id, document.getElementById(id)])
|
||||
);
|
||||
|
||||
const familyNames = {
|
||||
eot: "EOT-RESPONSE",
|
||||
int: "INT-YIELD",
|
||||
onset: "WEARER ONSET",
|
||||
offset: "LEGACY EOT PROXY",
|
||||
interaction: "LEGACY INTERACTION",
|
||||
};
|
||||
|
||||
function escapeText(value) {
|
||||
return String(value ?? "");
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
if (value === null || value === undefined || Number.isNaN(Number(value))) return "—";
|
||||
const seconds = Number(value);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${String(minutes).padStart(2, "0")}:${(seconds % 60).toFixed(1).padStart(4, "0")}`;
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
elements.toast.textContent = message;
|
||||
elements.toast.classList.add("visible");
|
||||
window.clearTimeout(toast.timeout);
|
||||
toast.timeout = window.setTimeout(() => elements.toast.classList.remove("visible"), 1800);
|
||||
}
|
||||
|
||||
async function request(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || `请求失败 (${response.status})`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const total = state.candidates.length;
|
||||
const reviewed = Object.keys(state.reviews).length;
|
||||
const percent = total ? (reviewed / total) * 100 : 0;
|
||||
elements.progressText.textContent = `${reviewed} / ${total} 已审核`;
|
||||
elements.progressMeta.textContent = `${percent.toFixed(1)}%`;
|
||||
elements.progressBar.style.width = `${percent}%`;
|
||||
}
|
||||
|
||||
function populateSessions() {
|
||||
const sessions = [...new Set(state.candidates.map((item) => item.session))].sort((a, b) =>
|
||||
a.localeCompare(b, undefined, { numeric: true })
|
||||
);
|
||||
for (const session of sessions) {
|
||||
const option = document.createElement("option");
|
||||
option.value = session;
|
||||
option.textContent = session;
|
||||
elements.sessionFilter.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters({ keepCurrent = true } = {}) {
|
||||
const search = elements.searchFilter.value.trim().toLowerCase();
|
||||
state.filtered = state.candidates.filter((item) => {
|
||||
const reviewed = Boolean(state.reviews[item.candidate_id]);
|
||||
const status = elements.statusFilter.value;
|
||||
return (
|
||||
(elements.sessionFilter.value === "all" || item.session === elements.sessionFilter.value) &&
|
||||
(elements.splitFilter.value === "all" || item.split === elements.splitFilter.value) &&
|
||||
(elements.familyFilter.value === "all" || item.family === elements.familyFilter.value) &&
|
||||
(status === "all" || (status === "reviewed" ? reviewed : !reviewed)) &&
|
||||
(!search || `${item.uid} ${item.automatic_label} ${item.automatic_protocol_label || ""} ${item.automatic_hard_negative_type || ""} ${item.session}`.toLowerCase().includes(search))
|
||||
);
|
||||
});
|
||||
renderQueue();
|
||||
if (!keepCurrent || !state.filtered.some((item) => item.candidate_id === state.currentId)) {
|
||||
selectCandidate(state.filtered[0]?.candidate_id || null);
|
||||
} else {
|
||||
updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
function renderQueue() {
|
||||
elements.queueList.replaceChildren();
|
||||
elements.queueCount.textContent = String(state.filtered.length);
|
||||
elements.queueTitle.textContent = elements.statusFilter.value === "unreviewed" ? "待审核候选" : "候选事件";
|
||||
const limit = 250;
|
||||
for (const item of state.filtered.slice(0, limit)) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `queue-item${item.candidate_id === state.currentId ? " active" : ""}${state.reviews[item.candidate_id] ? " reviewed" : ""}`;
|
||||
button.dataset.id = item.candidate_id;
|
||||
|
||||
const line = document.createElement("div");
|
||||
line.className = "queue-line";
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "queue-status";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.uid;
|
||||
line.append(dot, title);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "queue-meta";
|
||||
meta.textContent = `${item.session} · ${familyNames[item.family] || item.family} · ${formatTime(item.event_time_seconds)}`;
|
||||
const label = document.createElement("div");
|
||||
label.className = "queue-label";
|
||||
label.textContent = item.automatic_protocol_label
|
||||
? `${item.automatic_protocol_label} · ${item.automatic_hard_negative_type || item.automatic_label}`
|
||||
: item.automatic_label;
|
||||
button.append(line, meta, label);
|
||||
button.addEventListener("click", () => selectCandidate(item.candidate_id));
|
||||
elements.queueList.append(button);
|
||||
}
|
||||
elements.queueLimit.hidden = state.filtered.length <= limit;
|
||||
elements.queueLimit.textContent = state.filtered.length > limit ? `列表显示前 ${limit} 条;可用筛选缩小范围。` : "";
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
const index = state.filtered.findIndex((item) => item.candidate_id === state.currentId);
|
||||
elements.eventPosition.textContent = index < 0 ? `— / ${state.filtered.length}` : `${index + 1} / ${state.filtered.length}`;
|
||||
}
|
||||
|
||||
async function selectCandidate(candidateId) {
|
||||
state.currentId = candidateId;
|
||||
renderQueue();
|
||||
updatePosition();
|
||||
if (!candidateId) {
|
||||
elements.eventTitle.textContent = "当前筛选下没有候选";
|
||||
elements.eventSubtitle.textContent = "调整筛选条件后继续。";
|
||||
elements.eventVideo.removeAttribute("src");
|
||||
elements.eventVideo.load();
|
||||
elements.mediaEmpty.hidden = false;
|
||||
elements.llmSuggestion.hidden = true;
|
||||
elements.reviewForm.reset();
|
||||
return;
|
||||
}
|
||||
elements.eventTitle.textContent = "载入事件…";
|
||||
elements.saveButton.disabled = true;
|
||||
try {
|
||||
const detail = await request(`/api/event?id=${encodeURIComponent(candidateId)}`);
|
||||
if (state.currentId !== candidateId) return;
|
||||
state.currentDetail = detail;
|
||||
renderDetail(detail);
|
||||
restoreForm(detail.review);
|
||||
} catch (error) {
|
||||
elements.eventTitle.textContent = "事件载入失败";
|
||||
elements.eventSubtitle.textContent = error.message;
|
||||
} finally {
|
||||
elements.saveButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetail(detail) {
|
||||
elements.familyBadge.textContent = familyNames[detail.family] || detail.family;
|
||||
elements.splitBadge.textContent = `${detail.split} · ${detail.session}`;
|
||||
elements.autoConfidenceBadge.textContent = detail.automatic_confidence ? `auto ${detail.automatic_confidence}` : "auto confidence —";
|
||||
elements.eventTitle.textContent = detail.automatic_label;
|
||||
elements.eventSubtitle.textContent = `${detail.uid} · ${detail.source_file}:${detail.source_row}`;
|
||||
elements.clipLabel.textContent = detail.clip_stem ? `${detail.session} / ${detail.clip_stem}` : detail.session;
|
||||
elements.rawCandidate.textContent = JSON.stringify(detail.candidate, null, 2);
|
||||
elements.saveState.textContent = detail.review ? "已保存" : "未保存";
|
||||
elements.saveState.classList.toggle("saved", Boolean(detail.review));
|
||||
|
||||
const suggestion = detail.llm_suggestion;
|
||||
elements.llmSuggestion.hidden = !suggestion;
|
||||
if (suggestion) {
|
||||
const disposition = suggestion.protocol_disposition || suggestion.final_protocol_disposition || "uncertain";
|
||||
const subtype = suggestion.hard_negative_type || suggestion.final_hard_negative_type || "uncertain";
|
||||
const confidence = suggestion.confidence || suggestion.final_confidence || "—";
|
||||
const rationale = suggestion.rationale || suggestion.final_rationale || "";
|
||||
elements.llmSuggestionText.textContent = `${disposition} · ${subtype} · ${confidence}${rationale ? ` — ${rationale}` : ""}`;
|
||||
}
|
||||
|
||||
elements.mediaEmpty.hidden = detail.media_available;
|
||||
if (detail.media_available) {
|
||||
elements.eventVideo.src = `/media?id=${encodeURIComponent(detail.candidate_id)}`;
|
||||
elements.eventVideo.load();
|
||||
} else {
|
||||
elements.eventVideo.removeAttribute("src");
|
||||
elements.eventVideo.load();
|
||||
}
|
||||
|
||||
elements.transcriptList.replaceChildren();
|
||||
if (!detail.transcript.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "empty-copy";
|
||||
empty.textContent = "当前上下文未找到 transcript。";
|
||||
elements.transcriptList.append(empty);
|
||||
} else {
|
||||
for (const row of detail.transcript) {
|
||||
const item = document.createElement("div");
|
||||
item.className = `transcript-row${row.is_anchor ? " anchor" : ""}`;
|
||||
const speaker = document.createElement("div");
|
||||
speaker.className = "speaker";
|
||||
speaker.textContent = `P${row.participant_id ?? "?"}`;
|
||||
const time = document.createElement("small");
|
||||
time.textContent = `${formatTime(row.start_seconds)}–${formatTime(row.end_seconds)}`;
|
||||
speaker.append(time);
|
||||
const utterance = document.createElement("div");
|
||||
utterance.className = "utterance";
|
||||
utterance.textContent = escapeText(row.transcription);
|
||||
const target = document.createElement("small");
|
||||
target.textContent = `Target: ${typeof row.target_of_speech === "string" ? row.target_of_speech : JSON.stringify(row.target_of_speech ?? "unknown")}`;
|
||||
utterance.append(target);
|
||||
item.append(speaker, utterance);
|
||||
elements.transcriptList.append(item);
|
||||
}
|
||||
}
|
||||
updateTimeReadout();
|
||||
}
|
||||
|
||||
function restoreForm(review) {
|
||||
elements.reviewForm.reset();
|
||||
elements.formError.hidden = true;
|
||||
const reviewer = review?.reviewer || localStorage.getItem("floor-reviewer") || "";
|
||||
elements.reviewerInput.value = reviewer;
|
||||
if (!review) {
|
||||
elements.reviewForm.elements.anchor_adjustment_bins.value = "0";
|
||||
elements.reviewForm.elements.behavior_adjustment_bins.value = "0";
|
||||
elements.reviewForm.elements.region_end_adjustment_bins.value = "0";
|
||||
elements.reviewForm.elements.confidence.value = "medium";
|
||||
return;
|
||||
}
|
||||
for (const [name, value] of Object.entries(review)) {
|
||||
const control = elements.reviewForm.elements[name];
|
||||
if (!control) continue;
|
||||
if (control instanceof RadioNodeList) control.value = String(value);
|
||||
else control.value = value === null ? "" : String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function formPayload() {
|
||||
const data = new FormData(elements.reviewForm);
|
||||
const adjustment = (name) => {
|
||||
const value = data.get(name);
|
||||
return value === "" ? null : Number(value);
|
||||
};
|
||||
return {
|
||||
protocol_disposition: data.get("protocol_disposition"),
|
||||
hard_negative_type: data.get("hard_negative_type"),
|
||||
validity: data.get("validity"),
|
||||
onset_function: data.get("onset_function"),
|
||||
context: data.get("context"),
|
||||
offset_outcome: data.get("offset_outcome"),
|
||||
anchor_adjustment_bins: adjustment("anchor_adjustment_bins"),
|
||||
behavior_adjustment_bins: adjustment("behavior_adjustment_bins"),
|
||||
region_end_adjustment_bins: adjustment("region_end_adjustment_bins"),
|
||||
confidence: data.get("confidence"),
|
||||
reviewer: String(data.get("reviewer") || "").trim(),
|
||||
notes: String(data.get("notes") || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function validateReview(review) {
|
||||
const missing = [];
|
||||
if (!review.protocol_disposition) missing.push("协议结论");
|
||||
if (!review.hard_negative_type) missing.push("hard-negative 类型");
|
||||
if (!review.validity) missing.push("活动边界");
|
||||
if (!review.onset_function) missing.push("onset 功能");
|
||||
if (!review.context) missing.push("对话场景");
|
||||
if (!review.offset_outcome) missing.push("offset 结果");
|
||||
if (!review.reviewer) missing.push("审核者");
|
||||
return missing;
|
||||
}
|
||||
|
||||
async function saveCurrent({ advance = true } = {}) {
|
||||
if (!state.currentId || state.saving) return;
|
||||
const review = formPayload();
|
||||
const missing = validateReview(review);
|
||||
if (missing.length) {
|
||||
elements.formError.textContent = `请完成:${missing.join("、")}`;
|
||||
elements.formError.hidden = false;
|
||||
return;
|
||||
}
|
||||
elements.formError.hidden = true;
|
||||
state.saving = true;
|
||||
elements.saveButton.disabled = true;
|
||||
elements.saveState.textContent = "保存中…";
|
||||
try {
|
||||
await request("/api/review", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ candidate_id: state.currentId, review }),
|
||||
});
|
||||
state.reviews[state.currentId] = review;
|
||||
localStorage.setItem("floor-reviewer", review.reviewer);
|
||||
elements.saveState.textContent = "已保存";
|
||||
elements.saveState.classList.add("saved");
|
||||
updateProgress();
|
||||
toast("审核结果已保存");
|
||||
if (advance) {
|
||||
const previousCandidates = [...state.filtered];
|
||||
const index = previousCandidates.findIndex((item) => item.candidate_id === state.currentId);
|
||||
applyFilters();
|
||||
const next = state.filtered[index] || state.filtered[index - 1] || state.filtered[0];
|
||||
if (elements.statusFilter.value === "all") {
|
||||
const sequential = previousCandidates[index + 1] || previousCandidates[index];
|
||||
selectCandidate(sequential?.candidate_id || null);
|
||||
} else {
|
||||
selectCandidate(next?.candidate_id || null);
|
||||
}
|
||||
} else {
|
||||
renderQueue();
|
||||
}
|
||||
} catch (error) {
|
||||
elements.formError.textContent = error.message;
|
||||
elements.formError.hidden = false;
|
||||
elements.saveState.textContent = "保存失败";
|
||||
} finally {
|
||||
state.saving = false;
|
||||
elements.saveButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function moveCandidate(delta) {
|
||||
if (!state.filtered.length) return;
|
||||
const index = state.filtered.findIndex((item) => item.candidate_id === state.currentId);
|
||||
const nextIndex = Math.max(0, Math.min(state.filtered.length - 1, (index < 0 ? 0 : index) + delta));
|
||||
selectCandidate(state.filtered[nextIndex].candidate_id);
|
||||
}
|
||||
|
||||
function seek(delta) {
|
||||
const video = elements.eventVideo;
|
||||
if (!Number.isFinite(video.duration)) return;
|
||||
video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + delta));
|
||||
}
|
||||
|
||||
function jumpToEvent() {
|
||||
const value = state.currentDetail?.event_local_seconds;
|
||||
if (value !== null && value !== undefined) elements.eventVideo.currentTime = Number(value);
|
||||
}
|
||||
|
||||
function updateTimeReadout() {
|
||||
const eventTime = state.currentDetail?.event_local_seconds;
|
||||
const current = Number.isFinite(elements.eventVideo.currentTime) ? elements.eventVideo.currentTime : 0;
|
||||
const delta = eventTime === null || eventTime === undefined ? null : current - Number(eventTime);
|
||||
elements.timeReadout.textContent = delta === null ? formatTime(current) : `${formatTime(current)} · event ${delta >= 0 ? "+" : ""}${delta.toFixed(1)} s`;
|
||||
}
|
||||
|
||||
function isTypingTarget(target) {
|
||||
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement;
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
for (const control of [elements.sessionFilter, elements.splitFilter, elements.familyFilter, elements.statusFilter]) {
|
||||
control.addEventListener("change", () => applyFilters({ keepCurrent: false }));
|
||||
}
|
||||
elements.searchFilter.addEventListener("input", () => applyFilters({ keepCurrent: false }));
|
||||
elements.clearFilters.addEventListener("click", () => {
|
||||
elements.sessionFilter.value = "all";
|
||||
elements.splitFilter.value = "all";
|
||||
elements.familyFilter.value = "all";
|
||||
elements.statusFilter.value = "unreviewed";
|
||||
elements.searchFilter.value = "";
|
||||
applyFilters({ keepCurrent: false });
|
||||
});
|
||||
elements.reviewForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
saveCurrent();
|
||||
});
|
||||
elements.uncertainButton.addEventListener("click", () => {
|
||||
elements.reviewForm.elements.protocol_disposition.value = "uncertain";
|
||||
elements.reviewForm.elements.hard_negative_type.value = "uncertain";
|
||||
elements.reviewForm.elements.validity.value = "uncertain";
|
||||
elements.reviewForm.elements.onset_function.value = "uncertain";
|
||||
elements.reviewForm.elements.context.value = "unknown";
|
||||
elements.reviewForm.elements.offset_outcome.value = "uncertain";
|
||||
elements.reviewForm.elements.confidence.value = "low";
|
||||
});
|
||||
elements.jumpEvent.addEventListener("click", jumpToEvent);
|
||||
document.querySelectorAll("[data-seek]").forEach((button) =>
|
||||
button.addEventListener("click", () => seek(Number(button.dataset.seek)))
|
||||
);
|
||||
elements.eventVideo.addEventListener("loadedmetadata", () => {
|
||||
const start = state.currentDetail?.context_start_seconds;
|
||||
if (start !== null && start !== undefined) elements.eventVideo.currentTime = Number(start);
|
||||
});
|
||||
elements.eventVideo.addEventListener("timeupdate", updateTimeReadout);
|
||||
elements.eventVideo.addEventListener("error", () => { elements.mediaEmpty.hidden = false; });
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveCurrent();
|
||||
return;
|
||||
}
|
||||
if (isTypingTarget(event.target)) return;
|
||||
if (event.key === "ArrowLeft") { event.preventDefault(); moveCandidate(-1); }
|
||||
else if (event.key === "ArrowRight") { event.preventDefault(); moveCandidate(1); }
|
||||
else if (event.key === "[") { event.preventDefault(); seek(-0.2); }
|
||||
else if (event.key === "]") { event.preventDefault(); seek(0.2); }
|
||||
else if (event.code === "Space") {
|
||||
event.preventDefault();
|
||||
elements.eventVideo.paused ? elements.eventVideo.play() : elements.eventVideo.pause();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
bindEvents();
|
||||
try {
|
||||
const payload = await request("/api/bootstrap");
|
||||
state.candidates = payload.candidates;
|
||||
state.reviews = payload.reviews || {};
|
||||
populateSessions();
|
||||
updateProgress();
|
||||
applyFilters({ keepCurrent: false });
|
||||
} catch (error) {
|
||||
elements.progressText.textContent = "载入失败";
|
||||
elements.progressMeta.textContent = error.message;
|
||||
elements.eventTitle.textContent = "无法载入候选事件";
|
||||
elements.eventSubtitle.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
285
floor_event_review_web/index.html
Normal file
285
floor_event_review_web/index.html
Normal file
@@ -0,0 +1,285 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="EasyCom wearer floor-event local review workspace" />
|
||||
<title>Floor Event Review</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230b1324'/%3E%3Cpath d='M14 39h8l5-17 9 29 6-22 4 10h5' fill='none' stroke='%235eead4' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
<div>
|
||||
<h1>Floor Event Review</h1>
|
||||
<p>EasyCom 事件审核</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-block" aria-live="polite">
|
||||
<div class="progress-copy">
|
||||
<strong id="progressText">载入中</strong>
|
||||
<span id="progressMeta">—</span>
|
||||
</div>
|
||||
<div class="progress-track"><span id="progressBar"></span></div>
|
||||
</div>
|
||||
<nav class="export-actions" aria-label="导出审核结果">
|
||||
<a href="/api/export.jsonl" download>导出 JSONL</a>
|
||||
<a href="/api/export.csv" download>导出 CSV</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="filters" aria-label="候选筛选">
|
||||
<label>
|
||||
<span>Session</span>
|
||||
<select id="sessionFilter"><option value="all">全部</option></select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Split</span>
|
||||
<select id="splitFilter">
|
||||
<option value="all">全部</option>
|
||||
<option value="train">train</option>
|
||||
<option value="test">test</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>事件族</span>
|
||||
<select id="familyFilter">
|
||||
<option value="all">全部</option>
|
||||
<option value="eot">EOT-response</option>
|
||||
<option value="int">INT-yield</option>
|
||||
<option value="onset">wearer onset</option>
|
||||
<option value="offset">旧 EOT proxy</option>
|
||||
<option value="interaction">旧 interaction proxy</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>审核状态</span>
|
||||
<select id="statusFilter">
|
||||
<option value="unreviewed">未审核</option>
|
||||
<option value="all">全部</option>
|
||||
<option value="reviewed">已审核</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="search-field">
|
||||
<span>搜索</span>
|
||||
<input id="searchFilter" type="search" placeholder="UID、类别、session" />
|
||||
</label>
|
||||
<button id="clearFilters" class="button ghost" type="button">清除筛选</button>
|
||||
</section>
|
||||
|
||||
<main class="workspace">
|
||||
<aside class="queue-panel panel" aria-label="事件候选列表">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">QUEUE</span>
|
||||
<h2 id="queueTitle">候选</h2>
|
||||
</div>
|
||||
<span id="queueCount" class="count-pill">0</span>
|
||||
</div>
|
||||
<div id="queueList" class="queue-list"></div>
|
||||
<p id="queueLimit" class="queue-limit" hidden></p>
|
||||
</aside>
|
||||
|
||||
<section class="evidence-panel panel" aria-label="事件证据">
|
||||
<div class="event-header">
|
||||
<div>
|
||||
<div class="event-badges">
|
||||
<span id="familyBadge" class="badge accent">—</span>
|
||||
<span id="splitBadge" class="badge">—</span>
|
||||
<span id="autoConfidenceBadge" class="badge">—</span>
|
||||
</div>
|
||||
<h2 id="eventTitle">请选择一个候选事件</h2>
|
||||
<p id="eventSubtitle">—</p>
|
||||
</div>
|
||||
<div class="event-position"><strong id="eventPosition">0 / 0</strong></div>
|
||||
</div>
|
||||
|
||||
<div id="llmSuggestion" class="llm-suggestion" hidden>
|
||||
<strong>DeepSeek 预标注(仅供复核)</strong>
|
||||
<span id="llmSuggestionText">—</span>
|
||||
</div>
|
||||
|
||||
<div class="video-shell">
|
||||
<video id="eventVideo" controls preload="metadata"></video>
|
||||
<div id="mediaEmpty" class="empty-state" hidden>
|
||||
<strong>当前候选未找到视频</strong>
|
||||
<span>检查 dataset root、session 和 clip stem。</span>
|
||||
</div>
|
||||
<div class="time-marker" aria-hidden="true"><span></span></div>
|
||||
</div>
|
||||
|
||||
<div class="transport" aria-label="视频定位">
|
||||
<button type="button" data-seek="-2">−2 s</button>
|
||||
<button type="button" data-seek="-0.2">−0.2 s</button>
|
||||
<button id="jumpEvent" class="jump-event" type="button">回到事件</button>
|
||||
<button type="button" data-seek="0.2">+0.2 s</button>
|
||||
<button type="button" data-seek="2">+2 s</button>
|
||||
<span id="timeReadout">—</span>
|
||||
</div>
|
||||
|
||||
<section class="transcript-section">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">TRANSCRIPT</span>
|
||||
<h3>事件上下文</h3>
|
||||
</div>
|
||||
<span id="clipLabel">—</span>
|
||||
</div>
|
||||
<div id="transcriptList" class="transcript-list">
|
||||
<div class="empty-copy">选择事件后显示 transcript。</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="raw-details">
|
||||
<summary>查看自动规则与原始候选字段</summary>
|
||||
<pre id="rawCandidate">{}</pre>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<aside class="review-panel panel" aria-label="人工审核表单">
|
||||
<div class="panel-heading review-heading">
|
||||
<div>
|
||||
<span class="eyebrow">HUMAN REVIEW</span>
|
||||
<h2>审核结论</h2>
|
||||
</div>
|
||||
<span id="saveState" class="save-state">未保存</span>
|
||||
</div>
|
||||
|
||||
<form id="reviewForm">
|
||||
<fieldset>
|
||||
<legend>1. 稿件协议结论 <span class="required">必填</span></legend>
|
||||
<div class="choice-grid two compact">
|
||||
<label><input type="radio" name="protocol_disposition" value="positive" /><span><b>Positive</b><small>EOT-response / INT-yield 正例</small></span></label>
|
||||
<label><input type="radio" name="protocol_disposition" value="hard_negative" /><span><b>Hard negative</b><small>错误 commit 的计分区间</small></span></label>
|
||||
<label><input type="radio" name="protocol_disposition" value="exclude" /><span><b>Exclude</b><small>歧义或标注损坏</small></span></label>
|
||||
<label><input type="radio" name="protocol_disposition" value="uncertain" /><span><b>不确定</b><small>需要裁决</small></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>2. Hard-negative 类型 <span class="required">必填</span></legend>
|
||||
<div class="choice-grid two compact">
|
||||
<label><input type="radio" name="hard_negative_type" value="eot_midturn_pause" /><span><b>EOT 句中停顿</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="eot_other_to_other" /><span><b>EOT other→other</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="eot_no_response" /><span><b>EOT 保持沉默</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="int_backchannel" /><span><b>INT backchannel</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="int_non_takeover_overlap" /><span><b>INT 非接管重叠</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="int_retain_floor" /><span><b>INT 保持话轮</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="other" /><span><b>其他</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="not_applicable" /><span><b>不适用</b></span></label>
|
||||
<label><input type="radio" name="hard_negative_type" value="uncertain" /><span><b>不确定</b></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>3. 活动边界是否真实? <span class="required">必填</span></legend>
|
||||
<div class="choice-grid three">
|
||||
<label><input type="radio" name="validity" value="valid" /><span><b>真实</b><small>有效活动边界</small></span></label>
|
||||
<label><input type="radio" name="validity" value="artifact" /><span><b>伪边界</b><small>噪声、断点或错标</small></span></label>
|
||||
<label><input type="radio" name="validity" value="uncertain" /><span><b>不确定</b><small>证据不足</small></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>4. Onset 的 floor 功能 <span class="required">必填</span></legend>
|
||||
<div class="choice-grid two compact">
|
||||
<label><input type="radio" name="onset_function" value="floor_taking" /><span><b>取得话轮</b></span></label>
|
||||
<label><input type="radio" name="onset_function" value="backchannel" /><span><b>Backchannel</b></span></label>
|
||||
<label><input type="radio" name="onset_function" value="within_turn_resume" /><span><b>句内恢复</b></span></label>
|
||||
<label><input type="radio" name="onset_function" value="nonverbal" /><span><b>非语言发声</b></span></label>
|
||||
<label><input type="radio" name="onset_function" value="not_applicable" /><span><b>不适用</b></span></label>
|
||||
<label><input type="radio" name="onset_function" value="uncertain" /><span><b>不确定</b></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>5. 对话指向/场景 <span class="required">必填</span></legend>
|
||||
<div class="choice-grid two compact">
|
||||
<label><input type="radio" name="context" value="addressed_wearer" /><span><b>对 wearer</b></span></label>
|
||||
<label><input type="radio" name="context" value="third_party" /><span><b>第三方交谈</b></span></label>
|
||||
<label><input type="radio" name="context" value="group" /><span><b>群体</b></span></label>
|
||||
<label><input type="radio" name="context" value="unknown" /><span><b>未知</b></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>6. Offset 的结果 <span class="required">必填</span></legend>
|
||||
<div class="choice-grid two compact">
|
||||
<label><input type="radio" name="offset_outcome" value="yield_to_other" /><span><b>让给 other</b></span></label>
|
||||
<label><input type="radio" name="offset_outcome" value="completion_to_silence" /><span><b>结束后静音</b></span></label>
|
||||
<label><input type="radio" name="offset_outcome" value="temporary_pause" /><span><b>短暂停顿</b></span></label>
|
||||
<label><input type="radio" name="offset_outcome" value="backchannel_end" /><span><b>Backchannel 结束</b></span></label>
|
||||
<label><input type="radio" name="offset_outcome" value="cutoff" /><span><b>截断/边界</b></span></label>
|
||||
<label><input type="radio" name="offset_outcome" value="not_applicable" /><span><b>不适用</b></span></label>
|
||||
<label><input type="radio" name="offset_outcome" value="uncertain" /><span><b>不确定</b></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="inline-fields">
|
||||
<label>
|
||||
<span>other 锚点校正</span>
|
||||
<select name="anchor_adjustment_bins">
|
||||
<option value="">无法判断</option>
|
||||
<option value="-1">−1 bin(−200 ms)</option>
|
||||
<option value="0" selected>原 bin</option>
|
||||
<option value="1">+1 bin(+200 ms)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>wearer 行为锚点校正</span>
|
||||
<select name="behavior_adjustment_bins">
|
||||
<option value="">不适用/无法判断</option>
|
||||
<option value="-1">−1 bin(−200 ms)</option>
|
||||
<option value="0" selected>原 bin</option>
|
||||
<option value="1">+1 bin(+200 ms)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>负区间终点校正</span>
|
||||
<select name="region_end_adjustment_bins">
|
||||
<option value="">不适用/无法判断</option>
|
||||
<option value="-1">−1 bin(−200 ms)</option>
|
||||
<option value="0" selected>原 bin</option>
|
||||
<option value="1">+1 bin(+200 ms)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>置信度</span>
|
||||
<select name="confidence" required>
|
||||
<option value="high">高</option>
|
||||
<option value="medium" selected>中</option>
|
||||
<option value="low">低</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="text-field">
|
||||
<span>审核者</span>
|
||||
<input id="reviewerInput" name="reviewer" maxlength="100" placeholder="姓名或代号" required />
|
||||
</label>
|
||||
<label class="text-field">
|
||||
<span>备注</span>
|
||||
<textarea name="notes" maxlength="4000" rows="3" placeholder="记录歧义、说话对象或边界问题"></textarea>
|
||||
</label>
|
||||
|
||||
<div id="formError" class="form-error" role="alert" hidden></div>
|
||||
<div class="review-actions">
|
||||
<button id="uncertainButton" class="button ghost" type="button">全部标为不确定</button>
|
||||
<button id="saveButton" class="button primary" type="submit">保存并看下一个 <kbd>⌘↵</kbd></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="shortcuts">
|
||||
<span><kbd>←</kbd><kbd>→</kbd> 前后事件</span>
|
||||
<span><kbd>[</kbd><kbd>]</kbd> ±200 ms</span>
|
||||
<span><kbd>Space</kbd> 播放/暂停</span>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
<script src="/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user