[Model] Support MOSS-Transcribe-Diarize (#47729)
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
Canlin Guo committed
Jul 8, 2026 at 19:05 UTC
285c08c036880d404e35b175008934a6aeeb4832
7 files changed
+925
docs/models/supported_models.md
+2
@@ -569,6 +569,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
569
| `MolmoForCausalLM` | Molmo | T + I<sup>+</sup> | `allenai/Molmo-7B-D-0924`, `allenai/Molmo-7B-O-0924`, etc. | ✅︎ | ✅︎ |
570
| `Molmo2ForConditionalGeneration` | Molmo2 | T + I<sup>+</sup> / V | `allenai/Molmo2-4B`, `allenai/Molmo2-8B`, `allenai/Molmo2-O-7B`, `allenai/MolmoWeb-4B`<sup>^</sup>, `allenai/MolmoWeb-8B`<sup>^</sup> | ✅︎ | ✅︎ |
571
| `MossAudioModel` | MOSS-Audio | T + A<sup>+</sup> | `OpenMOSS-Team/MOSS-Audio-4B-Instruct`, `OpenMOSS-Team/MOSS-Audio-4B-Thinking`, `OpenMOSS-Team/MOSS-Audio-8B-Instruct`, `OpenMOSS-Team/MOSS-Audio-8B-Thinking` | ✅︎ | ✅︎ |
572
+| `MossTranscribeDiarizeForConditionalGeneration` | MOSS-Transcribe-Diarize | T + A | `OpenMOSS-Team/MOSS-Transcribe-Diarize` | | ✅︎ |
573
| `Moondream3ForCausalLM` | Moondream3 | T + I | `moondream/moondream3-preview` | | ✅︎ |
574
| `NVLM_D_Model` | NVLM-D 1.0 | T + I<sup>+</sup> | `nvidia/NVLM-D-72B`, etc. | | ✅︎ |
575
| `OpenCUAForConditionalGeneration` | OpenCUA-7B | T + I<sup>E+</sup> | `xlangai/OpenCUA-7B` | ✅︎ | ✅︎ |
@@ -671,6 +672,7 @@ Speech2Text models trained specifically for Automatic Speech Recognition.
672
| `GlmAsrForConditionalGeneration` | GLM-ASR | `zai-org/GLM-ASR-Nano-2512` | ✅︎ | ✅︎ |
673
| `GraniteSpeechForConditionalGeneration` | Granite Speech | `ibm-granite/granite-4.0-1b-speech`, `ibm-granite/granite-speech-3.3-2b`, etc. | ✅︎ | ✅︎ |
674
| `GraniteSpeechPlusForConditionalGeneration` | Granite Speech Plus | `ibm-granite/granite-speech-4.1-2b-plus` | ✅︎ | ✅︎ |
675
+| `MossTranscribeDiarizeForConditionalGeneration` | MOSS-Transcribe-Diarize | `OpenMOSS-Team/MOSS-Transcribe-Diarize` | | ✅︎ |
676
| `Qwen3ASRForConditionalGeneration` | Qwen3-ASR | `Qwen/Qwen3-ASR-1.7B`, etc. | ✅︎ | ✅︎ |
677
| `Qwen3OmniMoeThinkerForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct`, etc. | | ✅︎ |
678
| `VoxtralForConditionalGeneration` | Voxtral (Mistral format) | `mistralai/Voxtral-Mini-3B-2507`, `mistralai/Voxtral-Small-24B-2507`, etc. | ✅︎ | ✅︎ |
tests/models/registry.py
+5
@@ -1127,6 +1127,11 @@ _MULTIMODAL_EXAMPLE_MODELS = {
1127
},
1128
trust_remote_code=True,
1129
),
1130
+ "MossTranscribeDiarizeForConditionalGeneration": _HfExamplesInfo(
1131
+ "OpenMOSS-Team/MOSS-Transcribe-Diarize",
1132
+ trust_remote_code=True,
1133
+ is_available_online=False,
1134
+ ),
1135
"HfMoondream": _HfExamplesInfo(
1136
"moondream/moondream3-preview",
1137
tokenizer="moondream/starmie-v1",
vllm/model_executor/models/moss_transcribe_diarize.py
new
+801
@@ -0,0 +1,801 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+"""Inference-only MOSS-Transcribe-Diarize ASR model.
5
+
6
+The checkpoint layout is:
7
+
8
+* ``model.whisper_encoder.*``: Whisper-medium encoder weights.
9
+* ``model.vq_adaptor.*``: 4x time-merge projector.
10
+* ``model.language_model.*``: Qwen3-0.6B decoder weights.
11
+"""
12
+
13
+import math
14
+from collections.abc import Iterable, Mapping, Sequence
15
+from typing import Annotated, Any, Literal, TypeAlias
16
+
17
+import torch
18
+from torch import nn
19
+from transformers import BatchFeature
20
+
21
+from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
22
+from vllm.config.multimodal import BaseDummyOptions
23
+from vllm.config.speech_to_text import SpeechToTextParams
24
+from vllm.inputs import ModalityData, MultiModalDataDict, PromptType, TextPrompt
25
+from vllm.model_executor.models.interfaces import (
26
+ MultiModalEmbeddings,
27
+ SupportsMultiModal,
28
+ SupportsPP,
29
+ SupportsTranscription,
30
+ _require_is_multimodal,
31
+)
32
+from vllm.model_executor.models.utils import (
33
+ AutoWeightsLoader,
34
+ WeightsMapper,
35
+ _merge_multimodal_embeddings,
36
+ init_vllm_registered_model,
37
+ maybe_prefix,
38
+)
39
+from vllm.model_executor.models.whisper import (
40
+ WhisperEncoder,
41
+ _create_fake_bias_for_k_proj,
42
+)
43
+from vllm.model_executor.models.whisper_utils import ISO639_1_SUPPORTED_LANGS
44
+from vllm.multimodal import MULTIMODAL_REGISTRY
45
+from vllm.multimodal.inputs import (
46
+ AudioItem,
47
+ MultiModalFieldConfig,
48
+ MultiModalKwargsItems,
49
+)
50
+from vllm.multimodal.parse import (
51
+ DictEmbeddingItems,
52
+ ModalityDataItems,
53
+ MultiModalDataItems,
54
+ MultiModalDataParser,
55
+)
56
+from vllm.multimodal.processing import (
57
+ BaseDummyInputsBuilder,
58
+ BaseMultiModalProcessor,
59
+ BaseProcessingInfo,
60
+ PromptReplacement,
61
+ PromptUpdate,
62
+ PromptUpdateDetails,
63
+)
64
+from vllm.multimodal.processing.processor import ProcessorInputs
65
+from vllm.sequence import IntermediateTensors
66
+from vllm.transformers_utils.processor import cached_processor_from_config
67
+from vllm.utils.tensor_schema import TensorSchema, TensorShape
68
+
69
+WHISPER_ENCODER_STRIDE = 2
70
+
71
+AUDIO_PLACEHOLDER = "<|audio_start|><|audio_pad|><|audio_end|>"
72
+
73
+DEFAULT_MOSS_TRANSCRIBE_DIARIZE_PROMPT = (
74
+ "请将音频转写为文本,每一段需以起始时间戳和说话人编号"
75
+ "([S01]、[S02]、[S03]…)开头,正文为对应的语音内容,"
76
+ "并在段末标注结束时间戳,以清晰标明该段语音范围。"
77
+)
78
+
79
+
80
+class MossTranscribeDiarizeAudioInputs(TensorSchema):
81
+ """
82
+ Dimensions:
83
+ - c: Audio chunks
84
+ - m: Mel bins
85
+ - f: Mel frames
86
+ - n: Number of audio items
87
+ """
88
+
89
+ type: Literal["audio_features"] = "audio_features"
90
+
91
+ input_features: Annotated[
92
+ torch.Tensor | None,
93
+ TensorShape("c", "m", "f"),
94
+ ]
95
+ audio_feature_lengths: Annotated[
96
+ torch.Tensor | None,
97
+ TensorShape("c"),
98
+ ]
99
+ audio_chunk_counts: Annotated[
100
+ torch.Tensor | None,
101
+ TensorShape("n"),
102
+ ]
103
+
104
+
105
+class MossTranscribeDiarizeEmbeddingInputs(TensorSchema):
106
+ """
107
+ Dimensions:
108
+ - n: Number of audio items
109
+ - t: Number of audio tokens
110
+ - h: Hidden size
111
+ """
112
+
113
+ type: Literal["audio_embeds"] = "audio_embeds"
114
+
115
+ audio_embeds: Annotated[
116
+ list[torch.Tensor],
117
+ TensorShape("n", "t", "h", dynamic_dims={"t"}),
118
+ ]
119
+
120
+
121
+MossTranscribeDiarizeInputs: TypeAlias = (
122
+ MossTranscribeDiarizeAudioInputs | MossTranscribeDiarizeEmbeddingInputs
123
+)
124
+
125
+
126
+def _compute_total_audio_tokens(
127
+ num_samples: int,
128
+ feature_extractor: Any,
129
+ audio_merge_size: int,
130
+) -> int:
131
+ if num_samples <= 0:
132
+ return 0
133
+
134
+ n_samples = int(feature_extractor.n_samples)
135
+ stride = (
136
+ int(feature_extractor.hop_length)
137
+ * WHISPER_ENCODER_STRIDE
138
+ * int(audio_merge_size)
139
+ )
140
+ total = 0
141
+ for start in range(0, num_samples, n_samples):
142
+ chunk_samples = min(n_samples, num_samples - start)
143
+ total += (chunk_samples - 1) // stride + 1
144
+ return total
145
+
146
+
147
+def _get_max_audio_samples(feature_extractor: Any) -> int:
148
+ if hasattr(feature_extractor, "chunk_length"):
149
+ return int(feature_extractor.chunk_length * feature_extractor.sampling_rate)
150
+ return int(feature_extractor.n_samples)
151
+
152
+
153
+def _as_audio_embedding_list(audio_embeds: object) -> list[torch.Tensor]:
154
+ if isinstance(audio_embeds, torch.Tensor):
155
+ if audio_embeds.ndim == 2:
156
+ return [audio_embeds]
157
+ if audio_embeds.ndim == 3:
158
+ return list(audio_embeds.unbind(dim=0))
159
+ raise ValueError(
160
+ f"`audio_embeds` must be a 2D or 3D tensor, got {audio_embeds.ndim}D."
161
+ )
162
+
163
+ if isinstance(audio_embeds, (list, tuple)) and all(
164
+ isinstance(audio_embed, torch.Tensor) for audio_embed in audio_embeds
165
+ ):
166
+ return list(audio_embeds)
167
+
168
+ raise TypeError(
169
+ "`audio_embeds` must be a torch.Tensor or a list of torch.Tensor "
170
+ f"objects, got {type(audio_embeds)!r}."
171
+ )
172
+
173
+
174
+def _get_required_token_id(tokenizer: Any, token: str) -> int:
175
+ token_id = tokenizer.convert_tokens_to_ids(token)
176
+ if token_id is None or (isinstance(token_id, int) and token_id < 0):
177
+ raise ValueError(f"Tokenizer is missing required token {token!r}.")
178
+ return int(token_id)
179
+
180
+
181
+def _get_audios_from_mm_data(mm_data: Mapping[str, object]) -> list[Any]:
182
+ mm_data_dict = dict(mm_data)
183
+ audio_data = mm_data_dict.pop("audios", mm_data_dict.pop("audio", []))
184
+ if isinstance(audio_data, list):
185
+ audios = audio_data
186
+ elif isinstance(audio_data, tuple) and not (
187
+ len(audio_data) == 2 and isinstance(audio_data[1], (int, float))
188
+ ):
189
+ audios = list(audio_data)
190
+ elif audio_data is None:
191
+ audios = []
192
+ else:
193
+ audios = [audio_data]
194
+
195
+ audio_arrays: list[Any] = []
196
+ for audio in audios:
197
+ if isinstance(audio, (tuple, list)) and len(audio) == 2:
198
+ audio = audio[0]
199
+ audio_arrays.append(audio)
200
+ return audio_arrays
201
+
202
+
203
+def _add_vllm_audio_metadata(
204
+ processed: BatchFeature,
205
+ num_audios: int,
206
+) -> BatchFeature:
207
+ audio_feature_lengths = processed["audio_feature_lengths"]
208
+ if not isinstance(audio_feature_lengths, torch.Tensor):
209
+ audio_feature_lengths = torch.tensor(audio_feature_lengths, dtype=torch.long)
210
+ audio_feature_lengths = audio_feature_lengths.to(dtype=torch.long)
211
+
212
+ audio_chunk_mapping = processed.get("audio_chunk_mapping")
213
+ if audio_chunk_mapping is None:
214
+ if num_audios == 1:
215
+ audio_chunk_mapping = torch.zeros_like(audio_feature_lengths)
216
+ elif audio_feature_lengths.numel() == num_audios:
217
+ audio_chunk_mapping = torch.arange(num_audios, dtype=torch.long)
218
+ else:
219
+ raise ValueError(
220
+ "The MOSS processor did not return `audio_chunk_mapping`, and "
221
+ "the chunk-to-audio mapping cannot be inferred."
222
+ )
223
+ elif not isinstance(audio_chunk_mapping, torch.Tensor):
224
+ audio_chunk_mapping = torch.tensor(audio_chunk_mapping, dtype=torch.long)
225
+ audio_chunk_mapping = audio_chunk_mapping.to(dtype=torch.long)
226
+ if audio_chunk_mapping.numel() != audio_feature_lengths.numel():
227
+ raise ValueError(
228
+ "`audio_chunk_mapping` must contain one item per audio chunk: got "
229
+ f"{audio_chunk_mapping.numel()} mappings for "
230
+ f"{audio_feature_lengths.numel()} chunks."
231
+ )
232
+ if audio_chunk_mapping.numel() > 0 and (
233
+ audio_chunk_mapping.min().item() < 0
234
+ or audio_chunk_mapping.max().item() >= num_audios
235
+ ):
236
+ raise ValueError("`audio_chunk_mapping` contains an out-of-range audio index.")
237
+
238
+ audio_chunk_counts = torch.bincount(
239
+ audio_chunk_mapping.cpu(),
240
+ minlength=num_audios,
241
+ ).to(dtype=torch.long)
242
+ audio_token_lengths = torch.zeros(
243
+ num_audios,
244
+ dtype=torch.long,
245
+ device=audio_feature_lengths.device,
246
+ )
247
+ audio_token_lengths.scatter_add_(
248
+ 0,
249
+ audio_chunk_mapping.to(device=audio_feature_lengths.device),
250
+ audio_feature_lengths,
251
+ )
252
+
253
+ processed["audio_feature_lengths"] = audio_feature_lengths
254
+ processed["audio_chunk_counts"] = audio_chunk_counts
255
+ processed["audio_token_lengths"] = audio_token_lengths
256
+ return processed
257
+
258
+
259
+class MossTranscribeDiarizeWhisperEncoder(WhisperEncoder):
260
+ packed_modules_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
261
+ hf_to_vllm_mapper = WeightsMapper(
262
+ orig_to_new_substr={".fc1.": ".mlp.fc1.", ".fc2.": ".mlp.fc2."},
263
+ orig_to_new_stacked={
264
+ ".self_attn.q_proj": (".self_attn.qkv_proj", "q"),
265
+ ".self_attn.k_proj": (".self_attn.qkv_proj", "k"),
266
+ ".self_attn.v_proj": (".self_attn.qkv_proj", "v"),
267
+ },
268
+ )
269
+
270
+ def __init__(
271
+ self,
272
+ *,
273
+ vllm_config: VllmConfig,
274
+ prefix: str = "",
275
+ ) -> None:
276
+ super().__init__(
277
+ vllm_config=vllm_config.with_hf_config(
278
+ vllm_config.model_config.hf_config.audio_config
279
+ ),
280
+ prefix=prefix,
281
+ )
282
+ self.audio_merge_size = int(vllm_config.model_config.hf_config.audio_merge_size)
283
+ self.max_encoder_batch: int | None = None
284
+
285
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
286
+ weights = _create_fake_bias_for_k_proj(weights, ".k_proj.weight")
287
+ loader = AutoWeightsLoader(self)
288
+ return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
289
+
290
+ def forward(
291
+ self,
292
+ input_features: torch.Tensor,
293
+ audio_feature_lengths: torch.Tensor,
294
+ ) -> torch.Tensor:
295
+ if input_features.numel() == 0:
296
+ return input_features.new_empty((1, 0, self.conv1.out_channels))
297
+ device = self.conv1.weight.device
298
+ dtype = self.conv1.weight.dtype
299
+ input_features = input_features.to(device=device, dtype=dtype)
300
+ audio_feature_lengths = audio_feature_lengths.to(device=device)
301
+ batch_size = self.max_encoder_batch or input_features.shape[0]
302
+ encoded_parts: list[torch.Tensor] = []
303
+
304
+ for start in range(0, input_features.shape[0], batch_size):
305
+ encoded_parts.append(
306
+ super().forward([input_features[start : start + batch_size]])
307
+ )
308
+
309
+ hidden = torch.cat(encoded_parts, dim=0)
310
+ chunks = [
311
+ hidden[idx : idx + 1, : int(token_len.item()) * self.audio_merge_size]
312
+ for idx, token_len in enumerate(audio_feature_lengths)
313
+ ]
314
+ return torch.cat(chunks, dim=1)
315
+
316
+
317
+class MossTranscribeDiarizeVQAdaptor(nn.Module):
318
+ def __init__(self, input_dim: int, hidden_size: int, eps: float) -> None:
319
+ super().__init__()
320
+ self.layers = nn.Sequential(
321
+ nn.Linear(input_dim, hidden_size, bias=True),
322
+ nn.SiLU(),
323
+ nn.Linear(hidden_size, hidden_size, bias=True),
324
+ nn.LayerNorm(hidden_size, eps=eps, bias=True),
325
+ )
326
+
327
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
328
+ return self.layers(x)
329
+
330
+
331
+def _mtd_field_config(
332
+ hf_inputs: Mapping[str, torch.Tensor],
333
+) -> Mapping[str, MultiModalFieldConfig]:
334
+ fields: dict[str, MultiModalFieldConfig] = {}
335
+ if "audio_embeds" in hf_inputs:
336
+ fields["audio_embeds"] = MultiModalFieldConfig.batched("audio")
337
+ if "audio_chunk_counts" in hf_inputs:
338
+ audio_chunk_counts = hf_inputs["audio_chunk_counts"]
339
+ fields.update(
340
+ input_features=MultiModalFieldConfig.flat_from_sizes(
341
+ "audio",
342
+ audio_chunk_counts,
343
+ ),
344
+ audio_feature_lengths=MultiModalFieldConfig.flat_from_sizes(
345
+ "audio",
346
+ audio_chunk_counts,
347
+ ),
348
+ audio_chunk_counts=MultiModalFieldConfig.batched("audio"),
349
+ audio_token_lengths=MultiModalFieldConfig.batched("audio"),
350
+ )
351
+ return fields
352
+
353
+
354
+class MossTranscribeDiarizeMultiModalDataParser(MultiModalDataParser):
355
+ def _parse_audio_data(
356
+ self,
357
+ data: dict[str, torch.Tensor] | ModalityData[AudioItem],
358
+ ) -> ModalityDataItems[Any, Any] | None:
359
+ if isinstance(data, dict):
360
+ return DictEmbeddingItems(
361
+ data,
362
+ modality="audio",
363
+ required_fields={"audio_embeds"},
364
+ fields_factory=_mtd_field_config,
365
+ )
366
+
367
+ return super()._parse_audio_data(data)
368
+
369
+
370
+class MossTranscribeDiarizeProcessingInfo(BaseProcessingInfo):
371
+ def get_supported_mm_limits(self) -> Mapping[str, int | None]:
372
+ return {"audio": 1}
373
+
374
+ def get_hf_processor(self, **kwargs: object) -> Any:
375
+ return self.ctx.get_hf_processor(**kwargs)
376
+
377
+ def get_feature_extractor(self, **kwargs: object) -> Any:
378
+ return self.get_hf_processor(**kwargs).feature_extractor
379
+
380
+ def get_data_parser(self) -> MultiModalDataParser:
381
+ feature_extractor = self.get_feature_extractor()
382
+ return MossTranscribeDiarizeMultiModalDataParser(
383
+ target_sr=feature_extractor.sampling_rate,
384
+ target_channels=1,
385
+ expected_hidden_size=self._get_expected_hidden_size(),
386
+ )
387
+
388
+ def get_mm_max_tokens_per_item(
389
+ self,
390
+ seq_len: int,
391
+ mm_counts: Mapping[str, int],
392
+ ) -> Mapping[str, int]:
393
+ if mm_counts.get("audio", 0) <= 0:
394
+ return {}
395
+
396
+ feature_extractor = self.get_feature_extractor()
397
+ max_audio_samples = _get_max_audio_samples(feature_extractor)
398
+ max_audio_tokens = _compute_total_audio_tokens(
399
+ max_audio_samples,
400
+ feature_extractor,
401
+ self.get_hf_processor().audio_merge_size,
402
+ )
403
+ return {"audio": min(seq_len, max_audio_tokens)}
404
+
405
+
406
+class MossTranscribeDiarizeDummyInputsBuilder(
407
+ BaseDummyInputsBuilder[MossTranscribeDiarizeProcessingInfo]
408
+):
409
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
410
+ return AUDIO_PLACEHOLDER * mm_counts.get("audio", 0)
411
+
412
+ def get_dummy_mm_data(
413
+ self,
414
+ seq_len: int,
415
+ mm_counts: Mapping[str, int],
416
+ mm_options: Mapping[str, BaseDummyOptions],
417
+ ) -> MultiModalDataDict:
418
+ num_audios = mm_counts.get("audio", 0)
419
+ if num_audios == 0:
420
+ return {}
421
+
422
+ feature_extractor = self.info.get_feature_extractor()
423
+ return {
424
+ "audio": self._get_dummy_audios(
425
+ length=_get_max_audio_samples(feature_extractor),
426
+ num_audios=num_audios,
427
+ overrides=mm_options.get("audio"),
428
+ )
429
+ }
430
+
431
+ def get_dummy_processor_inputs(
432
+ self,
433
+ seq_len: int,
434
+ mm_counts: Mapping[str, int],
435
+ mm_options: Mapping[str, BaseDummyOptions],
436
+ ) -> ProcessorInputs:
437
+ dummy_mm_data = self.get_dummy_mm_data(seq_len, mm_counts, mm_options)
438
+ dummy_mm_items = self.info.parse_mm_data(dummy_mm_data)
439
+ num_audios = mm_counts.get("audio", 0)
440
+ tokenizer = self.info.get_tokenizer()
441
+ prompt = tokenizer.encode(
442
+ AUDIO_PLACEHOLDER * num_audios,
443
+ add_special_tokens=False,
444
+ ) or tokenizer.encode(
445
+ "\n",
446
+ add_special_tokens=False,
447
+ )
448
+ return ProcessorInputs(prompt=prompt, mm_data_items=dummy_mm_items)
449
+
450
+
451
+class MossTranscribeDiarizeMultiModalProcessor(
452
+ BaseMultiModalProcessor[MossTranscribeDiarizeProcessingInfo]
453
+):
454
+ def _call_hf_processor(
455
+ self,
456
+ prompt: str,
457
+ mm_data: Mapping[str, object],
458
+ mm_kwargs: Mapping[str, object],
459
+ tok_kwargs: Mapping[str, object],
460
+ ) -> BatchFeature:
461
+ tokenizer = self.info.get_tokenizer()
462
+ audios = _get_audios_from_mm_data(mm_data)
463
+ if not audios:
464
+ input_ids = tokenizer.encode(
465
+ prompt,
466
+ add_special_tokens=tok_kwargs.get("add_special_tokens", False),
467
+ )
468
+ return BatchFeature({"input_ids": [input_ids]}, tensor_type="pt")
469
+
470
+ processed = self.info.ctx.call_hf_processor(
471
+ self.info.get_hf_processor(**mm_kwargs),
472
+ dict(text=prompt, audio=audios),
473
+ dict(**mm_kwargs, **tok_kwargs),
474
+ )
475
+ return _add_vllm_audio_metadata(processed, len(audios))
476
+
477
+ def _hf_processor_applies_updates(
478
+ self,
479
+ prompt_text: str,
480
+ mm_items: MultiModalDataItems,
481
+ hf_processor_mm_kwargs: Mapping[str, object],
482
+ tokenization_kwargs: Mapping[str, object],
483
+ ) -> bool:
484
+ return mm_items.get_count("audio", strict=False) > 0
485
+
486
+ def _get_mm_fields_config(
487
+ self,
488
+ hf_inputs: BatchFeature,
489
+ hf_processor_mm_kwargs: Mapping[str, object],
490
+ ) -> Mapping[str, MultiModalFieldConfig]:
491
+ return _mtd_field_config(hf_inputs)
492
+
493
+ def _get_prompt_updates(
494
+ self,
495
+ mm_items: MultiModalDataItems,
496
+ hf_processor_mm_kwargs: Mapping[str, object],
497
+ out_mm_kwargs: MultiModalKwargsItems,
498
+ ) -> Sequence[PromptUpdate]:
499
+ out_mm_data = out_mm_kwargs.get_data()
500
+ audio_token_lengths_tensor = out_mm_data.get("audio_token_lengths")
501
+ if audio_token_lengths_tensor is None:
502
+ audio_embeds = out_mm_data.get("audio_embeds")
503
+ if audio_embeds is None:
504
+ audio_token_lengths: list[int] = []
505
+ else:
506
+ audio_token_lengths = [
507
+ int(audio_embed.shape[0])
508
+ for audio_embed in _as_audio_embedding_list(audio_embeds)
509
+ ]
510
+ else:
511
+ if not isinstance(audio_token_lengths_tensor, torch.Tensor):
512
+ raise TypeError(
513
+ "`audio_token_lengths` must be a torch.Tensor, got "
514
+ f"{type(audio_token_lengths_tensor)!r}."
515
+ )
516
+ audio_token_lengths = [
517
+ int(length) for length in audio_token_lengths_tensor.tolist()
518
+ ]
519
+ processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
520
+ tokenizer = self.info.get_tokenizer()
521
+ audio_start_id = _get_required_token_id(tokenizer, processor.audio_start_token)
522
+ audio_token_id = int(processor.audio_token_id)
523
+ audio_end_id = _get_required_token_id(tokenizer, processor.audio_end_token)
524
+
525
+ def get_num_tokens(item_idx: int) -> int:
526
+ if item_idx >= len(audio_token_lengths):
527
+ raise ValueError(
528
+ "Cannot determine the number of audio tokens for audio item "
529
+ f"{item_idx}."
530
+ )
531
+ num_tokens = audio_token_lengths[item_idx]
532
+ if num_tokens <= 0:
533
+ raise ValueError("Audio input is too short to produce any tokens.")
534
+ return num_tokens
535
+
536
+ def get_replacement(item_idx: int) -> PromptUpdateDetails[list[int]]:
537
+ num_tokens = get_num_tokens(item_idx)
538
+ audio_tokens = processor._audio_span_ids(num_tokens)
539
+ return PromptUpdateDetails.select_token_id(
540
+ [audio_start_id] + audio_tokens + [audio_end_id],
541
+ embed_token_id=audio_token_id,
542
+ )
543
+
544
+ return [
545
+ PromptReplacement(
546
+ modality="audio",
547
+ target=AUDIO_PLACEHOLDER,
548
+ replacement=get_replacement,
549
+ ),
550
+ ]
551
+
552
+
553
+@MULTIMODAL_REGISTRY.register_processor(
554
+ MossTranscribeDiarizeMultiModalProcessor,
555
+ info=MossTranscribeDiarizeProcessingInfo,
556
+ dummy_inputs=MossTranscribeDiarizeDummyInputsBuilder,
557
+)
558
+class MossTranscribeDiarizeForConditionalGeneration(
559
+ nn.Module,
560
+ SupportsMultiModal,
561
+ SupportsPP,
562
+ SupportsTranscription,
563
+):
564
+ supports_transcription = True
565
+ supports_transcription_only = True
566
+ supports_segment_timestamp = False
567
+ supported_languages = ISO639_1_SUPPORTED_LANGS
568
+ hf_to_vllm_mapper = WeightsMapper(
569
+ orig_to_new_prefix={
570
+ "language_model.layers.": "language_model.model.layers.",
571
+ "language_model.embed_tokens.": "language_model.model.embed_tokens.",
572
+ "language_model.norm.": "language_model.model.norm.",
573
+ "model.language_model.model.": "language_model.model.",
574
+ "model.language_model.lm_head.": "language_model.lm_head.",
575
+ "model.language_model.": "language_model.model.",
576
+ "model.whisper_encoder.": "whisper_encoder.",
577
+ "model.vq_adaptor.": "vq_adaptor.",
578
+ "model.": None,
579
+ },
580
+ )
581
+
582
+ @classmethod
583
+ def get_placeholder_str(cls, modality: str, i: int) -> str | None:
584
+ return AUDIO_PLACEHOLDER if modality.startswith("audio") else None
585
+
586
+ @classmethod
587
+ def get_speech_to_text_config(
588
+ cls,
589
+ model_config: ModelConfig,
590
+ task_type: str,
591
+ ) -> SpeechToTextConfig:
592
+ processor = cached_processor_from_config(model_config)
593
+ return SpeechToTextConfig(
594
+ max_audio_clip_s=None,
595
+ sample_rate=processor.feature_extractor.sampling_rate,
596
+ min_energy_split_window_size=None,
597
+ )
598
+
599
+ @classmethod
600
+ def get_num_audio_tokens(
601
+ cls,
602
+ audio_duration_s: float,
603
+ stt_config: SpeechToTextConfig,
604
+ model_config: ModelConfig,
605
+ ) -> int | None:
606
+ processor = cached_processor_from_config(model_config)
607
+ num_samples = math.ceil(audio_duration_s * stt_config.sample_rate)
608
+ return _compute_total_audio_tokens(
609
+ num_samples,
610
+ processor.feature_extractor,
611
+ processor.audio_merge_size,
612
+ )
613
+
614
+ @classmethod
615
+ def get_generation_prompt(cls, stt_params: SpeechToTextParams) -> PromptType:
616
+ stt_config = stt_params.stt_config
617
+ question = stt_params.request_prompt or DEFAULT_MOSS_TRANSCRIBE_DIARIZE_PROMPT
618
+ question = question.strip() or DEFAULT_MOSS_TRANSCRIBE_DIARIZE_PROMPT
619
+ prompt = (
620
+ "<|im_start|>system\n"
621
+ "You are a helpful assistant.<|im_end|>\n"
622
+ f"<|im_start|>user\n{AUDIO_PLACEHOLDER}\n"
623
+ f"{question}<|im_end|>\n"
624
+ "<|im_start|>assistant\n"
625
+ )
626
+ return TextPrompt(
627
+ prompt=prompt,
628
+ multi_modal_data={"audio": (stt_params.audio, stt_config.sample_rate)},
629
+ )
630
+
631
+ @classmethod
632
+ def post_process_output(cls, text: str) -> str:
633
+ return text.strip()
634
+
635
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
636
+ super().__init__()
637
+ self.config = vllm_config.model_config.hf_config
638
+ self.dtype = vllm_config.model_config.dtype
639
+
640
+ with self._mark_tower_model(vllm_config, "audio"):
641
+ self.whisper_encoder = MossTranscribeDiarizeWhisperEncoder(
642
+ vllm_config=vllm_config,
643
+ prefix=maybe_prefix(prefix, "whisper_encoder"),
644
+ )
645
+ self.vq_adaptor = MossTranscribeDiarizeVQAdaptor(
646
+ input_dim=int(self.config.adaptor_input_dim),
647
+ hidden_size=int(self.config.text_config.hidden_size),
648
+ eps=float(self.config.text_config.rms_norm_eps),
649
+ )
650
+
651
+ with self._mark_language_model(vllm_config):
652
+ self.language_model = init_vllm_registered_model(
653
+ vllm_config=vllm_config,
654
+ hf_config=self.config.text_config,
655
+ prefix=maybe_prefix(prefix, "language_model"),
656
+ architectures=["Qwen3ForCausalLM"],
657
+ )
658
+
659
+ self.make_empty_intermediate_tensors = (
660
+ self.language_model.make_empty_intermediate_tensors
661
+ )
662
+
663
+ def _time_merge(self, features: torch.Tensor) -> torch.Tensor:
664
+ batch, seq_len, dim = features.shape
665
+ merge_size = int(self.config.audio_merge_size)
666
+ seq_len_trim = (seq_len // merge_size) * merge_size
667
+ return features[:, :seq_len_trim, :].reshape(
668
+ batch,
669
+ seq_len_trim // merge_size,
670
+ dim * merge_size,
671
+ )
672
+
673
+ def _parse_and_validate_audio_input(
674
+ self,
675
+ **kwargs: object,
676
+ ) -> MossTranscribeDiarizeInputs | None:
677
+ input_features = kwargs.pop("input_features", None)
678
+ audio_embeds = kwargs.pop("audio_embeds", None)
679
+ audio_feature_lengths = kwargs.pop("audio_feature_lengths", None)
680
+ audio_chunk_counts = kwargs.pop("audio_chunk_counts", None)
681
+ if input_features is None and audio_embeds is None:
682
+ return None
683
+ if audio_embeds is not None:
684
+ return MossTranscribeDiarizeEmbeddingInputs(
685
+ type="audio_embeds",
686
+ audio_embeds=_as_audio_embedding_list(audio_embeds),
687
+ )
688
+ return MossTranscribeDiarizeAudioInputs(
689
+ type="audio_features",
690
+ input_features=input_features,
691
+ audio_feature_lengths=audio_feature_lengths,
692
+ audio_chunk_counts=audio_chunk_counts,
693
+ )
694
+
695
+ def _process_audio_input(
696
+ self, audio_input: MossTranscribeDiarizeInputs
697
+ ) -> list[torch.Tensor]:
698
+ if audio_input["type"] == "audio_embeds":
699
+ return list(audio_input["audio_embeds"])
700
+
701
+ input_features = audio_input["input_features"]
702
+ audio_feature_lengths = audio_input["audio_feature_lengths"]
703
+ audio_chunk_counts = audio_input["audio_chunk_counts"]
704
+ if input_features is None or audio_feature_lengths is None:
705
+ raise ValueError(
706
+ "MOSS-Transcribe-Diarize audio inputs require both "
707
+ "`input_features` and `audio_feature_lengths`."
708
+ )
709
+ if audio_feature_lengths.numel() != input_features.shape[0]:
710
+ raise ValueError(
711
+ "`audio_feature_lengths` must contain one length per "
712
+ "`input_features` chunk: got "
713
+ f"{audio_feature_lengths.numel()} lengths for "
714
+ f"{input_features.shape[0]} chunks."
715
+ )
716
+ if audio_chunk_counts is None:
717
+ audio_chunk_counts = audio_feature_lengths.new_tensor(
718
+ [input_features.shape[0]],
719
+ dtype=torch.long,
720
+ )
721
+ else:
722
+ audio_chunk_counts = audio_chunk_counts.to(dtype=torch.long)
723
+ if audio_chunk_counts.numel() == 0:
724
+ raise ValueError("`audio_chunk_counts` must contain at least one item.")
725
+ if torch.any(audio_chunk_counts <= 0):
726
+ raise ValueError("`audio_chunk_counts` must contain positive counts.")
727
+ num_audio_chunks = int(audio_chunk_counts.sum().item())
728
+ if num_audio_chunks != input_features.shape[0]:
729
+ raise ValueError(
730
+ "`audio_chunk_counts` must sum to the number of input feature chunks: "
731
+ f"got {num_audio_chunks} chunks for "
732
+ f"{input_features.shape[0]} input chunks."
733
+ )
734
+
735
+ features = self.whisper_encoder(input_features, audio_feature_lengths)
736
+ merged = self._time_merge(features.to(dtype=self.dtype))
737
+ projected = self.vq_adaptor(merged).squeeze(0)
738
+
739
+ audio_chunk_offsets = torch.cumsum(audio_chunk_counts, dim=0)
740
+ audio_chunk_offsets = torch.cat(
741
+ [audio_chunk_offsets.new_zeros(1), audio_chunk_offsets]
742
+ )
743
+ tokens_per_item = [
744
+ int(audio_feature_lengths[start:end].sum().item())
745
+ for start, end in zip(
746
+ audio_chunk_offsets[:-1].tolist(),
747
+ audio_chunk_offsets[1:].tolist(),
748
+ )
749
+ ]
750
+ if any(num_tokens <= 0 for num_tokens in tokens_per_item):
751
+ raise ValueError("Audio input is too short to produce any tokens.")
752
+ return list(projected.split(tokens_per_item, dim=0))
753
+
754
+ def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
755
+ audio_input = self._parse_and_validate_audio_input(**kwargs)
756
+ if audio_input is None:
757
+ return []
758
+ return self._process_audio_input(audio_input)
759
+
760
+ def embed_input_ids(
761
+ self,
762
+ input_ids: torch.Tensor,
763
+ multimodal_embeddings: MultiModalEmbeddings | None = None,
764
+ *,
765
+ is_multimodal: torch.Tensor | None = None,
766
+ ) -> torch.Tensor:
767
+ inputs_embeds = self.language_model.embed_input_ids(input_ids)
768
+ if not multimodal_embeddings:
769
+ return inputs_embeds
770
+ return _merge_multimodal_embeddings(
771
+ inputs_embeds=inputs_embeds,
772
+ multimodal_embeddings=multimodal_embeddings,
773
+ is_multimodal=_require_is_multimodal(is_multimodal),
774
+ )
775
+
776
+ def forward(
777
+ self,
778
+ input_ids: torch.Tensor | None,
779
+ positions: torch.Tensor,
780
+ intermediate_tensors: IntermediateTensors | None = None,
781
+ inputs_embeds: torch.Tensor | None = None,
782
+ **kwargs: object,
783
+ ) -> torch.Tensor | IntermediateTensors:
784
+ if intermediate_tensors is not None:
785
+ inputs_embeds = None
786
+ return self.language_model(
787
+ input_ids=input_ids,
788
+ positions=positions,
789
+ intermediate_tensors=intermediate_tensors,
790
+ inputs_embeds=inputs_embeds,
791
+ )
792
+
793
+ def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
794
+ return self.language_model.compute_logits(hidden_states)
795
+
796
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
797
+ loader = AutoWeightsLoader(self)
798
+ return loader.load_weights(
799
+ weights,
800
+ mapper=self.hf_to_vllm_mapper,
801
+ )
vllm/model_executor/models/registry.py
+4
@@ -447,6 +447,10 @@ _MULTIMODAL_MODELS = {
447
"KimiVLForConditionalGeneration": ("kimi_vl", "KimiVLForConditionalGeneration"),
448
"KimiK25ForConditionalGeneration": ("kimi_k25", "KimiK25ForConditionalGeneration"),
449
"MoonshotKimiaForCausalLM": ("kimi_audio", "KimiAudioForConditionalGeneration"),
450
+ "MossTranscribeDiarizeForConditionalGeneration": (
451
+ "moss_transcribe_diarize",
452
+ "MossTranscribeDiarizeForConditionalGeneration",
453
+ ),
454
"LightOnOCRForConditionalGeneration": (
455
"lightonocr",
456
"LightOnOCRForConditionalGeneration",
vllm/transformers_utils/config.py
+1
@@ -107,6 +107,7 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
107
minimax_m3_vl="MiniMaxM3Config",
108
minimax_m3_mtp="MiniMaxM3MTPConfig",
109
moondream3="Moondream3Config",
110
+ moss_transcribe_diarize="MossTranscribeDiarizeConfig",
111
eagle="EAGLEConfig",
112
speculators="SpeculatorsConfig",
113
nemotron="NemotronConfig",
vllm/transformers_utils/configs/__init__.py
+4
@@ -61,6 +61,9 @@ _CLASS_TO_MODULE: dict[str, str] = {
61
"Moondream3Config": "vllm.transformers_utils.configs.moondream3",
62
"Moondream3TextConfig": "vllm.transformers_utils.configs.moondream3",
63
"Moondream3VisionConfig": "vllm.transformers_utils.configs.moondream3",
64
+ "MossTranscribeDiarizeConfig": (
65
+ "vllm.transformers_utils.configs.moss_transcribe_diarize"
66
+ ),
67
"MoonViTConfig": "vllm.transformers_utils.configs.moonvit",
68
"KimiLinearConfig": "vllm.transformers_utils.configs.kimi_linear",
69
"KimiVLConfig": "vllm.transformers_utils.configs.kimi_vl",
@@ -136,6 +139,7 @@ __all__ = [
139
"Moondream3Config",
140
"Moondream3TextConfig",
141
"Moondream3VisionConfig",
142
+ "MossTranscribeDiarizeConfig",
143
"MoonViTConfig",
144
"KimiLinearConfig",
145
"KimiVLConfig",
vllm/transformers_utils/configs/moss_transcribe_diarize.py
new
+108
@@ -0,0 +1,108 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from typing import Any
5
+
6
+from transformers import PretrainedConfig, Qwen3Config
7
+from transformers.models.whisper.configuration_whisper import WhisperConfig
8
+
9
+
10
+class MossTranscribeDiarizeConfig(PretrainedConfig):
11
+ """Configuration for MOSS-Transcribe-Diarize."""
12
+
13
+ model_type = "moss_transcribe_diarize"
14
+ sub_configs = {"text_config": Qwen3Config, "audio_config": WhisperConfig}
15
+ keys_to_ignore_at_inference = ["past_key_values"]
16
+
17
+ def __init__(
18
+ self,
19
+ text_config: dict[str, Any] | Qwen3Config | None = None,
20
+ audio_config: dict[str, Any] | WhisperConfig | None = None,
21
+ audio_token_id: int = 151671,
22
+ audio_merge_size: int = 4,
23
+ adaptor_input_dim: int | None = None,
24
+ tie_word_embeddings: bool = True,
25
+ **kwargs: Any,
26
+ ) -> None:
27
+ text_config_obj: Qwen3Config
28
+ if text_config is None:
29
+ text_config_obj = Qwen3Config(
30
+ vocab_size=151936,
31
+ hidden_size=1024,
32
+ intermediate_size=3072,
33
+ num_hidden_layers=28,
34
+ num_attention_heads=16,
35
+ num_key_value_heads=8,
36
+ head_dim=128,
37
+ max_position_embeddings=40960,
38
+ tie_word_embeddings=tie_word_embeddings,
39
+ rope_theta=1_000_000.0,
40
+ layer_types=["full_attention"] * 28,
41
+ )
42
+ elif isinstance(text_config, dict):
43
+ text_config_obj = Qwen3Config(**text_config)
44
+ else:
45
+ text_config_obj = text_config
46
+
47
+ audio_config_obj: WhisperConfig
48
+ if audio_config is None:
49
+ audio_config_obj = WhisperConfig(
50
+ num_mel_bins=80,
51
+ d_model=1024,
52
+ encoder_layers=24,
53
+ encoder_attention_heads=16,
54
+ encoder_ffn_dim=4096,
55
+ max_source_positions=1500,
56
+ dropout=0.0,
57
+ attention_dropout=0.0,
58
+ activation_dropout=0.0,
59
+ activation_function="gelu",
60
+ encoder_layerdrop=0.0,
61
+ scale_embedding=False,
62
+ )
63
+ elif isinstance(audio_config, dict):
64
+ audio_config_obj = WhisperConfig(**audio_config)
65
+ else:
66
+ audio_config_obj = audio_config
67
+
68
+ text_config_obj.tie_word_embeddings = tie_word_embeddings
69
+ if not getattr(text_config_obj, "layer_types", None):
70
+ text_config_obj.layer_types = [
71
+ "full_attention"
72
+ ] * text_config_obj.num_hidden_layers
73
+
74
+ super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs)
75
+
76
+ self.text_config = text_config_obj
77
+ self.audio_config = audio_config_obj
78
+ self.audio_token_id = int(audio_token_id)
79
+ self.audio_merge_size = int(audio_merge_size)
80
+ self.adaptor_input_dim = (
81
+ int(adaptor_input_dim)
82
+ if adaptor_input_dim is not None
83
+ else int(audio_config_obj.d_model) * int(audio_merge_size)
84
+ )
85
+
86
+ self.vocab_size = int(text_config_obj.vocab_size)
87
+ self.hidden_size = int(text_config_obj.hidden_size)
88
+ self.intermediate_size = int(text_config_obj.intermediate_size)
89
+ self.num_hidden_layers = int(text_config_obj.num_hidden_layers)
90
+ self.num_attention_heads = int(text_config_obj.num_attention_heads)
91
+ self.num_key_value_heads = int(text_config_obj.num_key_value_heads)
92
+ self.head_dim = int(text_config_obj.head_dim)
93
+ self.hidden_act = text_config_obj.hidden_act
94
+ self.max_position_embeddings = int(text_config_obj.max_position_embeddings)
95
+ self.rms_norm_eps = float(text_config_obj.rms_norm_eps)
96
+ rope_parameters = getattr(text_config_obj, "rope_parameters", None)
97
+ rope_theta = float(getattr(text_config_obj, "rope_theta", 1_000_000.0))
98
+ if rope_parameters is None:
99
+ rope_parameters = {
100
+ "rope_type": "default",
101
+ "rope_theta": rope_theta,
102
+ }
103
+ text_config_obj.rope_parameters = rope_parameters
104
+ self.rope_parameters = rope_parameters
105
+ self.rope_theta = float(rope_parameters.get("rope_theta", rope_theta))
106
+ self.attention_bias = bool(text_config_obj.attention_bias)
107
+ self.attention_dropout = float(text_config_obj.attention_dropout)
108
+ self.is_causal = True