Files
icassp2027-experiment/test_local_turn_features.py
2026-08-28 00:46:05 +08:00

604 lines
36 KiB
Python

"""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
from unittest.mock import patch
import numpy as np
import torch
from PIL import Image, ImageDraw
from easycom_head import TurnPredictionHead, checkpoint_head_config
from extract_turn_local_features import (
FrozenQwenLocalEncoder, ego4d_clip_boxes, extract_local,
validate_source_timeline,
model_config_digest,
box_coordinate_offset, frame_crops, parse_args, preview_boxes, preview_ticks, preview_time_bounds,
TESTED_TRANSFORMERS_VERSIONS, validate_local_transformers_version,
main as extract_main,
)
from local_turn_features import (
LOCAL_FORMAT, causal_frame_ticks, crop_box, easycom_boxes, load_local_features,
local_head_dimensions, 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
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
class LocalTests(unittest.TestCase):
def setUp(self):
torch.set_num_threads(1)
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_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.]}]})
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())
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})
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")
self.assertTrue(torch.equal(data[-1][:, :5], side["audio"][[1, 4]]))
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.]])))
# 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())
@unittest.skipUnless(importlib.util.find_spec("transformers"), "optional: install a tested Transformers version")
class TinyQwenEncoderTests(unittest.TestCase):
def setUp(self):
import transformers
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),
)
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)
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)
@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", "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")
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"]
for mode in ("baseline", "audio", "visual", "av", "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"]
if mode != "baseline":
train += ["--local-feature-root", str(root / "local"), "--local-modalities", "av" if mode == "ego4d" else mode]
infer += ["--local-feature-root", str(root / "local")]
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"])
# 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()