first commit
Some checks failed
Self-hosted runner (nightly-past-ci-caller) / Get number (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.11 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.10 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.9 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.8 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.7 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.6 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.5 (push) Has been cancelled
Self-hosted runner (benchmark) / Benchmark (aws-g5-4xlarge-cache) (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Build documentation / build_other_lang (push) Has been cancelled
CodeQL Security Analysis / CodeQL Analysis (push) Has been cancelled
New model PR merged notification / Notify new model (push) Has been cancelled
PR CI / pr-ci (push) Has been cancelled
Slow tests on important models (on Push - A10) / Get all modified files (push) Has been cancelled
Secret Leaks / trufflehog (push) Has been cancelled
Update Transformers metadata / build_and_package (push) Has been cancelled
Slow tests on important models (on Push - A10) / Model CI (push) Has been cancelled
Check Tiny Models / Check tiny models (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Model CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Pipeline CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Example CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / DeepSpeed CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Trainer/FSDP CI (push) Has been cancelled
Nvidia CI - Flash Attn / Setup (push) Has been cancelled
Nvidia CI - Flash Attn / Model CI (push) Has been cancelled
Nvidia CI / Setup (push) Has been cancelled
Nvidia CI / Model CI (push) Has been cancelled
Nvidia CI / Torch pipeline CI (push) Has been cancelled
Nvidia CI / Example CI (push) Has been cancelled
Nvidia CI / Trainer/FSDP CI (push) Has been cancelled
Nvidia CI / DeepSpeed CI (push) Has been cancelled
Nvidia CI / Quantization CI (push) Has been cancelled
Nvidia CI / Kernels CI (push) Has been cancelled
Doctests / Setup (push) Has been cancelled
Doctests / Call doctest jobs (push) Has been cancelled
Doctests / Send results to webhook (push) Has been cancelled
Extras Smoke Test / Get supported Python versions (push) Has been cancelled
Extras Smoke Test / Test extras on Python ${{ matrix.python-version }} (push) Has been cancelled
Extras Smoke Test / Check Slack token availability (push) Has been cancelled
Extras Smoke Test / Notify failures to Slack (push) Has been cancelled
Self-hosted runner (AMD scheduled CI caller) / Trigger Scheduled AMD CI (push) Has been cancelled
Stale Bot / Close Stale Issues (push) Has been cancelled

This commit is contained in:
陈赣
2026-06-05 16:53:03 +08:00
commit 06f1fd69a6
6047 changed files with 1895387 additions and 0 deletions

View File

View File

@@ -0,0 +1,232 @@
# Copyright 2026 OpenBMB and the HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import unittest
import numpy as np
from transformers.image_utils import get_image_size
from transformers.testing_utils import require_torch, require_vision
from transformers.utils import is_torch_available, is_vision_available
from ...test_image_processing_common import ImageProcessingTestMixin, prepare_image_inputs
if is_torch_available():
import torch
if is_vision_available():
from PIL import Image
class MiniCPMV4_6ImageProcessingTester:
def __init__(
self,
parent,
batch_size=2,
num_channels=3,
min_resolution=64,
max_resolution=128,
do_resize=True,
do_rescale=True,
rescale_factor=1 / 255,
do_normalize=True,
image_mean=[0.5, 0.5, 0.5],
image_std=[0.5, 0.5, 0.5],
max_slice_nums=9,
scale_resolution=448,
patch_size=14,
slice_mode=True,
downsample_mode="16x",
):
self.parent = parent
self.batch_size = batch_size
self.num_channels = num_channels
self.min_resolution = min_resolution
self.max_resolution = max_resolution
self.do_resize = do_resize
self.do_rescale = do_rescale
self.rescale_factor = rescale_factor
self.do_normalize = do_normalize
self.image_mean = image_mean
self.image_std = image_std
self.max_slice_nums = max_slice_nums
self.scale_resolution = scale_resolution
self.patch_size = patch_size
self.slice_mode = slice_mode
self.downsample_mode = downsample_mode
def prepare_image_processor_dict(self):
return {
"do_resize": self.do_resize,
"do_rescale": self.do_rescale,
"rescale_factor": self.rescale_factor,
"do_normalize": self.do_normalize,
"image_mean": self.image_mean,
"image_std": self.image_std,
"max_slice_nums": self.max_slice_nums,
"scale_resolution": self.scale_resolution,
"patch_size": self.patch_size,
"slice_mode": self.slice_mode,
"downsample_mode": self.downsample_mode,
}
def expected_output_image_shape(self, image_inputs):
"""Return the expected NaViT-packed shape [C, P, total_L] for pixel_values[0]."""
total_L = 0
for image in image_inputs:
if isinstance(image, Image.Image):
height, width = image.height, image.width
else:
height, width = get_image_size(image)
aspect_ratio = width / height
new_height = int(self.scale_resolution / math.sqrt(aspect_ratio))
new_width = int(new_height * aspect_ratio)
divisor = self.patch_size * 4
best_height = max(round(new_height / divisor) * divisor, divisor)
best_width = max(round(new_width / divisor) * divisor, divisor)
total_L += best_height * best_width // self.patch_size
return [self.num_channels, self.patch_size, total_L]
def prepare_image_inputs(self, equal_resolution=False, numpify=False, torchify=False):
return prepare_image_inputs(
batch_size=self.batch_size,
num_channels=self.num_channels,
min_resolution=self.min_resolution,
max_resolution=self.max_resolution,
equal_resolution=equal_resolution,
numpify=numpify,
torchify=torchify,
)
@require_torch
@require_vision
class MiniCPMV4_6ImageProcessingTest(ImageProcessingTestMixin, unittest.TestCase):
def setUp(self):
super().setUp()
self.image_processor_tester = MiniCPMV4_6ImageProcessingTester(self)
@property
def image_processor_dict(self):
return self.image_processor_tester.prepare_image_processor_dict()
def test_call_pil(self):
for image_processing_class in self.image_processing_classes.values():
image_processing = image_processing_class(**self.image_processor_dict)
image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False)
for image in image_inputs:
self.assertIsInstance(image, Image.Image)
# Test not batched input
encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
expected_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))
# Test batched
encoded_images = image_processing(image_inputs, return_tensors="pt").pixel_values
expected_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))
def test_call_numpy(self):
for image_processing_class in self.image_processing_classes.values():
image_processing = image_processing_class(**self.image_processor_dict)
image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, numpify=True)
for image in image_inputs:
self.assertIsInstance(image, np.ndarray)
# Test not batched input
encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
expected_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))
# Test batched
encoded_images = image_processing(image_inputs, return_tensors="pt").pixel_values
expected_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))
def test_call_pytorch(self):
for image_processing_class in self.image_processing_classes.values():
image_processing = image_processing_class(**self.image_processor_dict)
image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, torchify=True)
for image in image_inputs:
self.assertIsInstance(image, torch.Tensor)
# Test not batched input
encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
expected_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))
# Test batched
encoded_images = image_processing(image_inputs, return_tensors="pt").pixel_values
expected_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))
@unittest.skip("NaViT expected_output_image_shape cannot infer channel dim for 4-channel images")
def test_call_numpy_4_channels(self):
pass
def test_image_processor_properties(self):
for image_processing_class in self.image_processing_classes.values():
image_processing = image_processing_class(**self.image_processor_dict)
self.assertTrue(hasattr(image_processing, "do_rescale"))
self.assertTrue(hasattr(image_processing, "rescale_factor"))
self.assertTrue(hasattr(image_processing, "do_normalize"))
self.assertTrue(hasattr(image_processing, "image_mean"))
self.assertTrue(hasattr(image_processing, "image_std"))
self.assertTrue(hasattr(image_processing, "max_slice_nums"))
self.assertTrue(hasattr(image_processing, "scale_resolution"))
self.assertTrue(hasattr(image_processing, "patch_size"))
self.assertTrue(hasattr(image_processing, "slice_mode"))
self.assertTrue(hasattr(image_processing, "downsample_mode"))
def test_call_returns_expected_keys(self):
for image_processing_class in self.image_processing_classes.values():
image_processor = image_processing_class(**self.image_processor_dict)
images = self.image_processor_tester.prepare_image_inputs(torchify=True)
result = image_processor(images, return_tensors="pt")
self.assertIn("pixel_values", result)
self.assertIn("target_sizes", result)
self.assertIn("num_patches_per_image", result)
self.assertIn("grids", result)
def test_pixel_values_are_tensors(self):
for image_processing_class in self.image_processing_classes.values():
image_processor = image_processing_class(**self.image_processor_dict)
images = self.image_processor_tester.prepare_image_inputs(torchify=True)
result = image_processor(images, return_tensors="pt")
for pv in result["pixel_values"]:
self.assertIsInstance(pv, torch.Tensor)
def test_downsample_modes(self):
for image_processing_class in self.image_processing_classes.values():
images = self.image_processor_tester.prepare_image_inputs(equal_resolution=True, torchify=True)
ip_16x = image_processing_class(**{**self.image_processor_dict, "downsample_mode": "16x"})
result_16x = ip_16x(images, return_tensors="pt")
ip_4x = image_processing_class(**{**self.image_processor_dict, "downsample_mode": "4x"})
result_4x = ip_4x(images, return_tensors="pt")
for ts_4x, ts_16x in zip(result_4x["target_sizes"], result_16x["target_sizes"]):
h4, w4 = (ts_4x[0], ts_4x[1]) if not hasattr(ts_4x[0], "item") else (ts_4x[0].item(), ts_4x[1].item())
h16, w16 = (
(ts_16x[0], ts_16x[1]) if not hasattr(ts_16x[0], "item") else (ts_16x[0].item(), ts_16x[1].item())
)
self.assertGreaterEqual(h4 * w4, h16 * w16)

