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
+46 -36
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
"""Generate wearer-centric EasyCom EOT/INT candidates for AI and human review.
"""Generate wearer-centric EasyCom entry/INT candidates for AI and human review.
This stage never changes EasyCom speech boundaries and never claims semantic
ground truth. It groups short wearer resumptions into floor-level episodes,
starts EOT candidates from observed wearer entries and retrieves preceding
non-wearer release options, requires a new
starts entry candidates from every observed floor-level wearer onset and
retains preceding non-wearer release options as context, requires a new
non-wearer onset inside the wearer floor episode for INT candidates, and keeps
unpaired or ineligible raw boundaries as deterministic controls. All timing
ineligible raw boundaries as deterministic controls. All timing
thresholds are CLI parameters, and raw evidence is retained for DeepSeek
pre-labeling and human correction.
"""
@@ -28,8 +28,8 @@ from derive_easycom_interaction_events import (
from easycom_manifest import ALL_SESSIONS, TEST_SESSIONS
SCHEMA_VERSION = 4
PROTOCOL_VERSION = "easycom-wearer-anchor-first-eot-int-candidates-v6"
SCHEMA_VERSION = 5
PROTOCOL_VERSION = "easycom-wearer-entry-int-candidates-v7"
@dataclass(frozen=True)
@@ -88,7 +88,7 @@ class ProtocolCandidate:
def requires_semantic_adjudication(row: dict[str, Any]) -> bool:
"""Return whether a stored row belongs in the primary AI/human queue.
Explicit v3--v5 control sources are authoritative. For old v2 candidate
Explicit legacy control sources are authoritative. For old v2 candidate
files, reconstruct the same-speaker crossing requirement from the stored
activity IDs. Missing legacy evidence remains reviewable rather than being
silently discarded.
@@ -354,8 +354,8 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
# A: classify raw wearer onsets before proposing any behavioral event.
# Short uncompeted gaps are internal to a floor episode. All other raw
# onsets begin a new floor episode, but become EOT candidates only when
# they can be paired with a preceding non-wearer release opportunity.
# onsets begin a new floor episode and are all entry candidates. A
# preceding non-wearer release is optional context, not a gate.
onset_records: dict[int, dict[str, Any]] = {}
raw_offsets: list[int] = []
for index in range(1, len(wearer)):
@@ -412,10 +412,10 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
for frame in sorted(resume_onsets):
record = onset_records[frame]
add(_candidate(
session=session, task="eot_response_control", source="WEARER_RESUME",
session=session, task="wearer_entry_control", source="WEARER_RESUME",
rule_type="WITHIN_TURN_RESUME_CONTROL", anchor_type="wearer_onset", frame=frame,
region_start=frame, region_end=min(segment_end, frame + relation_frames),
label="not_applicable", subtype=None,
label="control", subtype=None,
previous_ids=_active_other_ids(rows, frame - 1, wearer_id=wearer_id),
incoming_ids=_active_other_ids(rows, frame, wearer_id=wearer_id),
evidence=record["evidence"], clips=segment_clips, rows=rows,
@@ -423,11 +423,9 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
reason="short raw wearer pause without sustained competing other takeover; merged into the existing floor episode",
), ("WEARER_RESUME", frame, ()))
# A1: start from each observed floor-level wearer entry, then search
# backward for release anchors that survive deterministic timing checks.
# This yields at most one review item per wearer behavior. The semantic
# annotator may select only one of the stored options; it cannot invent
# a trigger or move either timestamp. Long silence alone is not a veto.
# A1: every observed floor-level wearer onset is an entry candidate.
# Search backward for release anchors only to preserve response context;
# no preceding release is required for a positive WEARER_ENTRY.
for onset_frame in floor_onsets:
release_options: list[tuple[Utterance, int]] = []
for release in other_rows:
@@ -481,19 +479,24 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
minimum_response_delay_seconds=(onset_frame - latest_opportunity_frame) / SOURCE_FPS,
maximum_response_delay_seconds=(onset_frame - earliest_opportunity_frame) / SOURCE_FPS,
competing_other_onsets_before_behavior=[],
eot_opportunity_paired=True,
preceding_release_paired=True,
entry_context_timing=(
"OTHER_ACTIVE"
if _active_other_ids(rows, onset_frame, wearer_id=wearer_id)
else "AFTER_OTHER_RELEASE"
),
)
previous_ids = tuple(sorted({row.participant_id for row, _ in release_options}))
add(_candidate(
session=session, task="eot_response", source="WEARER_ONSET",
rule_type="EOT_OPPORTUNITY_CANDIDATE", anchor_type="wearer_onset",
session=session, task="wearer_entry", source="WEARER_ONSET",
rule_type="WEARER_ENTRY_CANDIDATE", anchor_type="wearer_onset",
frame=onset_frame, region_start=earliest_opportunity_frame,
region_end=min(segment_end, onset_frame + relation_frames),
label="unsure", subtype=None, previous_ids=previous_ids,
label="candidate", subtype=None, previous_ids=previous_ids,
incoming_ids=_active_other_ids(rows, onset_frame, wearer_id=wearer_id),
evidence=evidence, clips=segment_clips, rows=rows, wearer_id=wearer_id,
context_frames=context_frames,
reason="floor-level wearer entry with one or more surviving preceding release options",
reason="floor-level wearer onset; preceding release options retained as context",
), ("WEARER_ONSET", onset_frame, previous_ids))
continue
@@ -507,19 +510,26 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
behavior_anchor_type="wearer_floor_onset",
behavior_anchor_frame=frame,
behavior_anchor_seconds=frame / SOURCE_FPS,
eot_opportunity_paired=False,
preceding_release_paired=False,
entry_context_timing=(
"OTHER_ACTIVE"
if _active_other_ids(rows, frame, wearer_id=wearer_id)
else "FROM_SILENCE_OR_UNRESOLVED"
),
)
previous_ids = _active_other_ids(rows, frame - 1, wearer_id=wearer_id)
add(_candidate(
session=session, task="eot_response_control", source="WEARER_ONSET_CONTROL",
rule_type="UNPAIRED_WEARER_ENTRY_CONTROL", anchor_type="wearer_onset", frame=frame,
region_start=frame, region_end=min(segment_end, frame + relation_frames),
label="not_applicable", subtype=None,
previous_ids=_active_other_ids(rows, frame - 1, wearer_id=wearer_id),
session=session, task="wearer_entry", source="WEARER_ONSET",
rule_type="WEARER_ENTRY_CANDIDATE", anchor_type="wearer_onset", frame=frame,
region_start=max(segment_start, frame - relation_frames),
region_end=min(segment_end, frame + relation_frames),
label="candidate", subtype=None,
previous_ids=previous_ids,
incoming_ids=_active_other_ids(rows, frame, wearer_id=wearer_id),
evidence=evidence, clips=segment_clips, rows=rows, wearer_id=wearer_id,
context_frames=context_frames,
reason="floor-level wearer entry has no surviving preceding non-wearer release opportunity",
), ("WEARER_ONSET_CONTROL", frame, ()))
reason="floor-level wearer onset without a surviving preceding release option",
), ("WEARER_ONSET", frame, previous_ids))
# A2: a raw wearer offset is an INT candidate only at the end of a
# floor-level wearer episode and only if a new non-wearer onset obeys
@@ -604,7 +614,7 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
anchor_type="wearer_offset", frame=frame,
region_start=max(segment_start, frame - relation_frames),
region_end=min(segment_end, frame + relation_frames),
label="unsure" if is_int_candidate else "not_applicable", subtype=None,
label="candidate" if is_int_candidate else "control", subtype=None,
previous_ids=_active_other_ids(rows, frame - 1, wearer_id=wearer_id),
incoming_ids=eligible_ids,
evidence=evidence, clips=segment_clips, rows=rows, wearer_id=wearer_id,
@@ -636,10 +646,10 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
"original_timestamp_seconds": frame / SOURCE_FPS,
}
add(_candidate(
session=session, task="eot_response", source="SAME_SPEAKER_PAUSE",
rule_type="EOT_HARD_NEGATIVE_REGION", anchor_type="other_offset", frame=frame,
session=session, task="wearer_entry", source="SAME_SPEAKER_PAUSE",
rule_type="ENTRY_HARD_NEGATIVE_REGION", anchor_type="other_offset", frame=frame,
region_start=frame, region_end=resumed.dense_start_frame,
label="hard_negative", subtype="EOT_MIDTURN_PAUSE",
label="hard_negative", subtype="ENTRY_OTHER_MIDTURN_PAUSE",
previous_ids=(participant_id,), incoming_ids=(participant_id,), evidence=evidence,
clips=segment_clips, rows=rows, wearer_id=wearer_id, context_frames=context_frames,
reason="same participant resumes after a configurable pause while wearer stays silent",
@@ -669,10 +679,10 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
"wearer_active_during_handover": False, "original_timestamp_seconds": frame / SOURCE_FPS,
}
add(_candidate(
session=session, task="eot_response", source="OTHER_TO_OTHER",
rule_type="EOT_HARD_NEGATIVE_REGION", anchor_type="other_offset", frame=frame,
session=session, task="wearer_entry", source="OTHER_TO_OTHER",
rule_type="ENTRY_HARD_NEGATIVE_REGION", anchor_type="other_offset", frame=frame,
region_start=frame, region_end=incoming.dense_start_frame,
label="hard_negative", subtype="EOT_OTHER_TO_OTHER",
label="hard_negative", subtype="ENTRY_OTHER_TO_OTHER",
previous_ids=(first.participant_id,), incoming_ids=(incoming.participant_id,),
evidence=evidence, clips=segment_clips, rows=rows, wearer_id=wearer_id,
context_frames=context_frames,