chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,65 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
import transformers.image_utils
|
||||
from PIL import Image
|
||||
|
||||
from vllm.transformers_utils.processors.pixtral import MistralCommonImageProcessor
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def image_processor() -> MistralCommonImageProcessor:
|
||||
return MistralCommonImageProcessor(mm_encoder=None)
|
||||
|
||||
|
||||
def test_fetch_images_passes_through_decoded_image(
|
||||
image_processor: MistralCommonImageProcessor,
|
||||
):
|
||||
image = Image.new("RGB", (4, 4))
|
||||
result = image_processor.fetch_images(image)
|
||||
assert result is image
|
||||
|
||||
|
||||
def test_fetch_images_recurses_over_list(
|
||||
image_processor: MistralCommonImageProcessor,
|
||||
):
|
||||
a = Image.new("RGB", (4, 4))
|
||||
b = Image.new("RGB", (8, 8))
|
||||
result = image_processor.fetch_images([a, b])
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert result[0] is a
|
||||
assert result[1] is b
|
||||
|
||||
|
||||
def test_fetch_images_recurses_over_nested_list(
|
||||
image_processor: MistralCommonImageProcessor,
|
||||
):
|
||||
a = Image.new("RGB", (4, 4))
|
||||
b = Image.new("RGB", (8, 8))
|
||||
result = image_processor.fetch_images([[a], [b]])
|
||||
assert result == [[a], [b]]
|
||||
|
||||
|
||||
def test_fetch_images_str_delegates_to_load_image(
|
||||
monkeypatch, image_processor: MistralCommonImageProcessor
|
||||
):
|
||||
sentinel = Image.new("RGB", (2, 2))
|
||||
received: dict[str, object] = {}
|
||||
|
||||
def fake_load_image(path):
|
||||
received["path"] = path
|
||||
return sentinel
|
||||
|
||||
monkeypatch.setattr(transformers.image_utils, "load_image", fake_load_image)
|
||||
|
||||
result = image_processor.fetch_images("/tmp/fake.png")
|
||||
assert result is sentinel
|
||||
assert received["path"] == "/tmp/fake.png"
|
||||
|
||||
|
||||
def test_fetch_images_rejects_unsupported_type(
|
||||
image_processor: MistralCommonImageProcessor,
|
||||
):
|
||||
with pytest.raises(TypeError, match="only a single or a list"):
|
||||
image_processor.fetch_images(42)
|
||||
@@ -0,0 +1,127 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for ``MistralCommonFeatureExtractor.fetch_audio``.
|
||||
|
||||
``transformers>=5.10`` adds a ``ProcessorMixin.prepare_inputs_layout`` helper
|
||||
that calls ``self.feature_extractor.fetch_audio(...)`` unconditionally. The
|
||||
duck-typed :class:`MistralCommonFeatureExtractor` previously did not implement
|
||||
that method, so loading any voxtral model under transformers 5.10.x raised
|
||||
``AttributeError: 'MistralCommonFeatureExtractor' object has no attribute
|
||||
'fetch_audio'``. These tests pin the new ``fetch_audio`` method to the same
|
||||
contract as ``transformers.SequenceFeatureExtractor.fetch_audio``.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.tokenizers.mistral import MistralTokenizer
|
||||
from vllm.transformers_utils.processors.voxtral import (
|
||||
MistralCommonFeatureExtractor,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def feature_extractor() -> MistralCommonFeatureExtractor:
|
||||
tokenizer = MistralTokenizer.from_pretrained("mistralai/Voxtral-Mini-3B-2507")
|
||||
return MistralCommonFeatureExtractor(tokenizer.instruct.audio_encoder)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"audio",
|
||||
[
|
||||
np.zeros(1024, dtype=np.float32),
|
||||
torch.zeros(1024),
|
||||
[0.0, 1.0, 2.0],
|
||||
],
|
||||
ids=["numpy_array", "torch_tensor", "list_of_floats"],
|
||||
)
|
||||
def test_fetch_audio_passes_through(
|
||||
feature_extractor: MistralCommonFeatureExtractor, audio
|
||||
):
|
||||
result = feature_extractor.fetch_audio(audio)
|
||||
assert result is audio
|
||||
|
||||
|
||||
def test_fetch_audio_recurses_over_list_of_arrays(
|
||||
feature_extractor: MistralCommonFeatureExtractor,
|
||||
):
|
||||
a = np.zeros(8, dtype=np.float32)
|
||||
b = np.ones(8, dtype=np.float32)
|
||||
result = feature_extractor.fetch_audio([a, b])
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert result[0] is a
|
||||
assert result[1] is b
|
||||
|
||||
|
||||
def test_fetch_audio_uses_self_sampling_rate_when_none(
|
||||
monkeypatch, feature_extractor: MistralCommonFeatureExtractor
|
||||
):
|
||||
"""If ``sampling_rate`` is None, ``self.sampling_rate`` must be used.
|
||||
|
||||
Verified indirectly via the recursion path: when we pass a list of arrays
|
||||
without sampling_rate, recursive calls receive the resolved rate.
|
||||
"""
|
||||
captured: list[int | None] = []
|
||||
original = feature_extractor.fetch_audio
|
||||
|
||||
def spy(audio, sampling_rate=None):
|
||||
captured.append(sampling_rate)
|
||||
return original(audio, sampling_rate=sampling_rate)
|
||||
|
||||
monkeypatch.setattr(feature_extractor, "fetch_audio", spy)
|
||||
feature_extractor.fetch_audio([np.zeros(4, dtype=np.float32)])
|
||||
# Top-level call has sampling_rate=None; inner recursive call sees the
|
||||
# resolved rate from self.sampling_rate.
|
||||
assert captured[0] is None
|
||||
assert captured[1] == 16000
|
||||
|
||||
|
||||
def test_fetch_audio_explicit_sampling_rate_propagates(
|
||||
monkeypatch, feature_extractor: MistralCommonFeatureExtractor
|
||||
):
|
||||
captured: list[int | None] = []
|
||||
original = feature_extractor.fetch_audio
|
||||
|
||||
def spy(audio, sampling_rate=None):
|
||||
captured.append(sampling_rate)
|
||||
return original(audio, sampling_rate=sampling_rate)
|
||||
|
||||
monkeypatch.setattr(feature_extractor, "fetch_audio", spy)
|
||||
feature_extractor.fetch_audio([np.zeros(4, dtype=np.float32)], sampling_rate=8000)
|
||||
assert captured[0] == 8000
|
||||
assert captured[1] == 8000
|
||||
|
||||
|
||||
def test_fetch_audio_rejects_unsupported_type(
|
||||
feature_extractor: MistralCommonFeatureExtractor,
|
||||
):
|
||||
with pytest.raises(TypeError, match="only a numpy array"):
|
||||
feature_extractor.fetch_audio(42) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_fetch_audio_str_delegates_to_load_audio(
|
||||
monkeypatch, feature_extractor: MistralCommonFeatureExtractor
|
||||
):
|
||||
"""A str input must round-trip through ``transformers.audio_utils.load_audio``.
|
||||
|
||||
We monkey-patch ``load_audio`` so the test stays offline (no real URL/path
|
||||
fetched) and still asserts the delegation contract.
|
||||
"""
|
||||
sentinel = np.array([0.5, -0.5], dtype=np.float32)
|
||||
received: dict[str, object] = {}
|
||||
|
||||
def fake_load_audio(path, sampling_rate=None):
|
||||
received["path"] = path
|
||||
received["sampling_rate"] = sampling_rate
|
||||
return sentinel
|
||||
|
||||
import transformers.audio_utils
|
||||
|
||||
monkeypatch.setattr(transformers.audio_utils, "load_audio", fake_load_audio)
|
||||
|
||||
result = feature_extractor.fetch_audio("/tmp/fake.wav")
|
||||
assert result is sentinel
|
||||
assert received["path"] == "/tmp/fake.wav"
|
||||
assert received["sampling_rate"] == 16000
|
||||
Reference in New Issue
Block a user