View File

@@ -0,0 +1,553 @@
# Copyright 2026 OpenBMB and the HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Testing suite for the PyTorch MiniCPM-V 4.6 model."""
import unittest
import pytest
from transformers import (
AutoProcessor,
MiniCPMV4_6Config,
is_torch_available,
)
from transformers.models.minicpmv4_6.configuration_minicpmv4_6 import MiniCPMV4_6VisionConfig
from transformers.testing_utils import (
Expectations,
cleanup,
require_torch,
require_torch_accelerator,
slow,
torch_device,
)
from ...test_modeling_common import floats_tensor
from ...test_processing_common import url_to_local_path
from ...vlm_tester import VLMModelTest, VLMModelTester
if is_torch_available():
import torch
from transformers import MiniCPMV4_6ForConditionalGeneration, MiniCPMV4_6Model
from transformers.models.qwen3_5.configuration_qwen3_5 import Qwen3_5TextConfig
class MiniCPMV4_6VisionText2TextModelTester(VLMModelTester):
base_model_class = MiniCPMV4_6Model if is_torch_available() else None
config_class = MiniCPMV4_6Config
text_config_class = Qwen3_5TextConfig if is_torch_available() else None
vision_config_class = MiniCPMV4_6VisionConfig
conditional_generation_class = MiniCPMV4_6ForConditionalGeneration if is_torch_available() else None
def __init__(self, parent, **kwargs):
kwargs.setdefault("batch_size", 2)
kwargs.setdefault("image_token_id", 100)
# patch_size=8, image_size=32 → 4×4 grid → vit_merger [2×2] → merger [1×1] = 1 token
kwargs.setdefault("image_size", 32)
kwargs.setdefault("patch_size", 8)
kwargs.setdefault("num_image_tokens", 1)
kwargs.setdefault("vocab_size", 256)
kwargs.setdefault("hidden_size", 32)
kwargs.setdefault("intermediate_size", 37)
kwargs.setdefault("num_hidden_layers", 2)
kwargs.setdefault("num_attention_heads", 4)
kwargs.setdefault("num_key_value_heads", 2)
kwargs.setdefault("head_dim", 8)
kwargs.setdefault("hidden_act", "silu")
kwargs.setdefault("max_position_embeddings", 512)
kwargs.setdefault("rope_parameters", {"rope_type": "default"})
kwargs.setdefault("tie_word_embeddings", True)
kwargs.setdefault("bos_token_id", 0)
kwargs.setdefault("eos_token_id", 1)
kwargs.setdefault("pad_token_id", 2)
# Qwen3.5 hybrid attention
kwargs.setdefault("layer_types", ["full_attention", "linear_attention"])
kwargs.setdefault("linear_conv_kernel_dim", 2)
kwargs.setdefault("linear_key_head_dim", 16)
kwargs.setdefault("linear_value_head_dim", 16)
kwargs.setdefault("linear_num_key_heads", 4)
kwargs.setdefault("linear_num_value_heads", 8)
# Vision config overrides
kwargs.setdefault("vision_hidden_act", "gelu_pytorch_tanh")
kwargs.setdefault("vision_intermediate_size", 128)
# MiniCPM-V 4.6 specific
kwargs.setdefault("insert_layer_id", 0)
super().__init__(parent, **kwargs)
def _navit_pixel_values(self, batch_size):
"""Build NaViT-packed pixel_values: (1, C, patch_size, total_L)."""
C = self.num_channels
P = self.patch_size
h_patches = self.image_size // self.patch_size
w_patches = self.image_size // self.patch_size
total_L = batch_size * h_patches * w_patches * P
return floats_tensor([1, C, P, total_L])
def _target_sizes(self, batch_size):
h_patches = self.image_size // self.patch_size
w_patches = self.image_size // self.patch_size
return torch.tensor([[h_patches, w_patches]] * batch_size, dtype=torch.int32)
def create_pixel_values(self):
return self._navit_pixel_values(self.batch_size)
def get_additional_inputs(self, config, input_ids, pixel_values):
return {"target_sizes": self._target_sizes(self.batch_size)}
def get_config(self):
text_config = {
"model_type": "qwen3_5_text",
"vocab_size": self.vocab_size,
"hidden_size": self.hidden_size,
"head_dim": self.head_dim,
"intermediate_size": self.intermediate_size,
"num_hidden_layers": self.num_hidden_layers,
"num_attention_heads": self.num_attention_heads,
"num_key_value_heads": self.num_key_value_heads,
"hidden_act": "silu",
"max_position_embeddings": self.max_position_embeddings,
"rope_theta": 10000,
"rope_parameters": self.rope_parameters,
"tie_word_embeddings": self.tie_word_embeddings,
"bos_token_id": self.bos_token_id,
"eos_token_id": self.eos_token_id,
"pad_token_id": self.pad_token_id,
"layer_types": self.layer_types,
"linear_conv_kernel_dim": self.linear_conv_kernel_dim,
"linear_key_head_dim": self.linear_key_head_dim,
"linear_value_head_dim": self.linear_value_head_dim,
"linear_num_key_heads": self.linear_num_key_heads,
"linear_num_value_heads": self.linear_num_value_heads,
}
vision_config = {
"hidden_size": self.hidden_size,
"num_hidden_layers": self.num_hidden_layers,
"num_attention_heads": self.num_attention_heads,
"intermediate_size": self.vision_intermediate_size,
"image_size": self.image_size,
"patch_size": self.patch_size,
"num_channels": self.num_channels,
"hidden_act": self.vision_hidden_act,
}
return MiniCPMV4_6Config(
text_config=text_config,
vision_config=vision_config,
image_token_id=self.image_token_id,
image_size=self.image_size,
drop_vision_last_layer=False,
insert_layer_id=self.insert_layer_id,
)
@require_torch
class MiniCPMV4_6ModelTest(VLMModelTest, unittest.TestCase):
model_tester_class = MiniCPMV4_6VisionText2TextModelTester
def prepare_config_and_inputs_for_generate(self, batch_size=2):
config, inputs_dict = super().prepare_config_and_inputs_for_generate(batch_size=batch_size)
inputs_dict["pixel_values"] = self.model_tester._navit_pixel_values(batch_size)
inputs_dict["target_sizes"] = self.model_tester._target_sizes(batch_size)
return config, inputs_dict
def _image_features_prepare_config_and_inputs(self):
config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
inputs_dict = {
key: value
for key, value in inputs_dict.items()
if ("pixel" in key or "image" in key or key == "target_sizes") and "video" not in key
}
return config, inputs_dict
def _video_features_prepare_config_and_inputs(self):
config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
return config, {
"pixel_values_videos": inputs_dict["pixel_values"],
"target_sizes_videos": inputs_dict["target_sizes"],
}
@unittest.skip(
"NaViT packing puts all images in a single tensor with dim-0 = 1; "
"the default test cannot correctly simulate image count mismatches"
)
def test_mismatching_num_image_tokens(self):
pass
@unittest.skip(reason="MiniCPM-V uses custom pixel_values format (list-of-list), skipping common input tests")
def test_inputs_embeds(self):
pass
@unittest.skip(reason="MiniCPM-V uses custom pixel_values format (list-of-list), skipping common input tests")
def test_inputs_embeds_matches_input_ids(self):
pass
@unittest.skip(reason="Compile not yet supported for MiniCPM-V models")
@pytest.mark.torch_compile_test
def test_sdpa_can_compile_dynamic(self):
pass
@unittest.skip("FlashAttention only supports fp16 and bf16 data type")
def test_flash_attn_2_fp32_ln(self):
pass
@unittest.skip("The Qwen3.5 hybrid cache format cannot be instantiated from dp/ddp data.")
def test_multi_gpu_data_parallel_forward(self):
pass
@unittest.skip(reason="MiniCPM-V 4.6 uses Qwen3.5 hybrid cache layers that are incompatible with QuantizedCache.")
def test_generate_with_quant_cache(self):
pass
@unittest.skip(reason="Conversion only for CausalLM loading from saved ConditionalLM")
def test_reverse_loading_mapping(self, check_keys_were_modified=True):
pass
@unittest.skip(
reason="NaViT packs all images into a single tensor (batch dim=1); "
"generic batch-splitting logic cannot separate individual samples"
)
def test_batching_equivalence(self):
pass
@unittest.skip(
reason="NaViT packs all images into a single tensor (batch dim=1); "
"generic batch-splitting logic cannot separate individual samples"
)
def test_model_forward_default_config_values(self):
pass
@unittest.skip(
reason="get_image_features uses a custom pipeline (vision_tower -> vit_merger -> merger) "
"that does not accept output_attentions/output_hidden_states kwargs"
)
def test_get_image_features_attentions(self):
pass
@unittest.skip(
reason="get_image_features uses a custom pipeline (vision_tower -> vit_merger -> merger) "
"that does not accept output_attentions/output_hidden_states kwargs"
)
def test_get_image_features_hidden_states(self):
pass
@unittest.skip(
reason="get_video_features uses a custom pipeline that does not accept "
"output_attentions/output_hidden_states kwargs"
)
def test_get_video_features_attentions(self):
pass
@unittest.skip(
reason="get_video_features uses a custom pipeline that does not accept "
"output_attentions/output_hidden_states kwargs"
)
def test_get_video_features_hidden_states(self):
pass
@unittest.skip(
"MiniCPM-V generate creates vision-aware embeddings via _build_vlm_inputs; "
"text-only get_input_embeddings bypass produces different outputs"
)
def test_generate_from_inputs_embeds(self):
pass
@unittest.skip(reason="Same as test_generate_from_inputs_embeds: vision-aware vs text-only embeddings mismatch")
def test_generate_from_inputs_embeds_with_static_cache(self):
pass
@unittest.skip(
"Manual left-padding in test does not adjust image_bound offsets, "
"causing vision features to be placed at wrong positions"
)
def test_left_padding_compatibility(self):
pass
@unittest.skip(reason="Batch splitting in compile test incompatible with list-of-list pixel_values")
@pytest.mark.torch_compile_test
def test_generate_compile_model_forward_fullgraph(self):
pass
@unittest.skip(reason="Batch splitting in compile test incompatible with list-of-list pixel_values")
@pytest.mark.torch_compile_test
def test_generate_compilation_all_outputs(self):
pass
@unittest.skip(reason="FA works on generate test, inference needs override to pass target sizes")
def test_flash_attn_2_inference_equivalence(self):
pass
@unittest.skip(reason="FA works on generate, inference needs override to pass target sizes")
def test_flash_attn_2_inference_equivalence_right_padding(self):
pass
def _get_conv_state_shape(self, batch_size: int, config):
num_v_heads = config.linear_num_value_heads
num_k_heads = config.linear_num_key_heads
head_k_dim = config.linear_key_head_dim
head_v_dim = config.linear_value_head_dim
intermediate_size = 2 * num_k_heads * head_k_dim + num_v_heads * head_v_dim
return (batch_size, intermediate_size, config.linear_conv_kernel_dim)
def _get_recurrent_state_shape(self, batch_size: int, config):
num_v_heads = config.linear_num_value_heads
head_k_dim = config.linear_key_head_dim
head_v_dim = config.linear_value_head_dim
return (batch_size, num_v_heads, head_k_dim, head_v_dim)
def test_attention_outputs(self):
"""Overwritten: Qwen3.5 alternates between full attention and gated deltanet layers."""
config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
config.return_dict = True
config._attn_implementation = "eager"
seq_len = getattr(self.model_tester, "seq_length", None)
for model_class in self.all_model_classes:
inputs_dict["output_attentions"] = True
inputs_dict["output_hidden_states"] = False
config.return_dict = True
model = model_class._from_config(config, attn_implementation="eager")
config = model.config
model.to(torch_device)
model.eval()
with torch.no_grad():
outputs = model(**self._prepare_for_class(inputs_dict, model_class))
attentions = outputs.attentions
self.assertEqual(
len(attentions), sum(layer == "full_attention" for layer in config.text_config.layer_types)
)
del inputs_dict["output_attentions"]
config.text_config.output_attentions = True
model = model_class(config)
model.to(torch_device)
model.eval()
with torch.no_grad():
outputs = model(**self._prepare_for_class(inputs_dict, model_class))
attentions = outputs.attentions
self.assertEqual(
len(attentions), sum(layer == "full_attention" for layer in config.text_config.layer_types)
)
self.assertListEqual(
list(attentions[0].shape[-3:]), [config.text_config.num_attention_heads, seq_len, seq_len]
)
out_len = len(outputs)
inputs_dict["output_attentions"] = True
inputs_dict["output_hidden_states"] = True
model = model_class(config)
model.to(torch_device)
model.eval()
with torch.no_grad():
outputs = model(**self._prepare_for_class(inputs_dict, model_class))
self_attentions = outputs.attentions
self.assertEqual(out_len + 1, len(outputs))
self.assertEqual(
len(self_attentions), sum(layer == "full_attention" for layer in config.text_config.layer_types)
)
self.assertListEqual(
list(self_attentions[0].shape[-3:]), [config.text_config.num_attention_heads, seq_len, seq_len]
)
@slow
@require_torch_accelerator
class MiniCPMV4_6IntegrationTest(unittest.TestCase):
model_id = "openbmb/MiniCPM-V-4_6"
def setUp(self):
cleanup(torch_device, gc_collect=True)
def tearDown(self):
cleanup(torch_device, gc_collect=True)
@slow
def test_small_model_logits(self):
processor = AutoProcessor.from_pretrained(self.model_id)
model = MiniCPMV4_6ForConditionalGeneration.from_pretrained(
self.model_id, device_map="auto", dtype=torch.bfloat16
)
messages = [{"role": "user", "content": [{"type": "text", "text": "Hi"}]}]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt"
).to(model.device)
with torch.no_grad():
logits = model(**inputs).logits.float().cpu()
self.assertEqual(logits.shape[0], 1)
self.assertTrue(torch.isfinite(logits).all().item())
@slow
def test_small_model_vision_generation(self):
processor = AutoProcessor.from_pretrained(self.model_id)
model = MiniCPMV4_6ForConditionalGeneration.from_pretrained(
self.model_id, device_map="auto", dtype=torch.bfloat16
)
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg",
},
{"type": "text", "text": "What kind of animal is this?"},
],
}
]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt"
).to(model.device, dtype=torch.bfloat16)
output = model.generate(**inputs, max_new_tokens=30, do_sample=False)
decoded_text = processor.decode(output[0, inputs["input_ids"].shape[1] :], skip_special_tokens=True)
self.assertEqual(
"The animal in the image is a Pystylus, also known as a Eurasian pystylus or snow leopard cat. It's a",
decoded_text,
)
@slow
def test_small_model_video_generation(self):
processor = AutoProcessor.from_pretrained(self.model_id)
model = MiniCPMV4_6ForConditionalGeneration.from_pretrained(
self.model_id, device_map="auto", dtype=torch.bfloat16
)
messages = [
{
"role": "user",
"content": [
{
"type": "video",
"url": url_to_local_path(
"https://huggingface.co/datasets/hf-internal-testing/fixtures_videos/resolve/main/tennis.mp4"
),
},
{"type": "text", "text": "What is shown in this video?"},
],
}
]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt"
).to(model.device, dtype=torch.bfloat16)
output = model.generate(**inputs, max_new_tokens=30, do_sample=False)
decoded_text = processor.decode(output[0, inputs["input_ids"].shape[1] :], skip_special_tokens=True)
expected_texts = Expectations(
{
("cuda", None): "The video shows two tennis players engaged in a match or practice session on an indoor tennis court. The player in the foreground is positioned at the net,",
}
) # fmt: skip
EXPECTED_TEXT = expected_texts.get_expectation()
self.assertEqual(EXPECTED_TEXT, decoded_text)
@slow
def test_small_model_vision_generation_batch(self):
processor = AutoProcessor.from_pretrained(self.model_id)
model = MiniCPMV4_6ForConditionalGeneration.from_pretrained(
self.model_id, device_map="auto", dtype=torch.bfloat16
)
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"url": url_to_local_path(
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
),
},
{"type": "text", "text": "What kind of animal is this?"},
],
}
]
batch_messages = [messages, messages]
inputs = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
padding=True,
).to(model.device, dtype=torch.bfloat16)
output = model.generate(**inputs, max_new_tokens=30, do_sample=False)
decoded_texts = processor.batch_decode(output[:, inputs["input_ids"].shape[1] :], skip_special_tokens=True)
expected_texts = Expectations(
{
("cuda", None): [
"The animal in the image is a Pystylus, also known as the Eurasian pystylus or snow leopard cat. It's a",
"The animal in the image is a Pystylus, also known as the Eurasian pystylus or snow leopard cat. It's a",
],
}
) # fmt: skip
EXPECTED_TEXT = expected_texts.get_expectation()
self.assertListEqual(decoded_texts, EXPECTED_TEXT)
@slow
def test_small_model_vision_generation_batch_mixed(self):
processor = AutoProcessor.from_pretrained(self.model_id)
model = MiniCPMV4_6ForConditionalGeneration.from_pretrained(
self.model_id, device_map="auto", dtype=torch.bfloat16
)
image_message = [
{
"role": "user",
"content": [
{
"type": "image",
"url": url_to_local_path(
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
),
},
{"type": "text", "text": "What kind of animal is this?"},
],
}
]
text_only_message = [{"role": "user", "content": [{"type": "text", "text": "Who are you?"}]}]
batch_messages = [image_message, text_only_message]
inputs = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
padding=True,
).to(model.device, dtype=torch.bfloat16)
output = model.generate(**inputs, max_new_tokens=30, do_sample=False)
decoded_texts = processor.batch_decode(output[:, inputs["input_ids"].shape[1] :], skip_special_tokens=True)
expected_texts = Expectations(
{
("cuda", None): [
"The animal in the image is a Pystylus, also known as the Eurasian pystylus or snow leopard cat. It's a",
"I'm a model from the MiniCPM series, developed by Modelbest and OpenBMB. For more details, you can visit https://github",
],
}
) # fmt: skip
EXPECTED_TEXT = expected_texts.get_expectation()
self.assertListEqual(decoded_texts, EXPECTED_TEXT)

