Files
icassp2027-experiment/test_local_turn_features.py
T

1573 lines
75 KiB
Python
Raw Normal View History

"""CPU tests for B1 alignment, causal pooling, fusion, and train/infer round trips."""
from __future__ import annotations
import copy
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from types import SimpleNamespace
2026-08-27 15:24:59 +08:00
from unittest.mock import patch
import numpy as np
import torch
2026-08-27 15:08:38 +08:00
from PIL import Image, ImageDraw
2026-09-06 21:48:32 +08:00
from easycom_head import (
LOCAL_FUSION_SCOPES,
TurnPredictionHead,
checkpoint_head_config,
)
from extract_turn_local_features import (
FrozenQwenLocalEncoder, ego4d_clip_boxes, extract_local,
validate_source_timeline,
model_config_digest,
2026-08-28 00:46:05 +08:00
box_coordinate_offset, frame_crops, parse_args, preview_boxes, preview_ticks, preview_time_bounds,
2026-08-27 15:24:59 +08:00
TESTED_TRANSFORMERS_VERSIONS, validate_local_transformers_version,
main as extract_main,
)
from extract_highrate_person_av_features import (
MODEL_FPS as HIGHRATE_MODEL_FPS,
_chunked_audio_encode,
_chunked_person_encode,
_model_tick_bounds,
_square_gray_face,
)
from local_turn_features import (
HIGHRATE_PERSON_AV_FORMAT, LOCAL_FORMAT, FramePersonFeatures,
causal_frame_ticks, crop_box, easycom_boxes,
load_frame_set_features, load_highrate_person_av_features,
load_local_features, load_track_features,
local_head_dimensions,
2026-09-02 18:48:26 +08:00
pooled_recent_visual, reference_alignment,
)
from train_turn_head import _load_examples
def fixture(n=20, name="Session_4", dataset="easycom"):
times = (torch.arange(n, dtype=torch.float64) + 1) / 5
reference = {"states": torch.randn(n, 4, 8), "label_indices": torch.arange(n),
"times": times, "mode": "open-stream-replay", "start": 0}
side = {"format": LOCAL_FORMAT, "dataset": dataset, "sample_id": name,
"times": times.clone(), "label_indices": torch.arange(n), "partial": False,
"visual": torch.randn(n, 6), "audio": torch.randn(n, 5),
"visual_valid": torch.ones(n, dtype=torch.bool),
"audio_valid": torch.ones(n, dtype=torch.bool),
"config": {"visual_dim": 6, "audio_dim": 5, "encoder_id": "synthetic-test",
"causal_policy": "half-open-trailing-window", "uses_speech_labels": False}}
side["visual_valid"][0] = False
side["visual"][0] = 0
2026-09-02 18:48:26 +08:00
frame_times = torch.arange(max(1, (n + 2) // 3), dtype=torch.float64) * .5
counts = [2 if index % 3 == 0 else (0 if index % 3 == 1 else 1)
for index in range(len(frame_times))]
offsets = torch.tensor([0, *np.cumsum(counts).tolist()], dtype=torch.long)
person_features = torch.randn(int(offsets[-1]), 6)
side["persons"] = {
"features": person_features,
"frame_times": frame_times,
"frame_offsets": offsets,
"records": [{"id": str(index)} for index in range(len(person_features))],
}
labels = np.tile(np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1]], dtype=np.float32), (n // 4 + 1, 1))[:n]
return reference, side, labels
def highrate_fixture(n=20, name="Session_4", dataset="easycom"):
reference, side, labels = fixture(n=n, name=name, dataset=dataset)
side["format"] = HIGHRATE_PERSON_AV_FORMAT
side["visual"] = torch.randn(n, 6)
side["visual_valid"] = torch.ones(n, dtype=torch.bool)
side["persons"] = {
"features": side["visual"].clone(),
"frame_times": reference["times"].clone(),
"frame_offsets": torch.arange(n + 1, dtype=torch.long),
"records": [
{"id": "p", "asd_interval_end": float(reference["times"][row])}
for row in range(n)
],
}
side["config"].update(
{
"model_fps": 25.0,
"output_hz": 5.0,
"person_feature_kind": (
"muvap-asd-causal-visual-plus-activity-logit"
),
"person_visual_dim": 5,
"person_activity_logit_dim": 1,
"audio_feature_kind": "muvap-cpc-causal-audio-state",
"audio_source": "single-shared-traditional-cpc-stream",
"person_tokens_duplicate_audio": False,
"asd_audio_preprocessing": (
"causal-float-pcm-clip-only-no-rms-normalization"
),
"asd_temporal_attention": "causal",
"causal_policy": "right-edge-causal-highrate-asd",
}
)
return reference, side, labels
class LocalTests(unittest.TestCase):
def setUp(self):
torch.set_num_threads(1)
2026-08-27 15:24:59 +08:00
def test_tested_transformers_versions(self):
for version in ("4.52.3", "4.57.6"):
self.assertEqual(validate_local_transformers_version(version), version)
for version in ("4.50.0.dev0", "4.57.6.dev0", "5.0.0"):
with self.subTest(version=version), self.assertRaisesRegex(RuntimeError, "found " + version):
validate_local_transformers_version(version)
def test_half_open_grid_and_pooling(self):
self.assertEqual(list(causal_frame_ticks(.2, 2)), [0])
self.assertEqual(list(causal_frame_ticks(1., 2)), [0, 1])
ts = torch.tensor([0., .5, 1., 1.5], dtype=torch.float64)
vectors = torch.tensor([[2.], [4.], [90.], [100.]])
valid = torch.ones(4, dtype=torch.bool)
value, present = pooled_recent_visual(ts, vectors, valid, 1., 1.)
self.assertTrue(present)
self.assertEqual(value.item(), 3.) # no frame at t=1
value, _ = pooled_recent_visual(ts, vectors, valid, 1.2, 1.)
self.assertEqual(value.item(), 47.) # .5 and 1.0, not 0.0
value, present = pooled_recent_visual(ts, vectors, valid * False, 1., 1.)
self.assertFalse(present)
self.assertEqual(value.item(), 0.)
def test_highrate_person_av_right_edges_are_causal_and_inclusive(self):
reference, side, _ = highrate_fixture(n=5)
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "Session_4.pt"
torch.save(side, path)
local, people, contract = load_highrate_person_av_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
)
packed_audio, audio_contract = load_local_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
modalities="audio",
)
self.assertEqual(local.shape, (5, 6))
self.assertEqual(people.row_to_frame.tolist(), [0, 1, 2, 3, 4])
self.assertTrue(torch.equal(people.chosen_frame_times, reference["times"]))
self.assertEqual(contract["format"], HIGHRATE_PERSON_AV_FORMAT)
self.assertEqual(packed_audio.shape, (5, 6))
self.assertEqual(audio_contract["modalities"], "audio")
values, mask = people.padded()
self.assertEqual(values.shape, (5, 1, 6))
self.assertTrue(mask.all())
def test_highrate_person_av_rejects_old_or_duplicated_audio_contracts(self):
reference, side, _ = highrate_fixture(n=5)
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "Session_4.pt"
old = copy.deepcopy(side)
old["format"] = "causal-highrate-person-av-v1"
torch.save(old, path)
with self.assertRaisesRegex(ValueError, "Invalid local feature format"):
load_highrate_person_av_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
)
duplicated = copy.deepcopy(side)
duplicated["config"]["person_tokens_duplicate_audio"] = True
torch.save(duplicated, path)
with self.assertRaisesRegex(ValueError, "causal ASD contract"):
load_highrate_person_av_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
)
def test_highrate_face_geometry_and_chunked_causal_coverage(self):
image = Image.new("RGB", (40, 20), "white")
face = _square_gray_face(image)
self.assertEqual(face.shape, (112, 112))
self.assertEqual(face.dtype, np.uint8)
self.assertEqual(int(face[0, 0]), 128)
clip = SimpleNamespace(dense_start=0.0, dense_end=1.0)
self.assertEqual(_model_tick_bounds(clip, 1.0), (0, 25))
self.assertEqual(HIGHRATE_MODEL_FPS, 25.0)
class FakeEncoder:
audio_dim = 2
feature_dim = 3
def __init__(self):
self.audio_lengths = []
self.person_lengths = []
def encode_audio(self, waveform, num_frames):
self.audio_lengths.append(num_frames)
return torch.stack(
(torch.arange(num_frames), torch.ones(num_frames)), dim=-1
).float()
def encode_person(self, faces, audio_states):
self.person_lengths.append(len(faces))
return torch.cat((audio_states, torch.ones(len(faces), 1)), dim=-1)
encoder = FakeEncoder()
faces = np.zeros((9, 112, 112), dtype=np.uint8)
waveform = np.zeros(9 * 640, dtype=np.float32)
audio_result = _chunked_audio_encode(
encoder,
waveform,
num_frames=len(faces),
output_frames=4,
context_frames=2,
)
person_result = _chunked_person_encode(
encoder,
faces,
audio_result,
output_frames=4,
context_frames=2,
)
self.assertEqual(audio_result.shape, (9, 2))
self.assertEqual(person_result.shape, (9, 3))
self.assertEqual(encoder.audio_lengths, [4, 6, 3])
self.assertEqual(encoder.person_lengths, [4, 6, 3])
def test_box_formats_and_crop(self):
person = {"Participant_ID": -1, "x1": 2, "y1": 3, "x2": 7, "y2": 9}
faces = easycom_boxes([{"Frame_Number": 1, "Participants": [person]}], "face")
heads = easycom_boxes([{"Frame_Number": 0, "Participants": person}], "head")
self.assertEqual(faces, heads)
self.assertEqual(crop_box([202., 3., 207., 9.], 1920, 1080, offset_x=200), (2, 3, 7, 9))
self.assertIsNone(crop_box([4., 4., 3., 5.], 10, 10))
self.assertIsNone(crop_box([float("nan"), 4., 9., 5.], 10, 10))
def test_ego4d_clip_frames(self):
boxes = [{"frame": 3, "clip_frame": None, "x": 1, "y": 2, "width": 3, "height": 4}]
person = {"person_id": "x", "camera_wearer": False, "tracking_paths": [{"track": boxes}, {"track": boxes}]}
wearer = {**person, "camera_wearer": True}
actual = ego4d_clip_boxes({"persons": [wearer, person]})
self.assertEqual(actual, {3: [{"id": "x", "xyxy": [1., 2., 4., 6.]}]})
2026-08-27 15:08:38 +08:00
def test_dataset_specific_coordinate_defaults_and_explicit_overrides(self):
required = ["--data-root", "/raw", "--feature-root", "/global",
"--target-root", "/targets", "--output-root", "/local", "--samples", "4"]
self.assertEqual(parse_args(required).box_coordinate_space, "encoded")
self.assertEqual(parse_args(required + ["--dataset", "ego4d"]).box_coordinate_space, "camera")
self.assertEqual(parse_args(required + ["--box-coordinate-space", "camera"]).box_coordinate_space, "camera")
# An appended correction overrides an old LOCAL_ARGS array in the shell.
self.assertEqual(parse_args(required + ["--box-coordinate-space", "camera",
"--box-coordinate-space", "encoded"]).box_coordinate_space, "encoded")
def test_frame_crops_subtract_each_clips_banner_before_encoding(self):
expected = (100, 80, 160, 140)
image = Image.new("RGB", (1920, 1080))
ImageDraw.Draw(image).rectangle(expected, fill="red")
clips, annotations = [], []
for width in (2123, 2124, 2203, 1920):
offset = width - 1920
clips.append(SimpleNamespace(crop=(offset, 0, 1920, 1080), dense_start=float(len(clips))))
annotations.append({0: [{"id": "3", "xyxy": [100 + offset, 80, 160 + offset, 140]}]})
media = SimpleNamespace(clips=clips,
_source_frame_for_time=lambda t: (int(t), 0, 0.),
frame_at_or_before=lambda t: image.copy())
args = SimpleNamespace(box_coordinate_space="encoded", crop_expansion=1.)
for index, clip in enumerate(clips):
with self.subTest(crop=clip.crop):
_, crops, records = frame_crops(media, annotations, float(index), args)
self.assertEqual(records[0]["box"], expected)
self.assertEqual(records[0]["coordinate_offset"], [clip.crop[0], 0])
self.assertEqual(records[0]["annotation_box"], annotations[index][0][0]["xyxy"])
self.assertEqual(crops[0].size, (60, 60))
self.assertEqual(crops[0].getpixel((30, 30)), (255, 0, 0))
def test_cropped_camera_annotations_and_uncropped_ego4d_are_not_shifted_twice(self):
clip = SimpleNamespace(crop=(204, 12, 1920, 1080), dense_start=0.)
self.assertEqual(box_coordinate_offset(clip, "encoded"), (204, 12))
self.assertEqual(box_coordinate_offset(clip, "camera"), (0, 0))
self.assertEqual(box_coordinate_offset(SimpleNamespace(crop=None), "encoded"), (0, 0))
with self.assertRaisesRegex(ValueError, "coordinate space"):
box_coordinate_offset(clip, "typo")
media = SimpleNamespace(clips=[clip], _source_frame_for_time=lambda t: (0, 0, 0.),
frame_at_or_before=lambda t: Image.new("RGB", (1920, 1080)))
annotations = [{0: [{"id": "3", "xyxy": [100, 80, 160, 140]}]}]
args = SimpleNamespace(box_coordinate_space="camera", crop_expansion=1.)
_, _, records = frame_crops(media, annotations, .1, args)
self.assertEqual(records[0]["box"], (100, 80, 160, 140))
# Encoded boxes translate on BOTH axes, then clip at the visible edge.
annotations = [{0: [{"id": "3", "xyxy": [190, 4, 230, 50]}]}]
args.box_coordinate_space = "encoded"
_, _, records = frame_crops(media, annotations, .1, args)
self.assertEqual(records[0]["box"], (0, 0, 26, 38))
def test_preview_records_the_same_transformation_used_for_extraction(self):
clip = SimpleNamespace(path=Path("source.mp4"), width=2124, height=1080,
crop=(204, 0, 1920, 1080), dense_start=0.)
media = SimpleNamespace(clips=[clip], _source_frame_for_time=lambda t: (0, 0, 0.),
frame_at_or_before=lambda t: Image.new("RGB", (1920, 1080)))
annotations = [{0: [{"id": "3", "xyxy": [304, 80, 364, 140]}]}]
with tempfile.TemporaryDirectory() as directory:
args = SimpleNamespace(box_coordinate_space="encoded", crop_expansion=1.,
video_fps=2., output_root=Path(directory))
preview_boxes(media, annotations, args, "Session_4", .2)
folder = Path(directory) / "Session_4_preview"
audit = json.loads((folder / "boxes.json").read_text())
self.assertEqual(audit[0]["coordinate_offset"], [204, 0])
self.assertEqual(audit[0]["boxes"][0]["box"], [100, 80, 160, 140])
self.assertTrue((folder / audit[0]["image"]).is_file())
2026-08-28 00:46:05 +08:00
def test_preview_can_save_more_than_four_images_in_a_later_range(self):
clip = SimpleNamespace(path=Path("source.mp4"), width=640, height=360,
crop=None, dense_start=0.)
media = SimpleNamespace(clips=[clip],
_source_frame_for_time=lambda t: (0, round(t * 2), t),
frame_at_or_before=lambda t: Image.new("RGB", (640, 360)))
with tempfile.TemporaryDirectory() as directory:
args = SimpleNamespace(box_coordinate_space="camera", crop_expansion=1., video_fps=2.,
output_root=Path(directory), preview_count=8,
preview_start=2., preview_end=6., preview_with_boxes=False)
preview_boxes(media, [{}], args, "clip", 10.)
folder = Path(directory) / "clip_preview"
audit = json.loads((folder / "boxes.json").read_text())
self.assertEqual([row["time"] for row in audit], [2., 2.5, 3., 3.5, 4., 4.5, 5., 5.5])
self.assertEqual(audit[0]["image"], "frame_000004.jpg")
self.assertEqual(len(list(folder.glob("*.jpg"))), 8)
report = json.loads((folder / "preview.json").read_text())
self.assertEqual(report["saved_count"], 8)
self.assertEqual(report["saved_with_boxes"], 0)
def test_boxed_preview_filters_valid_corrected_crops_without_decoding(self):
# Encoded boxes are corrected by the crop origin, as in feature extraction.
clip = SimpleNamespace(crop=(204, 0, 640, 360), width=844, height=360)
media = SimpleNamespace(clips=[clip], _source_frame_for_time=lambda t: (0, round(t * 2), t))
valid = {"id": "p", "xyxy": [304, 80, 364, 140]}
invalid = {"id": "p", "xyxy": [4, 80, 64, 140]} # entirely inside removed banner
annotations = [{1: [valid], 3: [invalid], 5: [valid], 7: [valid]}]
args = SimpleNamespace(box_coordinate_space="encoded", crop_expansion=1., video_fps=2.,
preview_count=2, preview_start=0., preview_end=None, preview_with_boxes=True)
selected, grid_count, eligible_count = preview_ticks(media, annotations, args, 4.)
self.assertEqual(selected, [1, 7]) # uniform among valid frames, not first N
self.assertEqual((grid_count, eligible_count), (8, 3))
# No RGB reader is needed by the scan, and annotations are not changed.
self.assertEqual(len(annotations[0]), 4)
def test_empty_boxed_preview_writes_an_explicit_empty_report(self):
clip = SimpleNamespace(crop=None, width=640, height=360)
media = SimpleNamespace(clips=[clip], _source_frame_for_time=lambda t: (0, round(t * 2), t))
with tempfile.TemporaryDirectory() as directory:
args = SimpleNamespace(box_coordinate_space="camera", crop_expansion=1., video_fps=2.,
output_root=Path(directory), preview_count=40,
preview_start=0., preview_end=None, preview_with_boxes=True)
preview_boxes(media, [{}], args, "empty_clip", 4.)
folder = Path(directory) / "empty_clip_preview"
self.assertEqual(json.loads((folder / "boxes.json").read_text()), [])
report = json.loads((folder / "preview.json").read_text())
self.assertEqual(report["eligible_frame_count"], 0)
self.assertEqual(report["saved_count"], 0)
self.assertEqual(list(folder.glob("*.jpg")), [])
def test_preview_range_clips_to_available_end_and_excludes_endpoint(self):
args = SimpleNamespace(video_fps=2., preview_start=.1, preview_end=100., preview_count=100)
self.assertEqual(preview_time_bounds(args, 1.), (.1, 1.))
self.assertEqual(preview_ticks(None, [], args, 1.), ([1], 1, 1))
args.preview_start = 2.
with self.assertRaisesRegex(ValueError, "outside the available prefix"):
preview_time_bounds(args, 1.)
def test_preview_cli_validation(self):
import contextlib
import io
required = ["--data-root", "/raw", "--feature-root", "/global", "--target-root", "/targets",
"--output-root", "/preview", "--samples", "4"]
valid = parse_args(required + ["--preview-only", "--preview-count", "40", "--preview-start", "30",
"--preview-end", "120", "--preview-with-boxes"])
self.assertEqual((valid.preview_count, valid.preview_start, valid.preview_end), (40, 30., 120.))
for extra in (["--preview-only", "--preview-count", "0"],
["--preview-only", "--preview-start", "-1"],
["--preview-only", "--preview-end", "nan"],
["--preview-only", "--preview-start", "10", "--preview-end", "10"],
["--preview-only", "--max-steps", "20", "--preview-end", "60"],
["--preview-count", "40"], ["--preview-with-boxes"]):
with self.subTest(extra=extra), contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
parse_args(required + extra)
def test_preview_end_limits_media_reader_without_loading_encoder(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
reference, _, labels = fixture()
torch.save(reference, root / "Session_4.pt")
np.save(root / "Session_4.npy", labels)
args = ["--data-root", str(root), "--feature-root", str(root), "--target-root", str(root),
"--output-root", str(root / "preview"), "--samples", "4",
"--preview-only", "--preview-end", "2", "--preview-count", "20"]
media = SimpleNamespace(clips=[], metadata=lambda: {})
with patch("extract_turn_local_features.build_media_and_boxes", return_value=(media, [])) as build, \
patch("extract_turn_local_features.preview_boxes") as preview, \
patch("extract_turn_local_features.FrozenQwenLocalEncoder") as encoder:
extract_main(args)
self.assertEqual(build.call_args.args[2], 2.)
preview.assert_called_once()
encoder.assert_not_called()
def test_sidecar_alignment_contract_and_modalities(self):
reference, side, _ = fixture()
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "Session_4.pt"
for mode, width in (("visual", 7), ("audio", 6), ("av", 13)):
torch.save(side, path)
features, contract = load_local_features(path, reference, sample_id="Session_4", dataset="easycom", modalities=mode)
self.assertEqual(features.shape, (20, width))
dims = local_head_dimensions(contract)
self.assertEqual(dims["local_visual_dim"], 0 if mode == "audio" else 6)
for field, value in (("times", side["times"] + .2), ("label_indices", torch.arange(20) + 1),
("sample_id", "Session_5"), ("partial", True),
("visual", torch.full((20, 6), float("nan"))),
("visual_valid", torch.ones(20))):
broken = {**side, field: value}
torch.save(broken, path)
with self.subTest(field=field), self.assertRaises(ValueError):
load_local_features(path, reference, sample_id="Session_4", dataset="easycom", modalities="av")
torch.save(side, path)
wrong_contract = copy.deepcopy(contract)
wrong_contract["config"]["encoder_id"] = "other-weights"
with self.assertRaisesRegex(ValueError, "contract"):
load_local_features(path, reference, sample_id="Session_4", dataset="easycom", modalities="av", expected_contract=wrong_contract)
with self.assertRaisesRegex(ValueError, "times"):
reference_alignment({**reference, "times": reference["times"] + .2})
2026-09-02 18:48:26 +08:00
def test_frame_set_loader_is_causal_ragged_and_fail_closed(self):
reference, side, _ = fixture()
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "Session_4.pt"
torch.save(side, path)
audio, people, contract = load_frame_set_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
)
self.assertEqual(audio.shape, (20, 6))
self.assertEqual(contract["modalities"], "av")
self.assertEqual(people.dimension, 6)
self.assertTrue(
torch.all(
people.chosen_frame_times[
torch.isfinite(people.chosen_frame_times)
]
< reference["times"][
torch.isfinite(people.chosen_frame_times)
]
)
)
# At 0.2/0.4s, both rows reuse the 0.0s frame with two people.
values, mask = people.padded(torch.tensor([0, 1, 2]))
self.assertEqual(values.shape, (3, 2, 6))
self.assertEqual(mask.tolist(), [[True, True], [True, True], [False, False]])
self.assertTrue(torch.equal(values[0], values[1]))
self.assertEqual(torch.count_nonzero(values[2]), 0)
# The frame stamped exactly 1.0s is excluded at the 1.0s cutoff.
self.assertEqual(people.chosen_frame_times[4].item(), .5)
broken_cases = []
broken = copy.deepcopy(side)
broken["persons"]["frame_offsets"][-1] -= 1
broken_cases.append(broken)
broken = copy.deepcopy(side)
broken["persons"]["frame_times"][1] = broken["persons"]["frame_times"][0]
broken_cases.append(broken)
broken = copy.deepcopy(side)
broken["persons"]["records"] = []
broken_cases.append(broken)
broken = copy.deepcopy(side)
broken["persons"]["features"][0, 0] = float("nan")
broken_cases.append(broken)
for index, broken in enumerate(broken_cases):
torch.save(broken, path)
with self.subTest(index=index), self.assertRaises(ValueError):
load_frame_set_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
)
def test_track_loader_builds_causal_histories_and_resets_tracks(self):
reference, side, _ = fixture(n=12)
frame_times = torch.tensor([0., .5, 1., 1.5, 2.], dtype=torch.float64)
side["persons"] = {
"features": torch.arange(1, 9, dtype=torch.float32)[:, None].repeat(1, 6),
"frame_times": frame_times,
"frame_offsets": torch.tensor([0, 2, 4, 6, 7, 8]),
"records": [
{"id": "A", "source_clip_index": 0},
{"id": "B", "source_clip_index": 0},
{"id": "B", "source_clip_index": 0},
{"id": "A", "source_clip_index": 0},
{"id": "A", "source_clip_index": 0},
{"id": -1, "source_clip_index": 0},
# The same ID in another retained source clip is a new track.
{"id": "A", "source_clip_index": 1},
{"id": "A", "source_clip_index": 1},
],
}
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "Session_4.pt"
torch.save(side, path)
pooled, people, contract = load_track_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
track_max_gap_seconds=.75,
)
self.assertEqual(pooled.shape, (12, 13))
self.assertEqual(contract["modalities"], "av")
self.assertEqual(
people.previous_observation.tolist(),
[-1, -1, 1, 0, 3, -1, -1, 6],
)
# Cutoff 1.2s selects frame 1.0s. A has observations 1,4,5;
# anonymous -1 remains a one-observation track.
history, mask = people.padded_history(
torch.tensor([5]),
max_observations=4,
)
self.assertEqual(mask.tolist(), [[[True, True, True, False],
[True, False, False, False]]])
self.assertEqual(history[0, 0, :, 0].tolist(), [1., 4., 5., 0.])
self.assertEqual(history[0, 1, :, 0].tolist(), [6., 0., 0., 0.])
# Cutoff 1.8s selects frame 1.5s; source clip reset prevents A's
# earlier observations from crossing the boundary.
history, mask = people.padded_history(
torch.tensor([8]),
max_observations=4,
)
self.assertEqual(mask.tolist(), [[[True, False, False, False]]])
self.assertEqual(history[0, 0, :, 0].tolist(), [7., 0., 0., 0.])
broken = copy.deepcopy(side)
broken["persons"]["records"][1]["id"] = "A"
torch.save(broken, path)
with self.assertRaisesRegex(ValueError, "Duplicate person/track ID"):
load_track_features(
path,
reference,
sample_id="Session_4",
dataset="easycom",
track_max_gap_seconds=.75,
)
2026-09-02 18:48:26 +08:00
def test_frame_person_concatenation_selection_and_empty_padding(self):
features = torch.tensor([[1., 2.], [3., 4.]])
a = FramePersonFeatures(
features,
torch.tensor([0, 2], dtype=torch.long),
torch.tensor([0, -1], dtype=torch.long),
torch.tensor([0., float("nan")], dtype=torch.float64),
)
b = FramePersonFeatures(
torch.empty(0, 2),
torch.tensor([0, 0], dtype=torch.long),
torch.tensor([0], dtype=torch.long),
torch.tensor([.5], dtype=torch.float64),
)
combined = FramePersonFeatures.concatenate([a, b])
self.assertEqual(len(combined), 3)
self.assertEqual(combined.row_to_frame.tolist(), [0, -1, 1])
selected = combined.select_rows(torch.tensor([2, 0]))
values, mask = selected.padded()
self.assertEqual(mask.tolist(), [[False, False], [True, True]])
self.assertEqual(torch.count_nonzero(values[0]), 0)
self.assertTrue(torch.equal(values[1], features))
def test_frame_set_head_is_permutation_invariant_and_masks_padding(self):
torch.manual_seed(7)
head = TurnPredictionHead(
hidden_size=8,
num_input_layers=4,
layer_fusion="concat-mlp",
local_visual_dim=6,
local_audio_dim=5,
local_projection_dim=3,
local_fusion="frame-set-attention",
person_attention_dim=4,
person_attention_heads=2,
transition_auxiliary=True,
dropout=0,
).eval()
states = torch.randn(3, 4, 8)
local = torch.randn(3, 6)
local[:, -1] = 1
people = torch.randn(3, 3, 6)
mask = torch.tensor(
[[True, True, False], [True, False, False], [False, False, False]]
)
people[~mask] = 0
logits, transitions = head.forward_with_transition(
states,
local,
people,
mask,
)
self.assertEqual(logits.shape, (3, 10, 3))
self.assertEqual(transitions.shape, (3, 10))
(logits.square().mean() + transitions.square().mean()).backward()
self.assertIsNotNone(head.person_attention.in_proj_weight.grad)
permutation = torch.tensor([1, 0, 2])
permuted = head(states, local, people[:, permutation], mask[:, permutation])
self.assertTrue(torch.allclose(logits, permuted, atol=1e-6, rtol=1e-6))
changed = people.clone()
changed[~mask] = 1000
self.assertTrue(torch.allclose(logits, head(states, local, changed, mask)))
with self.assertRaisesRegex(ValueError, "person features"):
head(states, local)
config = {
"hidden_size": 8,
"num_input_layers": 4,
"layer_fusion": "concat-mlp",
"local_visual_dim": 6,
"local_audio_dim": 5,
"local_projection_dim": 3,
"local_fusion": "frame-set-attention",
"person_attention_dim": 4,
"person_attention_heads": 2,
"transition_auxiliary": True,
"dropout": 0,
}
restored = TurnPredictionHead(
**checkpoint_head_config({"head_config": config})
)
restored.load_state_dict(head.state_dict())
restored.eval()
self.assertTrue(torch.equal(restored(states, local, people, mask), logits))
def test_highrate_person_av_starts_from_exact_audio_only_base(self):
torch.manual_seed(19)
audio = TurnPredictionHead(
hidden_size=8,
local_visual_dim=0,
local_audio_dim=5,
local_projection_dim=3,
local_fusion="pooled",
person_attention_dim=4,
person_attention_heads=2,
dropout=0,
).eval()
highrate = TurnPredictionHead(
hidden_size=8,
local_visual_dim=6,
local_audio_dim=5,
local_projection_dim=3,
local_fusion="highrate-person-av-attention",
person_attention_dim=4,
person_attention_heads=2,
dropout=0,
).eval()
highrate_state = highrate.state_dict()
for name, value in audio.state_dict().items():
self.assertIn(name, highrate_state)
self.assertEqual(tuple(value.shape), tuple(highrate_state[name].shape))
highrate_state[name] = value
highrate.load_state_dict(highrate_state)
states = torch.randn(4, 8)
local = torch.randn(4, 6)
local[:, -1] = 1
people = torch.randn(4, 3, 6)
mask = torch.tensor(
[[True, True, False], [True, False, False], [False, False, False],
[True, True, True]]
)
people[~mask] = 0
reference = audio(states, local)
actual = highrate(states, local, people, mask)
self.assertTrue(torch.equal(reference, actual))
diagnostics = highrate.person_adapter_diagnostics()
self.assertEqual(diagnostics["residual_output_weight_norm"], 0.0)
self.assertEqual(diagnostics["delta_logit_max_abs"], 0.0)
actual.sum().backward()
assert highrate.highrate_person_residual is not None
self.assertGreater(
float(highrate.highrate_person_residual.weight.grad.abs().sum()),
0.0,
)
def test_highrate_person_av_head_uses_variable_person_sets(self):
torch.manual_seed(9)
head = TurnPredictionHead(
hidden_size=8,
local_visual_dim=6,
local_audio_dim=5,
local_projection_dim=3,
local_fusion="highrate-person-av-attention",
person_attention_dim=4,
person_attention_heads=2,
dropout=0,
).eval()
states = torch.randn(3, 8)
local = torch.randn(3, 6)
local[:, -1] = 1
people = torch.randn(3, 3, 6)
mask = torch.tensor(
[[True, True, False], [True, False, False], [False, False, False]]
)
people[~mask] = 0
logits = head(states, local, people, mask)
self.assertEqual(logits.shape, (3, 10, 3))
permutation = torch.tensor([1, 0, 2])
self.assertTrue(
torch.allclose(
logits,
head(states, local, people[:, permutation], mask[:, permutation]),
atol=1e-6,
rtol=1e-6,
)
)
changed = people.clone()
changed[~mask] = 1000
self.assertTrue(torch.equal(logits, head(states, local, changed, mask)))
logits.square().mean().backward()
self.assertIsNotNone(head.person_attention.in_proj_weight.grad)
2026-09-06 21:48:32 +08:00
def test_f5_screening_audio_person_read_is_gated_and_permutation_invariant(self):
torch.manual_seed(11)
common = {
"hidden_size": 8,
"local_visual_dim": 6,
"local_audio_dim": 5,
"local_projection_dim": 3,
"person_attention_dim": 4,
"person_attention_heads": 2,
"dropout": 0,
}
f1 = TurnPredictionHead(
**common,
local_fusion="frame-set-attention",
).eval()
f5 = TurnPredictionHead(
**common,
local_fusion="audio-person-frame-set",
).eval()
# Align all shared parameters. A zero F5 gate must reproduce F1
# exactly, independent of the newly initialized reverse-read block.
f5_state = f5.state_dict()
for name, value in f1.state_dict().items():
if name in f5_state and f5_state[name].shape == value.shape:
f5_state[name] = value
f5.load_state_dict(f5_state)
states = torch.randn(3, 8)
local = torch.randn(3, 6)
local[:, -1] = 1
people = torch.randn(3, 3, 6)
mask = torch.tensor(
[[True, True, False], [True, False, False], [False, False, False]]
)
people[~mask] = 0
baseline = f1(states, local, people, mask)
self.assertTrue(torch.equal(f5(states, local, people, mask), baseline))
self.assertEqual(f5.person_interaction_gates(), {"audio_person": 0.0})
with torch.no_grad():
assert f5.audio_person_gate is not None
f5.audio_person_gate.fill_(0.25)
logits = f5(states, local, people, mask)
self.assertFalse(torch.equal(logits, baseline))
permutation = torch.tensor([1, 0, 2])
self.assertTrue(
torch.allclose(
logits,
f5(states, local, people[:, permutation], mask[:, permutation]),
atol=1e-6,
rtol=1e-6,
)
)
logits.square().mean().backward()
self.assertIsNotNone(f5.audio_person_attention.in_proj_weight.grad)
self.assertIsNotNone(f5.audio_person_gate.grad)
def test_f6_screening_adapter_is_zero_gated_and_visual_only_when_valid(self):
torch.manual_seed(13)
head = TurnPredictionHead(
hidden_size=8,
local_visual_dim=6,
local_audio_dim=5,
local_projection_dim=3,
local_fusion="gated-person-state-adapter",
person_attention_dim=4,
person_attention_heads=2,
dropout=0,
).eval()
states = torch.randn(3, 8)
local = torch.randn(3, 6)
local[:, -1] = 1
people = torch.randn(3, 2, 6)
mask = torch.tensor([[True, True], [True, False], [False, False]])
people[~mask] = 0
zero_gate = head(states, local, people, mask)
changed = people.clone()
changed[mask] += torch.arange(6, dtype=changed.dtype) * 10
# Before training, person memory is an exact zero residual.
self.assertTrue(torch.equal(head(states, local, changed, mask), zero_gate))
self.assertEqual(
head.person_interaction_gates(),
{"person_state_adapter": 0.0},
)
with torch.no_grad():
assert head.person_state_adapter_gate is not None
head.person_state_adapter_gate.fill_(0.25)
logits = head(states, local, people, mask)
self.assertFalse(torch.equal(head(states, local, changed, mask), logits))
# An empty set must remain visually inert even after opening the gate.
empty_changed = people.clone()
empty_changed[2] = 1000
self.assertTrue(
torch.equal(
logits[2],
head(states, local, empty_changed, mask)[2],
)
)
permutation = torch.tensor([1, 0])
self.assertTrue(
torch.allclose(
logits,
head(states, local, people[:, permutation], mask[:, permutation]),
atol=1e-6,
rtol=1e-6,
)
)
logits.square().mean().backward()
self.assertIsNotNone(head.person_attention.in_proj_weight.grad)
self.assertIsNotNone(head.person_state_adapter[-1].weight.grad)
self.assertIsNotNone(head.person_state_adapter_gate.grad)
config = {
"hidden_size": 8,
"local_visual_dim": 6,
"local_audio_dim": 5,
"local_projection_dim": 3,
"local_fusion": "gated-person-state-adapter",
"person_attention_dim": 4,
"person_attention_heads": 2,
"dropout": 0,
}
restored = TurnPredictionHead(
**checkpoint_head_config({"head_config": config})
)
restored.load_state_dict(head.state_dict())
restored.eval()
self.assertTrue(torch.equal(restored(states, local, people, mask), logits))
def test_track_gru_residual_starts_from_exact_pooled_logits(self):
torch.manual_seed(17)
common = {
"hidden_size": 8,
"num_input_layers": 4,
"layer_fusion": "concat-mlp",
"local_visual_dim": 6,
"local_audio_dim": 5,
"local_projection_dim": 3,
"dropout": 0,
}
pooled = TurnPredictionHead(**common, local_fusion="pooled").eval()
track = TurnPredictionHead(
**common,
local_fusion="track-gru-residual",
person_attention_dim=4,
person_attention_heads=2,
person_history_frames=3,
).eval()
track_state = track.state_dict()
for name, value in pooled.state_dict().items():
self.assertIn(name, track_state)
self.assertEqual(track_state[name].shape, value.shape)
track_state[name] = value
track.load_state_dict(track_state)
states = torch.randn(3, 4, 8)
local = torch.randn(3, 13)
local[:, 6] = 1
local[:, 12] = 1
histories = torch.randn(3, 2, 3, 6)
mask = torch.tensor(
[
[[True, True, True], [True, True, False]],
[[True, False, False], [False, False, False]],
[[False, False, False], [False, False, False]],
]
)
histories[~mask] = 0
baseline = pooled(states, local)
self.assertTrue(torch.equal(track(states, local, histories, mask), baseline))
self.assertEqual(track.person_interaction_gates(), {"person_track_residual": 0.0})
with torch.no_grad():
assert track.person_track_residual_gate is not None
track.person_track_residual_gate.fill_(.25)
logits = track(states, local, histories, mask)
self.assertFalse(torch.equal(logits[:2], baseline[:2]))
# With no current person the correction must be exactly zero.
self.assertTrue(torch.equal(logits[2], baseline[2]))
permutation = torch.tensor([1, 0])
self.assertTrue(
torch.allclose(
logits,
track(states, local, histories[:, permutation], mask[:, permutation]),
atol=1e-6,
rtol=1e-6,
)
)
changed_padding = histories.clone()
changed_padding[~mask] = 1000
self.assertTrue(torch.equal(logits, track(states, local, changed_padding, mask)))
logits.square().mean().backward()
self.assertIsNotNone(track.person_track_gru.weight_ih_l0.grad)
self.assertIsNotNone(track.person_track_delta[-1].weight.grad)
self.assertIsNotNone(track.person_track_residual_gate.grad)
config = {
**common,
"local_fusion": "track-gru-residual",
"person_attention_dim": 4,
"person_attention_heads": 2,
"person_history_frames": 3,
}
restored = TurnPredictionHead(
**checkpoint_head_config({"head_config": config})
).eval()
restored.load_state_dict(track.state_dict())
self.assertTrue(torch.equal(restored(states, local, histories, mask), logits))
def test_causal_person_adapter_v2_learns_after_exact_pooled_start(self):
common = {
"hidden_size": 8,
"num_input_layers": 4,
"layer_fusion": "concat-mlp",
"local_visual_dim": 6,
"local_audio_dim": 5,
"local_projection_dim": 3,
"dropout": 0,
}
for mode in (
"causal-person-adapter-v2",
"causal-person-adapter-egospeak-gru",
):
with self.subTest(mode=mode):
torch.manual_seed(23)
pooled = TurnPredictionHead(
**common,
local_fusion="pooled",
).eval()
adapter = TurnPredictionHead(
**common,
local_fusion=mode,
person_attention_dim=4,
person_attention_heads=2,
person_history_frames=3,
).eval()
adapter_state = adapter.state_dict()
for name, value in pooled.state_dict().items():
self.assertIn(name, adapter_state)
self.assertEqual(adapter_state[name].shape, value.shape)
adapter_state[name] = value
adapter.load_state_dict(adapter_state)
assert adapter.person_track_residual_output is not None
self.assertEqual(
torch.count_nonzero(
adapter.person_track_residual_output.weight
),
0,
)
self.assertEqual(
torch.count_nonzero(
adapter.person_track_residual_output.bias
),
0,
)
frozen_parameters = {}
for name, parameter in adapter.named_parameters():
if name == "layer_logits" or name.startswith(
("layer_fusion_net.", "local_projections.", "net.")
):
parameter.requires_grad_(False)
frozen_parameters[name] = parameter.detach().clone()
states = torch.randn(3, 4, 8)
local = torch.randn(3, 13)
local[:, 6] = 1
local[:, 12] = 1
histories = torch.randn(3, 2, 3, 6)
mask = torch.tensor(
[
[[True, True, True], [True, True, False]],
[[True, False, False], [False, False, False]],
[[False, False, False], [False, False, False]],
]
)
histories[~mask] = 0
baseline = pooled(states, local)
initial = adapter(states, local, histories, mask)
self.assertTrue(torch.equal(initial, baseline))
self.assertEqual(
adapter.person_adapter_diagnostics()["delta_logit_max_abs"],
0.0,
)
optimizer = torch.optim.SGD(adapter.parameters(), lr=0.1)
target = torch.randn_like(initial)
(initial - target).square().mean().backward()
output_gradient = (
adapter.person_track_residual_output.weight.grad
)
self.assertIsNotNone(output_gradient)
self.assertGreater(int(torch.count_nonzero(output_gradient)), 0)
optimizer.step()
optimizer.zero_grad(set_to_none=True)
trained = adapter(states, local, histories, mask)
(trained - target).square().mean().backward()
input_projection = (
adapter.person_track_input_embedding[0].weight
if adapter.person_track_input_embedding is not None
else adapter.person_input_projection[1].weight
)
for parameter in (
input_projection,
adapter.person_track_gru.weight_ih_l0,
adapter.person_track_gru.weight_hh_l0,
adapter.person_attention.in_proj_weight,
adapter.person_track_local_state[1].weight,
adapter.person_track_dynamic_gate[1].weight,
):
self.assertIsNotNone(parameter.grad)
self.assertTrue(torch.isfinite(parameter.grad).all())
self.assertGreater(int(torch.count_nonzero(parameter.grad)), 0)
for name, expected in frozen_parameters.items():
self.assertTrue(
torch.equal(dict(adapter.named_parameters())[name], expected),
name,
)
# Empty rows remain exactly on the pooled path after the
# residual opens.
self.assertTrue(
torch.equal(trained[2], pooled(states, local)[2])
)
permutation = torch.tensor([1, 0])
self.assertTrue(
torch.allclose(
trained,
adapter(
states,
local,
histories[:, permutation],
mask[:, permutation],
),
atol=1e-6,
rtol=1e-6,
)
)
changed_padding = histories.clone()
changed_padding[~mask] = 1000
self.assertTrue(
torch.equal(
trained,
adapter(states, local, changed_padding, mask),
)
)
config = {
**common,
"local_fusion": mode,
"person_attention_dim": 4,
"person_attention_heads": 2,
"person_history_frames": 3,
}
restored = TurnPredictionHead(
**checkpoint_head_config({"head_config": config})
).eval()
restored.load_state_dict(adapter.state_dict())
self.assertTrue(
torch.equal(
restored(states, local, histories, mask),
adapter(states, local, histories, mask),
)
)
def test_egospeak_style_track_gru_matches_explicit_reference(self):
torch.manual_seed(29)
head = TurnPredictionHead(
hidden_size=8,
local_visual_dim=6,
local_audio_dim=5,
local_projection_dim=3,
local_fusion="causal-person-adapter-egospeak-gru",
person_attention_dim=4,
person_attention_heads=2,
person_history_frames=3,
dropout=0,
).eval()
embedding = head.person_track_input_embedding
self.assertIsNotNone(embedding)
self.assertEqual(
[type(module) for module in embedding],
[
torch.nn.Linear,
torch.nn.LayerNorm,
torch.nn.ReLU,
torch.nn.Dropout,
],
)
self.assertEqual(head.person_track_gru.num_layers, 1)
self.assertFalse(head.person_track_gru.bidirectional)
self.assertTrue(head.person_track_gru.batch_first)
self.assertIsInstance(
head.person_track_output_activation,
torch.nn.ReLU,
)
histories = torch.randn(2, 2, 3, 6)
mask = torch.tensor(
[
[[True, True, True], [True, True, False]],
[[True, False, False], [False, False, False]],
]
)
histories[~mask] = 1234
actual, valid = head._encode_person_track_histories(histories, mask)
expected = torch.zeros_like(actual)
for batch in range(2):
for person in range(2):
length = int(mask[batch, person].sum())
if not length:
continue
sequence = embedding(
histories[batch, person, :length]
)[None]
initial = sequence.new_zeros(1, 1, 4)
_, hidden = head.person_track_gru(sequence, initial)
expected[batch, person] = torch.relu(hidden[-1, 0])
self.assertTrue(torch.equal(valid, mask.any(dim=-1)))
self.assertTrue(torch.allclose(actual, expected, atol=1e-7, rtol=0))
self.assertGreaterEqual(float(actual.min()), 0.0)
changed_padding = histories.clone()
changed_padding[~mask] = -9876
changed, _ = head._encode_person_track_histories(
changed_padding,
mask,
)
self.assertTrue(torch.equal(actual, changed))
def test_head_shapes_gradients_missing_and_roundtrip(self):
states = torch.randn(3, 4, 8)
for v, a in ((0, 0), (6, 0), (0, 5), (6, 5)):
for projection in (0, 3):
head = TurnPredictionHead(hidden_size=8, num_input_layers=4, layer_fusion="concat-mlp",
local_visual_dim=v, local_audio_dim=a, local_projection_dim=projection,
transition_auxiliary=True, dropout=0)
width = sum(d + 1 for d in (v, a) if d)
local = torch.randn(3, width)
offset = 0
for d in (v, a):
if d:
local[:, offset + d] = torch.tensor([0., 1., 1.])
offset += d + 1
logits, transition = head.forward_with_transition(states, local)
self.assertEqual(logits.shape, (3, 10, 3))
self.assertEqual(transition.shape, (3, 10))
(logits.square().mean() + transition.square().mean()).backward()
self.assertIsNotNone(head.net[1].weight.grad)
if width:
# Changing invalid raw features cannot change the readout.
changed = local.clone()
offset = 0
for d in (v, a):
if d:
changed[0, offset:offset + d] = 1000
offset += d + 1
self.assertTrue(torch.equal(head(states, local)[0], head(states, changed)[0]))
with self.assertRaisesRegex(ValueError, "requires aligned"):
head(states)
config = {"hidden_size": 8, "num_input_layers": 4, "layer_fusion": "concat-mlp",
"local_visual_dim": v, "local_audio_dim": a, "local_projection_dim": projection,
"transition_auxiliary": True, "dropout": 0}
restored = TurnPredictionHead(**checkpoint_head_config({"head_config": config}))
restored.load_state_dict(head.state_dict())
self.assertTrue(torch.equal(restored(states, local), head(states, local)))
def test_legacy_head_parameter_keys_and_values(self):
torch.manual_seed(42)
old = torch.nn.Sequential(torch.nn.LayerNorm(8), torch.nn.Linear(8, 4), torch.nn.GELU(),
torch.nn.Dropout(.1), torch.nn.Linear(4, 30))
torch.manual_seed(42)
head = TurnPredictionHead(hidden_size=8)
self.assertEqual(set(head.state_dict()), {f"net.{key}" for key in old.state_dict()})
for key, value in old.state_dict().items():
self.assertTrue(torch.equal(value, head.state_dict()[f"net.{key}"]))
def test_sparse_selection_keeps_local_rows(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for folder in ("features", "local", "targets"):
(root / folder).mkdir()
reference, side, targets = fixture()
torch.save(reference, root / "features/Session_4.pt")
torch.save(side, root / "local/Session_4.pt")
np.save(root / "targets/Session_4.npy", targets)
data = _load_examples(root / "features", root / "targets", ["Session_4"], "open-stream-replay",
"easycom", {"Session_4": {1, 4}}, root / "local", "audio")
2026-09-02 18:48:26 +08:00
self.assertTrue(torch.equal(data[-2][:, :5], side["audio"][[1, 4]]))
self.assertIsNone(data[-1])
self.assertTrue(torch.equal(data[0], reference["states"][[1, 4]]))
self.assertTrue(torch.equal(data[1][:, 0], torch.from_numpy(targets[[2, 5]])))
def test_source_inventory_rejects_different_clip_order(self):
source = {"path": "/old/00-00-000.mp4", "source_frames": 1200, "dense_start": 0., "duration": 60.}
media = SimpleNamespace(clips=[object()], metadata=lambda: {"source_clips": [source]})
validate_source_timeline({"direct_media": {"source_clips": [source]}}, media)
bad = {**source, "path": "/old/01-00-000.mp4"}
with self.assertRaisesRegex(ValueError, "timeline differs"):
validate_source_timeline({"direct_media": {"source_clips": [bad]}}, media)
def test_config_digest_ignores_only_checkpoint_location(self):
a = {"_name_or_path": "/old/model", "audio_config": {"_name_or_path": "/old/model", "output_dim": 8}}
b = {"_name_or_path": "/new/model", "audio_config": {"_name_or_path": "/new/model", "output_dim": 8}}
self.assertEqual(model_config_digest(a), model_config_digest(b))
b["audio_config"]["output_dim"] = 16
self.assertNotEqual(model_config_digest(a), model_config_digest(b))
def test_encoder_image_pool_splits_people_before_mean(self):
encoder = FrozenQwenLocalEncoder.__new__(FrozenQwenLocalEncoder)
encoder.visual_dim, encoder.device, encoder.image_batch_size, encoder.max_pixels = 2, torch.device("cpu"), 8, 50176
encoder.processor = SimpleNamespace(image_processor=lambda **kwargs: {
"pixel_values": torch.ones(12, 2), "image_grid_thw": torch.tensor([[1, 2, 2], [1, 2, 4]])})
encoder.model = SimpleNamespace(visual=SimpleNamespace(dtype=torch.float32),
config=SimpleNamespace(vision_config=SimpleNamespace(spatial_merge_size=2)),
get_image_features=lambda *_: torch.tensor([[1., 1.], [3., 3.], [5., 5.]]))
actual = encoder.images([object(), object()])
self.assertTrue(torch.equal(actual, torch.tensor([[1., 1.], [4., 4.]])))
2026-08-27 15:24:59 +08:00
# A future API returning per-image tensors must not silently change
# the pooling rule or crash with an uninformative AttributeError.
encoder.model.get_image_features = lambda *_: (torch.ones(1, 2), torch.ones(2, 2))
with self.assertRaisesRegex(RuntimeError, "Unexpected image encoder output"):
encoder.images([object(), object()])
def test_extraction_saves_runtime_version_and_rejects_mixed_versions(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for folder in ("features", "targets", "local"):
(root / folder).mkdir()
reference, side, labels = fixture()
torch.save(reference, root / "features/Session_4.pt")
np.save(root / "targets/Session_4.npy", labels)
media = SimpleNamespace(clips=[], metadata=lambda: {})
argv = ["extract_turn_local_features.py", "--data-root", str(root),
"--feature-root", str(root / "features"), "--target-root", str(root / "targets"),
"--output-root", str(root / "local"), "--samples", "4", "--overwrite"]
contracts = []
for version in TESTED_TRANSFORMERS_VERSIONS:
fake_encoder = SimpleNamespace(transformers_version=version, visual_dim=6, audio_dim=5,
model_config_sha256="same-checkpoint")
with patch.object(sys, "argv", argv), \
patch.dict(sys.modules, {"transformers": SimpleNamespace(__version__=version)}), \
patch("extract_turn_local_features.build_media_and_boxes", return_value=(media, [])), \
patch("extract_turn_local_features.FrozenQwenLocalEncoder", return_value=fake_encoder), \
patch("extract_turn_local_features.extract_local", return_value=copy.deepcopy(side)):
extract_main()
output = root / "local/Session_4.pt"
_, contract = load_local_features(output, reference, sample_id="Session_4",
dataset="easycom", modalities="av")
self.assertEqual(contract["config"]["transformers"], version)
contracts.append(contract)
with self.assertRaisesRegex(ValueError, "contract differs"):
load_local_features(output, reference, sample_id="Session_4", dataset="easycom",
modalities="av", expected_contract=contracts[0])
def test_unsupported_version_fails_before_media_and_preview_still_works(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
reference, _, labels = fixture()
torch.save(reference, root / "Session_4.pt")
np.save(root / "Session_4.npy", labels)
argv = ["extract_turn_local_features.py", "--data-root", str(root),
"--feature-root", str(root), "--target-root", str(root),
"--output-root", str(root / "preview"), "--samples", "4"]
with patch.object(sys, "argv", argv), \
patch.dict(sys.modules, {"transformers": SimpleNamespace(__version__="5.0.0")}), \
patch("extract_turn_local_features.build_media_and_boxes") as build:
with self.assertRaisesRegex(RuntimeError, "found 5.0.0"):
extract_main()
build.assert_not_called()
media = SimpleNamespace(clips=[], metadata=lambda: {})
with patch.object(sys, "argv", argv + ["--preview-only"]), \
patch.dict(sys.modules, {"transformers": SimpleNamespace(__version__="5.0.0")}), \
patch("extract_turn_local_features.build_media_and_boxes", return_value=(media, [])), \
patch("extract_turn_local_features.preview_boxes") as preview, \
patch("extract_turn_local_features.FrozenQwenLocalEncoder") as encoder:
extract_main()
preview.assert_called_once()
encoder.assert_not_called()
def test_extraction_never_encodes_future_audio_and_saves_ragged_people(self):
class Media:
clips = [SimpleNamespace(crop=None, dense_start=0.)]
def _source_frame_for_time(self, t):
return 0, round(t * 20), t
def frame_at_or_before(self, t):
return Image.new("RGB", (16, 16))
def audio_slice(self, start, end):
return np.arange(round(start * 16000), round(end * 16000), dtype=np.float32)
class Encoder:
visual_dim = audio_dim = 2
def __init__(self):
self.ends = []
def images(self, crops):
return torch.ones(len(crops), 2)
def audio(self, waveform):
self.ends.append(float(waveform[-1]))
return torch.tensor([float(waveform[0]), float(waveform[-1])])
encoder = Encoder()
args = SimpleNamespace(video_fps=2., visual_window_seconds=1., audio_window_seconds=1.,
box_coordinate_space="camera", crop_expansion=1., no_progress=True)
annotations = [{0: [{"id": "1", "xyxy": [0., 0., 8., 8.]}]}]
result = extract_local(Media(), annotations, encoder, torch.arange(1, 11, dtype=torch.float64) / 5, args)
self.assertEqual(encoder.ends, [3200 * (i + 1) - 1 for i in range(10)])
self.assertEqual(result["persons"]["frame_offsets"].tolist(), [0, 1, 1, 1, 1])
self.assertTrue(result["visual_valid"][:5].all())
self.assertFalse(result["visual_valid"][5:].any())
self.assertTrue(result["audio_valid"].all())
2026-08-27 15:24:59 +08:00
@unittest.skipUnless(importlib.util.find_spec("transformers"), "optional: install a tested Transformers version")
class TinyQwenEncoderTests(unittest.TestCase):
2026-08-27 15:24:59 +08:00
def setUp(self):
import transformers
2026-08-27 15:24:59 +08:00
if transformers.__version__ not in TESTED_TRANSFORMERS_VERSIONS:
self.skipTest(f"Encoder API tests require one of {TESTED_TRANSFORMERS_VERSIONS}")
torch.set_num_threads(1)
def tiny_model_and_processor(self):
from transformers import (
Qwen2_5OmniThinkerConfig, Qwen2_5OmniThinkerForConditionalGeneration,
Qwen2VLImageProcessor, WhisperFeatureExtractor,
)
config = Qwen2_5OmniThinkerConfig(
audio_config=dict(d_model=16, encoder_layers=1, encoder_attention_heads=2,
encoder_ffn_dim=32, output_dim=8),
vision_config=dict(depth=1, hidden_size=16, intermediate_size=32, num_heads=2,
out_hidden_size=8, fullatt_block_indexes=[0]),
text_config=dict(vocab_size=32, hidden_size=8, intermediate_size=16,
num_hidden_layers=1, num_attention_heads=2, num_key_value_heads=2),
)
2026-08-27 15:24:59 +08:00
model = Qwen2_5OmniThinkerForConditionalGeneration(config).eval().requires_grad_(False)
processor = SimpleNamespace(image_processor=Qwen2VLImageProcessor(),
feature_extractor=WhisperFeatureExtractor(feature_size=128))
return model, processor
def test_real_encoder_apis_without_downloading_weights(self):
encoder = FrozenQwenLocalEncoder.__new__(FrozenQwenLocalEncoder)
2026-08-27 15:24:59 +08:00
encoder.model, encoder.processor = self.tiny_model_and_processor()
encoder.model.model = None
encoder.model.lm_head = None
encoder.visual_dim = encoder.audio_dim = 8
encoder.device, encoder.max_pixels, encoder.image_batch_size = torch.device("cpu"), 224 * 224, 8
crops = [Image.new("RGB", (56, 84), "red"), Image.new("RGB", (112, 56), "blue")]
batch = encoder.images(crops)
single = encoder.images(crops[:1])
self.assertEqual(batch.shape, (2, 8))
self.assertTrue(torch.allclose(batch[0], single[0], atol=1e-5))
for n in (3200, 6400, 16000):
audio = encoder.audio(np.sin(np.arange(n, dtype=np.float32) / 10))
self.assertEqual(audio.shape, (8,))
self.assertTrue(torch.isfinite(audio).all())
self.assertFalse(audio.requires_grad)
2026-08-27 15:24:59 +08:00
@unittest.skipUnless(importlib.util.find_spec("accelerate"), "optional: install accelerate for checkpoint loading")
def test_constructor_loads_local_checkpoint_and_preserves_encoder_weights(self):
import transformers
model, processor = self.tiny_model_and_processor()
expected = {key: value.clone() for key, value in model.state_dict().items()
if key.startswith(("visual.", "audio_tower."))}
with tempfile.TemporaryDirectory() as directory:
model.save_pretrained(directory)
args = SimpleNamespace(model_id=directory, device="cpu", dtype="float32",
flash_attention_2=False, max_pixels=224 * 224, image_batch_size=8)
with patch.object(transformers.Qwen2_5OmniProcessor, "from_pretrained", return_value=processor) as load:
encoder = FrozenQwenLocalEncoder(args)
load.assert_called_once_with(directory, use_fast=False)
self.assertEqual(encoder.transformers_version, transformers.__version__)
self.assertIsNone(encoder.model.model)
self.assertIsNone(encoder.model.lm_head)
self.assertFalse(encoder.model.training)
self.assertFalse(any(p.requires_grad for p in encoder.model.parameters()))
for key, value in encoder.model.state_dict().items():
self.assertTrue(torch.equal(value, expected[key]), key)
self.assertEqual(encoder.images([Image.new("RGB", (56, 84), "red")]).shape, (1, 8))
self.assertEqual(encoder.audio(np.zeros(3200, dtype=np.float32)).shape, (8,))
def test_constructor_rejects_incomplete_encoder_weights(self):
import transformers
args = SimpleNamespace(model_id="unused", device="cpu", dtype="float32", flash_attention_2=False)
for info in ({"missing_keys": ["visual.patch_embed.proj.weight"]},
{"missing_keys": ["audio_tower.conv1.weight"]},
{"mismatched_keys": [("visual.weight", (2,), (3,))]},
{"error_msgs": ["broken checkpoint"]}):
with self.subTest(info=info), \
patch.object(transformers.Qwen2_5OmniThinkerForConditionalGeneration,
"from_pretrained", return_value=(None, info)), \
self.assertRaisesRegex(RuntimeError, "incompletely loaded weights"):
FrozenQwenLocalEncoder(args)
class RoundTripTests(unittest.TestCase):
def test_train_infer_easycom_and_ego4d(self):
repo = Path(__file__).resolve().parent
env = dict(os.environ, OMP_NUM_THREADS="1", MKL_NUM_THREADS="1", OPENBLAS_NUM_THREADS="1")
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for folder in ("features", "local", "highrate", "targets"):
(root / folder).mkdir()
train_uid = "00000000-0000-0000-0000-000000000004"
test_uid = "00000000-0000-0000-0000-000000000001"
manifest = root / "ego4d.tsv"
manifest.write_text(f"split\tuid\ntrain\t{train_uid}\ntest\t{test_uid}\n")
for name in ("Session_1", "Session_4", "Session_5", train_uid, test_uid):
dataset = "easycom" if name.startswith("Session") else "ego4d"
reference, side, targets = fixture(name=name, dataset=dataset)
torch.save(reference, root / "features" / f"{name}.pt")
torch.save(side, root / "local" / f"{name}.pt")
_, highrate_side, _ = highrate_fixture(
name=name,
dataset=dataset,
)
torch.save(highrate_side, root / "highrate" / f"{name}.pt")
np.save(root / "targets" / f"{name}.npy", targets)
common = ["--feature-root", str(root / "features"), "--target-root", str(root / "targets"),
"--device", "cpu", "--no-progress", "--expected-mode", "open-stream-replay"]
2026-09-06 21:48:32 +08:00
for mode in (
"baseline",
"audio",
"visual",
"av",
"track-gru",
"person-adapter-v2",
"egospeak-person-gru",
2026-09-06 21:48:32 +08:00
"frame-set",
"f5-screen",
"f6-screen",
"traditional-audio",
"highrate-person-av",
2026-09-06 21:48:32 +08:00
"ego4d",
):
checkpoint = root / f"{mode}.pth"
train = [sys.executable, str(repo / "train_turn_head.py"), *common,
"--epochs", "1", "--warmup-epochs", "0", "--batch-size", "4",
"--layer-fusion", "concat-mlp", "--output", str(checkpoint)]
infer = [sys.executable, str(repo / "infer_turn_head.py"), *common,
"--checkpoint", str(checkpoint), "--output-dir", str(root / f"result_{mode}")]
if mode == "ego4d":
train += ["--dataset", "ego4d", "--manifest", str(manifest)]
infer += ["--dataset", "ego4d", "--manifest", str(manifest)]
else:
train += ["--sessions", "4", "5"]
infer += ["--sessions", "1"]
if mode == "visual":
train += ["--dev-sessions", "5", "--local-projection-dim", "0"]
if mode == "av":
train += ["--transition-objective", "r1-supervised"]
2026-09-06 21:48:32 +08:00
person_mode = {
"track-gru": "track-gru-residual",
"person-adapter-v2": "causal-person-adapter-v2",
"egospeak-person-gru": (
"causal-person-adapter-egospeak-gru"
),
2026-09-06 21:48:32 +08:00
"frame-set": "frame-set-attention",
"f5-screen": "audio-person-frame-set",
"f6-screen": "gated-person-state-adapter",
"highrate-person-av": "highrate-person-av-attention",
2026-09-06 21:48:32 +08:00
}.get(mode)
if person_mode is not None:
2026-09-02 18:48:26 +08:00
train += [
2026-09-06 21:48:32 +08:00
"--local-fusion", person_mode,
2026-09-02 18:48:26 +08:00
"--person-attention-dim", "4",
"--person-attention-heads", "2",
]
if mode in (
"track-gru",
"person-adapter-v2",
"egospeak-person-gru",
):
train += [
"--person-history-frames", "3",
"--person-track-max-gap-seconds", "1",
"--initialize-pooled-checkpoint", str(root / "av.pth"),
"--transition-objective", "r1-supervised",
]
if mode == "highrate-person-av":
train += [
"--initialize-pooled-checkpoint",
str(root / "traditional-audio.pth"),
]
if mode != "baseline":
2026-09-06 21:48:32 +08:00
modality = (
"av"
if mode in (
"ego4d", "track-gru", "person-adapter-v2",
"egospeak-person-gru",
"frame-set", "f5-screen", "f6-screen",
"highrate-person-av",
)
else "audio"
if mode == "traditional-audio"
2026-09-06 21:48:32 +08:00
else mode
)
local_root = root / (
"highrate"
if mode in ("traditional-audio", "highrate-person-av")
else "local"
)
train += ["--local-feature-root", str(local_root), "--local-modalities", modality]
infer += ["--local-feature-root", str(local_root)]
for command in (train, infer):
completed = subprocess.run(command, cwd=repo, env=env, capture_output=True, text=True, timeout=60)
self.assertEqual(completed.returncode, 0, f"{mode}: {completed.stdout}\n{completed.stderr}")
metrics = json.loads((root / f"result_{mode}/metrics.json").read_text())
self.assertEqual(metrics["checkpoint_epoch"], 1)
self.assertEqual(bool(metrics["local_contract"]), mode != "baseline")
if mode == "av":
self.assertTrue(metrics["transition_auxiliary"])
2026-09-06 21:48:32 +08:00
if person_mode is not None:
self.assertEqual(metrics["local_fusion"], person_mode)
self.assertEqual(
metrics["local_fusion_scope"],
LOCAL_FUSION_SCOPES[person_mode],
)
# The reporting command uses the same metric keys as inference.
completed = subprocess.run(
[sys.executable, str(repo / "summarize_local_baselines.py"), "--root", str(root)],
cwd=repo, capture_output=True, text=True, timeout=20,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn("Mean ± sample standard deviation", completed.stdout)
if __name__ == "__main__":
unittest.main()