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
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:
0
tests/models/minicpmv4_6/__init__.py
Normal file
0
tests/models/minicpmv4_6/__init__.py
Normal file
232
tests/models/minicpmv4_6/test_image_processing_minicpmv4_6.py
Normal file
232
tests/models/minicpmv4_6/test_image_processing_minicpmv4_6.py
Normal 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)
|
||||
553
tests/models/minicpmv4_6/test_modeling_minicpmv4_6.py
Normal file
553
tests/models/minicpmv4_6/test_modeling_minicpmv4_6.py
Normal 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)
|
||||
351
tests/models/minicpmv4_6/test_processing_minicpmv4_6.py
Normal file
351
tests/models/minicpmv4_6/test_processing_minicpmv4_6.py
Normal 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
|
||||
282
tests/models/minicpmv4_6/test_video_processing_minicpmv4_6.py
Normal file
282
tests/models/minicpmv4_6/test_video_processing_minicpmv4_6.py
Normal 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]]),
|
||||
)
|
||||
Reference in New Issue
Block a user