View File

@@ -0,0 +1,351 @@
# Copyright 2026 OpenBMB and the HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import numpy as np
from parameterized import parameterized
from transformers.testing_utils import require_torch, require_torchvision, require_vision
from transformers.utils import is_torch_available, is_vision_available
from ...test_processing_common import ProcessorTesterMixin, url_to_local_path
if is_vision_available():
from transformers import MiniCPMV4_6Processor
if is_torch_available():
import torch
@require_vision
@require_torch
@require_torchvision
class MiniCPMV4_6ProcessorTest(ProcessorTesterMixin, unittest.TestCase):
processor_class = MiniCPMV4_6Processor
model_id = "openbmb/MiniCPM-V-4_6"
video_text_kwargs_max_length = 600
video_text_kwargs_override_max_length = 550
video_unstructured_max_length = 600
@classmethod
def _setup_test_attributes(cls, processor):
cls.image_token = processor.image_token
cls.video_token = processor.video_token
def test_image_processing(self):
"""Test that the processor correctly handles image inputs."""
processor = self.get_processor()
text = self.prepare_text_inputs(modalities=["image"])
image_input = self.prepare_image_inputs()
inputs = processor(text=text, images=image_input, return_tensors="pt")
self.assertIn("pixel_values", inputs)
self.assertIn("input_ids", inputs)
self.assertIn("attention_mask", inputs)
self.assertIn("target_sizes", inputs)
self.assertIsInstance(inputs["pixel_values"], torch.Tensor)
self.assertEqual(inputs["pixel_values"].shape[0], 1)
def test_video_processing(self):
"""Test that the processor correctly handles video inputs."""
processor = self.get_processor()
text = self.prepare_text_inputs(modalities=["video"])
video_input = self.prepare_video_inputs()
inputs = processor(text=text, videos=video_input, do_sample_frames=False, return_tensors="pt")
self.assertIn("pixel_values_videos", inputs)
self.assertIn("input_ids", inputs)
self.assertIn("attention_mask", inputs)
self.assertIn("target_sizes_videos", inputs)
self.assertIsInstance(inputs["pixel_values_videos"], torch.Tensor)
self.assertEqual(inputs["pixel_values_videos"].shape[0], 1)
def test_text_only_processing(self):
"""Test that the processor works with text-only input (no images)."""
processor = self.get_processor()
text = "Hello, how are you?"
inputs = processor(text=text, return_tensors="pt")
self.assertIn("input_ids", inputs)
self.assertIn("attention_mask", inputs)
self.assertEqual(inputs["input_ids"].ndim, 2)
self.assertEqual(inputs["attention_mask"].ndim, 2)
def test_batch_text_only(self):
"""Test batch text-only processing."""
processor = self.get_processor()
texts = ["Hello", "World, this is a longer sentence"]
inputs = processor(text=texts, return_tensors="pt")
self.assertEqual(inputs["input_ids"].shape[0], 2)
self.assertEqual(inputs["attention_mask"].shape[0], 2)
def test_post_process_image_text_to_text(self):
"""Test the post-processing method."""
processor = self.get_processor()
generated_ids = torch.tensor([[1, 2, 3, 4, 5]])
texts = processor.post_process_image_text_to_text(generated_ids)
self.assertEqual(len(texts), 1)
self.assertIsInstance(texts[0], str)
def test_post_process_skip_special_tokens_param(self):
"""Verify skip_special_tokens can be passed as argument without conflict."""
processor = self.get_processor()
generated_ids = torch.tensor([[1, 2, 3, 4, 5]])
texts_skip = processor.post_process_image_text_to_text(generated_ids, skip_special_tokens=True)
texts_no_skip = processor.post_process_image_text_to_text(generated_ids, skip_special_tokens=False)
self.assertEqual(len(texts_skip), 1)
self.assertEqual(len(texts_no_skip), 1)
def test_use_image_id_kwarg(self):
"""Test that use_image_id is correctly routed through _merge_kwargs."""
processor = self.get_processor()
text = f"{self.image_token}Describe."
image_input = self.prepare_image_inputs()
inputs_with_id = processor(text=text, images=image_input, use_image_id=True, return_tensors="pt")
inputs_without_id = processor(text=text, images=image_input, use_image_id=False, return_tensors="pt")
# With use_image_id=True, input_ids should contain image_id tokens -> different sequences
self.assertFalse(
torch.equal(inputs_with_id["input_ids"], inputs_without_id["input_ids"]),
"use_image_id should produce different input_ids when True vs False",
)
def _test_apply_chat_template(
self,
modality: str,
batch_size: int,
return_tensors: str,
input_name: str,
processor_name: str,
input_data: list[str],
):
processor = self.get_processor()
if processor_name not in self.processor_class.get_attributes():
self.skipTest(f"{processor_name} attribute not present in {self.processor_class}")
# some models have only Fast image processor
if getattr(processor, processor_name).__class__.__name__.endswith("Fast"):
return_tensors = "pt"
batch_messages = [
[
{"role": "system", "content": [{"type": "text", "text": "You are a helpful assistant."}]},
{"role": "user", "content": [{"type": "text", "text": "Describe this."}]},
]
] * batch_size
# Test that jinja can be applied
formatted_prompt = processor.apply_chat_template(batch_messages, add_generation_prompt=True, tokenize=False)
self.assertEqual(len(formatted_prompt), batch_size)
# Test that tokenizing with template and directly with `self.tokenizer` gives same output
formatted_prompt_tokenized = processor.apply_chat_template(
batch_messages, add_generation_prompt=True, tokenize=True, return_tensors=return_tensors
)
add_special_tokens = True
if processor.tokenizer.bos_token is not None and formatted_prompt[0].startswith(processor.tokenizer.bos_token):
add_special_tokens = False
tok_output = processor.tokenizer(
formatted_prompt, return_tensors=return_tensors, add_special_tokens=add_special_tokens
)
expected_output = tok_output.input_ids
self.assertListEqual(expected_output.tolist(), formatted_prompt_tokenized.tolist())
# Test that kwargs passed to processor's `__call__` are actually used
tokenized_prompt_100 = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_tensors=return_tensors,
processor_kwargs={
"padding": "max_length",
"truncation": True,
"max_length": self.chat_template_max_length,
},
)
self.assertEqual(len(tokenized_prompt_100[0]), self.chat_template_max_length)
# Test that `return_dict=True` returns text related inputs in the dict
out_dict_text = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors=return_tensors,
)
self.assertTrue(all(key in out_dict_text for key in ["input_ids", "attention_mask"]))
self.assertEqual(len(out_dict_text["input_ids"]), batch_size)
self.assertEqual(len(out_dict_text["attention_mask"]), batch_size)
# Test that with modality URLs and `return_dict=True`, we get modality inputs in the dict
for idx, url in enumerate(input_data[:batch_size]):
batch_messages[idx][1]["content"] = [batch_messages[idx][1]["content"][0], {"type": modality, "url": url}]
out_dict = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors=return_tensors,
processor_kwargs={"num_frames": 2}, # by default no more than 2 frames, otherwise too slow
)
input_name = getattr(self, input_name)
self.assertTrue(input_name in out_dict)
self.assertEqual(len(out_dict["input_ids"]), batch_size)
self.assertEqual(len(out_dict["attention_mask"]), batch_size)
self.assertEqual(len(out_dict[input_name]), 1) # always 1 in this model
return_tensor_to_type = {"pt": torch.Tensor, "np": np.ndarray, None: list}
for k in out_dict:
self.assertIsInstance(out_dict[k], return_tensor_to_type[return_tensors])
# Test continue from final message
assistant_message = {
"role": "assistant",
"content": [{"type": "text", "text": "It is the sound of"}],
}
for idx, url in enumerate(input_data[:batch_size]):
batch_messages[idx] = batch_messages[idx] + [assistant_message]
continue_prompt = processor.apply_chat_template(batch_messages, continue_final_message=True, tokenize=False)
for prompt in continue_prompt:
self.assertTrue(prompt.endswith("It is the sound of")) # no `eos` token at the end
def test_apply_chat_template_video_frame_sampling(self):
processor = self.get_processor()
messages = [
[
{
"role": "user",
"content": [
{
"type": "video",
"url": url_to_local_path(
"https://huggingface.co/datasets/raushan-testing-hf/videos-test/resolve/main/tiny_video.mp4"
),
},
{"type": "text", "text": "What is shown in this video?"},
],
},
]
]
num_frames = 3
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
processor_kwargs={"num_frames": num_frames, "fps": None},
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 1)
self.assertEqual(len(out_dict_with_video[self.videos_input_name][0]), num_frames)
# Load with `fps` arg
fps = 10
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
processor_kwargs={"fps": fps, "num_frames": None},
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 1)
# 3 frames are inferred from input video's length and FPS, so can be hardcoded
self.assertEqual(out_dict_with_video[self.videos_input_name].shape[-1], 129472)
# When `do_sample_frames=False` no sampling is done and whole video is loaded, even if number of frames is passed
fps = 10
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
processor_kwargs={
"do_sample_frames": False,
"fps": fps,
"return_tensors": "pt",
},
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 1)
self.assertEqual(out_dict_with_video[self.videos_input_name].shape[-1], 1424192)
# Load without any arg should load the whole video
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 1)
self.assertEqual(out_dict_with_video[self.videos_input_name].shape[-1], 129472)
# Load video as a list of frames (i.e. images).
# NOTE: each frame should have same size because we assume they come from one video
messages[0][0]["content"][0] = {
"type": "video",
"url": [
url_to_local_path(
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/australia.jpg"
)
]
* 2,
}
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
do_sample_frames=False,
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 1)
self.assertEqual(out_dict_with_video[self.videos_input_name].shape[-1], 203392)
@require_torch
def test_apply_chat_template_tool_calls_no_content(self):
# MiniCPM needs different format for tools as per saved jinja template
processor = self.get_processor()
messages = [
{
"role": "user",
"content": [{"type": "text", "text": "What is the weather?"}],
},
{
"role": "assistant",
"tool_calls": [{"type": "function", "function": {"name": "get_weather", "arguments": {}}}],
},
]
# Regression test for #45290: tokenize=True used to raise KeyError when "content" was missing
result = processor.apply_chat_template(messages, tokenize=True)
self.assertIsInstance(result, torch.Tensor)
@unittest.skip("MiniCPM can't sample already decoded videos, have to turn off sampling!")
@parameterized.expand([(1, "pt")])
def test_apply_chat_template_decoded_video(self, batch_size: int, return_tensors: str):
pass

