Revise wearer entry and yield annotation protocol

This commit is contained in:
Xin Wang
2026-09-15 01:25:39 +08:00
parent 026d89978f
commit 71dfd8808a
12 changed files with 478 additions and 248 deletions
+31 -29
View File
@@ -34,10 +34,10 @@ SOURCE_FPS = 20.0
STATIC_ROOT = Path(__file__).resolve().parent / "floor_event_review_web"
VIDEO_ROOT_PARTS = ("Main", "Video_Compressed")
TRANSCRIPT_ROOT_PARTS = ("Main", "Speech_Transcriptions")
REVIEW_SCHEMA_VERSION = 4
REVIEW_SCHEMA_VERSION = 5
MAX_REQUEST_BYTES = 128 * 1024
PROTOCOL_DISPOSITION_VALUES = {"positive", "hard_negative", "exclude", "uncertain"}
PROTOCOL_DISPOSITION_VALUES = {"positive", "hard_negative", "exclude"}
HARD_NEGATIVE_TYPE_VALUES = {
"eot_midturn_pause",
"eot_other_to_other",
@@ -46,17 +46,13 @@ HARD_NEGATIVE_TYPE_VALUES = {
"int_non_takeover_overlap",
"int_retain_floor",
"other",
"not_applicable",
"uncertain",
}
VALIDITY_VALUES = {"valid", "artifact", "uncertain"}
VALIDITY_VALUES = {"valid", "artifact"}
ONSET_FUNCTION_VALUES = {
"floor_taking",
"backchannel",
"within_turn_resume",
"nonverbal",
"not_applicable",
"uncertain",
}
CONTEXT_VALUES = {"addressed_wearer", "third_party", "group", "unknown"}
OFFSET_OUTCOME_VALUES = {
@@ -65,21 +61,20 @@ OFFSET_OUTCOME_VALUES = {
"temporary_pause",
"backchannel_end",
"cutoff",
"not_applicable",
"uncertain",
}
CONFIDENCE_VALUES = {"high", "medium", "low"}
HUMAN_ACTION_VALUES = {"accept", "relabel", "exclude", "second_review"}
HUMAN_VERDICT_VALUES = {"POSITIVE", "HARD_NEGATIVE", "NOT_APPLICABLE", "EXCLUDE", "UNSURE"}
HUMAN_EVENT_TYPE_VALUES = {"EOT_RESPONSE", "INT_YIELD"}
HUMAN_ACTION_VALUES = {"accept", "relabel", "exclude"}
HUMAN_VERDICT_VALUES = {"POSITIVE", "HARD_NEGATIVE", "EXCLUDE"}
HUMAN_EVENT_TYPE_VALUES = {"WEARER_ENTRY", "INT_YIELD"}
HUMAN_SUBTYPE_VALUES = {
"EOT_MIDTURN_PAUSE", "EOT_OTHER_TO_OTHER", "EOT_WEARER_BACKCHANNEL",
"ENTRY_OTHER_MIDTURN_PAUSE", "ENTRY_OTHER_TO_OTHER", "ENTRY_WEARER_BACKCHANNEL",
"INT_OTHER_BACKCHANNEL", "INT_NON_TAKEOVER_OVERLAP", "INT_WEARER_HOLDS_FLOOR",
"OTHER_HARD_NEGATIVE",
}
EXCLUDE_REASON_VALUES = {
"ANNOTATION_ERROR", "RECORDING_BOUNDARY", "MISSING_CONTEXT", "NON_SPEECH_ARTIFACT",
"TIMESTAMP_CORRUPTION", "UNRESOLVABLE",
"TIMESTAMP_CORRUPTION", "OUT_OF_SCOPE_NORMAL_COMPLETION", "OUT_OF_SCOPE_NON_EVENT",
"UNRESOLVABLE",
}
@@ -123,12 +118,12 @@ class Candidate:
@property
def family(self) -> str:
if self.payload.get("candidate_source"):
return "eot" if self.task == "eot_response" else "int"
if self.task == "eot_response":
return "eot"
if self.task == "int_yield":
if self.task in {"wearer_entry", "wearer_entry_control", "eot_response", "eot_response_control"}:
return "entry"
if self.task in {"int_yield", "int_yield_control"}:
return "int"
if self.payload.get("candidate_source") == "WEARER_ONSET":
return "entry"
if "holder_uid" in self.payload or self.automatic_label.startswith("eot_"):
return "offset"
if self.payload.get("participant_role") == "wearer":
@@ -454,7 +449,8 @@ class ReviewStore:
normalized[key] = value
action = normalized["human_action"]
candidate = self.candidates[candidate_id].payload
candidate_record = self.candidates[candidate_id]
candidate = candidate_record.payload
if action == "accept":
verdict = candidate.get("ai_verdict") or (candidate.get("llm_suggestion") or {}).get("verdict")
event_type = candidate.get("ai_event_type") or (candidate.get("llm_suggestion") or {}).get("event_type")
@@ -467,22 +463,28 @@ class ReviewStore:
elif action == "exclude":
verdict, event_type, subtype = "EXCLUDE", None, None
selected_opportunity_uid = None
elif action == "second_review":
verdict, event_type, subtype = "UNSURE", None, None
selected_opportunity_uid = None
else:
verdict, event_type, subtype = payload.get("human_verdict"), payload.get("human_event_type"), payload.get("human_subtype")
selected_opportunity_uid = payload.get("selected_opportunity_uid")
if verdict not in HUMAN_VERDICT_VALUES:
raise ValueError(f"Invalid human_verdict: {verdict!r}")
if verdict == "POSITIVE" and event_type not in HUMAN_EVENT_TYPE_VALUES:
raise ValueError("POSITIVE requires EOT_RESPONSE or INT_YIELD")
raise ValueError("POSITIVE requires WEARER_ENTRY or INT_YIELD")
if verdict != "POSITIVE" and event_type is not None:
raise ValueError("Only POSITIVE may have a human_event_type")
if verdict == "HARD_NEGATIVE" and subtype not in HUMAN_SUBTYPE_VALUES:
raise ValueError("HARD_NEGATIVE requires a valid human_subtype")
if verdict != "HARD_NEGATIVE" and subtype is not None:
raise ValueError("Only HARD_NEGATIVE may have a human_subtype")
family = candidate_record.family
if verdict == "POSITIVE" and family in {"entry", "int"}:
required_event_type = "WEARER_ENTRY" if family == "entry" else "INT_YIELD"
if event_type != required_event_type:
raise ValueError(f"{family.upper()} candidate requires {required_event_type}")
if verdict == "HARD_NEGATIVE" and family in {"entry", "int"}:
prefix = family.upper() + "_"
if subtype != "OTHER_HARD_NEGATIVE" and not str(subtype).startswith(prefix):
raise ValueError(f"{family.upper()} candidate requires {prefix}* hard-negative subtype")
evidence = candidate.get("temporal_evidence") or {}
opportunity_options = evidence.get("opportunity_options") or []
allowed_opportunity_uids = {
@@ -490,11 +492,11 @@ class ReviewStore:
for option in opportunity_options
if isinstance(option, dict) and option.get("uid") is not None
}
if verdict == "POSITIVE" and event_type == "EOT_RESPONSE" and allowed_opportunity_uids:
if selected_opportunity_uid is not None:
if verdict != "POSITIVE" or event_type != "WEARER_ENTRY":
raise ValueError("selected_opportunity_uid is only valid for WEARER_ENTRY")
if selected_opportunity_uid not in allowed_opportunity_uids:
raise ValueError("EOT_RESPONSE requires one supplied opportunity anchor")
elif selected_opportunity_uid is not None:
raise ValueError("selected_opportunity_uid is only valid for EOT_RESPONSE")
raise ValueError("WEARER_ENTRY may select only a supplied opportunity anchor")
selected_opportunity = next(
(
option for option in opportunity_options
@@ -521,7 +523,7 @@ class ReviewStore:
if correction not in {-0.2, 0, 0.2, None}:
raise ValueError("boundary_correction_seconds must be -0.2, 0, 0.2, or null")
normalized.update(
human_status="SECOND_REVIEW" if action == "second_review" else "REVIEWED",
human_status="REVIEWED",
human_verdict=verdict,
human_event_type=event_type,
human_subtype=subtype,