From 4ab491c14e6d9968eac10c50c44e964cd3716ffe Mon Sep 17 00:00:00 2001 From: Kunal Pareek Date: Tue, 8 Sep 2026 16:06:56 +0530 Subject: [PATCH] fix(vid_vec_rep): handle transformers >=5.0 wrapping get_image_features in BaseModelOutputWithPooling CLIPModel.get_image_features() returned a plain torch.Tensor in transformers <5.0. Starting in v5 (huggingface/transformers PR #42564) it returns a BaseModelOutputWithPooling wrapper instead, whose .pooler_output is the documented equivalent of the old return value. feluda-vid-vec-rep-clip only declares transformers>=4.51.1 (no upper bound), so any environment that resolves the newest available transformers at install time silently breaks with: mean() received an invalid combination of arguments - got (BaseModelOutputWithPooling, dim=int), but expected ... Not specific to this project -- other libraries hit the identical break upgrading to transformers 5 (e.g. Lightning-AI/torchmetrics#3407, huggingface/lerobot#3418). Fix checks the actual return type rather than the installed version, so it's correct on both transformers 4.x and 5.x without needing an upper-bound pin. Added unit tests covering both return shapes via mocking, so this doesn't depend on which transformers version CI happens to install. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0163s8jEcoELUcWm1xd5w9Sb --- operators/vid_vec_rep/pyproject.toml | 2 +- operators/vid_vec_rep/test.py | 27 +++++++++++++++++++++++++++ operators/vid_vec_rep/vid_vec_rep.py | 7 +++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/operators/vid_vec_rep/pyproject.toml b/operators/vid_vec_rep/pyproject.toml index 1a5494d0..89f691ae 100644 --- a/operators/vid_vec_rep/pyproject.toml +++ b/operators/vid_vec_rep/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "feluda-vid-vec-rep-clip" -version = "0.1.5" +version = "0.1.6" requires-python = ">=3.10" dependencies = [ "feluda[video]", diff --git a/operators/vid_vec_rep/test.py b/operators/vid_vec_rep/test.py index 9a1c1b81..37466df5 100644 --- a/operators/vid_vec_rep/test.py +++ b/operators/vid_vec_rep/test.py @@ -1,4 +1,9 @@ +import types +from unittest.mock import patch + import pytest +import torch +from PIL import Image from feluda.factory import VideoFactory @@ -45,3 +50,25 @@ def test_run_invalid_file_object(operator: VidVecRep): def test_run_file_not_found(operator: VidVecRep): with pytest.raises(FileNotFoundError): operator.run({"path": "fake.mp4"}) + + +def test_extract_features_accepts_plain_tensor(operator: VidVecRep): + # transformers <5.0 behavior: get_image_features() returns the tensor directly. + plain_tensor = torch.randn(1, 512) + image = Image.new("RGB", (32, 32)) + with patch.object(operator.model, "get_image_features", return_value=plain_tensor): + features = operator.extract_features([image]) + assert features is plain_tensor + + +def test_extract_features_unwraps_transformers_v5_output(operator: VidVecRep): + # transformers >=5.0 wraps get_image_features()'s return in a BaseModelOutputWithPooling + # instead of returning a plain tensor (huggingface/transformers PR #42564). Simulated + # via a stand-in object rather than depending on which transformers version is actually + # installed in CI. + wrapped_output = types.SimpleNamespace(pooler_output=torch.randn(1, 512)) + image = Image.new("RGB", (32, 32)) + with patch.object(operator.model, "get_image_features", return_value=wrapped_output): + features = operator.extract_features([image]) + assert isinstance(features, torch.Tensor) + assert features is wrapped_output.pooler_output diff --git a/operators/vid_vec_rep/vid_vec_rep.py b/operators/vid_vec_rep/vid_vec_rep.py index 8a935ce9..2bb933c4 100644 --- a/operators/vid_vec_rep/vid_vec_rep.py +++ b/operators/vid_vec_rep/vid_vec_rep.py @@ -142,6 +142,13 @@ def extract_features(self, images: list) -> torch.Tensor: inputs = {k: v.to(self.device) for k, v in inputs.items()} # move to device with torch.no_grad(): features = self.model.get_image_features(**inputs) + # transformers >=5.0 wraps get_image_features()'s return in a + # BaseModelOutputWithPooling instead of returning the tensor directly + # (huggingface/transformers PR #42564); .pooler_output is the documented + # equivalent of the old plain-tensor return. Checking the type instead of the + # installed version keeps this correct on both 4.x and 5.x. + if not isinstance(features, torch.Tensor): + features = features.pooler_output return features def gendata(self) -> Generator[dict, None, None]: