@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 21dfb842d7
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 8, 12:30 UTC

[model] support FunASR model (#33247)

Signed-off-by: zixiao <shunli.dsl@alibaba-inc.com> Co-authored-by: zixiao <shunli.dsl@alibaba-inc.com>

AllenDou committed Feb 11, 2026 at 15:37 UTC 21dfb842d76c61204d44f6f1dd1e99f55a9b2cf4
7 files changed +1585 -3
docs/models/supported_models.md
+1
@@ -790,6 +790,7 @@ Speech2Text models trained specifically for Automatic Speech Recognition.
790
791 | Architecture | Models | Example HF Models | [LoRA](../features/lora.md) | [PP](../serving/parallelism_scaling.md) |
792 |--------------|--------|-------------------|----------------------|---------------------------|
793 +| `FunASRForConditionalGeneration` | FunASR | `allendou/Fun-ASR-Nano-2512-vllm`, etc. | | |
794 | `Gemma3nForConditionalGeneration` | Gemma3n | `google/gemma-3n-E2B-it`, `google/gemma-3n-E4B-it`, etc. | | |
795 | `GlmAsrForConditionalGeneration` | GLM-ASR | `zai-org/GLM-ASR-Nano-2512` | ✅︎ | ✅︎ |
796 | `GraniteSpeechForConditionalGeneration` | Granite Speech | `ibm-granite/granite-speech-3.3-2b`, `ibm-granite/granite-speech-3.3-8b`, etc. | ✅︎ | ✅︎ |
examples/online_serving/openai_transcription_client.py
+16 -3
@@ -26,7 +26,9 @@ from openai import AsyncOpenAI, OpenAI
26 from vllm.assets.audio import AudioAsset
27
28
29 -def sync_openai(audio_path: str, client: OpenAI, model: str):
29 +def sync_openai(
30 + audio_path: str, client: OpenAI, model: str, *, repetition_penalty: float = 1.3
31 +):
32 """
33 Perform synchronous transcription using OpenAI-compatible API.
34 """
@@ -40,7 +42,7 @@ def sync_openai(audio_path: str, client: OpenAI, model: str):
42 # Additional sampling params not provided by OpenAI API.
43 extra_body=dict(
44 seed=4419,
43 - repetition_penalty=1.3,
45 + repetition_penalty=repetition_penalty,
46 ),
47 )
48 print("transcription result [sync]:", transcription.text)
@@ -129,7 +131,12 @@ def main(args):
131 print(f"Using model: {model}")
132
133 # Run the synchronous function
132 - sync_openai(args.audio_path if args.audio_path else mary_had_lamb, client, model)
134 + sync_openai(
135 + audio_path=args.audio_path if args.audio_path else mary_had_lamb,
136 + client=client,
137 + model=model,
138 + repetition_penalty=args.repetition_penalty,
139 + )
140
141 # Run the asynchronous function
142 if "openai" in model:
@@ -161,5 +168,11 @@ if __name__ == "__main__":
168 default=None,
169 help="The path to the audio file to transcribe.",
170 )
171 + parser.add_argument(
172 + "--repetition_penalty",
173 + type=float,
174 + default=1.3,
175 + help="repetition penalty",
176 + )
177 args = parser.parse_args()
178 main(args)
tests/models/registry.py
+4
@@ -713,6 +713,10 @@ _MULTIMODAL_EXAMPLE_MODELS = {
713 "baidu/ERNIE-4.5-VL-28B-A3B-PT",
714 trust_remote_code=True,
715 ),
716 + "FunASRForConditionalGeneration": _HfExamplesInfo(
717 + "allendou/Fun-ASR-Nano-2512-vllm",
718 + is_available_online=False,
719 + ),
720 "FunAudioChatForConditionalGeneration": _HfExamplesInfo(
721 "funaudiochat", is_available_online=False
722 ),
vllm/model_executor/models/funasr.py new
+1057
@@ -0,0 +1,1057 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import math
5 +from collections.abc import Iterable, Mapping, Sequence
6 +from typing import Annotated, Literal, cast
7 +
8 +import numpy as np
9 +import torch
10 +import torch.nn.functional as F
11 +from torch import nn
12 +from transformers import (
13 + BatchFeature,
14 + Qwen3Config,
15 +)
16 +
17 +from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
18 +from vllm.config.multimodal import BaseDummyOptions
19 +from vllm.distributed import get_tensor_model_parallel_world_size
20 +from vllm.inputs.data import PromptType
21 +from vllm.logger import init_logger
22 +from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
23 +from vllm.model_executor.layers.attention.mm_encoder_attention import (
24 + MMEncoderAttention,
25 +)
26 +from vllm.model_executor.layers.linear import (
27 + ColumnParallelLinear,
28 + QKVParallelLinear,
29 + ReplicatedLinear,
30 + RowParallelLinear,
31 +)
32 +from vllm.model_executor.layers.logits_processor import LogitsProcessor
33 +from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
34 +from vllm.model_executor.model_loader.weight_utils import default_weight_loader
35 +from vllm.model_executor.models.whisper_utils import (
36 + ISO639_1_SUPPORTED_LANGS,
37 +)
38 +from vllm.multimodal import MULTIMODAL_REGISTRY
39 +from vllm.multimodal.inputs import (
40 + MultiModalDataDict,
41 + MultiModalFieldConfig,
42 + MultiModalKwargsItems,
43 +)
44 +from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
45 +from vllm.multimodal.processing import (
46 + BaseDummyInputsBuilder,
47 + BaseMultiModalProcessor,
48 + BaseProcessingInfo,
49 + PromptReplacement,
50 + PromptUpdate,
51 + PromptUpdateDetails,
52 +)
53 +from vllm.transformers_utils.processor import cached_processor_from_config
54 +from vllm.transformers_utils.processors.funasr_processor import FunASRFeatureExtractor
55 +from vllm.utils.jsontree import json_map_leaves
56 +from vllm.utils.tensor_schema import TensorSchema, TensorShape
57 +
58 +from .interfaces import (
59 + MultiModalEmbeddings,
60 + SupportsMultiModal,
61 + SupportsTranscription,
62 + _require_is_multimodal,
63 +)
64 +from .qwen3 import Qwen3Model
65 +from .utils import (
66 + AutoWeightsLoader,
67 + WeightsMapper,
68 + _merge_multimodal_embeddings,
69 + maybe_prefix,
70 +)
71 +
72 +logger = init_logger(__name__)
73 +
74 +
75 +def sequence_mask(lengths, maxlen=None, dtype=torch.float32, device=None):
76 + if maxlen is None:
77 + maxlen = lengths.max()
78 + row_vector = torch.arange(0, maxlen, 1).to(lengths.device)
79 + matrix = torch.unsqueeze(lengths, dim=-1)
80 + mask = row_vector < matrix
81 + mask = mask.detach()
82 +
83 + return mask.type(dtype).to(device) if device is not None else mask.type(dtype)
84 +
85 +
86 +class LayerNorm(torch.nn.LayerNorm):
87 + def __init__(self, nout, dim=-1):
88 + super().__init__(nout, eps=1e-12)
89 + self.dim = dim
90 +
91 + def forward(self, x: torch.Tensor):
92 + if self.dim == -1:
93 + return super().forward(x)
94 + return super().forward(x.transpose(self.dim, -1)).transpose(self.dim, -1)
95 +
96 +
97 +class EncoderLayerSANM(nn.Module):
98 + def __init__(
99 + self,
100 + in_size: int,
101 + size: int,
102 + self_attn: nn.Module,
103 + feed_forward: nn.Module,
104 + normalize_before=True,
105 + ):
106 + super().__init__()
107 + self.self_attn = self_attn
108 + self.feed_forward = feed_forward
109 + self.norm1 = LayerNorm(in_size)
110 + self.norm2 = LayerNorm(size)
111 + self.in_size = in_size
112 + self.size = size
113 + self.normalize_before = normalize_before
114 +
115 + def forward(
116 + self,
117 + hidden_states: torch.Tensor,
118 + mask: torch.Tensor | None = None,
119 + cache=None,
120 + mask_shfit_chunk=None,
121 + mask_att_chunk_encoder=None,
122 + ):
123 + residual = hidden_states
124 + hidden_states = self.norm1(hidden_states)
125 +
126 + if self.in_size == self.size:
127 + hidden_states = residual + self.self_attn(
128 + hidden_states,
129 + mask,
130 + mask_shfit_chunk=mask_shfit_chunk,
131 + mask_att_chunk_encoder=mask_att_chunk_encoder,
132 + )
133 + else:
134 + hidden_states = self.self_attn(
135 + hidden_states,
136 + mask,
137 + mask_shfit_chunk=mask_shfit_chunk,
138 + mask_att_chunk_encoder=mask_att_chunk_encoder,
139 + )
140 +
141 + residual = hidden_states
142 + hidden_states = self.norm2(hidden_states)
143 + hidden_states = residual + self.feed_forward(hidden_states)
144 +
145 + return hidden_states, mask, cache, mask_shfit_chunk, mask_att_chunk_encoder
146 +
147 +
148 +class MultiHeadedAttentionSANM(nn.Module):
149 + def __init__(
150 + self,
151 + n_head: int,
152 + in_feat: int,
153 + n_feat: int,
154 + kernel_size: int,
155 + sanm_shift: int = 0,
156 + ):
157 + super().__init__()
158 + assert n_feat % n_head == 0
159 + # We assume d_v always equals d_k
160 + self.d_k = n_feat // n_head
161 + self.h = n_head
162 + self.out_proj = ReplicatedLinear(
163 + input_size=n_feat,
164 + output_size=n_feat,
165 + bias=True,
166 + )
167 + self.linear_q_k_v = ReplicatedLinear(
168 + input_size=in_feat,
169 + output_size=n_feat * 3,
170 + bias=True,
171 + )
172 + self.attn = None
173 +
174 + self.fsmn_block = nn.Conv1d(
175 + n_feat, n_feat, kernel_size, stride=1, padding=0, groups=n_feat, bias=False
176 + )
177 + # padding
178 + left_padding = (kernel_size - 1) // 2
179 + if sanm_shift > 0:
180 + left_padding = left_padding + sanm_shift
181 + right_padding = kernel_size - 1 - left_padding
182 + self.pad_fn = nn.ConstantPad1d((left_padding, right_padding), 0.0)
183 +
184 + def forward_fsmn(
185 + self,
186 + inputs: torch.Tensor,
187 + mask: torch.Tensor,
188 + mask_shfit_chunk: torch.Tensor = None,
189 + ):
190 + b, t, d = inputs.size()
191 + if mask is not None:
192 + mask = torch.reshape(mask, (b, -1, 1))
193 + if mask_shfit_chunk is not None:
194 + mask = mask * mask_shfit_chunk
195 + inputs = inputs * mask
196 +
197 + x = inputs.transpose(1, 2)
198 + x = self.pad_fn(x)
199 + x = self.fsmn_block(x)
200 + x = x.transpose(1, 2)
201 + x += inputs
202 + if mask is not None:
203 + x = x * mask
204 + return x
205 +
206 + def forward_qkv(self, x: torch.Tensor):
207 + b, t, d = x.size()
208 +
209 + q_k_v, _ = self.linear_q_k_v(x)
210 + q, k, v = torch.split(q_k_v, int(self.h * self.d_k), dim=-1)
211 + q_h = torch.reshape(q, (b, t, self.h, self.d_k)).transpose(1, 2)
212 + k_h = torch.reshape(k, (b, t, self.h, self.d_k)).transpose(1, 2)
213 + v_h = torch.reshape(v, (b, t, self.h, self.d_k)).transpose(1, 2)
214 +
215 + return q_h, k_h, v_h, v
216 +
217 + def forward_attention(
218 + self,
219 + value: torch.Tensor,
220 + scores: torch.Tensor,
221 + mask: torch.Tensor,
222 + mask_att_chunk_encoder: torch.Tensor = None,
223 + ):
224 + n_batch = value.size(0)
225 + if mask is not None:
226 + if mask_att_chunk_encoder is not None:
227 + mask = mask * mask_att_chunk_encoder
228 +
229 + mask = mask.unsqueeze(1).eq(0)
230 +
231 + min_value = -float("inf")
232 + scores = scores.masked_fill(mask, min_value)
233 + attn = torch.softmax(scores, dim=-1).masked_fill(mask, 0.0)
234 + else:
235 + attn = torch.softmax(scores, dim=-1)
236 +
237 + p_attn = attn
238 + x = torch.matmul(p_attn, value)
239 + x = x.transpose(1, 2).contiguous().view(n_batch, -1, self.h * self.d_k)
240 +
241 + out, _ = self.out_proj(x)
242 + return out
243 +
244 + def forward(
245 + self,
246 + hidden_states: torch.Tensor,
247 + mask: torch.Tensor,
248 + mask_shfit_chunk: torch.Tensor = None,
249 + mask_att_chunk_encoder: torch.Tensor = None,
250 + ):
251 + q_h, k_h, v_h, v = self.forward_qkv(hidden_states)
252 + fsmn_memory = self.forward_fsmn(v, mask, mask_shfit_chunk)
253 + q_h = q_h * self.d_k ** (-0.5)
254 + scores = torch.matmul(q_h, k_h.transpose(-2, -1))
255 + att_outs = self.forward_attention(v_h, scores, mask, mask_att_chunk_encoder)
256 + return att_outs + fsmn_memory
257 +
258 +
259 +class SinusoidalPositionEncoder(torch.nn.Module):
260 + def __init__(self, d_model=80):
261 + super().__init__()
262 +
263 + def encode(
264 + self,
265 + positions: torch.Tensor = None,
266 + depth: int = None,
267 + dtype: torch.dtype = torch.float32,
268 + ):
269 + batch_size = positions.size(0)
270 + positions = positions.type(dtype)
271 + device = positions.device
272 + log_timescale_increment = torch.log(
273 + torch.tensor([10000], dtype=dtype, device=device)
274 + ) / (depth / 2 - 1)
275 + inv_timescales = torch.exp(
276 + torch.arange(depth / 2, device=device).type(dtype)
277 + * (-log_timescale_increment)
278 + )
279 + inv_timescales = torch.reshape(inv_timescales, [batch_size, -1])
280 + scaled_time = torch.reshape(positions, [1, -1, 1]) * torch.reshape(
281 + inv_timescales, [1, 1, -1]
282 + )
283 + encoding = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=2)
284 + return encoding.type(dtype)
285 +
286 + def forward(self, hidden_states: torch.Tensor):
287 + batch_size, timesteps, input_dim = hidden_states.size()
288 + positions = torch.arange(1, timesteps + 1, device=hidden_states.device)[None, :]
289 + position_encoding = self.encode(positions, input_dim, hidden_states.dtype).to(
290 + hidden_states.device
291 + )
292 +
293 + return hidden_states + position_encoding
294 +
295 +
296 +class SenseVoiceEncoderSmall(nn.Module):
297 + def __init__(
298 + self,
299 + input_size: int,
300 + output_size: int = 256,
301 + attention_heads: int = 4,
302 + linear_units: int = 2048,
303 + num_blocks: int = 6,
304 + tp_blocks: int = 0,
305 + attention_dropout_rate: float = 0.0,
306 + normalize_before: bool = True,
307 + kernel_size: int = 11,
308 + sanm_shift: int = 0,
309 + **kwargs,
310 + ):
311 + super().__init__()
312 + self._output_size = output_size
313 + self.embed = SinusoidalPositionEncoder()
314 +
315 + self.normalize_before = normalize_before
316 +
317 + positionwise_layer = PositionwiseFeedForward
318 + positionwise_layer_args = (
319 + output_size,
320 + linear_units,
321 + )
322 +
323 + encoder_selfattn_layer = MultiHeadedAttentionSANM
324 + encoder_selfattn_layer_args0 = (
325 + attention_heads,
326 + input_size,
327 + output_size,
328 + kernel_size,
329 + sanm_shift,
330 + )
331 + encoder_selfattn_layer_args = (
332 + attention_heads,
333 + output_size,
334 + output_size,
335 + kernel_size,
336 + sanm_shift,
337 + )
338 +
339 + self.encoders0 = nn.ModuleList(
340 + [
341 + EncoderLayerSANM(
342 + input_size,
343 + output_size,
344 + encoder_selfattn_layer(*encoder_selfattn_layer_args0),
345 + positionwise_layer(*positionwise_layer_args),
346 + )
347 + for i in range(1)
348 + ]
349 + )
350 + self.encoders = nn.ModuleList(
351 + [
352 + EncoderLayerSANM(
353 + output_size,
354 + output_size,
355 + encoder_selfattn_layer(*encoder_selfattn_layer_args),
356 + positionwise_layer(*positionwise_layer_args),
357 + )
358 + for i in range(num_blocks - 1)
359 + ]
360 + )
361 +
362 + self.tp_encoders = nn.ModuleList(
363 + [
364 + EncoderLayerSANM(
365 + output_size,
366 + output_size,
367 + encoder_selfattn_layer(*encoder_selfattn_layer_args),
368 + positionwise_layer(*positionwise_layer_args),
369 + )
370 + for i in range(tp_blocks)
371 + ]
372 + )
373 +
374 + self.after_norm = LayerNorm(output_size)
375 +
376 + self.tp_norm = LayerNorm(output_size)
377 +
378 + def output_size(self) -> int:
379 + return self._output_size
380 +
381 + def forward(
382 + self,
383 + xs_pad: torch.Tensor,
384 + ilens: torch.Tensor,
385 + ):
386 + maxlen = xs_pad.shape[1]
387 + masks = sequence_mask(
388 + ilens, maxlen=maxlen, dtype=ilens.dtype, device=ilens.device
389 + )[:, None, :]
390 +
391 + xs_pad *= self.output_size() ** 0.5
392 +
393 + xs_pad = self.embed(xs_pad)
394 +
395 + for layer_idx, encoder_layer in enumerate(self.encoders0):
396 + encoder_outs = encoder_layer(xs_pad, masks)
397 + xs_pad, masks = encoder_outs[0], encoder_outs[1]
398 +
399 + for layer_idx, encoder_layer in enumerate(self.encoders):
400 + encoder_outs = encoder_layer(xs_pad, masks)
401 + xs_pad, masks = encoder_outs[0], encoder_outs[1]
402 +
403 + xs_pad = self.after_norm(xs_pad)
404 +
405 + olens = masks.squeeze(1).sum(1).int()
406 +
407 + for layer_idx, encoder_layer in enumerate(self.tp_encoders):
408 + encoder_outs = encoder_layer(xs_pad, masks)
409 + xs_pad, masks = encoder_outs[0], encoder_outs[1]
410 +
411 + xs_pad = self.tp_norm(xs_pad)
412 + return xs_pad, olens
413 +
414 +
415 +class PositionwiseFeedForward(nn.Module):
416 + def __init__(self, idim: int, hidden_units: int):
417 + super().__init__()
418 + self.w_1 = ColumnParallelLinear(
419 + input_size=idim,
420 + output_size=hidden_units,
421 + bias=True,
422 + )
423 + self.w_2 = RowParallelLinear(
424 + input_size=hidden_units,
425 + output_size=idim,
426 + bias=True,
427 + )
428 + self.activation = _ACTIVATION_REGISTRY["relu"]
429 +
430 + def forward(self, hidden_states: torch.Tensor):
431 + hidden_states, _ = self.w_1(hidden_states)
432 + hidden_states = self.activation(hidden_states)
433 + hidden_states, _ = self.w_2(hidden_states)
434 + return hidden_states
435 +
436 +
437 +class EncoderLayer(nn.Module):
438 + def __init__(
439 + self,
440 + size: int,
441 + self_attn: nn.Module,
442 + feed_forward: nn.Module,
443 + ):
444 + super().__init__()
445 + self.self_attn = self_attn
446 + self.feed_forward = feed_forward
447 + self.norm1 = LayerNorm(size)
448 + self.norm2 = LayerNorm(size)
449 +
450 + def forward(self, hidden_states: torch.Tensor):
451 + residual = hidden_states
452 + hidden_states = self.norm1(hidden_states)
453 + hidden_states = residual + self.self_attn(hidden_states, None, None)
454 + residual = hidden_states
455 + hidden_states = self.norm2(hidden_states)
456 + hidden_states = residual + self.feed_forward(hidden_states)
457 +
458 + return hidden_states
459 +
460 +
461 +class FunASRAudioAttention(nn.Module):
462 + def __init__(
463 + self,
464 + num_heads: int,
465 + embed_dim: int,
466 + prefix: str = "",
467 + ):
468 + super().__init__()
469 + self.embed_dim = embed_dim
470 + self.num_heads = num_heads
471 + self.head_dim = self.embed_dim // self.num_heads
472 + tp_size = get_tensor_model_parallel_world_size()
473 + self.num_local_heads = self.num_heads // tp_size
474 +
475 + if (self.head_dim * self.num_heads) != self.embed_dim:
476 + raise ValueError(
477 + f"embed_dim must be divisible by num_heads (got `embed_dim`: "
478 + f"{self.embed_dim} and `num_heads`: {self.num_heads})."
479 + )
480 +
481 + self.scaling = self.head_dim**-0.5
482 +
483 + self.qkv = QKVParallelLinear(
484 + hidden_size=self.embed_dim,
485 + head_size=self.head_dim,
486 + total_num_heads=self.num_heads,
487 + total_num_kv_heads=self.num_heads,
488 + bias=True,
489 + prefix=f"{prefix}.qkv",
490 + )
491 +
492 + self.out_proj = RowParallelLinear(
493 + input_size=self.embed_dim,
494 + output_size=self.embed_dim,
495 + bias=True,
496 + prefix=f"{prefix}.out_proj",
497 + )
498 +
499 + self.attn = MMEncoderAttention(
500 + num_heads=self.num_local_heads,
501 + head_size=self.head_dim,
502 + scale=self.scaling,
503 + )
504 +
505 + def forward(
506 + self,
507 + hidden_states: torch.Tensor,
508 + cu_seqlens: torch.Tensor,
509 + max_seqlen: torch.Tensor | None,
510 + ) -> torch.Tensor:
511 + bs, seq_length, _ = hidden_states.size()
512 + qkv, _ = self.qkv(hidden_states)
513 + q, k, v = qkv.chunk(3, dim=-1)
514 + q = q.view(bs, seq_length, -1, self.head_dim)
515 + k = k.view(bs, seq_length, -1, self.head_dim)
516 + v = v.view(bs, seq_length, -1, self.head_dim)
517 +
518 + attn_output = self.attn(
519 + query=q,
520 + key=k,
521 + value=v,
522 + cu_seqlens=cu_seqlens,
523 + max_seqlen=max_seqlen,
524 + )
525 +
526 + attn_output = attn_output.view(bs, seq_length, -1)
527 + output, _ = self.out_proj(attn_output)
528 + return output
529 +
530 +
531 +class Transformer(nn.Module):
532 + def __init__(
533 + self,
534 + downsample_rate=2,
535 + encoder_dim=1280,
536 + llm_dim=4096,
537 + ffn_dim: int = 2048,
538 + prefix: str = "",
539 + **kwargs,
540 + ):
541 + super().__init__()
542 + self.k = downsample_rate
543 + self.encoder_dim = encoder_dim
544 + self.llm_dim = llm_dim
545 + self.linear1 = ColumnParallelLinear(
546 + input_size=self.encoder_dim * self.k,
547 + output_size=ffn_dim,
548 + bias=True,
549 + )
550 + self.relu = nn.ReLU()
551 + self.linear2 = RowParallelLinear(
552 + input_size=ffn_dim,
553 + output_size=self.llm_dim,
554 + bias=True,
555 + )
556 +
557 + self.blocks = None
558 + if kwargs.get("n_layer", 2) > 0:
559 + self.blocks = nn.ModuleList(
560 + [
561 + EncoderLayer(
562 + llm_dim,
563 + FunASRAudioAttention(
564 + kwargs.get("attention_heads", 8),
565 + llm_dim,
566 + prefix=f"{prefix}.self_attn",
567 + ),
568 + PositionwiseFeedForward(
569 + llm_dim,
570 + llm_dim // 4,
571 + ),
572 + )
573 + for _ in range(kwargs.get("n_layer", 2))
574 + ]
575 + )
576 +
577 + def forward(self, hidden_states: torch.Tensor, ilens: int = 0):
578 + batch_size, seq_len, dim = hidden_states.size()
579 + chunk_num = (seq_len - 1) // self.k + 1
580 + pad_num = chunk_num * self.k - seq_len
581 + hidden_states = F.pad(hidden_states, (0, 0, 0, pad_num, 0, 0), value=0.0)
582 + seq_len = hidden_states.size(1)
583 +
584 + hidden_states = hidden_states.contiguous()
585 + hidden_states = hidden_states.view(batch_size, chunk_num, dim * self.k)
586 + hidden_states, _ = self.linear1(hidden_states)
587 + hidden_states = self.relu(hidden_states)
588 + hidden_states, _ = self.linear2(hidden_states)
589 +
590 + olens = None
591 + olens = (ilens - 1) // self.k + 1
592 +
593 + if self.blocks is not None:
594 + for layer, block in enumerate(self.blocks):
595 + hidden_states = block(hidden_states)
596 + return hidden_states, olens
597 +
598 +
599 +class FunASRAudioInputs(TensorSchema):
600 + """
601 + Dimensions:
602 + - b: Batch size
603 + - nmb: Number of mel bins
604 + - t: Time frames (M)
605 + """
606 +
607 + input_features: Annotated[
608 + list[torch.Tensor] | None,
609 + TensorShape("b", "nmb", "t"),
610 + ]
611 + speech_lengths: Annotated[
612 + list[torch.Tensor] | None,
613 + TensorShape("b"),
614 + ]
615 +
616 +
617 +class FunASREncoder(nn.Module):
618 + def __init__(
619 + self, *, vllm_config: VllmConfig, prefix: str = "", init_in_fp32: bool = False
620 + ):
621 + super().__init__()
622 + self.audio_encoder = SenseVoiceEncoderSmall(
623 + input_size=560, **vllm_config.model_config.hf_config.audio_encoder_conf
624 + )
625 + self.audio_adaptor = Transformer(
626 + downsample_rate=1,
627 + use_low_frame_rate=True,
628 + ffn_dim=2048,
629 + llm_dim=1024,
630 + encoder_dim=512,
631 + n_layer=2,
632 + freeze=True,
633 + prefix=maybe_prefix(prefix, "audio_encoder"),
634 + )
635 +
636 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
637 + """Load weights with mapping from HuggingFace format."""
638 + stacked_params_mapping = [
639 + # (param_name, shard_name, shard_id)
640 + ("self_attn.qkv.", "self_attn.q_proj.", "q"),
641 + ("self_attn.qkv.", "self_attn.k_proj.", "k"),
642 + ("self_attn.qkv.", "self_attn.v_proj.", "v"),
643 + ]
644 + params_dict = dict(self.named_parameters(remove_duplicate=False))
645 + loaded_params: set[str] = set()
646 +
647 + for name, loaded_weight in weights:
648 + for param_name, weight_name, shard_id in stacked_params_mapping:
649 + if weight_name not in name:
650 + continue
651 + name = name.replace(weight_name, param_name)
652 +
653 + param = params_dict[name]
654 + weight_loader = param.weight_loader
655 + weight_loader(param, loaded_weight, shard_id)
656 + break
657 + else:
658 + param = params_dict.get(name)
659 + if param is not None:
660 + weight_loader = getattr(
661 + param, "weight_loader", default_weight_loader
662 + )
663 + weight_loader(param, loaded_weight)
664 + loaded_params.add(name)
665 + return loaded_params
666 +
667 +
668 +class FunASRModel(nn.Module):
669 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
670 + super().__init__()
671 + self.encoder = FunASREncoder(
672 + vllm_config=vllm_config, prefix=maybe_prefix(prefix, "encoder")
673 + )
674 + self.decoder = Qwen3Model(
675 + vllm_config=vllm_config, prefix=maybe_prefix(prefix, "decoder")
676 + )
677 +
678 + def forward(
679 + self,
680 + input_ids: torch.Tensor | None,
681 + positions: torch.Tensor,
682 + inputs_embeds: torch.Tensor | None = None,
683 + ) -> torch.Tensor:
684 + decoder_outputs = self.decoder(
685 + input_ids=input_ids,
686 + positions=positions,
687 + inputs_embeds=inputs_embeds,
688 + )
689 + return decoder_outputs
690 +
691 + def get_encoder_outputs(
692 + self,
693 + speech: torch.Tensor | list[torch.Tensor] | None,
694 + speech_lengths: torch.Tensor | list[torch.Tensor] | None,
695 + ) -> torch.Tensor | None:
696 + self.feat_permute = False
697 +
698 + if self.feat_permute:
699 + encoder_out, encoder_out_lens = self.encoder.audio_encoder(
700 + speech.permute(0, 2, 1), speech_lengths
701 + )
702 + else:
703 + encoder_out, encoder_out_lens = self.encoder.audio_encoder(
704 + speech, speech_lengths
705 + )
706 +
707 + encoder_out, encoder_out_lens = self.encoder.audio_adaptor(
708 + encoder_out, encoder_out_lens
709 + )
710 + return encoder_out
711 +
712 +
713 +class FunASRProcessingInfo(BaseProcessingInfo):
714 + def get_hf_config(self) -> Qwen3Config:
715 + return self.ctx.get_hf_config(Qwen3Config)
716 +
717 + @property
718 + def skip_prompt_length_check(self) -> bool:
719 + return True # Because the encoder prompt is padded
720 +
721 + def get_supported_mm_limits(self) -> Mapping[str, int | None]:
722 + return {"audio": 1}
723 +
724 + def get_feature_extractor(self, **kwargs: object) -> FunASRFeatureExtractor:
725 + hf_processor = self.get_hf_processor(**kwargs)
726 + feature_extractor = hf_processor.feature_extractor # type: ignore
727 + assert isinstance(feature_extractor, FunASRFeatureExtractor)
728 + return feature_extractor
729 +
730 + def get_target_channels(self) -> int:
731 + return 1
732 +
733 + def get_num_audio_tokens(self) -> int:
734 + return self.get_hf_config().max_source_positions
735 +
736 +
737 +class FunASRDummyInputsBuilder(BaseDummyInputsBuilder[FunASRProcessingInfo]):
738 + def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
739 + num_audios = mm_counts.get("audio", 0)
740 +
741 + return "<|AUDIO|>" * num_audios
742 +
743 + def get_dummy_mm_data(
744 + self,
745 + seq_len: int,
746 + mm_counts: Mapping[str, int],
747 + mm_options: Mapping[str, BaseDummyOptions] | None = None,
748 + ) -> MultiModalDataDict:
749 + feature_extractor = self.info.get_feature_extractor()
750 +
751 + sampling_rate = feature_extractor.sampling_rate
752 + audio_len = feature_extractor.chunk_length * sampling_rate
753 + num_audios = mm_counts.get("audio", 0)
754 +
755 + audio_overrides = mm_options.get("audio") if mm_options else None
756 +
757 + return {
758 + "audio": self._get_dummy_audios(
759 + length=audio_len, num_audios=num_audios, overrides=audio_overrides
760 + )
761 + }
762 +
763 +
764 +class FunASRMultiModalProcessor(BaseMultiModalProcessor[FunASRProcessingInfo]):
765 + def _get_data_parser(self) -> MultiModalDataParser:
766 + feature_extractor = self.info.get_feature_extractor()
767 + return MultiModalDataParser(
768 + target_sr=feature_extractor.sampling_rate,
769 + target_channels=self.info.get_target_channels(),
770 + )
771 +
772 + def _call_hf_processor(
773 + self,
774 + prompt: str,
775 + mm_data: Mapping[str, object],
776 + mm_kwargs: Mapping[str, object],
777 + tok_kwargs: Mapping[str, object],
778 + ) -> BatchFeature:
779 + if mm_data:
780 + feature_extractor = self.info.get_feature_extractor(**mm_kwargs)
781 + mm_data = dict(audio=mm_data.pop("audios"))
782 + mm_kwargs = dict(
783 + **mm_kwargs,
784 + sampling_rate=feature_extractor.sampling_rate,
785 + )
786 + processed_outputs = super()._call_hf_processor(
787 + prompt=prompt,
788 + mm_data=mm_data,
789 + mm_kwargs=mm_kwargs,
790 + tok_kwargs=tok_kwargs,
791 + )
792 + if "labels" in processed_outputs:
793 + processed_outputs["input_ids"] = processed_outputs.pop("labels")
794 + return processed_outputs
795 +
796 + def _get_mm_fields_config(
797 + self,
798 + hf_inputs: BatchFeature,
799 + hf_processor_mm_kwargs: Mapping[str, object],
800 + ) -> Mapping[str, MultiModalFieldConfig]:
801 + return dict(
802 + input_features=MultiModalFieldConfig.batched("audio"),
803 + speech_lengths=MultiModalFieldConfig.batched("audio"),
804 + fake_token_len=MultiModalFieldConfig.batched("audio"),
805 + )
806 +
807 + def _get_prompt_updates(
808 + self,
809 + mm_items: MultiModalDataItems,
810 + hf_processor_mm_kwargs: Mapping[str, object],
811 + out_mm_kwargs: MultiModalKwargsItems,
812 + ) -> Sequence[PromptUpdate]:
813 + processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
814 + tokenizer = self.info.get_tokenizer()
815 + vocab = tokenizer.get_vocab()
816 +
817 + # Use getattr with default to be compatible with transformers<4.48
818 + audio_token = getattr(processor, "audio_token", "<|AUDIO|>")
819 +
820 + audio_token_id = vocab[audio_token]
821 +
822 + out_mm_data = out_mm_kwargs.get_data()
823 +
824 + fake_token_len = out_mm_data.get("fake_token_len")
825 + if fake_token_len is None:
826 + audio_output_lengths = []
827 + else:
828 + assert isinstance(fake_token_len, torch.Tensor)
829 +
830 + audio_output_lengths = fake_token_len.tolist()
831 +
832 + def get_replacement_qwen2_audio(item_idx: int):
833 + if audio_output_lengths:
834 + num_features = audio_output_lengths[item_idx]
835 + else:
836 + audio_embeds = out_mm_data["audio_embeds"][item_idx]
837 + assert len(audio_embeds.shape) == 2, "audio_embeds must be a 2D tensor"
838 + num_features = audio_embeds.shape[0]
839 +
840 + audio_tokens = [audio_token_id] * num_features
841 +
842 + return PromptUpdateDetails.select_token_id(
843 + audio_tokens,
844 + embed_token_id=audio_token_id,
845 + )
846 +
847 + return [
848 + PromptReplacement(
849 + modality="audio",
850 + target=audio_token,
851 + replacement=get_replacement_qwen2_audio,
852 + )
853 + ]
854 +
855 +
856 +@MULTIMODAL_REGISTRY.register_processor(
857 + FunASRMultiModalProcessor,
858 + info=FunASRProcessingInfo,
859 + dummy_inputs=FunASRDummyInputsBuilder,
860 +)
861 +class FunASRForConditionalGeneration(
862 + nn.Module, SupportsTranscription, SupportsMultiModal
863 +):
864 + packed_modules_mapping = {
865 + "self_attn.qkv_proj": [
866 + "self_attn.q_proj",
867 + "self_attn.k_proj",
868 + "self_attn.v_proj",
869 + ],
870 + "encoder_attn.kv_proj": ["encoder_attn.k_proj", "encoder_attn.v_proj"],
871 + }
872 +
873 + hf_to_vllm_mapper = WeightsMapper(
874 + orig_to_new_substr={
875 + "linear_q.": "q_proj.",
876 + "linear_k.": "k_proj.",
877 + "linear_v.": "v_proj.",
878 + "linear_out.": "out_proj.",
879 + }
880 + )
881 +
882 + supports_transcription_only = True
883 + supports_segment_timestamp = True
884 + supported_languages = ISO639_1_SUPPORTED_LANGS
885 +
886 + @classmethod
887 + def validate_language(cls, language: str | None) -> str | None:
888 + if language is None:
889 + # TODO language should be optional and can be guessed.
890 + # For now we default to en. See
891 + # https://github.com/huggingface/transformers/blob/main/src/transformers/models/whisper/generation_whisper.py#L1520
892 + logger.warning(
893 + "Defaulting to language='en'. If you wish to transcribe "
894 + "audio in a different language, pass the `language` field "
895 + "in the TranscriptionRequest."
896 + )
897 + language = "en"
898 + return super().validate_language(language)
899 +
900 + @classmethod
901 + def get_generation_prompt(
902 + cls,
903 + audio: np.ndarray,
904 + model_config: ModelConfig, # not needed here
905 + stt_config: SpeechToTextConfig,
906 + language: str | None,
907 + task_type: Literal["transcribe", "translate"],
908 + request_prompt: str,
909 + to_language: str | None,
910 + ) -> PromptType:
911 + if language is None:
912 + raise ValueError(
913 + "Language must be specified when creating the funasr prompt"
914 + )
915 +
916 + funasr_prompt = "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n语音转写:<|AUDIO|><|im_end|>\n<|im_start|>assistant\n" # noqa: E501
917 + prompt = {
918 + "prompt": funasr_prompt,
919 + "multi_modal_data": {
920 + "audio": (audio, stt_config.sample_rate),
921 + },
922 + }
923 + return cast(PromptType, prompt)
924 +
925 + @classmethod
926 + def get_speech_to_text_config(
927 + cls, model_config: ModelConfig, task_type: str
928 + ) -> SpeechToTextConfig:
929 + processor = cached_processor_from_config(model_config)
930 +
931 + return SpeechToTextConfig(
932 + max_audio_clip_s=processor.feature_extractor.chunk_length,
933 + sample_rate=processor.feature_extractor.sampling_rate,
934 + )
935 +
936 + @classmethod
937 + def get_num_audio_tokens(
938 + cls,
939 + audio_duration_s: float,
940 + stt_config: SpeechToTextConfig,
941 + model_config: ModelConfig,
942 + ) -> int | None:
943 + processor = cached_processor_from_config(model_config)
944 + hop_length = processor.feature_extractor.hop_length
945 + assert hop_length is not None
946 + return math.ceil(audio_duration_s * stt_config.sample_rate / hop_length)
947 +
948 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
949 + super().__init__()
950 + config = vllm_config.model_config.hf_config
951 + quant_config = vllm_config.quant_config
952 + self.config = config
953 + self.dtype = vllm_config.model_config.dtype
954 +
955 + self.model = FunASRModel(
956 + vllm_config=vllm_config,
957 + prefix=maybe_prefix(prefix, "model"),
958 + )
959 + logit_scale = getattr(config, "logit_scale", 1.0)
960 +
961 + if config.tie_word_embeddings:
962 + self.lm_head = self.model.decoder.embed_tokens
963 + else:
964 + self.lm_head = ParallelLMHead(
965 + config.vocab_size,
966 + config.hidden_size,
967 + quant_config=quant_config,
968 + prefix=maybe_prefix(prefix, "lm_head"),
969 + )
970 + self.logits_processor = LogitsProcessor(config.vocab_size, scale=logit_scale)
971 +
972 + def forward(
973 + self,
974 + input_ids: torch.Tensor,
975 + positions: torch.Tensor,
976 + inputs_embeds: torch.Tensor | None = None,
977 + **kwargs,
978 + ) -> torch.Tensor:
979 + decoder_outputs = self.model(
980 + input_ids=input_ids,
981 + positions=positions,
982 + inputs_embeds=inputs_embeds,
983 + )
984 + return decoder_outputs
985 +
986 + def get_language_model(self) -> torch.nn.Module:
987 + return self.model.decoder
988 +
989 + def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
990 + audio_input = self._parse_and_validate_audio_input(**kwargs)
991 +
992 + speech = audio_input["input_features"]
993 + speech_lengths = audio_input["speech_lengths"]
994 + enc_output = self.model.get_encoder_outputs(
995 + speech=speech, speech_lengths=speech_lengths
996 + )
997 +
998 + return enc_output
999 +
1000 + def embed_input_ids(
1001 + self,
1002 + input_ids: torch.Tensor,
1003 + multimodal_embeddings: MultiModalEmbeddings | None = None,
1004 + *,
1005 + is_multimodal: torch.Tensor | None = None,
1006 + handle_oov_mm_token: bool = False,
1007 + ) -> torch.Tensor:
1008 + inputs_embeds = self.model.decoder.embed_input_ids(input_ids)
1009 +
1010 + return _merge_multimodal_embeddings(
1011 + inputs_embeds=inputs_embeds,
1012 + multimodal_embeddings=multimodal_embeddings,
1013 + is_multimodal=_require_is_multimodal(is_multimodal),
1014 + )
1015 +
1016 + def _parse_and_validate_audio_input(self, **kwargs: object) -> FunASRAudioInputs:
1017 + input_features = kwargs.pop("input_features", None)
1018 + speech_lengths = kwargs.pop("speech_lengths", None)
1019 +
1020 + if input_features is not None:
1021 + input_features = json_map_leaves(lambda x: x.to(self.dtype), input_features)
1022 +
1023 + if speech_lengths is not None:
1024 + speech_lengths = json_map_leaves(lambda x: x.to(self.dtype), speech_lengths)
1025 +
1026 + return FunASRAudioInputs(
1027 + input_features=input_features, speech_lengths=speech_lengths
1028 + )
1029 +
1030 + def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
1031 + logits = self.logits_processor(self.lm_head, hidden_states)
1032 + return logits
1033 +
1034 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1035 + loader = AutoWeightsLoader(
1036 + self,
1037 + )
1038 +
1039 + # add fake zeros bias for k_proj to state_dict
1040 + weights = _create_fake_bias_for_k_proj(weights)
1041 + return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1042 +
1043 +
1044 +def _create_fake_bias_for_k_proj(
1045 + weights: Iterable[tuple[str, torch.Tensor]],
1046 +) -> Iterable[tuple[str, torch.Tensor]]:
1047 + """
1048 + Create full zeros bias for k_proj weight in self-attn and x-attn layers.
1049 + So that the bias for k_proj in qkv_proj can be initialized with zeros.
1050 + """
1051 + for name, weight in weights:
1052 + if name.endswith(".k_proj.weight"):
1053 + bias = torch.zeros(weight.size(0))
1054 + bias_name = name.replace("weight", "bias")
1055 + yield from [(name, weight), (bias_name, bias)]
1056 + else:
1057 + yield name, weight
vllm/model_executor/models/registry.py
+1
@@ -325,6 +325,7 @@ _MULTIMODAL_MODELS = {
325 "ernie45_vl",
326 "Ernie4_5_VLMoeForConditionalGeneration",
327 ),
328 + "FunASRForConditionalGeneration": ("funasr", "FunASRForConditionalGeneration"), # noqa: E501
329 "FunAudioChatForConditionalGeneration": (
330 "funaudiochat",
331 "FunAudioChatForConditionalGeneration",
vllm/transformers_utils/processors/__init__.py
+2
@@ -10,6 +10,7 @@ reasons:
10
11 from vllm.transformers_utils.processors.bagel import BagelProcessor
12 from vllm.transformers_utils.processors.deepseek_vl2 import DeepseekVLV2Processor
13 +from vllm.transformers_utils.processors.funasr_processor import FunASRProcessor
14 from vllm.transformers_utils.processors.hunyuan_vl import HunYuanVLProcessor
15 from vllm.transformers_utils.processors.hunyuan_vl_image import HunYuanVLImageProcessor
16 from vllm.transformers_utils.processors.ovis import OvisProcessor
@@ -18,6 +19,7 @@ from vllm.transformers_utils.processors.ovis2_5 import Ovis2_5Processor
19 __all__ = [
20 "BagelProcessor",
21 "DeepseekVLV2Processor",
22 + "FunASRProcessor",
23 "HunYuanVLProcessor",
24 "HunYuanVLImageProcessor",
25 "OvisProcessor",
vllm/transformers_utils/processors/funasr_processor.py new
+504
@@ -0,0 +1,504 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +
5 +import numpy as np
6 +import torch
7 +import torch.nn as nn
8 +import torchaudio.compliance.kaldi as kaldi
9 +from torch.nn.utils.rnn import pad_sequence
10 +from transformers import (
11 + AutoFeatureExtractor,
12 + AutoProcessor,
13 + BatchFeature,
14 +)
15 +from transformers.feature_extraction_sequence_utils import SequenceFeatureExtractor
16 +from transformers.processing_utils import ProcessorMixin
17 +from transformers.utils import TensorType
18 +
19 +from vllm.logger import init_logger
20 +
21 +logger = init_logger(__name__)
22 +
23 +
24 +def apply_cmvn(inputs, cmvn): # noqa
25 + """
26 + Apply CMVN with mvn data
27 + """
28 +
29 + device = inputs.device
30 + # dtype = inputs.dtype
31 + frame, dim = inputs.shape
32 +
33 + means = cmvn[0:1, :dim]
34 + vars = cmvn[1:2, :dim]
35 + inputs += means.to(device)
36 + inputs *= vars.to(device)
37 +
38 + return inputs.type(torch.float32)
39 +
40 +
41 +def apply_lfr(inputs, lfr_m, lfr_n):
42 + # LFR_inputs = []
43 + T = inputs.shape[0]
44 + T_lfr = int(np.ceil(T / lfr_n))
45 + left_padding = inputs[0].repeat((lfr_m - 1) // 2, 1)
46 + inputs = torch.vstack((left_padding, inputs))
47 + T = T + (lfr_m - 1) // 2
48 + feat_dim = inputs.shape[-1]
49 + strides = (lfr_n * feat_dim, 1)
50 + sizes = (T_lfr, lfr_m * feat_dim)
51 + last_idx = (T - lfr_m) // lfr_n + 1
52 + num_padding = lfr_m - (T - last_idx * lfr_n)
53 + if num_padding > 0:
54 + num_padding = (
55 + (2 * lfr_m - 2 * T + (T_lfr - 1 + last_idx) * lfr_n)
56 + / 2
57 + * (T_lfr - last_idx)
58 + )
59 + inputs = torch.vstack([inputs] + [inputs[-1:]] * int(num_padding))
60 + LFR_outputs = inputs.as_strided(sizes, strides)
61 + return LFR_outputs.clone().type(torch.float32)
62 +
63 +
64 +def load_cmvn(cmvn_file):
65 + with open(cmvn_file, encoding="utf-8") as f:
66 + lines = f.readlines()
67 + means_list = []
68 + vars_list = []
69 + for i in range(len(lines)):
70 + line_item = lines[i].split()
71 + if line_item[0] == "<AddShift>":
72 + line_item = lines[i + 1].split()
73 + if line_item[0] == "<LearnRateCoef>":
74 + add_shift_line = line_item[3 : (len(line_item) - 1)]
75 + means_list = list(add_shift_line)
76 + continue
77 + elif line_item[0] == "<Rescale>":
78 + line_item = lines[i + 1].split()
79 + if line_item[0] == "<LearnRateCoef>":
80 + rescale_line = line_item[3 : (len(line_item) - 1)]
81 + vars_list = list(rescale_line)
82 + continue
83 + means = np.array(means_list).astype(np.float32)
84 + vars = np.array(vars_list).astype(np.float32)
85 + cmvn = np.array([means, vars])
86 + cmvn = torch.as_tensor(cmvn, dtype=torch.float32)
87 + return cmvn
88 +
89 +
90 +class WavFrontend(nn.Module):
91 + """Conventional frontend structure for ASR."""
92 +
93 + def __init__(
94 + self,
95 + cmvn_file: str = "null",
96 + fs: int = 16000,
97 + window: str = "hamming",
98 + n_mels: int = 80,
99 + frame_length: int = 25,
100 + frame_shift: int = 10,
101 + filter_length_min: int = -1,
102 + filter_length_max: int = -1,
103 + lfr_m: int = 1,
104 + lfr_n: int = 1,
105 + dither: float = 1.0,
106 + snip_edges: bool = True,
107 + upsacle_samples: bool = True,
108 + **kwargs,
109 + ):
110 + super().__init__()
111 + self.fs = fs
112 + self.window = window
113 + self.n_mels = n_mels
114 + self.frame_length = frame_length
115 + self.frame_shift = frame_shift
116 + self.filter_length_min = filter_length_min
117 + self.filter_length_max = filter_length_max
118 + self.lfr_m = lfr_m
119 + self.lfr_n = lfr_n
120 + self.cmvn_file = cmvn_file
121 + self.dither = dither
122 + self.snip_edges = snip_edges
123 + self.upsacle_samples = upsacle_samples
124 + self.cmvn = None if self.cmvn_file is None else load_cmvn(self.cmvn_file)
125 +
126 + def output_size(self) -> int:
127 + return self.n_mels * self.lfr_m
128 +
129 + def forward(
130 + self,
131 + input: torch.Tensor,
132 + input_lengths,
133 + **kwargs,
134 + ) -> tuple[torch.Tensor, torch.Tensor]:
135 + batch_size = input.size(0)
136 + feats = []
137 + feats_lens = []
138 + for i in range(batch_size):
139 + waveform_length = input_lengths[i]
140 + waveform = input[i][:waveform_length]
141 + if self.upsacle_samples:
142 + waveform = waveform * (1 << 15)
143 + waveform = waveform.unsqueeze(0)
144 + mat = kaldi.fbank(
145 + waveform,
146 + num_mel_bins=self.n_mels,
147 + frame_length=min(self.frame_length, waveform_length / self.fs * 1000),
148 + frame_shift=self.frame_shift,
149 + dither=self.dither,
150 + energy_floor=0.0,
151 + window_type=self.window,
152 + sample_frequency=self.fs,
153 + snip_edges=self.snip_edges,
154 + )
155 +
156 + if self.lfr_m != 1 or self.lfr_n != 1:
157 + mat = apply_lfr(mat, self.lfr_m, self.lfr_n)
158 + if self.cmvn is not None:
159 + mat = apply_cmvn(mat, self.cmvn)
160 + feat_length = mat.size(0)
161 + feats.append(mat)
162 + feats_lens.append(feat_length)
163 +
164 + feats_lens = torch.as_tensor(feats_lens)
165 + if batch_size == 1:
166 + feats_pad = feats[0][None, :, :]
167 + else:
168 + feats_pad = pad_sequence(feats, batch_first=True, padding_value=0.0)
169 + return feats_pad, feats_lens
170 +
171 + def forward_fbank(
172 + self, input: torch.Tensor, input_lengths: torch.Tensor
173 + ) -> tuple[torch.Tensor, torch.Tensor]:
174 + batch_size = input.size(0)
175 + feats = []
176 + feats_lens = []
177 + for i in range(batch_size):
178 + waveform_length = input_lengths[i]
179 + waveform = input[i][:waveform_length]
180 + waveform = waveform * (1 << 15)
181 + waveform = waveform.unsqueeze(0)
182 + mat = kaldi.fbank(
183 + waveform,
184 + num_mel_bins=self.n_mels,
185 + frame_length=self.frame_length,
186 + frame_shift=self.frame_shift,
187 + dither=self.dither,
188 + energy_floor=0.0,
189 + window_type=self.window,
190 + sample_frequency=self.fs,
191 + )
192 +
193 + feat_length = mat.size(0)
194 + feats.append(mat)
195 + feats_lens.append(feat_length)
196 +
197 + feats_lens = torch.as_tensor(feats_lens)
198 + feats_pad = pad_sequence(feats, batch_first=True, padding_value=0.0)
199 + return feats_pad, feats_lens
200 +
201 + def forward_lfr_cmvn(
202 + self, input: torch.Tensor, input_lengths: torch.Tensor
203 + ) -> tuple[torch.Tensor, torch.Tensor]:
204 + batch_size = input.size(0)
205 + feats = []
206 + feats_lens = []
207 + for i in range(batch_size):
208 + mat = input[i, : input_lengths[i], :]
209 + if self.lfr_m != 1 or self.lfr_n != 1:
210 + mat = apply_lfr(mat, self.lfr_m, self.lfr_n)
211 + if self.cmvn is not None:
212 + mat = apply_cmvn(mat, self.cmvn)
213 + feat_length = mat.size(0)
214 + feats.append(mat)
215 + feats_lens.append(feat_length)
216 +
217 + feats_lens = torch.as_tensor(feats_lens)
218 + feats_pad = pad_sequence(feats, batch_first=True, padding_value=0.0)
219 + return feats_pad, feats_lens
220 +
221 +
222 +class FunASRFeatureExtractor(SequenceFeatureExtractor):
223 + r"""
224 + Constructs a FunASR feature extractor.
225 +
226 + This feature extractor inherits from [`~feature_extraction_sequence_
227 + utils.SequenceFeatureExtractor`] which contains most of the main
228 + methods. Users should refer to this superclass for more information
229 + regarding those methods.
230 +
231 + This class extracts mel-filter bank features from raw speech using a custom
232 + numpy implementation of the `Short Time Fourier Transform` which should
233 + match pytorch's `torch.stft` equivalent.
234 +
235 + Args:
236 + feature_size (`int`, *optional*, defaults to 80):
237 + The feature dimension of the extracted features.
238 + sampling_rate (`int`, *optional*, defaults to 16000):
239 + The sampling rate at which the audio files should be digitalized
240 + expressed in hertz (Hz).
241 + hop_length (`int`, *optional*, defaults to 160):
242 + Length of the overlapping windows for the STFT used to obtain the
243 + Mel Frequency coefficients.
244 + chunk_length (`int`, *optional*, defaults to 30):
245 + The maximum number of chunks of `sampling_rate` samples used to
246 + trim and pad longer or shorter audio sequences.
247 + n_fft (`int`, *optional*, defaults to 400):
248 + Size of the Fourier transform.
249 + padding_value (`float`, *optional*, defaults to 0.0):
250 + Padding value used to pad the audio. Should correspond to silences.
251 + dither (`float`, *optional*, defaults to 0.0):
252 + Adds dithering. In other words, adds a small Gaussian noise to each frame.
253 + E.g. use 0.0001 to add dithering with a normal distribution centered
254 + around 0.0 with standard deviation 0.0001 (assuming [-1,+1] range
255 + of raw_speech). The value 0.0 means no dithering.
256 + Dithering has similar effect as `spectrogram(mel_floor=...)`. It reduces
257 + the high log_mel_fbank values for signals with hard-zero sections,
258 + when VAD cutoff is present in the signal.
259 + """
260 +
261 + model_input_names = ["input_features"]
262 +
263 + def __init__(
264 + self,
265 + feature_size=80,
266 + sampling_rate=16000,
267 + hop_length=160,
268 + chunk_length=30,
269 + n_fft=400,
270 + padding_value=0.0,
271 + dither=0.0,
272 + return_attention_mask=False,
273 + **kwargs,
274 + ):
275 + super().__init__(
276 + feature_size=feature_size,
277 + sampling_rate=sampling_rate,
278 + padding_value=padding_value,
279 + return_attention_mask=return_attention_mask,
280 + **kwargs,
281 + )
282 + self.frontend_conf = kwargs.get("frontend_conf", {})
283 + self.n_fft = n_fft
284 + self.hop_length = hop_length
285 + self.chunk_length = chunk_length
286 + self.n_samples = chunk_length * sampling_rate
287 + self.nb_max_frames = self.n_samples // hop_length
288 + self.sampling_rate = sampling_rate
289 + self.dither = dither
290 +
291 + def extract_fbank(
292 + self, data, data_len=None, data_type: str = "sound", frontend=None, **kwargs
293 + ):
294 + if isinstance(data, np.ndarray):
295 + data = torch.from_numpy(data)
296 + if len(data.shape) < 2:
297 + data = data[None, :] # data: [batch, N]
298 + data_len = [data.shape[1]] if data_len is None else data_len
299 + elif isinstance(data, torch.Tensor):
300 + if len(data.shape) < 2:
301 + data = data[None, :] # data: [batch, N]
302 + data_len = [data.shape[1]] if data_len is None else data_len
303 + elif isinstance(data, (list, tuple)):
304 + data_list, data_len = [], []
305 + for data_i in data:
306 + if isinstance(data_i, np.ndarray):
307 + data_i = torch.from_numpy(data_i)
308 + data_list.append(data_i)
309 + data_len.append(data_i.shape[0])
310 + data = pad_sequence(data_list, batch_first=True)
311 +
312 + data, data_len = frontend(data, data_len, **kwargs)
313 +
314 + if isinstance(data_len, (list, tuple)):
315 + data_len = torch.tensor([data_len])
316 + return data.to(torch.float32), data_len.to(torch.int32)
317 +
318 + def __call__(
319 + self,
320 + raw_speech: np.ndarray | list[float] | list[np.ndarray] | list[list[float]],
321 + truncation: bool = True,
322 + pad_to_multiple_of: int | None = None,
323 + return_tensors: str | TensorType | None = None,
324 + return_attention_mask: bool | None = None,
325 + padding: str | None = "max_length",
326 + max_length: int | None = None,
327 + sampling_rate: int | None = None,
328 + do_normalize: bool | None = None,
329 + device: str | None = "cpu",
330 + return_token_timestamps: bool | None = None,
331 + **kwargs,
332 + ) -> BatchFeature:
333 + is_batched = isinstance(raw_speech, (list, tuple)) and (
334 + isinstance(raw_speech[0], (np.ndarray, tuple, list))
335 + )
336 +
337 + if is_batched:
338 + raw_speech = [
339 + np.asarray([speech], dtype=np.float32).T for speech in raw_speech
340 + ]
341 + elif not is_batched and not isinstance(raw_speech, np.ndarray):
342 + raw_speech = np.asarray(raw_speech, dtype=np.float32)
343 + elif isinstance(raw_speech, np.ndarray) and raw_speech.dtype is np.dtype(
344 + np.float64
345 + ):
346 + raw_speech = raw_speech.astype(np.float32)
347 +
348 + if not is_batched:
349 + raw_speech = [np.asarray([raw_speech]).T]
350 +
351 + batched_speech = BatchFeature({"input_features": raw_speech})
352 +
353 + padded_inputs = self.pad(
354 + batched_speech,
355 + padding=padding,
356 + max_length=max_length if max_length else self.n_samples,
357 + truncation=truncation,
358 + pad_to_multiple_of=pad_to_multiple_of,
359 + return_attention_mask=return_attention_mask or do_normalize,
360 + )
361 +
362 + input_features = padded_inputs.get("input_features").transpose(2, 0, 1)
363 +
364 + self.frontend = WavFrontend(**self.frontend_conf)
365 + input_features, speech_lengths = self.extract_fbank(
366 + input_features[0],
367 + data_type=kwargs.get("data_type", "sound"),
368 + frontend=self.frontend,
369 + is_final=True,
370 + )
371 + olens = 1 + (speech_lengths - 3 + 2 * 1) // 2
372 + olens = 1 + (olens - 3 + 2 * 1) // 2
373 + fake_token_len = (olens - 1) // 2 + 1
374 + if isinstance(input_features[0], list):
375 + padded_inputs["input_features"] = [
376 + np.asarray(feature, dtype=np.float32) for feature in input_features
377 + ]
378 +
379 + else:
380 + padded_inputs["input_features"] = input_features
381 +
382 + if return_tensors is not None:
383 + padded_inputs = padded_inputs.convert_to_tensors(return_tensors)
384 +
385 + padded_inputs["speech_lengths"] = speech_lengths
386 + padded_inputs["fake_token_len"] = fake_token_len
387 +
388 + return padded_inputs
389 +
390 +
391 +class FunASRProcessor(ProcessorMixin):
392 + r"""
393 + Constructs a FunASR processor which wraps a FunASR feature extractor and
394 + a FunASR tokenizer into a single processor.
395 +
396 + [`FunASRProcessor`] offers all the functionalities of
397 + [`FunASRFeatureExtractor`] and [`Qwen2Tokenizer`]. See the
398 + [`~FunASRProcessor.__call__`] and [`~FunASRProcessor.decode`] for more
399 + information.
400 +
401 + Args:
402 + feature_extractor (`FunASRFeatureExtractor`): An instance of
403 + [`FunASRFeatureExtractor`].
404 + The feature extractor is a required input.
405 + tokenizer (`Qwen2Tokenizer`):
406 + An instance of [`Qwen2Tokenizer`]. The tokenizer is a required
407 + input.
408 + """
409 +
410 + feature_extractor_class = "FunASRFeatureExtractor"
411 + tokenizer_class = ("Qwen2Tokenizer", "Qwen2TokenizerFast")
412 +
413 + def __init__(
414 + self,
415 + feature_extractor,
416 + tokenizer,
417 + audio_token="<|AUDIO|>",
418 + ):
419 + super().__init__(feature_extractor, tokenizer)
420 + self.current_processor = self.feature_extractor
421 + self._in_target_context_manager = False
422 + self.audio_token = (
423 + tokenizer.audio_token if hasattr(tokenizer, "audio_token") else audio_token
424 + )
425 + self.audio_token_id = tokenizer.convert_tokens_to_ids(self.audio_token)
426 +
427 + def get_decoder_prompt_ids(self, task=None, language=None, no_timestamps=True):
428 + return self.tokenizer.get_decoder_prompt_ids(
429 + task=task, language=language, no_timestamps=no_timestamps
430 + )
431 +
432 + def __call__(self, *args, **kwargs):
433 + """
434 + Forwards the `audio` argument to FunASRFeatureExtractor's
435 + [`~FunASRFeatureExtractor.__call__`] and the `text` argument to
436 + [`~Qwen2Tokenizer.__call__`]. Please refer to the docstring of the
437 + above two methods for more information.
438 + """
439 + if self._in_target_context_manager:
440 + return self.current_processor(*args, **kwargs)
441 +
442 + audio = kwargs.pop("audio", None)
443 + sampling_rate = kwargs.pop("sampling_rate", None)
444 + text = kwargs.pop("text", None)
445 + if len(args) > 0:
446 + audio = args[0]
447 + args = args[1:]
448 +
449 + if text is None:
450 + raise ValueError("You need to specify `text` input to process.")
451 + elif isinstance(text, str):
452 + text = [text]
453 + elif not isinstance(text, list) and not isinstance(text[0], str):
454 + raise ValueError(
455 + "Invalid input text. Please provide a string, or a list of strings"
456 + )
457 +
458 + if audio is not None:
459 + # ensure we have as much audios as audio tokens
460 + num_audio_tokens = sum(sample.count(self.audio_token) for sample in text)
461 + num_audios = 1 if type(audio) is np.ndarray else len(audio)
462 + if num_audio_tokens != num_audios:
463 + raise ValueError(
464 + f"Found {num_audio_tokens} {self.audio_token} token{'s' if num_audio_tokens > 1 else ''} in provided text but received {num_audios} audio{'s' if num_audios > 1 else ''}" # noqa: E501
465 + )
466 + inputs = self.feature_extractor(
467 + audio, *args, sampling_rate=sampling_rate, **kwargs
468 + )
469 +
470 + expanded_text = []
471 + for sample in text:
472 + replace_str = []
473 + while self.audio_token in sample:
474 + num_audio_tokens = inputs["fake_token_len"].item()
475 +
476 + expanded_audio_token = self.audio_token * num_audio_tokens
477 +
478 + replace_str.append(expanded_audio_token)
479 + sample = sample.replace(self.audio_token, "<placeholder>", 1)
480 +
481 + while "<placeholder>" in sample:
482 + sample = sample.replace("<placeholder>", replace_str.pop(0), 1)
483 + expanded_text.append(sample)
484 + text = expanded_text
485 +
486 + if text is not None:
487 + encodings = self.tokenizer(text, **kwargs)
488 +
489 + if text is None:
490 + return inputs
491 +
492 + elif audio is None:
493 + return encodings
494 + else:
495 + inputs["labels"] = encodings["input_ids"]
496 +
497 + return inputs
498 +
499 + def get_prompt_ids(self, text: str, return_tensors="np"):
500 + return self.tokenizer.get_prompt_ids(text, return_tensors=return_tensors)
501 +
502 +
503 +AutoFeatureExtractor.register("FunASRFeatureExtractor", FunASRFeatureExtractor)
504 +AutoProcessor.register("FunASRProcessor", FunASRProcessor)