main
py 1,234 lines 47.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # mypy: ignore-errors
4 """MiMo-Omni multimodal processor for vLLM.
5
6 Ported from SGLang's MiMoV2OmniProcessor / MiMoVLProcessor implementations.
7 """
8
9 import contextlib
10 import logging
11 import math
12 from collections import OrderedDict
13 from concurrent.futures import ThreadPoolExecutor, as_completed
14 from dataclasses import dataclass, field
15 from typing import Any, Literal
16
17 import numpy as np
18 import regex as re
19 import torch
20 import torch.nn.functional as F
21 from PIL import Image
22 from transformers import BatchFeature, TensorType
23 from transformers.processing_utils import ProcessorMixin
24
25 try:
26 import torchaudio
27 from torchaudio.transforms import MelSpectrogram as _MelSpectrogram
28
29 _HAS_TORCHAUDIO = True
30 except ImportError:
31 torchaudio = None # type: ignore[assignment]
32 _MelSpectrogram = None # type: ignore[assignment,misc]
33 _HAS_TORCHAUDIO = False
34
35 logger = logging.getLogger(__name__)
36
37 # ---------------------------------------------------------------------------
38 # Constants
39 # ---------------------------------------------------------------------------
40
41 _PIXEL_MEAN = [123.675, 116.28, 103.53]
42 _PIXEL_STD = [58.395, 57.12, 57.375]
43 _mean_std_cache: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
44
45
46 # ---------------------------------------------------------------------------
47 # Data classes
48 # ---------------------------------------------------------------------------
49
50
51 @dataclass
52 class ImageInput:
53 # PIL.Image | torch.Tensor (C,H,W)
54 image: Any
55 max_pixels: int | None = None
56 min_pixels: int | None = None
57
58
59 @dataclass
60 class VideoInput:
61 # tuple[frames_TCHW: torch.Tensor, timestamps_T: torch.Tensor]
62 video: Any
63 min_pixels: int | None = None
64 max_pixels: int | None = None
65 total_max_pixels: int | None = None
66 fps: float | None = None
67 num_frames: int | None = None
68 max_frames: int | None = None
69 min_frames: int | None = None
70 do_include_last_frame: bool | None = False
71 start_time: float | None = None
72 end_time: float | None = None
73 segment_type: Literal["individual", "partial"] = "individual"
74
75
76 @dataclass
77 class AudioInput:
78 # tuple[waveform_1D, sr]
79 # | np.ndarray | torch.Tensor (T,n_vq)
80 audio: Any
81
82
83 @dataclass
84 class VideoAudioInput:
85 video: Any # same as VideoInput.video
86 audio: Any # same as AudioInput.audio
87 min_pixels: int | None = None
88 max_pixels: int | None = None
89 total_max_pixels: int | None = None
90 fps: float | None = None
91 num_frames: int | None = None
92 max_frames: int | None = None
93 min_frames: int | None = None
94 do_include_last_frame: bool | None = False
95 start_time: float | None = None
96 end_time: float | None = None
97 segment_type: Literal["individual", "partial"] = "individual"
98
99
100 @dataclass
101 class Content:
102 type: Literal["text", "image", "video", "audio", "video_audio"]
103 content: Any
104 is_target: bool | None = None
105
106
107 @dataclass
108 class MiMoVLInputSample:
109 input_ids: torch.Tensor
110 labels: torch.Tensor | None
111 pixel_values: list[torch.Tensor]
112 pixel_values_videos: list[torch.Tensor]
113 image_thw_grids: list[torch.Tensor]
114 video_thw_grids: list[torch.Tensor]
115 audio_inputs: list[torch.Tensor]
116 second_per_grid_ts: list[float] = field(default_factory=list)
117 video_start_times: list[float] = field(default_factory=list)
118 audio_token_lens: list[int] = field(default_factory=list)
119 va_audio_inputs: list[torch.Tensor] = field(default_factory=list)
120 video_audio_n_segs: list[int] = field(default_factory=list)
121 video_audio_seg_lens: list[int] = field(default_factory=list)
122 position_ids: torch.Tensor | None = None
123 rope_deltas: torch.Tensor | None = None
124 extra: dict = field(default_factory=dict)
125
126
127 # ---------------------------------------------------------------------------
128 # Vision utilities
129 # ---------------------------------------------------------------------------
130
131
132 def _format_timestamp(ts: float) -> str:
133 return f"{int(ts // 60):02d}:{int(ts % 60):02d}"
134
135
136 def _smart_resize(
137 h: int, w: int, factor: int, min_px: int, max_px: int
138 ) -> tuple[int, int]:
139 if min(h, w) < factor:
140 if h < w:
141 h, w = factor, int(w * factor / h)
142 else:
143 w, h = factor, int(h * factor / w)
144 elif max(h, w) / min(h, w) > 200:
145 raise ValueError(f"Aspect ratio > 200 not allowed: {h}x{w}")
146 h_bar = round(h / factor) * factor
147 w_bar = round(w / factor) * factor
148 if h_bar * w_bar > max_px:
149 beta = math.sqrt((h * w) / max_px)
150 h_bar = math.floor(h / beta / factor) * factor
151 w_bar = math.floor(w / beta / factor) * factor
152 elif h_bar * w_bar < min_px:
153 beta = math.sqrt(min_px / (h * w))
154 h_bar = math.ceil(h * beta / factor) * factor
155 w_bar = math.ceil(w * beta / factor) * factor
156 return int(h_bar), int(w_bar)
157
158
159 def _standardize(images: torch.Tensor) -> torch.Tensor:
160 key = str(images.device)
161 if key not in _mean_std_cache:
162 mean = torch.tensor(_PIXEL_MEAN, device=images.device).view(1, -1, 1, 1)
163 std = torch.tensor(_PIXEL_STD, device=images.device).view(1, -1, 1, 1)
164 _mean_std_cache[key] = (mean, std)
165 mean, std = _mean_std_cache[key]
166 return (images - mean) / std
167
168
169 def _transform_batch(
170 frames: torch.Tensor,
171 factor: int,
172 min_px: int,
173 max_px: int,
174 device: torch.device | None = None,
175 ) -> tuple[torch.Tensor, int, int]:
176 if device is not None:
177 frames = frames.to(device)
178 _, _, h, w = frames.shape
179 h_bar, w_bar = _smart_resize(h, w, factor, min_px, max_px)
180 resized = F.interpolate(
181 frames.float(), (h_bar, w_bar), mode="bilinear", align_corners=False
182 )
183 return _standardize(resized), w_bar, h_bar
184
185
186 def _transform_single(
187 img: Any,
188 factor: int,
189 min_px: int,
190 max_px: int,
191 device: torch.device | None = None,
192 ) -> tuple[torch.Tensor, int, int]:
193 if isinstance(img, torch.Tensor):
194 t = img.float()
195 _, h, w = t.shape
196 elif isinstance(img, Image.Image):
197 img = img.convert("RGB")
198 w, h = img.size
199 t = torch.from_numpy(np.array(img)).permute(2, 0, 1).float()
200 else:
201 raise TypeError(f"Expected Tensor or PIL.Image, got {type(img)}")
202 if device is not None:
203 t = t.to(device)
204 h_bar, w_bar = _smart_resize(h, w, factor, min_px, max_px)
205 out = F.interpolate(
206 t.unsqueeze(0), (h_bar, w_bar), mode="bilinear", align_corners=False
207 )
208 return _standardize(out).squeeze(0), w_bar, h_bar
209
210
211 # ---------------------------------------------------------------------------
212 # Core processor
213 # ---------------------------------------------------------------------------
214
215
216 class MiMoVLProcessor:
217 """Core MiMo-VL multimodal processor.
218
219 Handles image/video/audio preprocessing and token sequence construction.
220 Ported from SGLang's MiMoVLProcessor.
221
222 Media strings (URL / path / data:) are resolved upstream in vLLM's media
223 pipeline; this processor accepts only already-decoded inputs (``PIL.Image``
224 for images, ``(waveform, sr)`` tuples for audio).
225 """
226
227 def __init__(
228 self,
229 tokenizer: Any,
230 patch_size: int = 14,
231 merge_size: int = 2,
232 temporal_patch_size: int = 2,
233 temporal_compression_ratio: int = 1,
234 use_video_timestamps: bool = True,
235 video_audio_interleave_length: int = 0,
236 audio_kernel_size: int = 3,
237 audio_stride_size: int = 2,
238 audio_avg_pooler: int = 2,
239 audio_sampling_rate: int = 24000,
240 audio_nfft: int = 960,
241 audio_hop_length: int = 240,
242 audio_window_size: int = 960,
243 audio_fmin: float = 0.0,
244 audio_fmax: float | None = None,
245 audio_n_mels: int = 128,
246 audio_segment_size: int = 6000,
247 audio_channels: int = 8,
248 audio_group_size: int = 4,
249 audio_input_id_per_second: float = 25.0,
250 audio_zeroemb_idx: int = 4096,
251 image_min_pixels: int | None = None,
252 image_max_pixels: int | None = None,
253 video_min_pixels: int | None = None,
254 video_max_pixels: int | None = None,
255 video_total_max_pixels: int | None = None,
256 fps: float | None = None,
257 num_frames: int | None = None,
258 max_frames: int | None = None,
259 min_frames: int | None = None,
260 image_token_id: int | None = None,
261 video_token_id: int | None = None,
262 audio_token_id: int | None = None,
263 vision_start_token_id: int | None = None,
264 vision_end_token_id: int | None = None,
265 audio_start_token_id: int | None = None,
266 audio_end_token_id: int | None = None,
267 video_start_token_id: int | None = None,
268 video_end_token_id: int | None = None,
269 pad_token_id: int | None = None,
270 rope_type: str = "rope",
271 video_process_num_threads: int = 16,
272 device: Any | None = None,
273 **kwargs: Any,
274 ) -> None:
275 self.tokenizer = tokenizer
276 self.video_process_num_threads = video_process_num_threads
277 self.device = torch.device(device) if isinstance(device, str) else device
278
279 self.rope_type = "rope" if rope_type == "1d" else rope_type
280 assert self.rope_type in ("rope", "mrope"), (
281 f"Unknown rope_type: {self.rope_type}"
282 )
283
284 # video timestamps require 1-D rope
285 assert use_video_timestamps, "use_video_timestamps must be True"
286 assert self.rope_type == "rope", (
287 "use_video_timestamps requires rope_type='rope'"
288 )
289 self.use_video_timestamps = use_video_timestamps
290 self.video_audio_interleave_length = video_audio_interleave_length
291
292 self.image_token_id = image_token_id
293 self.video_token_id = video_token_id
294 self.audio_token_id = audio_token_id
295 self.vision_start_token_id = vision_start_token_id
296 self.vision_end_token_id = vision_end_token_id
297 self.audio_start_token_id = audio_start_token_id
298 self.audio_end_token_id = audio_end_token_id
299 self.video_start_token_id = video_start_token_id
300 self.video_end_token_id = video_end_token_id
301 self.pad_token_id = pad_token_id
302
303 self.patch_size = patch_size
304 self.merge_size = merge_size
305 self.temporal_patch_size = temporal_patch_size
306 self.temporal_compression_ratio = temporal_compression_ratio
307
308 self.audio_sampling_rate = audio_sampling_rate
309 self.audio_nfft = audio_nfft
310 self.audio_hop_length = audio_hop_length
311 self.audio_window_size = audio_window_size
312 self.audio_fmin = audio_fmin
313 self.audio_fmax = audio_fmax
314 self.audio_n_mels = audio_n_mels
315 self.audio_segment_size = audio_segment_size
316 self.audio_kernel_size = audio_kernel_size
317 self.audio_stride_size = audio_stride_size
318 self.audio_avg_pooler = audio_avg_pooler
319 self.audio_channels = audio_channels
320 self.audio_group_size = audio_group_size
321 self.audio_input_id_per_second = audio_input_id_per_second
322
323 self._mel_spec_kwargs = dict(
324 sample_rate=audio_sampling_rate,
325 n_fft=audio_nfft,
326 hop_length=audio_hop_length,
327 win_length=audio_window_size,
328 f_min=audio_fmin,
329 f_max=audio_fmax,
330 n_mels=audio_n_mels,
331 power=1.0,
332 center=True,
333 )
334 self._mel_spectrogram: Any | None = None
335 self._resamplers: OrderedDict = OrderedDict()
336 self._resamplers_max = 16
337
338 if isinstance(audio_zeroemb_idx, int):
339 self.audio_zeroemb_idxs = torch.tensor(
340 [audio_zeroemb_idx] * audio_channels, dtype=torch.int32
341 )
342 else:
343 self.audio_zeroemb_idxs = torch.tensor(audio_zeroemb_idx, dtype=torch.int32)
344
345 assert image_min_pixels is not None, "image_min_pixels must be set"
346 assert image_max_pixels is not None, "image_max_pixels must be set"
347 assert video_min_pixels is not None, "video_min_pixels must be set"
348 assert video_max_pixels is not None, "video_max_pixels must be set"
349 assert video_total_max_pixels is not None, "video_total_max_pixels must be set"
350 assert fps is not None or num_frames is not None, (
351 "fps or num_frames must be set"
352 )
353
354 self._img_kw = {"min_pixels": image_min_pixels, "max_pixels": image_max_pixels}
355 self._vid_kw = {
356 "min_pixels": video_min_pixels,
357 "max_pixels": video_max_pixels,
358 "total_max_pixels": video_total_max_pixels,
359 "fps": fps,
360 "num_frames": num_frames,
361 "max_frames": max_frames,
362 "min_frames": min_frames,
363 }
364
365 @property
366 def mel_spectrogram(self) -> Any:
367 if self._mel_spectrogram is None:
368 if _MelSpectrogram is None:
369 raise RuntimeError(
370 "torchaudio is required for audio. "
371 "Install with: pip install torchaudio"
372 )
373 self._mel_spectrogram = _MelSpectrogram(**self._mel_spec_kwargs)
374 return self._mel_spectrogram
375
376 def _resolve_img_kw(self, img: ImageInput) -> dict:
377 return {
378 "min_px": (
379 img.min_pixels
380 if img.min_pixels is not None
381 else self._img_kw["min_pixels"]
382 ),
383 "max_px": (
384 img.max_pixels
385 if img.max_pixels is not None
386 else self._img_kw["max_pixels"]
387 ),
388 }
389
390 def _resolve_vid_kw(self, vid: VideoInput) -> dict:
391 kw: dict = {}
392 for k in ("min_pixels", "max_pixels", "total_max_pixels"):
393 kw[k] = getattr(vid, k) or self._vid_kw[k]
394 if vid.num_frames is not None:
395 kw["num_frames"] = vid.num_frames
396 elif vid.fps is not None:
397 kw["fps"] = vid.fps
398 if vid.max_frames is not None:
399 kw["max_frames"] = vid.max_frames
400 if vid.min_frames is not None:
401 kw["min_frames"] = vid.min_frames
402 elif self._vid_kw["num_frames"] is not None:
403 kw["num_frames"] = self._vid_kw["num_frames"]
404 elif self._vid_kw["fps"] is not None:
405 kw["fps"] = self._vid_kw["fps"]
406 if self._vid_kw["max_frames"] is not None:
407 kw["max_frames"] = self._vid_kw["max_frames"]
408 if self._vid_kw["min_frames"] is not None:
409 kw["min_frames"] = self._vid_kw["min_frames"]
410 else:
411 raise ValueError(
412 "No video sampling strategy specified (fps or num_frames)."
413 )
414 return kw
415
416 def preprocess_audio(self, audio: Any) -> tuple[torch.Tensor, int]:
417 """Convert a pre-loaded ``(waveform, sr)`` tuple into (mel_spec, token_len)."""
418 if not isinstance(audio, tuple):
419 raise ValueError(
420 f"Unsupported audio source type: {type(audio)}. Audio must be a "
421 "pre-decoded (waveform, sample_rate) tuple; URL/path/bytes "
422 "resolution is handled upstream in vLLM's media pipeline."
423 )
424 waveform, original_sr = audio
425
426 if original_sr != self.audio_sampling_rate:
427 if original_sr not in self._resamplers:
428 if len(self._resamplers) >= self._resamplers_max:
429 self._resamplers.popitem(last=False)
430 self._resamplers[original_sr] = torchaudio.transforms.Resample(
431 orig_freq=original_sr, new_freq=self.audio_sampling_rate
432 )
433 self._resamplers.move_to_end(original_sr)
434 waveform = self._resamplers[original_sr](waveform)
435
436 if waveform.ndim == 2:
437 waveform = waveform.mean(dim=0)
438 spec = self.mel_spectrogram(waveform[None, :])
439 spec = torch.log(torch.clip(spec, min=1e-7)).squeeze().transpose(0, 1)
440
441 n = spec.shape[0]
442 n = n + 3 - self.audio_kernel_size
443 n = (n + 2 - self.audio_kernel_size) // self.audio_stride_size + 1
444 n = n // self.audio_avg_pooler + int(n % self.audio_avg_pooler != 0)
445 token_len = math.ceil(n / self.audio_group_size)
446 return spec, token_len
447
448 def process_image(self, image: ImageInput) -> torch.Tensor:
449 kw = self._resolve_img_kw(image)
450 src = image.image
451 tensor, _, _ = _transform_single(
452 src,
453 factor=self.patch_size * self.merge_size,
454 device=self.device,
455 **kw,
456 )
457 return tensor
458
459 def process_video(
460 self, video_input: VideoInput
461 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]:
462 kw = self._resolve_vid_kw(video_input)
463 video = video_input.video
464 if not isinstance(video, tuple):
465 raise ValueError(
466 f"video must be a (frames_TCHW, timestamps_T) tuple, "
467 f"got {type(video)}. "
468 "Decode the video before calling the processor."
469 )
470 frames, timestamps = video
471
472 fps = (
473 1.0
474 if len(timestamps) < 2
475 else float(1.0 / (float(timestamps[1]) - float(timestamps[0])))
476 )
477 start = (
478 video_input.start_time
479 if video_input.start_time is not None
480 else float(timestamps[0])
481 )
482 end = (
483 video_input.end_time
484 if video_input.end_time is not None
485 else float(timestamps[-1]) + 1.0 / fps
486 )
487
488 if video_input.segment_type != "individual":
489 mask = (timestamps >= start) & (timestamps < end)
490 idxs = torch.where(mask)[0]
491 if len(idxs) == 0:
492 idxs = torch.where(timestamps <= start)[0][-1:]
493 frames, timestamps = frames[idxs], timestamps[idxs]
494
495 tp = self.temporal_patch_size * self.temporal_compression_ratio
496 n = frames.shape[0]
497 total_px = kw["total_max_pixels"]
498 max_px = max(
499 kw["min_pixels"], min(total_px * tp // max(n, 1), kw["max_pixels"])
500 )
501
502 if n % tp != 0:
503 pad = tp - n % tp
504 frames = torch.cat(
505 [frames, frames[-1:].repeat(pad, *([1] * (frames.ndim - 1)))],
506 dim=0,
507 )
508 timestamps = torch.cat([timestamps, timestamps[-1:].repeat(pad)], dim=0)
509
510 transformed, _, _ = _transform_batch(
511 frames,
512 factor=self.patch_size * self.merge_size,
513 min_px=kw["min_pixels"],
514 max_px=max_px,
515 device=self.device,
516 )
517 patches, thw = self._flatten_visual(transformed, "video")
518 meta = {
519 "fps_sampled": fps,
520 "segment_start_time": start,
521 "segment_end_time": end,
522 }
523 return patches, thw, timestamps, meta
524
525 def process_audio(self, audio: AudioInput) -> Any:
526 src = audio.audio
527 if isinstance(src, np.ndarray):
528 src = (torch.from_numpy(src).float(), self.audio_sampling_rate)
529 if isinstance(src, tuple):
530 return self.preprocess_audio(src)
531 # Pre-tokenized tensor (T, n_vq)
532 assert isinstance(src, torch.Tensor) and src.ndim == 2
533 T = src.shape[0]
534 src = src[:, : self.audio_channels].to(torch.long)
535 pad_T = (
536 (T + self.audio_group_size - 1)
537 // self.audio_group_size
538 * self.audio_group_size
539 )
540 padding = (
541 torch.zeros(pad_T - T, self.audio_channels, dtype=torch.long) + src[-1]
542 )
543 src = torch.cat([src, padding], dim=0)
544 return src.reshape(
545 pad_T // self.audio_group_size, self.audio_group_size, self.audio_channels
546 )
547
548 def _flatten_visual(
549 self, visual: torch.Tensor, kind: str
550 ) -> tuple[torch.Tensor, torch.Tensor]:
551 if kind == "image":
552 h, w = visual.shape[-2:]
553 patches = visual.unsqueeze(0).repeat(self.temporal_patch_size, 1, 1, 1)
554 else: # video / video_audio
555 temporal_stride = self.temporal_compression_ratio * self.temporal_patch_size
556 assert visual.shape[0] % temporal_stride == 0
557 patches = visual
558 h, w = patches.shape[-2:]
559
560 C = patches.shape[1]
561 grid_t = patches.shape[0] // self.temporal_patch_size
562 grid_h, grid_w = h // self.patch_size, w // self.patch_size
563
564 patches = (
565 patches.contiguous()
566 .view(
567 grid_t,
568 self.temporal_patch_size,
569 C,
570 grid_h // self.merge_size,
571 self.merge_size,
572 self.patch_size,
573 grid_w // self.merge_size,
574 self.merge_size,
575 self.patch_size,
576 )
577 .permute(0, 3, 6, 4, 7, 2, 1, 5, 8)
578 .contiguous()
579 .view(
580 grid_t * grid_h * grid_w,
581 C * self.temporal_patch_size * self.patch_size * self.patch_size,
582 )
583 )
584 thw = torch.tensor([grid_t, grid_h, grid_w], dtype=torch.int32)
585 return patches, thw
586
587 def process(
588 self, contents: list[Content], verbose: bool = False
589 ) -> MiMoVLInputSample:
590 input_ids: list[int] = []
591 labels: list[int] = []
592 img_pv: list[torch.Tensor] = []
593 img_grids: list[torch.Tensor] = []
594 vid_pv: list[torch.Tensor] = []
595 vid_grids: list[torch.Tensor] = []
596 audio_inputs: list[torch.Tensor] = []
597 is_audio_tokenized: list[bool] = []
598 audio_token_lens: list[int] = []
599 second_per_grid_ts: list[float] = []
600 video_start_times: list[float] = []
601 va_audio_inputs: list[torch.Tensor] = []
602 video_audio_n_segs: list[int] = []
603 video_audio_seg_lens: list[int] = []
604
605 # Pre-decode videos in parallel
606 vid_info = [
607 (i, c.content, c.type == "video_audio")
608 for i, c in enumerate(contents)
609 if c.type in ("video", "video_audio")
610 ]
611 vid_results: dict[int, tuple] = {}
612 if vid_info:
613 n_t = min(self.video_process_num_threads, len(vid_info))
614 if n_t > 1 and len(vid_info) > 1:
615 with ThreadPoolExecutor(max_workers=n_t) as ex:
616 fut_map = {
617 ex.submit(self.process_video, vi): idx
618 for idx, vi, _ in vid_info
619 }
620 for fut in as_completed(fut_map):
621 vid_results[fut_map[fut]] = fut.result()
622 else:
623 for idx, vi, _ in vid_info:
624 vid_results[idx] = self.process_video(vi)
625
626 for ci, content in enumerate(contents):
627 _ids: list[int] = []
628 _lbls: list[int] | None = None
629
630 if content.type == "text":
631 _ids = (
632 self.tokenizer.encode(content.content)
633 if isinstance(content.content, str)
634 else list(content.content)
635 )
636 if content.is_target:
637 _lbls = _ids
638
639 elif content.type == "image":
640 tensor = self.process_image(content.content)
641 patches, thw = self._flatten_visual(tensor, "image")
642 t, h, w = thw.tolist()
643 n_tok = (t * h * w) // (self.merge_size**2)
644 img_pv.append(patches)
645 img_grids.append(thw)
646 _ids = (
647 [self.vision_start_token_id]
648 + [self.image_token_id] * n_tok
649 + [self.vision_end_token_id]
650 )
651
652 elif content.type == "video":
653 patches, thw, ts, meta = vid_results[ci]
654 t, h, w = thw.tolist()
655 n_per_grid = h * w // (self.merge_size**2)
656 vid_pv.append(patches)
657 vid_grids.append(thw)
658 second_per_grid_ts.append(
659 self.temporal_patch_size / meta["fps_sampled"]
660 )
661 video_start_times.append(float(ts[0]))
662 video_audio_n_segs.append(0)
663
664 stride = self.temporal_patch_size * self.temporal_compression_ratio
665 ts_texts = [_format_timestamp(float(x)) for x in ts[::stride]]
666 ts_ids_list = [self.tokenizer.encode(s) for s in ts_texts]
667
668 _ids = [self.video_start_token_id]
669 for ts_ids in ts_ids_list:
670 _ids += (
671 ts_ids
672 + [self.vision_start_token_id]
673 + [self.video_token_id] * n_per_grid
674 + [self.vision_end_token_id]
675 )
676 _ids += [self.video_end_token_id]
677
678 elif content.type == "audio":
679 processed = self.process_audio(content.content)
680 if isinstance(processed, tuple):
681 is_audio_tokenized.append(False)
682 spec, tok_len = processed
683 audio_inputs.append(spec)
684 else:
685 is_audio_tokenized.append(True)
686 tok_len = processed.shape[0]
687 audio_inputs.append(processed)
688 audio_token_lens.append(tok_len)
689 _ids = (
690 [self.audio_start_token_id]
691 + [self.audio_token_id] * tok_len
692 + [self.audio_end_token_id]
693 )
694
695 elif content.type == "video_audio":
696 patches, thw, ts, meta = vid_results[ci]
697 second_per_grid_ts.append(
698 self.temporal_patch_size / meta["fps_sampled"]
699 )
700 video_start_times.append(float(ts[0]))
701 processed_audio = self.process_audio(content.content)
702 tok_per_sec = self.audio_input_id_per_second / self.audio_group_size
703
704 t, h, w = thw.tolist()
705 vid_pv.append(patches)
706 vid_grids.append(thw)
707
708 if isinstance(processed_audio, tuple):
709 # Mel spec (not pre-tokenized): store in va_audio_inputs separately
710 spec, total_atok = processed_audio
711 va_audio_inputs.append(spec)
712 _va_is_tokenized = False
713 else:
714 # Pre-tokenized: not expected in vLLM, but handle defensively
715 total_atok = processed_audio.shape[0]
716 _va_is_tokenized = True
717
718 n_per_grid = h * w // (self.merge_size**2)
719 stride = self.temporal_patch_size * self.temporal_compression_ratio
720 grid_ts = ts[::stride]
721 ts_texts = [_format_timestamp(float(x)) for x in grid_ts]
722 ts_ids_list = [self.tokenizer.encode(s) for s in ts_texts]
723
724 units: list[tuple] = []
725 for i in range(len(grid_ts)):
726 a_start = int(float(grid_ts[i]) * tok_per_sec)
727 a_end = (
728 int(float(grid_ts[i + 1]) * tok_per_sec)
729 if i < len(grid_ts) - 1
730 else int(meta["segment_end_time"] * tok_per_sec)
731 )
732 seg_len = min(a_end, total_atok) - a_start
733 assert seg_len > 0, f"Zero-length audio segment at grid index {i}"
734 seg = (
735 processed_audio[a_start : a_start + seg_len]
736 if _va_is_tokenized
737 else None
738 )
739 units.append(
740 (
741 float(grid_ts[i]),
742 ts_texts[i],
743 ts_ids_list[i],
744 n_per_grid,
745 seg_len,
746 seg,
747 )
748 )
749
750 il = self.video_audio_interleave_length
751 if il == -1:
752 groups: list[list] = [list(enumerate(units))]
753 elif il == 0:
754 groups = [[(i, u)] for i, u in enumerate(units)]
755 else:
756 groups, cur, t_ptr = [], [], 0.0
757 for i, u in enumerate(units):
758 while u[0] >= t_ptr + il:
759 if cur:
760 groups.append(cur)
761 cur = []
762 t_ptr += il
763 cur.append((i, u))
764 if cur:
765 groups.append(cur)
766
767 # Track n_segs (= num groups) and per-group audio token counts
768 video_audio_n_segs.append(len(groups))
769 for group in groups:
770 group_seg_len = sum(u[4] for _, u in group)
771 video_audio_seg_lens.append(group_seg_len)
772
773 _ids = [self.video_start_token_id]
774 for group in groups:
775 _ids += group[0][1][2] # first-unit timestamp token ids
776 _vid_tok: list[int] = []
777 _aud_tok: list[int] = []
778 for _, u in group:
779 _, _, _, vid_n, seg_n, seg_audio = u
780 _vid_tok += (
781 [self.vision_start_token_id]
782 + [self.video_token_id] * vid_n
783 + [self.vision_end_token_id]
784 )
785 _aud_tok += [self.audio_token_id] * seg_n
786 if seg_audio is not None:
787 # Pre-tokenized per-frame segments (rare in vLLM)
788 audio_inputs.append(seg_audio)
789 _ids += (
790 _vid_tok
791 + [self.audio_start_token_id]
792 + _aud_tok
793 + [self.audio_end_token_id]
794 )
795 _ids += [self.video_end_token_id]
796
797 input_ids.extend(_ids)
798 labels.extend(
799 _lbls if _lbls is not None else [self.pad_token_id] * len(_ids)
800 )
801
802 ids_t = torch.tensor(input_ids)
803 lbl_arr = np.roll(labels, shift=-1)
804 lbl_arr[-1] = self.pad_token_id
805 lbl_t = torch.tensor(lbl_arr)
806
807 extra: dict = {}
808 if is_audio_tokenized:
809 assert all(is_audio_tokenized) or not any(is_audio_tokenized)
810 extra["is_audio_tokenized"] = is_audio_tokenized[0]
811
812 position_ids = torch.arange(ids_t.shape[0]).expand(3, -1)
813 rope_deltas = torch.zeros((1, 1), dtype=torch.int32)
814
815 return MiMoVLInputSample(
816 input_ids=ids_t,
817 labels=lbl_t,
818 pixel_values=img_pv,
819 pixel_values_videos=vid_pv,
820 image_thw_grids=img_grids,
821 video_thw_grids=vid_grids,
822 audio_inputs=audio_inputs,
823 second_per_grid_ts=second_per_grid_ts,
824 video_start_times=video_start_times,
825 audio_token_lens=audio_token_lens,
826 va_audio_inputs=va_audio_inputs,
827 video_audio_n_segs=video_audio_n_segs,
828 video_audio_seg_lens=video_audio_seg_lens,
829 position_ids=position_ids,
830 rope_deltas=rope_deltas,
831 extra=extra,
832 )
833
834
835 # ---------------------------------------------------------------------------
836 # vLLM ProcessorMixin wrapper
837 # ---------------------------------------------------------------------------
838
839
840 class MiMoOmniProcessor(ProcessorMixin):
841 """HuggingFace-compatible ProcessorMixin wrapper for MiMo-Omni.
842
843 Accepts PIL images, pre-decoded video tuples (frames_TCHW, timestamps_T),
844 and audio (file path / (waveform, sr) tuple / numpy array).
845 """
846
847 attributes = ["tokenizer"]
848 tokenizer_class = "AutoTokenizer"
849
850 # Single or multi-pad placeholders produced by the chat template / prior expansion
851 _IMG_RE = re.compile(r"<\|vision_start\|>(?:<\|image_pad\|>)+<\|vision_end\|>")
852 _VID_RE = re.compile(r"<\|vision_start\|>(?:<\|video_pad\|>)+<\|vision_end\|>")
853 _AUD_RE = re.compile(
854 r"<\|mimo_audio_start\|>(?:<\|audio_pad\|>)+<\|mimo_audio_end\|>"
855 )
856
857 _MM_RE = re.compile(
858 r"(<\|vision_start\|>(?:<\|image_pad\|>)+<\|vision_end\|>"
859 r"|<\|vision_start\|>(?:<\|video_pad\|>)+<\|vision_end\|>"
860 r"|<\|mimo_audio_start\|>(?:<\|audio_pad\|>)+<\|mimo_audio_end\|>)"
861 )
862
863 def __init__(
864 self,
865 tokenizer: Any,
866 *,
867 patch_size: int = 14,
868 merge_size: int = 2,
869 temporal_patch_size: int = 2,
870 temporal_compression_ratio: int = 1,
871 image_min_pixels: int | None = None,
872 image_max_pixels: int | None = None,
873 video_min_pixels: int | None = None,
874 video_max_pixels: int | None = None,
875 video_total_max_pixels: int | None = None,
876 fps: float = 2.0,
877 num_frames: int | None = None,
878 max_frames: int = 256,
879 min_frames: int = 8,
880 video_audio_interleave_length: int = 0,
881 audio_sampling_rate: int = 24000,
882 audio_nfft: int = 960,
883 audio_hop_length: int = 240,
884 audio_window_size: int = 960,
885 audio_fmin: float = 0.0,
886 audio_fmax: float | None = None,
887 audio_n_mels: int = 128,
888 audio_segment_size: int = 6000,
889 audio_kernel_size: int = 3,
890 audio_stride_size: int = 2,
891 audio_avg_pooler: int = 2,
892 audio_channels: int = 8,
893 audio_group_size: int = 4,
894 audio_input_id_per_second: float = 25.0,
895 audio_zeroemb_idx: int = 4096,
896 image_token_id: int | None = None,
897 video_token_id: int | None = None,
898 audio_token_id: int | None = None,
899 vision_start_token_id: int | None = None,
900 vision_end_token_id: int | None = None,
901 audio_start_token_id: int | None = None,
902 audio_end_token_id: int | None = None,
903 video_start_token_id: int | None = None,
904 video_end_token_id: int | None = None,
905 rope_type: str = "rope",
906 ) -> None:
907 self.tokenizer = tokenizer
908
909 unit = patch_size * merge_size
910 self.mimo_processor = MiMoVLProcessor(
911 tokenizer=tokenizer,
912 patch_size=patch_size,
913 merge_size=merge_size,
914 temporal_patch_size=temporal_patch_size,
915 temporal_compression_ratio=temporal_compression_ratio,
916 use_video_timestamps=True,
917 video_audio_interleave_length=video_audio_interleave_length,
918 audio_sampling_rate=audio_sampling_rate,
919 audio_nfft=audio_nfft,
920 audio_hop_length=audio_hop_length,
921 audio_window_size=audio_window_size,
922 audio_fmin=audio_fmin,
923 audio_fmax=audio_fmax,
924 audio_n_mels=audio_n_mels,
925 audio_segment_size=audio_segment_size,
926 audio_kernel_size=audio_kernel_size,
927 audio_stride_size=audio_stride_size,
928 audio_avg_pooler=audio_avg_pooler,
929 audio_channels=audio_channels,
930 audio_group_size=audio_group_size,
931 audio_input_id_per_second=audio_input_id_per_second,
932 audio_zeroemb_idx=audio_zeroemb_idx,
933 image_min_pixels=image_min_pixels or (4 * unit * unit),
934 image_max_pixels=image_max_pixels or (4096 * unit * unit),
935 video_min_pixels=video_min_pixels or (4 * unit * unit),
936 video_max_pixels=video_max_pixels or (4096 * unit * unit),
937 video_total_max_pixels=video_total_max_pixels or (16384 * unit * unit),
938 fps=fps,
939 num_frames=num_frames,
940 max_frames=max_frames,
941 min_frames=min_frames,
942 image_token_id=image_token_id,
943 video_token_id=video_token_id,
944 audio_token_id=audio_token_id,
945 vision_start_token_id=vision_start_token_id,
946 vision_end_token_id=vision_end_token_id,
947 audio_start_token_id=audio_start_token_id,
948 audio_end_token_id=audio_end_token_id,
949 video_start_token_id=video_start_token_id,
950 video_end_token_id=video_end_token_id,
951 pad_token_id=tokenizer.pad_token_id,
952 rope_type=rope_type,
953 )
954
955 @classmethod
956 def from_hf_config(
957 cls,
958 tokenizer: Any,
959 hf_config: Any,
960 ) -> "MiMoOmniProcessor":
961 """Instantiate directly from an HF model config object."""
962 vc = hf_config.vision_config
963 if isinstance(vc, dict):
964 patch_size = vc.get("patch_size", 14)
965 merge_size = vc.get("spatial_merge_size", 2)
966 temporal_patch_size = vc.get("temporal_patch_size", 2)
967 else:
968 patch_size = getattr(vc, "patch_size", 14)
969 merge_size = getattr(vc, "spatial_merge_size", 2)
970 temporal_patch_size = getattr(vc, "temporal_patch_size", 2)
971
972 pc: dict = getattr(hf_config, "processor_config", {}) or {}
973 ac = getattr(hf_config, "audio_config", None)
974 audio_sr: int | None = pc.get("audio_sampling_rate")
975 if audio_sr is None and ac is not None:
976 if isinstance(ac, dict):
977 audio_sr = ac.get("sampling_rate") or ac.get("sample_rate")
978 else:
979 audio_sr = getattr(ac, "sampling_rate", None) or getattr(
980 ac, "sample_rate", None
981 )
982
983 rope_type = "rope"
984 rs = getattr(hf_config, "rope_scaling", None)
985 if rs and rs.get("type") == "default" and rs.get("mrope_section") is not None:
986 rope_type = "mrope"
987
988 unit = patch_size * merge_size
989 return cls(
990 tokenizer,
991 patch_size=patch_size,
992 merge_size=merge_size,
993 temporal_patch_size=temporal_patch_size,
994 image_min_pixels=pc.get("image_min_pixels") or (4 * unit * unit),
995 image_max_pixels=pc.get("image_max_pixels") or (4096 * unit * unit),
996 video_min_pixels=pc.get("video_min_pixels") or (4 * unit * unit),
997 video_max_pixels=pc.get("video_max_pixels") or (4096 * unit * unit),
998 video_total_max_pixels=(
999 pc.get("video_total_max_pixels") or (16384 * unit * unit)
1000 ),
1001 fps=pc.get("fps") or 2.0,
1002 num_frames=pc.get("num_frames"),
1003 max_frames=pc.get("max_frames") or 256,
1004 min_frames=pc.get("min_frames") or 8,
1005 video_audio_interleave_length=pc.get("video_audio_interleave_length", 0),
1006 audio_sampling_rate=audio_sr or 24000,
1007 image_token_id=pc.get("image_token_id"),
1008 video_token_id=pc.get("video_token_id"),
1009 audio_token_id=pc.get("audio_token_id"),
1010 vision_start_token_id=pc.get("vision_start_token_id"),
1011 vision_end_token_id=pc.get("vision_end_token_id"),
1012 audio_start_token_id=pc.get("audio_start_token_id"),
1013 audio_end_token_id=pc.get("audio_end_token_id"),
1014 video_start_token_id=pc.get("video_start_token_id"),
1015 video_end_token_id=pc.get("video_end_token_id"),
1016 rope_type=rope_type,
1017 )
1018
1019 @property
1020 def image_token(self) -> str:
1021 """Token string used as image placeholder (for vLLM integration)."""
1022 return "<|image_pad|>"
1023
1024 @property
1025 def video_token(self) -> str:
1026 """Token string used as video placeholder (for vLLM integration)."""
1027 return "<|video_pad|>"
1028
1029 @property
1030 def image_processor(self) -> Any:
1031 """Minimal image-processor-like object for vLLM processing-info compat."""
1032 p = self.mimo_processor
1033
1034 class _ImageProcessor:
1035 merge_size = p.merge_size
1036 size = {
1037 "shortest_edge": p._img_kw["min_pixels"],
1038 "longest_edge": p._img_kw["max_pixels"],
1039 }
1040
1041 return _ImageProcessor()
1042
1043 def _modality(self, token: str) -> str:
1044 if self._IMG_RE.fullmatch(token):
1045 return "image"
1046 if self._VID_RE.fullmatch(token):
1047 return "video"
1048 if self._AUD_RE.fullmatch(token):
1049 return "audio"
1050 return "unknown"
1051
1052 def __call__(
1053 self,
1054 text: str | list[str] | None = None,
1055 images: Any = None,
1056 videos: Any = None,
1057 audio: Any = None,
1058 video_audio: Any = None,
1059 return_tensors: str | TensorType | None = None,
1060 **kwargs: Any,
1061 ) -> BatchFeature:
1062 """Process multimodal inputs into model-ready tensors.
1063
1064 Args:
1065 text: Prompt string(s) containing multimodal placeholders
1066 ``<|vision_start|><|image_pad|><|vision_end|>``,
1067 ``<|vision_start|><|video_pad|><|vision_end|>``, or
1068 ``<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>``.
1069 images: PIL.Image or list[PIL.Image].
1070 videos: list of ``(frames_TCHW: torch.Tensor, timestamps_T: torch.Tensor)``
1071 tuples (pre-decoded).
1072 audio: list of ``str`` (path/url/base64), ``bytes``,
1073 ``(waveform_1D, sample_rate)`` tuples, or ``np.ndarray``.
1074 video_audio: audio track extracted from ``videos``, in the same
1075 formats accepted by ``audio``.
1076 return_tensors: Passed to :class:`BatchFeature`.
1077
1078 Returns:
1079 :class:`BatchFeature` with keys:
1080 - ``input_ids``
1081 - ``pixel_values`` + ``image_grid_thw``
1082 - ``pixel_values_videos`` + ``video_grid_thw`` + ``second_per_grid_ts``
1083 - ``audio_features``
1084
1085 """
1086 if isinstance(text, list):
1087 text = text[0] if len(text) == 1 else "\n".join(text)
1088
1089 imgs: list = (
1090 ([images] if isinstance(images, Image.Image) else list(images))
1091 if images is not None
1092 else []
1093 )
1094 vids: list = list(videos) if videos is not None else []
1095 auds: list = list(audio) if audio is not None else []
1096 va_items: list = list(video_audio) if video_audio is not None else []
1097
1098 # If audio exists but text has no audio placeholder, prepend one
1099 _aud_placeholder = "<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>"
1100 if auds and text is not None and not self._AUD_RE.search(text):
1101 text = _aud_placeholder + text
1102
1103 # Build Content list
1104 contents: list[Content] = []
1105
1106 if text and (imgs or vids or auds or va_items):
1107 parts = self._MM_RE.split(text)
1108 img_it = iter(imgs)
1109 vid_it = iter(vids)
1110 aud_it = iter(auds)
1111 va_it = iter(va_items)
1112 for part in parts:
1113 if self._MM_RE.fullmatch(part):
1114 mod = self._modality(part)
1115 if mod == "image":
1116 with contextlib.suppress(StopIteration):
1117 contents.append(
1118 Content(
1119 type="image",
1120 content=ImageInput(image=next(img_it)),
1121 )
1122 )
1123 elif mod == "video":
1124 # Try regular video first, fall back to video_audio
1125 vid_item = None
1126 vid_type = "video"
1127 with contextlib.suppress(StopIteration):
1128 vid_item = next(vid_it)
1129 if vid_item is None:
1130 with contextlib.suppress(StopIteration):
1131 vid_item = next(va_it)
1132 vid_type = "video_audio"
1133 if vid_item is not None:
1134 if vid_type == "video":
1135 contents.append(
1136 Content(
1137 type="video",
1138 content=VideoInput(video=vid_item),
1139 )
1140 )
1141 else:
1142 contents.append(
1143 Content(
1144 type="video_audio",
1145 content=vid_item,
1146 )
1147 )
1148 elif mod == "audio":
1149 with contextlib.suppress(StopIteration):
1150 contents.append(
1151 Content(
1152 type="audio",
1153 content=AudioInput(audio=next(aud_it)),
1154 )
1155 )
1156 elif part:
1157 contents.append(Content(type="text", content=part))
1158 elif text:
1159 contents.append(Content(type="text", content=text))
1160 else:
1161 for img in imgs:
1162 contents.append(Content(type="image", content=ImageInput(image=img)))
1163 for vid in vids:
1164 contents.append(Content(type="video", content=VideoInput(video=vid)))
1165 for aud in auds:
1166 contents.append(Content(type="audio", content=AudioInput(audio=aud)))
1167 for va_item in va_items:
1168 contents.append(Content(type="video_audio", content=va_item))
1169
1170 if not contents:
1171 ids = self.tokenizer(text or "", return_tensors=return_tensors)["input_ids"]
1172 return BatchFeature(data={"input_ids": ids}, tensor_type=return_tensors)
1173
1174 sample = self.mimo_processor.process(contents, verbose=False)
1175
1176 # vLLM expects input_ids to have a batch dimension [1, seq_len].
1177 data: dict = {"input_ids": sample.input_ids.unsqueeze(0)}
1178
1179 if sample.pixel_values:
1180 data["pixel_values"] = torch.cat(sample.pixel_values, dim=0)
1181 data["image_grid_thw"] = torch.stack(sample.image_thw_grids)
1182
1183 if sample.pixel_values_videos:
1184 data["pixel_values_videos"] = torch.cat(sample.pixel_values_videos, dim=0)
1185 data["video_grid_thw"] = torch.stack(sample.video_thw_grids)
1186 if sample.second_per_grid_ts:
1187 data["second_per_grid_ts"] = torch.tensor(
1188 sample.second_per_grid_ts, dtype=torch.float32
1189 )
1190 if sample.video_start_times:
1191 data["video_start_times"] = torch.tensor(
1192 sample.video_start_times, dtype=torch.float32
1193 )
1194 if sample.video_audio_n_segs:
1195 data["video_audio_n_segs"] = torch.tensor(
1196 sample.video_audio_n_segs, dtype=torch.long
1197 )
1198 # video_audio_seg_lens: 2D padded tensor (num_videos, max_T).
1199 # Row i has the per-group audio token lengths for video i
1200 # (zeros for regular videos; valid values for video_audio videos).
1201 n_segs_list = sample.video_audio_n_segs
1202 max_segs = max(n_segs_list) if n_segs_list else 0
1203 if max_segs > 0:
1204 seg_lens_2d = torch.zeros(len(n_segs_list), max_segs, dtype=torch.long)
1205 flat_cursor = 0
1206 for vi, n in enumerate(n_segs_list):
1207 if n > 0:
1208 seg_lens_2d[vi, :n] = torch.tensor(
1209 sample.video_audio_seg_lens[flat_cursor : flat_cursor + n],
1210 dtype=torch.long,
1211 )
1212 flat_cursor += n
1213 data["video_audio_seg_lens"] = seg_lens_2d
1214
1215 # audio_features is a list of variable-length mel-spec tensors; pop it
1216 # before BatchFeature conversion to avoid "batched tensors of the same
1217 # length" errors, then re-attach it after.
1218 audio_features = None
1219 if sample.audio_inputs:
1220 audio_features = sample.audio_inputs
1221 if "is_audio_tokenized" in sample.extra:
1222 data["is_audio_tokenized"] = sample.extra["is_audio_tokenized"]
1223 if sample.audio_token_lens:
1224 data["audio_token_lens"] = torch.tensor(
1225 sample.audio_token_lens, dtype=torch.long
1226 )
1227
1228 bf = BatchFeature(data=data, tensor_type=return_tensors)
1229 if audio_features is not None:
1230 bf["audio_features"] = audio_features
1231 # va_audio_features: list of mel-spec tensors (one per video_audio item)
1232 if sample.va_audio_inputs:
1233 bf["va_audio_features"] = sample.va_audio_inputs
1234 return bf