Handle Ego4D terminal voice endpoints
This commit is contained in:
@@ -28,6 +28,7 @@ class CandidateConfig:
|
|||||||
relation_window_seconds: float = 3.0
|
relation_window_seconds: float = 3.0
|
||||||
same_speaker_pause_max_seconds: float = 2.0
|
same_speaker_pause_max_seconds: float = 2.0
|
||||||
other_handover_max_seconds: float = 1.5
|
other_handover_max_seconds: float = 1.5
|
||||||
|
max_terminal_overrun_seconds: float = 0.1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -296,14 +297,24 @@ def derive_clip_candidates(
|
|||||||
clip: dict[str, Any], *, split: str, config: CandidateConfig
|
clip: dict[str, Any], *, split: str, config: CandidateConfig
|
||||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||||
segments, wearer_id = load_clip_segments(clip)
|
segments, wearer_id = load_clip_segments(clip)
|
||||||
clip_end = _number(clip.get("clip_end_sec", max((row.end for row in segments), default=0.0)), field="clip_end_sec")
|
declared_clip_end = _number(
|
||||||
outside = [row for row in segments if row.end > clip_end + 1e-6]
|
clip.get("clip_end_sec", max((row.end for row in segments), default=0.0)),
|
||||||
if outside:
|
field="clip_end_sec",
|
||||||
first = outside[0]
|
)
|
||||||
|
maximum_segment_end = max((row.end for row in segments), default=declared_clip_end)
|
||||||
|
terminal_overrun = max(0.0, maximum_segment_end - declared_clip_end)
|
||||||
|
if terminal_overrun > config.max_terminal_overrun_seconds + 1e-9:
|
||||||
|
first = next(row for row in segments if row.end > declared_clip_end)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{clip.get('clip_uid')}: voice segment {first.uid} ends at {first.end:.6f}s "
|
f"{clip.get('clip_uid')}: voice segment {first.uid} ends at {first.end:.6f}s "
|
||||||
f"after clip_end_sec={clip_end:.6f}; refusing to clamp or drop it"
|
f"after clip_end_sec={declared_clip_end:.6f}; terminal overrun "
|
||||||
|
f"{terminal_overrun:.6f}s exceeds the audited limit "
|
||||||
|
f"{config.max_terminal_overrun_seconds:.6f}s; refusing to clamp or drop it"
|
||||||
)
|
)
|
||||||
|
# Ego4D v1 has quantized terminal voice endpoints a few milliseconds past
|
||||||
|
# the 30-fps video endpoint (for example 300.008 vs 299.966667 seconds).
|
||||||
|
# Preserve that official endpoint and make the bounded exception explicit.
|
||||||
|
clip_end = max(declared_clip_end, maximum_segment_end)
|
||||||
candidates: list[dict[str, Any]] = []
|
candidates: list[dict[str, Any]] = []
|
||||||
wearer_segments = [row for row in segments if row.person_id == wearer_id]
|
wearer_segments = [row for row in segments if row.person_id == wearer_id]
|
||||||
|
|
||||||
@@ -409,6 +420,14 @@ def derive_clip_candidates(
|
|||||||
))
|
))
|
||||||
control_sequence += 1
|
control_sequence += 1
|
||||||
candidates.sort(key=lambda row: (float(row["event_time"]), str(row["uid"])))
|
candidates.sort(key=lambda row: (float(row["event_time"]), str(row["uid"])))
|
||||||
|
for row in candidates:
|
||||||
|
row["temporal_evidence"].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,
|
||||||
|
"terminal_voice_endpoint_preserved_not_clamped": True,
|
||||||
|
})
|
||||||
return candidates, [_timeline_row(row) for row in segments]
|
return candidates, [_timeline_row(row) for row in segments]
|
||||||
|
|
||||||
|
|
||||||
@@ -462,6 +481,10 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--relation-window-seconds", type=float, default=3.0)
|
parser.add_argument("--relation-window-seconds", type=float, default=3.0)
|
||||||
parser.add_argument("--same-speaker-pause-max-seconds", type=float, default=2.0)
|
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("--other-handover-max-seconds", type=float, default=1.5)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-terminal-overrun-seconds", type=float, default=0.1,
|
||||||
|
help="Allow and audit a bounded official voice endpoint past clip_end_sec (default: %(default)s).",
|
||||||
|
)
|
||||||
parser.add_argument("--overwrite", action="store_true")
|
parser.add_argument("--overwrite", action="store_true")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
@@ -471,6 +494,7 @@ def main() -> None:
|
|||||||
numeric = [
|
numeric = [
|
||||||
args.context_seconds, args.relation_window_seconds,
|
args.context_seconds, args.relation_window_seconds,
|
||||||
args.same_speaker_pause_max_seconds, args.other_handover_max_seconds,
|
args.same_speaker_pause_max_seconds, args.other_handover_max_seconds,
|
||||||
|
args.max_terminal_overrun_seconds,
|
||||||
]
|
]
|
||||||
if any(value < 0 for value in numeric):
|
if any(value < 0 for value in numeric):
|
||||||
raise ValueError("all time windows must be non-negative")
|
raise ValueError("all time windows must be non-negative")
|
||||||
|
|||||||
@@ -114,6 +114,25 @@ class Ego4DAVCandidateTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(ValueError, "exactly one camera wearer"):
|
with self.assertRaisesRegex(ValueError, "exactly one camera wearer"):
|
||||||
load_clip_segments(clip)
|
load_clip_segments(clip)
|
||||||
|
|
||||||
|
def test_bounded_official_terminal_endpoint_is_preserved_and_audited(self) -> None:
|
||||||
|
clip = fixture_clip()
|
||||||
|
clip["clip_end_sec"] = 8.76
|
||||||
|
rows, _ = derive_clip_candidates(clip, split="val", config=CandidateConfig())
|
||||||
|
offset = next(
|
||||||
|
row for row in rows
|
||||||
|
if row["candidate_source"] == "WEARER_OFFSET" and row["event_time"] == 8.0
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
offset["temporal_evidence"]["terminal_voice_overrun_seconds"], 0.04
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
with self.assertRaisesRegex(ValueError, "exceeds the audited limit"):
|
||||||
|
derive_clip_candidates(clip, split="val", config=CandidateConfig())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user