Audit malformed Ego4D voice segments
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user