Delegate wearer boundary semantics to review

This commit is contained in:
Xin Wang
2026-09-15 02:12:18 +08:00
parent 71dfd8808a
commit 3a127a3f9f
12 changed files with 596 additions and 404 deletions
+244 -223
View File
@@ -2,13 +2,12 @@
"""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 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
ineligible raw boundaries as deterministic controls. All timing
thresholds are CLI parameters, and raw evidence is retained for DeepSeek
pre-labeling and human correction.
ground truth. Every observed wearer onset and offset is retained as a semantic
candidate. Deterministic code only records participant-resolved timing,
transcripts, and target-of-speech facts; it never decides floor entry,
within-turn resumption, or takeover semantics. All timing windows are CLI
parameters, and raw evidence is retained for DeepSeek pre-labeling and human
correction.
"""
from __future__ import annotations
@@ -28,8 +27,8 @@ from derive_easycom_interaction_events import (
from easycom_manifest import ALL_SESSIONS, TEST_SESSIONS
SCHEMA_VERSION = 5
PROTOCOL_VERSION = "easycom-wearer-entry-int-candidates-v7"
SCHEMA_VERSION = 6
PROTOCOL_VERSION = "easycom-wearer-entry-int-candidates-v8"
@dataclass(frozen=True)
@@ -41,7 +40,6 @@ class CandidateConfig:
short_vocalization_max_seconds: float = 1.0
continuation_min_seconds: float = 0.4
int_offset_tolerance_seconds: float = 0.05
wearer_resume_merge_max_seconds: float = 0.8
@dataclass(frozen=True)
@@ -88,26 +86,13 @@ class ProtocolCandidate:
def requires_semantic_adjudication(row: dict[str, Any]) -> bool:
"""Return whether a stored row belongs in the primary AI/human queue.
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.
Explicit legacy control sources are authoritative. Current WEARER_OFFSET
rows are always semantic candidates: crossing/order fields are evidence,
never admission gates.
"""
source = row.get("candidate_source")
if source in {"WEARER_ONSET_CONTROL", "WEARER_OFFSET_CONTROL", "WEARER_RESUME"}:
return False
if source != "WEARER_OFFSET":
return True
evidence = row.get("temporal_evidence")
if not isinstance(evidence, dict):
return True
explicit = evidence.get("other_crosses_wearer_offset")
if isinstance(explicit, bool):
return explicit
before = evidence.get("active_other_ids_before_offset")
after = evidence.get("active_other_ids_after_offset")
if isinstance(before, list) and isinstance(after, list):
return bool(set(before) & set(after))
return True
@@ -157,7 +142,7 @@ def _other_ids_crossing_offset(
) -> tuple[int, ...]:
"""Return other speakers whose annotated interval crosses a wearer offset.
With zero tolerance this encodes the strict prerequisite
With zero tolerance this records the strict temporal relation
``other.start < wearer_offset < other.end``. A one-frame tolerance accepts
an annotation boundary at the same 20 Hz frame, but not an other onset one
or more frames after the wearer offset.
@@ -170,26 +155,19 @@ def _other_ids_crossing_offset(
}))
def _wearer_gap_competition(
def _wearer_gap_evidence(
rows: Sequence[Utterance], *, gap_start_frame: int, onset_frame: int,
wearer_id: int, short_frames: int, continuation_frames: int,
wearer_id: int,
) -> dict[str, Any]:
"""Summarize plausible competing floor activity inside a short wearer gap.
Feedback-like short lexical/nonverbal activity is retained as evidence but
does not by itself split the wearer floor episode. Any other contribution
that occupies the gap for ``continuation_frames`` or continues across the
resumed wearer onset conservatively preserves a new ENTRY candidate.
"""
"""Return descriptive other-speaker facts within a wearer-silence gap."""
gap_rows = [
row for row in rows
if row.participant_id != wearer_id
and row.dense_start_frame < onset_frame
and row.dense_end_frame_exclusive > gap_start_frame
]
feedback_ids: set[int] = set()
competing_ids: set[int] = set()
max_gap_coverage = 0
intervals: list[dict[str, Any]] = []
for row in gap_rows:
coverage = max(
0,
@@ -197,20 +175,20 @@ def _wearer_gap_competition(
- max(row.dense_start_frame, gap_start_frame),
)
max_gap_coverage = max(max_gap_coverage, coverage)
feedback_like = (
row.dense_end_frame_exclusive - row.dense_start_frame <= short_frames
and (is_lexical_feedback(row.transcription) or is_pure_nonverbal(row.transcription))
)
if feedback_like:
feedback_ids.add(row.participant_id)
elif coverage >= continuation_frames or row.dense_end_frame_exclusive > onset_frame:
competing_ids.add(row.participant_id)
intervals.append({
"uid": row.uid,
"participant_id": row.participant_id,
"onset_seconds": row.dense_start_seconds,
"offset_seconds": row.dense_end_seconds,
"gap_overlap_seconds": coverage / SOURCE_FPS,
"continues_across_wearer_onset": row.dense_end_frame_exclusive > onset_frame,
"transcription": row.transcription,
"target_of_speech": list(row.target_of_speech),
})
return {
"gap_other_participant_ids": sorted({row.participant_id for row in gap_rows}),
"feedback_like_other_ids_during_gap": sorted(feedback_ids),
"sustained_competing_other_ids_during_gap": sorted(competing_ids),
"gap_other_intervals": intervals,
"max_other_gap_coverage_seconds": max_gap_coverage / SOURCE_FPS,
"sustained_other_takeover_during_gap": bool(competing_ids),
}
@@ -327,7 +305,6 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
short_frames = round(cfg.short_vocalization_max_seconds * SOURCE_FPS)
continuation_frames = round(cfg.continuation_min_seconds * SOURCE_FPS)
int_offset_tolerance_frames = round(cfg.int_offset_tolerance_seconds * SOURCE_FPS)
wearer_resume_merge_frames = round(cfg.wearer_resume_merge_max_seconds * SOURCE_FPS)
rows_by_segment: dict[int, list[Utterance]] = defaultdict(list)
clips_by_segment: dict[int, list[Clip]] = defaultdict(list)
for row in utterances:
@@ -352,10 +329,8 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
segment_end=segment_end, wearer_id=wearer_id)
other_rows = [row for row in rows if row.participant_id != wearer_id]
# 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 and are all entry candidates. A
# preceding non-wearer release is optional context, not a gate.
# A: retain every raw wearer onset and offset. Gap and nearby-speaker
# properties are evidence only; they never suppress a semantic item.
onset_records: dict[int, dict[str, Any]] = {}
raw_offsets: list[int] = []
for index in range(1, len(wearer)):
@@ -365,25 +340,16 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
previous_wearer_index = index - gap_frames - 1
has_previous_wearer = previous_wearer_index >= 0 and bool(wearer[previous_wearer_index])
previous_wearer_offset = segment_start + index - gap_frames
competition = _wearer_gap_competition(
gap_evidence = _wearer_gap_evidence(
rows,
gap_start_frame=previous_wearer_offset,
onset_frame=frame,
wearer_id=wearer_id,
short_frames=short_frames,
continuation_frames=continuation_frames,
) if has_previous_wearer else {
"gap_other_participant_ids": [],
"feedback_like_other_ids_during_gap": [],
"sustained_competing_other_ids_during_gap": [],
"gap_other_intervals": [],
"max_other_gap_coverage_seconds": 0.0,
"sustained_other_takeover_during_gap": False,
}
within_turn_resume = (
has_previous_wearer
and gap_frames <= wearer_resume_merge_frames
and not competition["sustained_other_takeover_during_gap"]
)
evidence = _relation_evidence(rows, frame=frame, wearer_id=wearer_id,
window_frames=relation_frames)
evidence.update(
@@ -392,166 +358,112 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
previous_wearer_offset_seconds=(
previous_wearer_offset / SOURCE_FPS if has_previous_wearer else None
),
wearer_resume_merge_max_seconds=cfg.wearer_resume_merge_max_seconds,
floor_episode_merged=within_turn_resume,
original_timestamp_seconds=frame / SOURCE_FPS,
**competition,
**gap_evidence,
)
onset_records[frame] = {
"within_turn_resume": within_turn_resume,
"evidence": evidence,
}
onset_records[frame] = {"evidence": evidence}
elif wearer[index - 1] and not wearer[index]:
raw_offsets.append(frame)
resume_onsets = {
frame for frame, record in onset_records.items() if record["within_turn_resume"]
}
floor_onsets = sorted(set(onset_records) - resume_onsets)
for frame in sorted(resume_onsets):
record = onset_records[frame]
add(_candidate(
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="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,
wearer_id=wearer_id, context_frames=context_frames,
reason="short raw wearer pause without sustained competing other takeover; merged into the existing floor episode",
), ("WEARER_RESUME", frame, ()))
# 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:
# A1: every observed raw wearer onset is an unresolved entry candidate.
# Every nearby preceding other offset is supplied with factual relation
# flags; no release is declared admissible or causal by this stage.
for onset_frame in sorted(onset_records):
release_options: list[tuple[Utterance, int]] = []
for release in other_rows:
opportunity_frame = release.dense_end_frame_exclusive
if opportunity_frame > onset_frame:
if opportunity_frame > onset_frame or onset_frame - opportunity_frame > relation_frames:
continue
wearer_silent = (
opportunity_frame == onset_frame
or _all_state(
wearer,
opportunity_frame - segment_start,
onset_frame - segment_start,
False,
)
)
competing_onset = any(
opportunity_frame <= row.dense_start_frame < onset_frame
for row in other_rows
)
other_already_holds = bool(
_active_other_ids(rows, opportunity_frame, wearer_id=wearer_id)
)
if wearer_silent and not competing_onset and not other_already_holds:
release_options.append((release, opportunity_frame))
release_options.append((release, opportunity_frame))
release_options.sort(
key=lambda item: (item[1], item[0].participant_id, item[0].uid)
)
if release_options:
_, latest_opportunity_frame = release_options[-1]
earliest_opportunity_frame = release_options[0][1]
option_rows = [{
earliest_opportunity_frame = (
release_options[0][1] if release_options else max(segment_start, onset_frame - relation_frames)
)
option_rows = []
for release, release_frame in release_options:
interval_left = release_frame - segment_start
interval_right = onset_frame - segment_start
option_rows.append({
"uid": release.uid,
"anchor_type": "other_offset",
"anchor_frame": frame,
"anchor_seconds": frame / SOURCE_FPS,
"anchor_frame": release_frame,
"anchor_seconds": release_frame / SOURCE_FPS,
"participant_id": release.participant_id,
"transcription": release.transcription,
"target_of_speech": list(release.target_of_speech),
"response_delay_seconds": (onset_frame - frame) / SOURCE_FPS,
} for release, frame in release_options]
evidence = dict(onset_records[onset_frame]["evidence"])
evidence.update(
opportunity_options=option_rows,
opportunity_anchor_type=None,
opportunity_anchor_frame=None,
opportunity_anchor_seconds=None,
behavior_anchor_type="wearer_floor_onset",
behavior_anchor_frame=onset_frame,
behavior_anchor_seconds=onset_frame / SOURCE_FPS,
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=[],
preceding_release_paired=True,
entry_context_timing=(
"OTHER_ACTIVE"
if _active_other_ids(rows, onset_frame, wearer_id=wearer_id)
else "AFTER_OTHER_RELEASE"
"response_delay_seconds": (onset_frame - release_frame) / SOURCE_FPS,
"wearer_active_between_release_and_onset": any(
bool(wearer[i]) for i in range(max(0, interval_left), min(len(wearer), interval_right))
),
)
previous_ids = tuple(sorted({row.participant_id for row, _ in release_options}))
add(_candidate(
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="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 onset; preceding release options retained as context",
), ("WEARER_ONSET", onset_frame, previous_ids))
continue
frame = onset_frame
evidence = dict(onset_records[frame]["evidence"])
"other_onset_between_release_and_onset": any(
release_frame <= row.dense_start_frame < onset_frame for row in other_rows
),
"other_active_at_wearer_onset": bool(
_active_other_ids(rows, onset_frame, wearer_id=wearer_id)
),
})
evidence = dict(onset_records[onset_frame]["evidence"])
evidence.update(
opportunity_options=[],
opportunity_options=option_rows,
opportunity_anchor_type=None,
opportunity_anchor_frame=None,
opportunity_anchor_seconds=None,
behavior_anchor_type="wearer_floor_onset",
behavior_anchor_frame=frame,
behavior_anchor_seconds=frame / SOURCE_FPS,
preceding_release_paired=False,
behavior_anchor_type="wearer_raw_onset",
behavior_anchor_frame=onset_frame,
behavior_anchor_seconds=onset_frame / SOURCE_FPS,
preceding_release_options_present=bool(option_rows),
minimum_response_delay_seconds=(
None if not option_rows
else min(option["response_delay_seconds"] for option in option_rows)
),
maximum_response_delay_seconds=(
None if not option_rows
else max(option["response_delay_seconds"] for option in option_rows)
),
entry_context_timing=(
"OTHER_ACTIVE"
if _active_other_ids(rows, frame, wearer_id=wearer_id)
else "FROM_SILENCE_OR_UNRESOLVED"
if _active_other_ids(rows, onset_frame, wearer_id=wearer_id)
else "NO_OTHER_ACTIVE_AT_ONSET"
),
)
previous_ids = _active_other_ids(rows, frame - 1, wearer_id=wearer_id)
previous_ids = tuple(sorted({row.participant_id for row, _ in release_options}))
add(_candidate(
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),
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="candidate", subtype=None,
previous_ids=previous_ids,
incoming_ids=_active_other_ids(rows, frame, wearer_id=wearer_id),
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 onset without a surviving preceding release option",
), ("WEARER_ONSET", frame, previous_ids))
reason=(
"raw wearer onset retained without floor-entry or within-turn-resumption gating; "
"nearby release relations are descriptive evidence"
),
), ("WEARER_ONSET", 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
# t_w < t_o < t_y and that same interval crosses the yield boundary.
# All other offsets remain auditable controls.
ordered_raw_onsets = sorted(onset_records)
current_floor_start = segment_start if wearer and wearer[0] else None
floor_start_by_offset: dict[int, tuple[int | None, bool]] = {}
# A2: every raw wearer offset is an unresolved INT candidate. The code
# records temporal relationships but does not infer takeover semantics.
# In particular, new/pre-existing/post-offset other activity and
# boundary crossing are evidence for DeepSeek and the human reviewer,
# never deterministic admission gates.
current_activity_run_start = segment_start if wearer and wearer[0] else None
activity_run_start_by_offset: dict[int, int | None] = {}
for index in range(1, len(wearer)):
frame = segment_start + index
if not wearer[index - 1] and wearer[index] and frame not in resume_onsets:
current_floor_start = frame
if not wearer[index - 1] and wearer[index]:
current_activity_run_start = frame
elif wearer[index - 1] and not wearer[index]:
next_onset = next((value for value in ordered_raw_onsets if value > frame), None)
internal_pause = next_onset in resume_onsets
floor_start_by_offset[frame] = (current_floor_start, internal_pause)
if not internal_pause:
current_floor_start = None
activity_run_start_by_offset[frame] = current_activity_run_start
current_activity_run_start = None
for frame in raw_offsets:
index = frame - segment_start
floor_start, internal_pause = floor_start_by_offset[frame]
activity_run_start = activity_run_start_by_offset[frame]
strict_crossing_ids = _other_ids_crossing_offset(
other_rows, frame=frame, wearer_id=wearer_id, tolerance_frames=0
)
@@ -559,71 +471,178 @@ def derive_protocol_candidates(utterances: Sequence[Utterance], clips: Sequence[
other_rows, frame=frame, wearer_id=wearer_id,
tolerance_frames=int_offset_tolerance_frames,
)
eligible_rows = [
new_other_rows = [
row for row in other_rows
if floor_start is not None
and floor_start < row.dense_start_frame < frame
and row.dense_end_frame_exclusive > frame - int_offset_tolerance_frames
if activity_run_start is not None
and activity_run_start < row.dense_start_frame < frame
]
eligible_ids = tuple(sorted({row.participant_id for row in eligible_rows}))
preexisting_ids = tuple(sorted({
row.participant_id for row in other_rows
if floor_start is not None
and row.dense_start_frame <= floor_start
and row.dense_end_frame_exclusive > frame - int_offset_tolerance_frames
}))
is_int_candidate = bool(eligible_ids) and not internal_pause
new_crossing_rows = [
row for row in new_other_rows
if row.dense_end_frame_exclusive > frame - int_offset_tolerance_frames
]
new_other_ids = tuple(sorted({row.participant_id for row in new_other_rows}))
new_crossing_ids = tuple(sorted({row.participant_id for row in new_crossing_rows}))
preexisting_rows = [
row for row in other_rows
if activity_run_start is not None
and row.dense_start_frame <= activity_run_start
and row.dense_end_frame_exclusive > activity_run_start
]
preexisting_ids = tuple(sorted({row.participant_id for row in preexisting_rows}))
nearby_other_rows = [
row for row in other_rows
if row.dense_end_frame_exclusive > frame - relation_frames
and row.dense_start_frame < frame + relation_frames
]
nearby_other_intervals = [{
"uid": row.uid,
"participant_id": row.participant_id,
"onset_seconds": row.dense_start_seconds,
"offset_seconds": row.dense_end_seconds,
"onset_relative_to_wearer_offset_seconds": (
row.dense_start_frame - frame
) / SOURCE_FPS,
"offset_relative_to_wearer_offset_seconds": (
row.dense_end_frame_exclusive - frame
) / SOURCE_FPS,
"starts_during_wearer_activity_run": bool(
activity_run_start is not None
and activity_run_start < row.dense_start_frame < frame
),
"preexisting_at_wearer_activity_run_start": bool(
activity_run_start is not None
and row.dense_start_frame <= activity_run_start
and row.dense_end_frame_exclusive > activity_run_start
),
"active_immediately_before_offset": bool(
row.dense_start_frame <= frame - 1 < row.dense_end_frame_exclusive
),
"active_immediately_after_offset": bool(
row.dense_start_frame <= frame < row.dense_end_frame_exclusive
),
"strictly_crosses_offset": bool(
row.dense_start_frame < frame < row.dense_end_frame_exclusive
),
"crosses_offset_with_tolerance": bool(
row.dense_start_frame < frame + int_offset_tolerance_frames
and row.dense_end_frame_exclusive > frame - int_offset_tolerance_frames
),
"overlap_with_wearer_before_offset_seconds": (
0.0 if activity_run_start is None else max(
0, min(row.dense_end_frame_exclusive, frame)
- max(row.dense_start_frame, activity_run_start)
) / SOURCE_FPS
),
"transcription": row.transcription,
"target_of_speech": list(row.target_of_speech),
} for row in nearby_other_rows]
next_rows = sorted(
[
row for row in rows
if frame <= row.dense_start_frame <= frame + relation_frames
],
key=lambda row: (row.dense_start_frame, row.participant_id, row.uid),
)
first_next_frame = next_rows[0].dense_start_frame if next_rows else None
first_next_rows = (
[] if first_next_frame is None
else [row for row in next_rows if row.dense_start_frame == first_next_frame]
)
active_other_ids_after_offset = _active_other_ids(
rows, frame, wearer_id=wearer_id
)
next_speaker_ids = (
list(active_other_ids_after_offset)
if active_other_ids_after_offset
else [row.participant_id for row in first_next_rows]
)
next_speaker_roles = (
["other"] * len(active_other_ids_after_offset)
if active_other_ids_after_offset
else [
"wearer" if row.participant_id == wearer_id else "other"
for row in first_next_rows
]
)
evidence = _relation_evidence(
rows, frame=frame, wearer_id=wearer_id, window_frames=relation_frames
)
evidence.update(
wearer_floor_episode_start_frame=floor_start,
wearer_floor_episode_start_seconds=(
None if floor_start is None else floor_start / SOURCE_FPS
wearer_activity_run_start_frame=activity_run_start,
wearer_activity_run_start_seconds=(
None if activity_run_start is None else activity_run_start / SOURCE_FPS
),
wearer_activity_run_duration_to_offset_seconds=(
None if activity_run_start is None
else (frame - activity_run_start) / SOURCE_FPS
),
wearer_offset_frame=frame,
wearer_offset_seconds=frame / SOURCE_FPS,
wearer_speech_duration_before_offset_seconds=_run_before(wearer, index, True) / SOURCE_FPS,
wearer_silence_duration_after_offset_seconds=_run_after(wearer, index, False) / SOURCE_FPS,
raw_offset_is_within_merged_floor_episode=internal_pause,
active_other_ids_before_offset=list(_active_other_ids(rows, frame - 1, wearer_id=wearer_id)),
active_other_ids_after_offset=list(_active_other_ids(rows, frame, wearer_id=wearer_id)),
active_other_ids_after_offset=list(active_other_ids_after_offset),
strict_crossing_other_ids=list(strict_crossing_ids),
tolerance_crossing_other_ids=list(tolerance_crossing_ids),
eligible_new_other_onset_ids=list(eligible_ids),
eligible_new_other_onsets=[{
new_other_onset_ids_during_wearer=list(new_other_ids),
new_other_onsets_during_wearer=[{
"participant_id": row.participant_id,
"onset_seconds": row.dense_start_seconds,
"offset_seconds": row.dense_end_seconds,
"uid": row.uid,
} for row in eligible_rows],
preexisting_crossing_other_ids=list(preexisting_ids),
} for row in new_other_rows],
new_other_onset_ids_crossing_offset=list(new_crossing_ids),
preexisting_other_ids_at_wearer_activity_run_start=list(preexisting_ids),
preexisting_other_intervals=[{
"participant_id": row.participant_id,
"onset_seconds": row.dense_start_seconds,
"offset_seconds": row.dense_end_seconds,
"uid": row.uid,
} for row in preexisting_rows],
nearby_other_intervals=nearby_other_intervals,
next_speech_onsets=[{
"participant_id": row.participant_id,
"participant_role": "wearer" if row.participant_id == wearer_id else "other",
"onset_seconds": row.dense_start_seconds,
"offset_seconds": row.dense_end_seconds,
"uid": row.uid,
"transcription": row.transcription,
"target_of_speech": list(row.target_of_speech),
} for row in next_rows],
next_speaker_ids=next_speaker_ids,
next_speaker_roles=next_speaker_roles,
next_speaker_basis=(
"active_across_offset"
if active_other_ids_after_offset
else ("first_new_onset_after_offset" if first_next_rows else None)
),
next_speech_onset_seconds=(
frame / SOURCE_FPS
if active_other_ids_after_offset
else (None if first_next_frame is None else first_next_frame / SOURCE_FPS)
),
other_crosses_wearer_offset=bool(tolerance_crossing_ids),
int_ordering_satisfied=bool(eligible_ids),
new_other_onset_during_wearer=bool(new_other_rows),
new_other_onset_crosses_offset=bool(new_crossing_rows),
int_offset_tolerance_seconds=cfg.int_offset_tolerance_seconds,
original_timestamp_seconds=frame / SOURCE_FPS,
)
control_reason = (
"raw wearer offset is internal to a merged floor episode"
if internal_pause
else "no new non-wearer onset satisfies t_w < t_o < t_y and crosses the wearer offset"
)
add(_candidate(
session=session,
task="int_yield" if is_int_candidate else "int_yield_control",
source="WEARER_OFFSET" if is_int_candidate else "WEARER_OFFSET_CONTROL",
rule_type="YIELD_CANDIDATE" if is_int_candidate else "WEARER_OFFSET_CONTROL",
session=session, task="int_yield", source="WEARER_OFFSET",
rule_type="YIELD_CANDIDATE",
anchor_type="wearer_offset", frame=frame,
region_start=max(segment_start, frame - relation_frames),
region_end=min(segment_end, frame + relation_frames),
label="candidate" if is_int_candidate else "control", subtype=None,
label="candidate", subtype=None,
previous_ids=_active_other_ids(rows, frame - 1, wearer_id=wearer_id),
incoming_ids=eligible_ids,
incoming_ids=new_other_ids,
evidence=evidence, clips=segment_clips, rows=rows, wearer_id=wearer_id,
context_frames=context_frames,
reason=(
"new non-wearer onset occurs inside the wearer floor episode and crosses the yield boundary; semantic takeover status is unresolved"
if is_int_candidate else control_reason
"raw wearer offset retained without timing-based takeover gating; "
"all nearby activity relations are descriptive evidence"
),
), ("WEARER_OFFSET" if is_int_candidate else "WEARER_OFFSET_CONTROL", frame, eligible_ids))
), ("WEARER_OFFSET", frame, new_other_ids))
# B1: same speaker pauses and resumes while the wearer remains silent.
by_speaker: dict[int, list[Utterance]] = defaultdict(list)
@@ -762,7 +781,10 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--short-vocalization-max-seconds", type=float, default=1.0)
parser.add_argument("--continuation-min-seconds", type=float, default=0.4)
parser.add_argument("--int-offset-tolerance-seconds", type=float, default=0.05)
parser.add_argument("--wearer-resume-merge-max-seconds", type=float, default=0.8)
parser.add_argument(
"--wearer-resume-merge-max-seconds", type=float,
help=argparse.SUPPRESS,
) # Deprecated compatibility option; raw wearer boundaries are never merged.
parser.add_argument("--candidate-sources", nargs="+")
parser.add_argument("--max-candidates", type=int)
parser.add_argument("--selection-seed", type=int, default=2027)
@@ -782,7 +804,6 @@ def main() -> None:
short_vocalization_max_seconds=args.short_vocalization_max_seconds,
continuation_min_seconds=args.continuation_min_seconds,
int_offset_tolerance_seconds=args.int_offset_tolerance_seconds,
wearer_resume_merge_max_seconds=args.wearer_resume_merge_max_seconds,
)
dataset_root, output_dir = args.dataset_root.expanduser().resolve(), args.output_dir.expanduser().resolve()
all_rows: list[ProtocolCandidate] = []