Make EasyCom event review wearer anchor first
This commit is contained in:
@@ -274,7 +274,9 @@ textarea { resize: vertical; line-height: 1.45; }
|
||||
.speaker-lane { display: grid; grid-template-columns: 58px 1fr; align-items: center; gap: 8px; }
|
||||
.speaker-lane > strong { color: var(--muted); text-align: right; font-size: .72rem; }
|
||||
.speaker-track { height: 19px; position: relative; border: 1px solid var(--line); border-radius: 5px; overflow: hidden; background: #07101d; }
|
||||
.speaker-track > i { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--amber); z-index: 3; }
|
||||
.speaker-track > i { position: absolute; top: 0; bottom: 0; width: 1px; z-index: 3; }
|
||||
.speaker-track > i.behavior-anchor { background: var(--amber); }
|
||||
.speaker-track > i.opportunity-anchor { width: 2px; background: #60a5fa; opacity: .9; }
|
||||
.speech-segment { position: absolute; top: 3px; height: 11px; min-width: 4px; padding: 0; border: 1px solid transparent; border-radius: 3px; background: #60a5fa; cursor: pointer; }
|
||||
.speech-segment.wearer { background: var(--accent); }
|
||||
.speech-segment:hover, .speech-segment:focus-visible, .speech-segment.active { border-color: #fff; box-shadow: 0 0 0 2px rgba(255,255,255,.28); z-index: 4; outline: none; }
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
<section id="relabelFields" class="conditional-fields" hidden>
|
||||
<label><span>一级 verdict</span><select name="human_verdict"><option value="">请选择</option><option>POSITIVE</option><option>HARD_NEGATIVE</option><option>NOT_APPLICABLE</option><option>UNSURE</option></select></label>
|
||||
<label id="eventTypeField" hidden><span>Positive event</span><select name="human_event_type"><option value="">请选择</option><option>EOT_RESPONSE</option><option>INT_YIELD</option></select></label>
|
||||
<label id="opportunityField" hidden><span>EOT opportunity anchor</span><select id="opportunitySelect" name="selected_opportunity_uid"><option value="">请选择 preceding release</option></select></label>
|
||||
<label id="subtypeField" hidden><span>Hard-negative subtype</span><select name="human_subtype"><option value="">请选择</option><option>EOT_MIDTURN_PAUSE</option><option>EOT_OTHER_TO_OTHER</option><option>EOT_WEARER_BACKCHANNEL</option><option>INT_OTHER_BACKCHANNEL</option><option>INT_NON_TAKEOVER_OVERLAP</option><option>INT_WEARER_HOLDS_FLOOR</option><option>OTHER_HARD_NEGATIVE</option></select></label>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user