Make EasyCom event review wearer anchor first

This commit is contained in:
Xin Wang
2026-09-14 23:30:31 +08:00
parent 06b5e6c385
commit 65347dd75d
12 changed files with 336 additions and 107 deletions
+37 -3
View File
@@ -26,7 +26,7 @@ const elements = Object.fromEntries(
"eventVideo", "mediaEmpty", "jumpEvent", "timeReadout", "clipLabel",
"transcriptList", "rawCandidate", "reviewForm", "formError",
"saveButton", "saveState", "toast", "relabelFields",
"excludeFields", "eventTypeField", "subtypeField",
"excludeFields", "eventTypeField", "opportunityField", "opportunitySelect", "subtypeField",
].map((id) => [id, document.getElementById(id)])
);
@@ -262,6 +262,18 @@ function renderDetail(detail) {
elements.eventSubtitle.textContent = detail.uid;
elements.clipLabel.textContent = detail.clip_stem ? `${detail.session} / ${detail.clip_stem}` : detail.session;
elements.rawCandidate.textContent = JSON.stringify(detail.candidate, null, 2);
const opportunityOptions = detail.candidate?.temporal_evidence?.opportunity_options || [];
elements.opportunitySelect.replaceChildren();
const emptyOption = document.createElement("option");
emptyOption.value = "";
emptyOption.textContent = "请选择 preceding release";
elements.opportunitySelect.append(emptyOption);
for (const option of opportunityOptions) {
const control = document.createElement("option");
control.value = option.uid;
control.textContent = `${formatTime(option.anchor_seconds)} · P${option.participant_id ?? "?"} · ${option.transcription || "无转写"}`;
elements.opportunitySelect.append(control);
}
elements.saveState.textContent = detail.review ? "已保存" : "未保存";
elements.saveState.classList.toggle("saved", Boolean(detail.review));
elements.reviewQuestion.textContent = reviewQuestions[detail.family] || "核对该候选是否属于目标 floor event。";
@@ -278,7 +290,10 @@ function renderDetail(detail) {
elements.aiRationale.textContent = view.rationale;
elements.acceptRadio.disabled = false;
elements.acceptTitle.textContent = `接受 ${view.verdict.replaceAll("_", " ")}`;
elements.acceptSummary.textContent = `${view.label} · ${view.meaning}`;
const selected = suggestion.selected_opportunity_uid;
const selectedOption = opportunityOptions.find((option) => option.uid === selected);
const anchorText = selectedOption ? ` · trigger ${formatTime(selectedOption.anchor_seconds)} P${selectedOption.participant_id}` : "";
elements.acceptSummary.textContent = `${view.label}${anchorText} · ${view.meaning}`;
} else {
elements.acceptRadio.disabled = true;
elements.acceptTitle.textContent = "没有 AI 结论";
@@ -435,6 +450,7 @@ function renderSpeakerTimeline(detail) {
return;
}
const eventTime = Number(detail.candidate.event_time ?? detail.candidate.anchor_dense_seconds);
const opportunityOptions = detail.candidate?.temporal_evidence?.opportunity_options || [];
const left = Math.min(...rows.map((row) => Number(row.start_seconds)), eventTime - 3);
const right = Math.max(...rows.map((row) => Number(row.end_seconds)), eventTime + 3);
const span = Math.max(0.001, right - left);
@@ -458,7 +474,18 @@ function renderSpeakerTimeline(detail) {
segment.addEventListener("click", () => showSpeakerEvidence(row, segment, true));
track.append(segment);
}
const anchor = document.createElement("i"); anchor.style.left = `${100 * (eventTime - left) / span}%`; track.append(anchor);
for (const option of opportunityOptions) {
const opportunity = document.createElement("i");
opportunity.className = "opportunity-anchor";
opportunity.style.left = `${100 * (Number(option.anchor_seconds) - left) / span}%`;
opportunity.title = `可选 release · ${formatTime(option.anchor_seconds)} · P${option.participant_id ?? "?"}`;
track.append(opportunity);
}
const anchor = document.createElement("i");
anchor.className = "behavior-anchor";
anchor.style.left = `${100 * (eventTime - left) / span}%`;
anchor.title = `wearer behavior · ${formatTime(eventTime)}`;
track.append(anchor);
lane.append(label, track); elements.speakerTimeline.append(lane);
}
const defaultRow = rows.find((row) =>
@@ -496,6 +523,7 @@ function formPayload() {
human_verdict: nullable("human_verdict"),
human_event_type: nullable("human_event_type"),
human_subtype: nullable("human_subtype"),
selected_opportunity_uid: nullable("selected_opportunity_uid"),
exclude_reason: nullable("exclude_reason"),
boundary_correction_seconds: correction === "" ? null : Number(correction),
confidence: data.get("confidence"),
@@ -508,6 +536,8 @@ function validateReview(review) {
if (!review.human_action) missing.push("主操作");
if (review.human_action === "relabel" && !review.human_verdict) missing.push("一级 verdict");
if (review.human_action === "relabel" && review.human_verdict === "POSITIVE" && !review.human_event_type) missing.push("positive event");
const options = state.currentDetail?.candidate?.temporal_evidence?.opportunity_options || [];
if (review.human_action === "relabel" && review.human_verdict === "POSITIVE" && review.human_event_type === "EOT_RESPONSE" && options.length && !review.selected_opportunity_uid) missing.push("EOT opportunity anchor");
if (review.human_action === "relabel" && review.human_verdict === "HARD_NEGATIVE" && !review.human_subtype) missing.push("hard-negative subtype");
if (review.human_action === "exclude" && !review.exclude_reason) missing.push("排除原因");
if (review.human_action === "accept" && !state.currentDetail?.candidate?.ai_verdict && !state.currentDetail?.llm_suggestion?.verdict) missing.push("AI 预标注(尚未运行,需 Relabel)");
@@ -517,9 +547,12 @@ function validateReview(review) {
function updateConditionalFields() {
const action = elements.reviewForm.elements.human_action.value;
const verdict = elements.reviewForm.elements.human_verdict.value;
const eventType = elements.reviewForm.elements.human_event_type.value;
const hasOpportunityOptions = Boolean(state.currentDetail?.candidate?.temporal_evidence?.opportunity_options?.length);
elements.relabelFields.hidden = action !== "relabel";
elements.excludeFields.hidden = action !== "exclude";
elements.eventTypeField.hidden = action !== "relabel" || verdict !== "POSITIVE";
elements.opportunityField.hidden = action !== "relabel" || verdict !== "POSITIVE" || eventType !== "EOT_RESPONSE" || !hasOpportunityOptions;
elements.subtypeField.hidden = action !== "relabel" || verdict !== "HARD_NEGATIVE";
}
@@ -619,6 +652,7 @@ function bindEvents() {
});
elements.reviewForm.elements.human_action.forEach((control) => control.addEventListener("change", updateConditionalFields));
elements.reviewForm.elements.human_verdict.addEventListener("change", updateConditionalFields);
elements.reviewForm.elements.human_event_type.addEventListener("change", updateConditionalFields);
elements.timelineZoomIn.addEventListener("click", () => changeTimelineZoom(2));
elements.timelineZoomOut.addEventListener("click", () => changeTimelineZoom(0.5));
elements.timelinePanLeft.addEventListener("click", () => panTimeline(-1));