main
py 127 lines 4.35 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for ``MistralCommonFeatureExtractor.fetch_audio``.
4
5 ``transformers>=5.10`` adds a ``ProcessorMixin.prepare_inputs_layout`` helper
6 that calls ``self.feature_extractor.fetch_audio(...)`` unconditionally. The
7 duck-typed :class:`MistralCommonFeatureExtractor` previously did not implement
8 that method, so loading any voxtral model under transformers 5.10.x raised
9 ``AttributeError: 'MistralCommonFeatureExtractor' object has no attribute
10 'fetch_audio'``. These tests pin the new ``fetch_audio`` method to the same
11 contract as ``transformers.SequenceFeatureExtractor.fetch_audio``.
12 """
13
14 import numpy as np
15 import pytest
16 import torch
17
18 from vllm.tokenizers.mistral import MistralTokenizer
19 from vllm.transformers_utils.processors.voxtral import (
20 MistralCommonFeatureExtractor,
21 )
22
23
24 @pytest.fixture(scope="module")
25 def feature_extractor() -> MistralCommonFeatureExtractor:
26 tokenizer = MistralTokenizer.from_pretrained("mistralai/Voxtral-Mini-3B-2507")
27 return MistralCommonFeatureExtractor(tokenizer.instruct.audio_encoder)
28
29
30 @pytest.mark.parametrize(
31 "audio",
32 [
33 np.zeros(1024, dtype=np.float32),
34 torch.zeros(1024),
35 [0.0, 1.0, 2.0],
36 ],
37 ids=["numpy_array", "torch_tensor", "list_of_floats"],
38 )
39 def test_fetch_audio_passes_through(
40 feature_extractor: MistralCommonFeatureExtractor, audio
41 ):
42 result = feature_extractor.fetch_audio(audio)
43 assert result is audio
44
45
46 def test_fetch_audio_recurses_over_list_of_arrays(
47 feature_extractor: MistralCommonFeatureExtractor,
48 ):
49 a = np.zeros(8, dtype=np.float32)
50 b = np.ones(8, dtype=np.float32)
51 result = feature_extractor.fetch_audio([a, b])
52 assert isinstance(result, list)
53 assert len(result) == 2
54 assert result[0] is a
55 assert result[1] is b
56
57
58 def test_fetch_audio_uses_self_sampling_rate_when_none(
59 monkeypatch, feature_extractor: MistralCommonFeatureExtractor
60 ):
61 """If ``sampling_rate`` is None, ``self.sampling_rate`` must be used.
62
63 Verified indirectly via the recursion path: when we pass a list of arrays
64 without sampling_rate, recursive calls receive the resolved rate.
65 """
66 captured: list[int | None] = []
67 original = feature_extractor.fetch_audio
68
69 def spy(audio, sampling_rate=None):
70 captured.append(sampling_rate)
71 return original(audio, sampling_rate=sampling_rate)
72
73 monkeypatch.setattr(feature_extractor, "fetch_audio", spy)
74 feature_extractor.fetch_audio([np.zeros(4, dtype=np.float32)])
75 # Top-level call has sampling_rate=None; inner recursive call sees the
76 # resolved rate from self.sampling_rate.
77 assert captured[0] is None
78 assert captured[1] == 16000
79
80
81 def test_fetch_audio_explicit_sampling_rate_propagates(
82 monkeypatch, feature_extractor: MistralCommonFeatureExtractor
83 ):
84 captured: list[int | None] = []
85 original = feature_extractor.fetch_audio
86
87 def spy(audio, sampling_rate=None):
88 captured.append(sampling_rate)
89 return original(audio, sampling_rate=sampling_rate)
90
91 monkeypatch.setattr(feature_extractor, "fetch_audio", spy)
92 feature_extractor.fetch_audio([np.zeros(4, dtype=np.float32)], sampling_rate=8000)
93 assert captured[0] == 8000
94 assert captured[1] == 8000
95
96
97 def test_fetch_audio_rejects_unsupported_type(
98 feature_extractor: MistralCommonFeatureExtractor,
99 ):
100 with pytest.raises(TypeError, match="only a numpy array"):
101 feature_extractor.fetch_audio(42)
102
103
104 def test_fetch_audio_str_delegates_to_load_audio(
105 monkeypatch, feature_extractor: MistralCommonFeatureExtractor
106 ):
107 """A str input must round-trip through ``transformers.audio_utils.load_audio``.
108
109 We monkey-patch ``load_audio`` so the test stays offline (no real URL/path
110 fetched) and still asserts the delegation contract.
111 """
112 sentinel = np.array([0.5, -0.5], dtype=np.float32)
113 received: dict[str, object] = {}
114
115 def fake_load_audio(path, sampling_rate=None):
116 received["path"] = path
117 received["sampling_rate"] = sampling_rate
118 return sentinel
119
120 import transformers.audio_utils
121
122 monkeypatch.setattr(transformers.audio_utils, "load_audio", fake_load_audio)
123
124 result = feature_extractor.fetch_audio("/tmp/fake.wav")
125 assert result is sentinel
126 assert received["path"] == "/tmp/fake.wav"
127 assert received["sampling_rate"] == 16000