Audit malformed Ego4D voice segments

This commit is contained in:
Xin Wang
2026-09-15 13:48:29 +08:00
parent a8a09d8fc5
commit af4817004b
3 changed files with 85 additions and 11 deletions
+12
View File
@@ -55,6 +55,18 @@ handoffs are added as independently reviewable hard-negative proposals.
Timing rules only select and describe candidates; they never assign semantic
positive labels.
Official zero-duration voice records cannot define an activity interval. They
are never expanded to an invented duration: the generator skips them, reports
the count on stdout, and stores their complete source records under
`clip_audits` and each candidate's `temporal_evidence`. A negative-duration
record remains a hard error.
Ego4D v1 also contains nine train clips whose official voice endpoint extends
past `clip_end_sec` by 0.42--1.03 seconds. The default audited ceiling is 1.1
seconds. These endpoints are preserved and flagged rather than silently
clamped; larger overruns remain errors. A human should normally exclude a
semantic boundary beyond observable media as `DATA_RECORDING_BOUNDARY`.
## 2. Inspect what will be uploaded
The LLM client refuses to upload transcript excerpts without an explicit
+58 -10
View File
@@ -28,7 +28,7 @@ class CandidateConfig:
relation_window_seconds: float = 3.0
same_speaker_pause_max_seconds: float = 2.0
other_handover_max_seconds: float = 1.5
max_terminal_overrun_seconds: float = 0.1
max_terminal_overrun_seconds: float = 1.1
@dataclass(frozen=True)
@@ -61,8 +61,8 @@ def _segment_times(row: dict[str, Any]) -> tuple[float, float]:
end = _number(row["end_time_sec"], field="end_time_sec")
else:
raise ValueError("voice segment lacks clip-local start/end time")
if end <= start:
raise ValueError(f"voice segment must have positive duration: {start}..{end}")
if end < start:
raise ValueError(f"voice segment has negative duration: {start}..{end}")
return start, end
@@ -111,7 +111,9 @@ def _addressed_to_wearer(
return any(overlaps)
def load_clip_segments(clip: dict[str, Any]) -> tuple[list[SpeechSegment], str]:
def load_clip_segments(
clip: dict[str, Any], *, audit_out: dict[str, Any] | None = None,
) -> tuple[list[SpeechSegment], str]:
"""Return validated clip-local speaker segments and the wearer person ID."""
clip_uid = str(clip.get("clip_uid", "")).strip()
if not clip_uid:
@@ -142,6 +144,7 @@ def load_clip_segments(clip: dict[str, Any]) -> tuple[list[SpeechSegment], str]:
segments: list[SpeechSegment] = []
seen: set[tuple[str, float, float]] = set()
zero_duration: list[dict[str, Any]] = []
ordinal = 0
for person in people:
person_id = _person_key(person.get("person_id"))
@@ -158,6 +161,15 @@ def load_clip_segments(clip: dict[str, Any]) -> tuple[list[SpeechSegment], str]:
if key in seen:
continue
seen.add(key)
if end == start:
zero_duration.append({
"person_id": person_id,
"participant_role": "wearer" if person_id == wearer_id else "other",
"time_seconds": start,
"source": dict(source),
"policy": "ignored-zero-duration-artifact-not-expanded",
})
continue
segments.append(
SpeechSegment(
uid=f"{clip_uid}/speech/{ordinal:05d}",
@@ -178,6 +190,12 @@ def load_clip_segments(clip: dict[str, Any]) -> tuple[list[SpeechSegment], str]:
for left, right in zip(segments, segments[1:]):
if left.person_id == right.person_id and left.start == right.start and left.end == right.end:
raise ValueError(f"{clip_uid}: duplicate voice segment after normalization")
if audit_out is not None:
audit_out.update({
"zero_duration_voice_segment_count": len(zero_duration),
"zero_duration_voice_segments": zero_duration,
"zero_duration_policy": "ignore-with-audit; never clamp or expand",
})
return segments, wearer_id
@@ -294,9 +312,11 @@ def _base_candidate(
def derive_clip_candidates(
clip: dict[str, Any], *, split: str, config: CandidateConfig
clip: dict[str, Any], *, split: str, config: CandidateConfig,
audit_out: dict[str, Any] | None = None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
segments, wearer_id = load_clip_segments(clip)
clip_audit: dict[str, Any] = {}
segments, wearer_id = load_clip_segments(clip, audit_out=clip_audit)
declared_clip_end = _number(
clip.get("clip_end_sec", max((row.end for row in segments), default=0.0)),
field="clip_end_sec",
@@ -427,7 +447,20 @@ def derive_clip_candidates(
"terminal_voice_overrun_seconds": terminal_overrun,
"max_allowed_terminal_overrun_seconds": config.max_terminal_overrun_seconds,
"terminal_voice_endpoint_preserved_not_clamped": True,
"zero_duration_voice_segment_count": clip_audit[
"zero_duration_voice_segment_count"
],
"zero_duration_voice_segments": clip_audit["zero_duration_voice_segments"],
"zero_duration_voice_policy": clip_audit["zero_duration_policy"],
})
clip_audit.update({
"declared_clip_end_seconds": declared_clip_end,
"maximum_voice_segment_end_seconds": maximum_segment_end,
"terminal_voice_overrun_seconds": terminal_overrun,
"max_allowed_terminal_overrun_seconds": config.max_terminal_overrun_seconds,
})
if audit_out is not None:
audit_out.update(clip_audit)
return candidates, [_timeline_row(row) for row in segments]
@@ -482,7 +515,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--same-speaker-pause-max-seconds", type=float, default=2.0)
parser.add_argument("--other-handover-max-seconds", type=float, default=1.5)
parser.add_argument(
"--max-terminal-overrun-seconds", type=float, default=0.1,
"--max-terminal-overrun-seconds", type=float, default=1.1,
help="Allow and audit a bounded official voice endpoint past clip_end_sec (default: %(default)s).",
)
parser.add_argument("--overwrite", action="store_true")
@@ -516,10 +549,16 @@ def main() -> None:
config = CandidateConfig(*numeric)
rows: list[dict[str, Any]] = []
timelines: dict[str, list[dict[str, Any]]] = {}
clip_audits: dict[str, dict[str, Any]] = {}
for clip, split in selected:
derived, timeline = derive_clip_candidates(clip, split=split, config=config)
audit: dict[str, Any] = {}
derived, timeline = derive_clip_candidates(
clip, split=split, config=config, audit_out=audit
)
rows.extend(derived)
timelines[str(clip["clip_uid"])] = timeline
clip_uid = str(clip["clip_uid"])
timelines[clip_uid] = timeline
clip_audits[clip_uid] = audit
if not rows:
raise ValueError("selected annotations produced no candidates")
missing_voice_count_by_clip = {
@@ -545,11 +584,20 @@ def main() -> None:
_write_jsonl(output, rows)
_write_json(
timeline_output,
{"schema_version": 1, "dataset": "ego4d_av", "sessions": timelines},
{
"schema_version": 1,
"dataset": "ego4d_av",
"sessions": timelines,
"clip_audits": clip_audits,
},
)
counts = Counter(row["candidate_source"] for row in rows)
print(f"clips={len(selected)} candidates={len(rows)} output={output}")
print("candidate_sources=" + json.dumps(dict(sorted(counts.items())), ensure_ascii=False))
print(
"zero_duration_voice_segments_ignored_with_audit="
+ str(sum(audit["zero_duration_voice_segment_count"] for audit in clip_audits.values()))
)
print(f"timelines={timeline_output}")
+15 -1
View File
@@ -129,10 +129,24 @@ class Ego4DAVCandidateTests(unittest.TestCase):
def test_large_terminal_endpoint_overrun_is_rejected(self) -> None:
clip = fixture_clip()
clip["clip_end_sec"] = 11.95
clip["camera_wearer"]["voice_segments"].append(segment("wearer", 11.8, 12.2))
clip["camera_wearer"]["voice_segments"].append(segment("wearer", 11.8, 13.2))
with self.assertRaisesRegex(ValueError, "exceeds the audited limit"):
derive_clip_candidates(clip, split="val", config=CandidateConfig())
def test_zero_duration_voice_segment_is_ignored_with_explicit_audit(self) -> None:
clip = fixture_clip()
clip["persons"][1]["voice_segments"].append(segment("p2", 9.0, 9.0))
audit: dict[str, object] = {}
rows, timeline = derive_clip_candidates(
clip, split="val", config=CandidateConfig(), audit_out=audit
)
self.assertEqual(audit["zero_duration_voice_segment_count"], 1)
self.assertFalse(any(row["start_seconds"] == 9.0 for row in timeline))
self.assertTrue(all(
row["temporal_evidence"]["zero_duration_voice_segment_count"] == 1
for row in rows
))
if __name__ == "__main__":
unittest.main()