Make EasyCom event review wearer anchor first
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user