View File

@@ -0,0 +1,282 @@
# Copyright 2026 HuggingFace Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import unittest
import numpy as np
from transformers.image_utils import IMAGENET_STANDARD_MEAN, IMAGENET_STANDARD_STD, get_image_size
from transformers.testing_utils import require_torch, require_vision
from transformers.utils import is_torch_available, is_torchvision_available, is_vision_available
from transformers.video_utils import VideoMetadata
from ...test_video_processing_common import VideoProcessingTestMixin, prepare_video_inputs
if is_torch_available():
import torch
if is_vision_available():
from PIL import Image
if is_torchvision_available():
from transformers import MiniCPMV4_6VideoProcessor
class MiniCPMV4_6VideoProcessingTester:
def __init__(
self,
parent,
batch_size=5,
num_frames=8,
num_channels=3,
min_resolution=30,
max_resolution=80,
do_resize=True,
do_normalize=True,
image_mean=IMAGENET_STANDARD_MEAN,
image_std=IMAGENET_STANDARD_STD,
do_convert_rgb=True,
max_slice_nums=5,
scale_resolution=448,
patch_size=14,
slice_mode=True,
):
self.parent = parent
self.batch_size = batch_size
self.num_frames = num_frames
self.num_channels = num_channels
self.min_resolution = min_resolution
self.max_resolution = max_resolution
self.do_resize = do_resize
self.do_normalize = do_normalize
self.image_mean = image_mean
self.image_std = image_std
self.do_convert_rgb = do_convert_rgb
self.patch_size = patch_size
self.max_slice_nums = max_slice_nums
self.scale_resolution = scale_resolution
self.slice_mode = slice_mode
def prepare_video_processor_dict(self):
return {
"do_resize": self.do_resize,
"do_normalize": self.do_normalize,
"image_mean": self.image_mean,
"image_std": self.image_std,
"do_convert_rgb": self.do_convert_rgb,
"do_sample_frames": False,
"max_slice_nums": self.max_slice_nums,
"scale_resolution": self.scale_resolution,
"patch_size": self.patch_size,
"slice_mode": self.slice_mode,
}
def expected_output_video_shape(self, video_inputs):
"""Return the expected NaViT-packed shape [C, P, total_L] for encoded_videos[0]."""
total_L = 0
for video in video_inputs:
if isinstance(video, list) and isinstance(video[0], Image.Image):
frames = np.stack([np.array(frame) for frame in video])
elif hasattr(video, "shape"):
frames = video
else:
frames = np.array(video)
height, width = get_image_size(frames[0])
num_frames = len(frames)
aspect_ratio = width / height
new_height = int(self.scale_resolution / math.sqrt(aspect_ratio))
new_width = int(new_height * aspect_ratio)
divisor = self.patch_size * 4
best_height = max(round(new_height / divisor) * divisor, divisor)
best_width = max(round(new_width / divisor) * divisor, divisor)
total_L += num_frames * (best_height * best_width // self.patch_size)
return [self.num_channels, self.patch_size, total_L]
def prepare_video_inputs(self, equal_resolution=False, return_tensors="pil"):
videos = prepare_video_inputs(
batch_size=self.batch_size,
num_frames=self.num_frames,
num_channels=self.num_channels,
min_resolution=self.min_resolution,
max_resolution=self.max_resolution,
equal_resolution=equal_resolution,
return_tensors=return_tensors,
)
return videos
@require_torch
@require_vision
class MiniCPMV4_6VideoProcessingTest(VideoProcessingTestMixin, unittest.TestCase):
fast_video_processing_class = MiniCPMV4_6VideoProcessor if is_torchvision_available() else None
input_name = "pixel_values_videos"
def setUp(self):
super().setUp()
self.video_processor_tester = MiniCPMV4_6VideoProcessingTester(self)
@property
def video_processor_dict(self):
return self.video_processor_tester.prepare_video_processor_dict()
def test_video_processor_from_dict_with_kwargs(self):
video_processor = self.fast_video_processing_class.from_dict(self.video_processor_dict)
self.assertEqual(video_processor.patch_size, 14)
video_processor = self.fast_video_processing_class.from_dict(self.video_processor_dict, patch_size=36)
self.assertEqual(video_processor.patch_size, 36)
def test_call_pil(self):
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(equal_resolution=False)
for video in video_inputs:
self.assertIsInstance(video[0], Image.Image)
# Test not batched input
encoded_videos = video_processing(video_inputs[0], return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_output_video_shape)
# Test batched
encoded_videos = video_processing(video_inputs, return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_output_video_shape)
def test_call_numpy(self):
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="np"
)
for video in video_inputs:
self.assertIsInstance(video, np.ndarray)
# Test not batched input
encoded_videos = video_processing(video_inputs[0], return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_output_video_shape)
# Test batched
encoded_videos = video_processing(video_inputs, return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_output_video_shape)
def test_call_pytorch(self):
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="torch"
)
for video in video_inputs:
self.assertIsInstance(video, torch.Tensor)
# Test not batched input
encoded_videos = video_processing(video_inputs[0], return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_output_video_shape)
# Test batched
encoded_videos = video_processing(video_inputs, return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_output_video_shape)
def test_nested_input(self):
"""NaViT packing: dim 0 is always 1 regardless of batch size."""
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="np"
)
video_inputs = [list(video) for video in video_inputs]
# Test not batched input
encoded_videos = video_processing(video_inputs[0], return_tensors="pt")[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(tuple(encoded_videos.shape), (1, *expected_output_video_shape))
# Test batched
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
encoded_videos = video_processing(video_inputs, return_tensors="pt")[self.input_name]
self.assertEqual(tuple(encoded_videos.shape), (1, *expected_output_video_shape))
@unittest.skip("NaViT expected_output_video_shape cannot infer channel dim for 4-channel images")
def test_call_numpy_4_channels(self):
pass
def test_call_sample_frames(self):
for video_processing_class in self.video_processor_list:
video_processor_dict = self.video_processor_dict.copy()
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False,
return_tensors="torch",
)
video_metadata = [
VideoMetadata(total_num_frames=len(video), duration=4.0, fps=2.0) for video in video_inputs
]
sampled_video_processor_dict = {**video_processor_dict, "do_sample_frames": True}
# stack_frames=1: sample one main frame per second from complete metadata.
video_processor = video_processing_class(**sampled_video_processor_dict, max_num_frames=20, stack_frames=1)
encoded_videos = video_processor(
video_inputs[0],
video_metadata=[video_metadata[0]],
return_tensors="pt",
)[self.input_name]
expected_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0][[0, 2, 4, 6]]])
self.assertEqual(len(encoded_videos), 1)
self.assertListEqual(list(encoded_videos[0].shape), expected_shape)
encoded_videos_batched = video_processor(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
self.input_name
]
expected_shape_batched = self.video_processor_tester.expected_output_video_shape(
[video[[0, 2, 4, 6]] for video in video_inputs]
)
self.assertEqual(len(encoded_videos_batched), 1)
self.assertListEqual(list(encoded_videos_batched[0].shape), expected_shape_batched)
# stack_frames=2, duration=4.0: tensor layout = [4 main | 4 sub]
# Each second gets 1 sub-frame composited (single frame → same size as main)
# → 8 visual units interleaved: [main_0, comp_0, ..., main_3, comp_3]
video_processor_sf2 = video_processing_class(
**sampled_video_processor_dict, max_num_frames=20, stack_frames=2
)
encoded_videos_sf2 = video_processor_sf2(
video_inputs[0],
video_metadata=[video_metadata[0]],
return_tensors="pt",
)[self.input_name]
self.assertEqual(len(encoded_videos_sf2), 1)
self.assertListEqual(
list(encoded_videos_sf2[0].shape),
self.video_processor_tester.expected_output_video_shape([video_inputs[0]]),
)