"""MiMoV2 multimodal processor -- protocol, utilities, and processor.""" import asyncio import base64 import copy import json import math import re import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field from io import BytesIO from typing import List, Literal, Optional, Union import numpy as np import requests import torch import torch.nn.functional as F from fastapi import HTTPException from PIL import Image from torchcodec.decoders import AudioDecoder from transformers.models.qwen2_5_vl.configuration_qwen2_5_vl import ( Qwen2_5_VLVisionConfig, ) from sglang.srt.environ import envs from sglang.srt.managers.schedule_batch import ( Modality, MultimodalDataItem, MultimodalProcessorOutput, ) from sglang.srt.models.mimo_v2 import MiMoV2ForCausalLM from sglang.srt.multimodal.processors.base_processor import ( BaseMultimodalProcessor, MultimodalSpecialTokens, ) from sglang.srt.multimodal.processors.mimo_audio import ( AudioInput, MiMoAudioPipeline, ) from sglang.srt.multimodal.processors.qwen_vl import smart_nframes from sglang.srt.utils import ImageData, VideoData from sglang.utils import logger @dataclass class ImageInput: image: Image.Image | str | bytes | torch.Tensor max_pixels: Optional[int] = None min_pixels: Optional[int] = None def __post_init__(self): if not isinstance(self.image, (Image.Image, str, bytes, torch.Tensor)): raise ValueError( f"image must be a PIL.Image.Image, str, bytes, or torch.Tensor, but got {type(self.image)}" ) @dataclass class VideoInput: video: str | bytes | tuple[torch.Tensor, torch.Tensor] min_pixels: Optional[int] = None max_pixels: Optional[int] = None total_max_pixels: Optional[int] = None fps: Optional[float] = None num_frames: Optional[int] = None max_frames: Optional[int] = None min_frames: Optional[int] = None do_include_last_frame: Optional[bool] = False start_time: Optional[float] = None end_time: Optional[float] = None segment_type: Literal["individual", "partial"] = "individual" def __post_init__(self): if not isinstance(self.video, (str, bytes, tuple)): raise ValueError( f"video must be a str, bytes, or tuple, but got {type(self.video)}" ) if isinstance(self.video, tuple): if len(self.video) != 2: raise ValueError( f"video must be a tuple of 2 elements (pixels, timestamps), but got {len(self.video)} elements" ) if not isinstance(self.video[0], torch.Tensor) or not isinstance( self.video[1], torch.Tensor ): raise ValueError( f"video must be a tuple of Tensors (pixels, timestamps), but got {type(self.video[0])} and {type(self.video[1])}" ) if ( self.video[0].ndim != 4 or self.video[1].ndim != 1 or self.video[0].shape[0] != self.video[1].shape[0] ): raise ValueError( f"video must be a tuple of (pixels-TCHW, timestamps-T), but got {self.video[0].shape} and {self.video[1].shape}" ) assert self.segment_type in ["individual", "partial"] assert self.segment_type == "partial" or ( self.start_time is None and self.end_time is None ) @dataclass class VideoAudioInput: video: str | bytes | tuple[torch.Tensor, torch.Tensor] audio: str | bytes | torch.Tensor min_pixels: Optional[int] = None max_pixels: Optional[int] = None total_max_pixels: Optional[int] = None fps: Optional[float] = None num_frames: Optional[int] = None max_frames: Optional[int] = None min_frames: Optional[int] = None do_include_last_frame: Optional[bool] = False start_time: Optional[float] = None end_time: Optional[float] = None segment_type: Literal["individual", "partial"] = "individual" def __post_init__(self): if not isinstance(self.video, (str, bytes, tuple)): raise ValueError( f"video must be a str, bytes, or tuple, but got {type(self.video)}" ) if isinstance(self.video, tuple): if len(self.video) != 2: raise ValueError( f"video must be a tuple of 2 elements (pixels, timestamps), but got {len(self.video)} elements" ) if not isinstance(self.video[0], torch.Tensor) or not isinstance( self.video[1], torch.Tensor ): raise ValueError( f"video must be a tuple of Tensors (pixels, timestamps), but got {type(self.video[0])} and {type(self.video[1])}" ) if ( self.video[0].ndim != 4 or self.video[1].ndim != 1 or self.video[0].shape[0] != self.video[1].shape[0] ): raise ValueError( f"video must be a tuple of (pixels-TCHW, timestamps-T), but got {self.video[0].shape} and {self.video[1].shape}" ) assert self.segment_type in ["individual", "partial"] assert self.segment_type == "partial" or ( self.start_time is None and self.end_time is None ) if not isinstance(self.audio, (str, bytes, torch.Tensor)): raise ValueError( f"audio must be a str, bytes, or torch.Tensor, but got {type(self.audio)}" ) if isinstance(self.audio, torch.Tensor) and self.audio.ndim != 2: raise ValueError( f"audio must be a 2D tensor, but got {self.audio.ndim}D tensor" ) TextInput = str | list[int] @dataclass class MiMoInputSample: input_ids: torch.Tensor labels: Optional[torch.Tensor] pixel_values: list[torch.Tensor] pixel_values_videos: list[torch.Tensor] image_thw_grids: list[torch.Tensor] video_thw_grids: list[torch.Tensor] audio_inputs: list[torch.Tensor] position_ids: Optional[torch.Tensor] = None rope_deltas: Optional[torch.Tensor] = None extra: dict = field(default_factory=dict) @dataclass class Content: type: Literal["text", "image", "video", "audio", "video_audio"] content: TextInput | ImageInput | VideoInput | AudioInput | VideoAudioInput is_target: Optional[bool] = None def __post_init__(self): if self.type not in ["text", "image", "video", "audio", "video_audio"]: raise ValueError( f"type must be one of text, image, video, audio, video_audio, but got {self.type}" ) if self.type == "text": if not isinstance(self.content, (str, list)) or ( isinstance(self.content, list) and not all(isinstance(item, int) for item in self.content) ): raise ValueError( f"content must be a str or a list of ints, but got {type(self.content)}" ) elif self.type == "image": if not isinstance(self.content, ImageInput): raise ValueError( f"content must be a ImageInput, but got {type(self.content)}" ) elif self.type == "video": if not isinstance(self.content, VideoInput): raise ValueError( f"content must be a VideoInput, but got {type(self.content)}" ) elif self.type == "audio": if not isinstance(self.content, AudioInput): raise ValueError( f"content must be a AudioInput, but got {type(self.content)}" ) elif self.type == "video_audio": if not isinstance(self.content, VideoAudioInput): raise ValueError( f"content must be a VideoAudioInput, but got {type(self.content)}" ) _QWEN2VL_PIXEL_MEAN = torch.Tensor([123.675, 116.28, 103.53]).view(-1, 1, 1) _QWEN2VL_PIXEL_STD = torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1) _mean_std_cache = {} def _decode_frames_and_timestamps(vdw, ele): # Shared E/D frame-sampling recipe: smart_nframes + linspace + permute. total_frames, video_fps = len(vdw), vdw.avg_fps nframes = smart_nframes(ele, total_frames=total_frames, video_fps=video_fps) idx = list(np.unique(np.linspace(0, total_frames - 1, num=nframes, dtype=np.int64))) video_tensor = vdw.get_frames_as_tensor(idx).permute(0, 3, 1, 2).float() timestamps = torch.as_tensor(idx, dtype=torch.float32) / video_fps return video_tensor, timestamps def _ffprobe_has_audio(src, stdin=None, label=None) -> bool: # Header-only audio-stream probe for HTTP URLs; avoids full download. try: r = subprocess.run( [ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-select_streams", "a", src, ], input=stdin, capture_output=True, timeout=30, ) if r.returncode != 0: stderr = r.stderr.decode("utf-8", errors="replace") raise RuntimeError(f"ffprobe failed for {label}: {stderr}") return bool(json.loads(r.stdout).get("streams")) except subprocess.TimeoutExpired: logger.error("ffprobe timed out for %s", label) raise except FileNotFoundError as e: raise RuntimeError("ffprobe not found; install ffmpeg") from e except json.JSONDecodeError: logger.error("ffprobe returned invalid JSON for %s", label) raise class MiMoProcessor: def __init__( self, tokenizer, patch_size=14, merge_size=2, temporal_patch_size=2, temporal_compression_ratio=1, video_tokens_per_second=2, use_video_timestamps=False, video_audio_interleave_length=0, use_per_grid_t_timestamps=True, audio_kernel_size=3, audio_stride_size=2, audio_avg_pooler=2, audio_sampling_rate=24000, audio_nfft=960, audio_hop_length=240, audio_window_size=960, audio_fmin=0, audio_fmax=None, audio_n_mels=128, audio_channels=8, audio_group_size=4, audio_input_id_per_second=25, image_min_pixels=None, image_max_pixels=None, video_min_pixels=None, video_max_pixels=None, video_total_max_pixels=None, fps=None, num_frames=None, max_frames=None, min_frames=None, image_token_id=None, video_token_id=None, audio_token_id=None, vision_start_token_id=None, vision_end_token_id=None, audio_start_token_id=None, audio_end_token_id=None, video_start_token_id=None, video_end_token_id=None, pad_token_id=None, rope_type="rope", video_process_num_threads=16, video_decode_num_threads=0, device=None, **kwargs, ): self.tokenizer = tokenizer self.video_process_num_threads = video_process_num_threads self.video_decode_num_threads = video_decode_num_threads if device is None: self.device = None else: self.device = torch.device(device) if isinstance(device, str) else device self.rope_type = rope_type if self.rope_type == "1d": self.rope_type = "rope" assert self.rope_type in ["rope", "mrope"] self.use_video_timestamps = use_video_timestamps assert self.use_video_timestamps assert ( not self.use_video_timestamps or self.rope_type == "rope" ), "use_video_timestamps only supports 1d rope" self.video_audio_interleave_length = video_audio_interleave_length self.use_per_grid_t_timestamps = False assert ( self.video_audio_interleave_length == -1 or self.rope_type == "rope" ), "video_audio_interleave_length != -1 only supports 1d rope" assert ( self.video_audio_interleave_length == -1 or self.video_audio_interleave_length >= 0 ) self.image_token_id = image_token_id self.video_token_id = video_token_id self.vision_start_token_id = vision_start_token_id self.vision_end_token_id = vision_end_token_id self.video_start_token_id = video_start_token_id self.video_end_token_id = video_end_token_id self.pad_token_id = pad_token_id self.patch_size = patch_size self.merge_size = merge_size self.temporal_patch_size = temporal_patch_size self.temporal_compression_ratio = temporal_compression_ratio self.video_tokens_per_second = video_tokens_per_second self.audio_pipeline = MiMoAudioPipeline( audio_token_id=audio_token_id, audio_start_token_id=audio_start_token_id, audio_end_token_id=audio_end_token_id, audio_kernel_size=audio_kernel_size, audio_stride_size=audio_stride_size, audio_avg_pooler=audio_avg_pooler, audio_group_size=audio_group_size, audio_channels=audio_channels, audio_sampling_rate=audio_sampling_rate, audio_nfft=audio_nfft, audio_hop_length=audio_hop_length, audio_window_size=audio_window_size, audio_fmin=audio_fmin, audio_fmax=audio_fmax, audio_n_mels=audio_n_mels, audio_input_id_per_second=audio_input_id_per_second, ) assert image_min_pixels is not None assert image_max_pixels is not None assert video_min_pixels is not None assert video_max_pixels is not None assert video_total_max_pixels is not None assert fps is not None or num_frames is not None self.default_image_processor_kwargs = { "min_pixels": image_min_pixels, "max_pixels": image_max_pixels, } self.default_video_processor_kwargs = { "min_pixels": video_min_pixels, "max_pixels": video_max_pixels, "total_max_pixels": video_total_max_pixels, "fps": fps, "num_frames": num_frames, "max_frames": max_frames, "min_frames": min_frames, } for k in kwargs: logger.info(f"[Warning] Ignored unknown parameter {k} for MiMoProcessor") def __getattr__(self, name): # Delegate audio_pipeline fields so callers can use self.audio_token_id # etc. directly. Only triggers when normal attribute lookup fails; # __dict__.get avoids recursion before audio_pipeline is assigned. pipeline = self.__dict__.get("audio_pipeline") if pipeline is not None and hasattr(pipeline, name): return getattr(pipeline, name) raise AttributeError(name) @classmethod def from_hf_config(cls, hf_config, mm_config=None, **overrides): # Params must come from hf_config.processor_config so E and D agree; # any drift shifts input_ids on the D side. def _as_dict(obj): if isinstance(obj, dict): return obj return obj.to_dict() if obj and hasattr(obj, "to_dict") else {} pc = _as_dict(getattr(hf_config, "processor_config", None)) ac = _as_dict(getattr(hf_config, "audio_config", None)) vc = hf_config.vision_config vget = vc.get if isinstance(vc, dict) else (lambda k, d=None: getattr(vc, k, d)) patch_size = vget("patch_size", 14) merge_size = vget("spatial_merge_size", 2) f = patch_size * merge_size kwargs = { "tokenizer": None, "patch_size": patch_size, "merge_size": merge_size, "temporal_patch_size": vget("temporal_patch_size", 2), "image_min_pixels": pc.get("image_min_pixels") or 4 * f * f, "image_max_pixels": pc.get("image_max_pixels") or 4096 * f * f, "video_min_pixels": pc.get("video_min_pixels") or 4 * f * f, "video_max_pixels": pc.get("video_max_pixels") or 4096 * f * f, "video_total_max_pixels": pc.get("video_total_max_pixels") or 16384 * f * f, "fps": pc.get("fps") or 2, "num_frames": pc.get("num_frames"), "max_frames": pc.get("max_frames") or 256, "min_frames": pc.get("min_frames") or 8, "video_audio_interleave_length": pc.get("video_audio_interleave_length", 0), "use_per_grid_t_timestamps": pc.get("use_per_grid_t_timestamps", False), "use_video_timestamps": pc.get("use_video_timestamps", False), } # audio_sampling_rate: processor_config > audio_config > mm_config.audio. asr = ( pc.get("audio_sampling_rate") or ac.get("sampling_rate") or ac.get("sample_rate") ) if asr is not None: kwargs["audio_sampling_rate"] = asr audio_cfg = (mm_config or {}).get("audio", {}) for k in ( "audio_sampling_rate", "audio_hop_length", "audio_n_mels", "audio_kernel_size", "audio_stride_size", "audio_avg_pooler", ): if k in audio_cfg: kwargs[k] = audio_cfg[k] if "sampling_rate" in audio_cfg and "audio_sampling_rate" not in kwargs: kwargs["audio_sampling_rate"] = audio_cfg["sampling_rate"] image_cfg = (mm_config or {}).get("image", {}) if "device" in image_cfg: kwargs["device"] = image_cfg["device"] video_cfg = (mm_config or {}).get("video", {}) if "video_decode_num_threads" in video_cfg: kwargs["video_decode_num_threads"] = video_cfg["video_decode_num_threads"] else: from sglang.srt.utils.common import get_int_env_var kwargs["video_decode_num_threads"] = get_int_env_var( "SGLANG_ENCODER_VIDEO_DECODE_NUM_THREADS", 0 ) kwargs.update(overrides) return cls(**kwargs) @staticmethod def has_audio_track(path_or_data) -> bool: # In-process probe via torchcodec for bytes/path; ffprobe range # request for HTTP URLs so we do not pre-download the blob here. if isinstance(path_or_data, str) and path_or_data.startswith( ("http://", "https://") ): return _ffprobe_has_audio(path_or_data, stdin=None, label=path_or_data) if isinstance(path_or_data, bytes): source = BytesIO(path_or_data) elif ( isinstance(path_or_data, str) and path_or_data.startswith("data:") and ";base64," in path_or_data ): source = BytesIO(base64.b64decode(path_or_data.split(";base64,")[1])) else: source = path_or_data # local path or file:// try: AudioDecoder(source) return True except Exception: return False def _load_video_for_encoder(self, video_data): # Normalise once to bytes-or-path; reused by frame decode, audio # detection, and audio preprocessing without re-downloading. from sglang.srt.utils.common import VideoData, _normalize_video_input from sglang.srt.utils.video_decoder import VideoDecoderWrapper if isinstance(video_data, VideoData): video_data = video_data.url if isinstance(video_data, bytes): video_blob = video_data else: video_blob = _normalize_video_input(video_data) if video_blob is None: raise ValueError( f"Unsupported video input type for EPD encoder: {type(video_data)}" ) vdw = VideoDecoderWrapper( video_blob, device="cpu", num_decode_threads=self.video_decode_num_threads, ) try: video_tuple = _decode_frames_and_timestamps( vdw, self.default_video_processor_kwargs ) finally: if hasattr(vdw, "close"): vdw.close() return video_blob, video_tuple def preprocess_for_encoder(self, mm_data, modality): # EPD encoder-side features. video_audio_* fields appear when any # video has audio; the D side uses them to rebuild input_ids. from sglang.srt.managers.schedule_batch import Modality if not isinstance(mm_data, (list, tuple)): mm_data = [mm_data] if modality == Modality.IMAGE: factor = self.patch_size * self.merge_size min_pixels = self.default_image_processor_kwargs["min_pixels"] max_pixels = self.default_image_processor_kwargs["max_pixels"] all_patches, all_grids = [], [] for img in mm_data: img_tensor, _, _ = self.get_visual_transform( img, factor=factor, min_pixels=min_pixels, max_pixels=max_pixels, device=self.device, ) patches, grid = self._flatten_visual_inputs(img_tensor, "image") all_patches.append(patches) all_grids.append(grid) return { "pixel_values": torch.cat(all_patches, dim=0), "image_grid_thw": torch.stack(all_grids), } if modality == Modality.VIDEO: all_patches, all_grids, all_timestamps = [], [], [] audio_features, audio_feature_lens = [], [] seg_lens_flat, seg_starts_flat, per_video_num_units = [], [], [] for video_data in mm_data: video_blob, video_tuple = self._load_video_for_encoder(video_data) patches, grid, aligned_ts, video_meta = self.process_video( VideoInput(video=video_tuple) ) all_patches.append(patches) all_grids.append(grid) step = self.temporal_patch_size * self.temporal_compression_ratio all_timestamps.extend(aligned_ts[::step].tolist()) if self.has_audio_track(video_blob): audio_spec, audio_token_len = self.audio_pipeline.preprocess_audio( video_blob ) units = self._build_video_audio_units( grid, aligned_ts, video_meta, processed_audio=audio_spec, is_tokenized=False, audio_token_len=audio_token_len, ) audio_features.append(audio_spec) audio_feature_lens.append(audio_token_len) seg_lens_flat.extend(u["segment_audio_token_len"] for u in units) seg_starts_flat.extend(u["audio_start_token_idx"] for u in units) per_video_num_units.append(len(units)) else: per_video_num_units.append(0) result = { "pixel_values_videos": torch.cat(all_patches, dim=0), "video_grid_thw": torch.stack(all_grids), "video_timestamps": all_timestamps, } if audio_features: result["video_audio_features"] = audio_features result["video_audio_feature_lens"] = torch.tensor( audio_feature_lens, dtype=torch.long ) result["video_audio_segment_lens_flat"] = seg_lens_flat result["video_audio_segment_starts_flat"] = seg_starts_flat result["video_audio_per_video_num_units"] = per_video_num_units return result if modality == Modality.AUDIO: all_specs, all_lens = [], [] for audio in mm_data: if isinstance(audio, np.ndarray): audio = (torch.from_numpy(audio).float(), self.audio_sampling_rate) spec, token_len = self.audio_pipeline.preprocess_audio(audio) all_specs.append(spec) all_lens.append(token_len) return { "input_features": all_specs, "audio_feature_lens_raw": torch.tensor(all_lens, dtype=torch.long), } raise ValueError(f"Unsupported modality for EPD preprocessing: {modality}") def prepare_image_kwargs(self, image: ImageInput): kwargs = {} for k in ["min_pixels", "max_pixels"]: if getattr(image, k) is not None: kwargs[k] = getattr(image, k) else: kwargs[k] = self.default_image_processor_kwargs[k] return kwargs def prepare_video_kwargs(self, video: VideoInput | VideoAudioInput): kwargs = {} for k in ["min_pixels", "max_pixels", "total_max_pixels"]: if getattr(video, k) is not None: kwargs[k] = getattr(video, k) else: kwargs[k] = self.default_video_processor_kwargs[k] if video.num_frames is not None: kwargs["num_frames"] = video.num_frames elif video.fps is not None: kwargs["fps"] = video.fps if video.max_frames is not None: kwargs["max_frames"] = video.max_frames if video.min_frames is not None: kwargs["min_frames"] = video.min_frames elif self.default_video_processor_kwargs["num_frames"] is not None: kwargs["num_frames"] = self.default_video_processor_kwargs["num_frames"] elif self.default_video_processor_kwargs["fps"] is not None: kwargs["fps"] = self.default_video_processor_kwargs["fps"] if self.default_video_processor_kwargs["max_frames"] is not None: kwargs["max_frames"] = self.default_video_processor_kwargs["max_frames"] if self.default_video_processor_kwargs["min_frames"] is not None: kwargs["min_frames"] = self.default_video_processor_kwargs["min_frames"] else: raise ValueError("Video sampling strategy not specified") return kwargs def process_image(self, image: ImageInput): kwargs = self.prepare_image_kwargs(image) image = image.image if isinstance(image, (str, bytes)): image = self.fetch_image(image) image_transformed_tensor, _, _ = self.get_visual_transform( image, factor=self.patch_size * self.merge_size, min_pixels=kwargs["min_pixels"], max_pixels=kwargs["max_pixels"], device=self.device, ) return image_transformed_tensor def process_video( self, video_input: VideoInput | VideoAudioInput, temporal_padding_factor=None ): def smart_resize_video( num_total_frames, min_pixels, max_pixels, total_max_pixels, **kwargs ): max_pixels_per_frame = ( total_max_pixels * self.temporal_patch_size * self.temporal_compression_ratio // num_total_frames ) max_pixels = max(min_pixels, min(max_pixels_per_frame, max_pixels)) return min_pixels, max_pixels def segment_frame_selector(all_timestamps, start_time, end_time): """Select frame indices in [start_time, end_time). If none found, pick the nearest frame to the left.""" if not isinstance(all_timestamps, torch.Tensor): all_timestamps = torch.tensor(all_timestamps) mask = (all_timestamps >= start_time) & (all_timestamps < end_time) candidate_indices = torch.where(mask)[0] if len(candidate_indices) == 0: left_mask = all_timestamps <= start_time left_indices = torch.where(left_mask)[0] if len(left_indices) > 0: selected_frame_indices = left_indices[-1:].clone() else: raise ValueError( f"No frames before start_time {start_time} in all_timestamps {all_timestamps.tolist()}" ) else: selected_frame_indices = candidate_indices assert ( len(selected_frame_indices) > 0 ), f"No frames selected for segment {start_time} - {end_time} in all_timestamps {all_timestamps.tolist()}" return selected_frame_indices kwargs = self.prepare_video_kwargs(video_input) video = video_input.video if not isinstance(video, tuple): raise ValueError( f"video must be a tuple of (video_tensor, timestamps), but got {type(video)}. " "Video download and decoding should be done by sglang load_video before calling process_video." ) video_tensor, timestamps_sampled = video if len(timestamps_sampled) < 2: logger.info( "[Warning] Less than two frames are sampled, using default fps (1 fps)" ) fps_sampled = 1 else: fps_sampled = 1 / (timestamps_sampled[1] - timestamps_sampled[0]) num_frames_sampled = video_tensor.shape[0] start_time = ( video_input.start_time if video_input.start_time is not None else timestamps_sampled[0] ) end_time = ( video_input.end_time if video_input.end_time is not None else timestamps_sampled[-1] + (1 / fps_sampled) ) if video_input.segment_type == "individual": start_time_seg = start_time end_time_seg = end_time timestamps_seg = timestamps_sampled frames = video_tensor num_frames_seg = num_frames_sampled else: selected_indices = segment_frame_selector( timestamps_sampled, start_time, end_time ) timestamps_seg = timestamps_sampled[selected_indices] frames = video_tensor[selected_indices] num_frames_seg = len(timestamps_seg) start_time_seg = ( timestamps_seg[0].item() if isinstance(timestamps_seg[0], torch.Tensor) else timestamps_seg[0] ) end_time_seg = ( timestamps_seg[-1].item() if isinstance(timestamps_seg[-1], torch.Tensor) else timestamps_seg[-1] ) + (1 / fps_sampled).item() video_meta = { "fps_sampled": fps_sampled, "segment_start_time": start_time_seg, "segment_end_time": end_time_seg, } min_pixels, max_pixels = smart_resize_video(num_frames_sampled, **kwargs) assert ( num_frames_seg > 0 ), f"Sampled frame number must be >0. start_time {video_input.start_time}, end_time {video_input.end_time}, start_time_seg {start_time_seg}, end_time_seg {end_time_seg}. Full timestamps {timestamps_sampled.tolist()}. " temporal_padding_factor = ( self.temporal_patch_size * self.temporal_compression_ratio if temporal_padding_factor is None else temporal_padding_factor ) if num_frames_seg % temporal_padding_factor == 0: aligned_frames = frames aligned_timestamps = timestamps_seg else: aligned_num_frames = ( (num_frames_seg + temporal_padding_factor - 1) // temporal_padding_factor ) * temporal_padding_factor num_frames_needed = aligned_num_frames - num_frames_seg aligned_frames = torch.cat( [ frames, frames[-1:].repeat(num_frames_needed, *[1] * (frames.ndim - 1)), ], dim=0, ) aligned_timestamps = torch.cat( [timestamps_seg, timestamps_seg[-1:].repeat(num_frames_needed)], dim=0 ) video_transformed_tensor, _, _ = self.get_visual_transform_batch( aligned_frames, factor=self.patch_size * self.merge_size, min_pixels=min_pixels, max_pixels=max_pixels, device=self.device, ) visual_patches, thw_grid = self._flatten_visual_inputs( video_transformed_tensor, "video" ) return visual_patches, thw_grid, aligned_timestamps, video_meta def _process_videos_parallel(self, contents): video_contents_info = [] for idx, content in enumerate(contents): if content.type in ("video", "video_audio"): video_contents_info.append((idx, content.content)) video_results = {} if not video_contents_info: return video_results num_threads = min(self.video_process_num_threads, len(video_contents_info)) if num_threads > 1 and len(video_contents_info) > 1: with ThreadPoolExecutor(max_workers=num_threads) as executor: future_to_idx = { executor.submit(self.process_video, video_input): idx for idx, video_input in video_contents_info } for future in as_completed(future_to_idx): idx = future_to_idx[future] try: video_results[idx] = future.result() except Exception as e: raise RuntimeError( f"Error processing video at index {idx}: {e}" ) from e else: for idx, video_input in video_contents_info: video_results[idx] = self.process_video(video_input) return video_results def _process_text_content(self, content, verbose): if isinstance(content.content, str): _input_ids = self.tokenizer.encode(content.content) else: _input_ids = content.content _labels = _input_ids if content.is_target else None verbose_str = "" if verbose: if isinstance(content.content, str): verbose_str = f"Text: [{content.content}]\n" else: verbose_str = f"Text: [{self.tokenizer.decode(content.content)}]\n" return {"input_ids": _input_ids, "labels": _labels, "verbose": verbose_str} def _process_image_content(self, content, verbose): image_tensor = self.process_image(content.content) visual_patches, thw_grid = self._flatten_visual_inputs(image_tensor, "image") grid_t, grid_h, grid_w = thw_grid num_media_tokens = (grid_t * grid_h * grid_w) // (self.merge_size**2) _input_ids = ( [self.vision_start_token_id] + [self.image_token_id] * num_media_tokens + [self.vision_end_token_id] ) verbose_str = "" if verbose: verbose_str = f"Image (shape={image_tensor.shape}, image_thw_grid={thw_grid}): [ {num_media_tokens}* ]\n" return { "input_ids": _input_ids, "pixel_values": visual_patches, "thw_grid": thw_grid, "verbose": verbose_str, } def _process_video_content(self, content_idx, video_results, verbose): visual_patches, thw_grid, timestamps, video_meta = video_results[content_idx] grid_t, grid_h, grid_w = thw_grid num_media_tokens = ( (grid_t * grid_h * grid_w) // (self.merge_size**2) // self.temporal_compression_ratio ) assert ( len(timestamps) == grid_t * self.temporal_patch_size ), f"Expected {grid_t} * {self.temporal_patch_size} = {grid_t * self.temporal_patch_size} timestamps, but got {len(timestamps)}" if not self.use_video_timestamps: raise NotImplementedError num_media_tokens_per_grid = grid_h * grid_w // (self.merge_size**2) text_timestamps = [ self.format_timestamp(ts) for ts in timestamps[ :: self.temporal_patch_size * self.temporal_compression_ratio ] ] text_timestamp_ids = [self.tokenizer.encode(ts) for ts in text_timestamps] _input_ids = ( [self.video_start_token_id] + sum( [ ts_ids + [self.vision_start_token_id] + [self.video_token_id] * num_media_tokens_per_grid + [self.vision_end_token_id] for ts_ids in text_timestamp_ids ], [], ) + [self.video_end_token_id] ) verbose_str = "" if verbose: verbose_str = f"Video (video_thw_grid={thw_grid}, video_meta={video_meta}): [ " for i, ts in enumerate(text_timestamps): verbose_str += f"{ts} {timestamps.tolist()[i*self.temporal_patch_size*self.temporal_compression_ratio : (i+1)*self.temporal_patch_size*self.temporal_compression_ratio]} {num_media_tokens_per_grid}* " verbose_str += "]\n" return { "input_ids": _input_ids, "pixel_values": visual_patches, "thw_grid": thw_grid, "second_per_grid_t": self.temporal_patch_size / video_meta["fps_sampled"], "verbose": verbose_str, } def _process_audio_content(self, content, verbose): result = self.audio_pipeline.process_audio_input(content.content) verbose_str = "" if verbose: verbose_str = ( f"Audio (is_tokenized={result['is_tokenized']}): " f"[ {result['audio_token_len']}*