Delegate wearer boundary semantics to review
This commit is contained in:
+244
-223
@@ -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] = []
|
||||
|
||||
Reference in New Issue
Block a user