# Copyright 2025 the LlamaFactory team. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from types import SimpleNamespace import pytest import torch from PIL import Image from llamafactory.data.collator import MultiModalDataCollatorForSeq2Seq from llamafactory.data.mm_plugin import get_mm_plugin from llamafactory.data.processor.supervised import SupervisedDatasetProcessor from llamafactory.extras.constants import IGNORE_INDEX IMAGE_TOKEN_ID = 101 VIDEO_TOKEN_ID = 102 VISION_START_TOKEN_ID = 103 VISION_END_TOKEN_ID = 104 TIME_START_TOKEN_ID = 105 TIME_END_TOKEN_ID = 106 IM_END_TOKEN_ID = 107 class _ImageProcessor: def __init__(self): self.calls = [] def __call__(self, images, return_tensors, **kwargs): self.calls.append({"return_tensors": return_tensors, **kwargs}) values = [] for image in images: marker = image.getpixel((0, 0))[0] + 1 values.append(torch.full((1, 3), marker, dtype=torch.float32)) return { "pixel_values": torch.cat(values), "image_grid_thw": torch.tensor([[1, 1, 1]] * len(images)), } class _VideoProcessor: temporal_patch_size = 1 def __init__(self): self.calls = [] def __call__(self, videos, return_tensors, return_metadata, **kwargs): self.calls.append( { "return_tensors": return_tensors, "return_metadata": return_metadata, **kwargs, } ) result = { "pixel_values_videos": torch.cat( [torch.full((2, 3), 9 + index, dtype=torch.float32) for index in range(len(videos))] ), "video_grid_thw": torch.tensor([[2, 1, 1]] * len(videos)), } if return_metadata: result["video_metadata"] = [ SimpleNamespace(frames_indices=[0, 2], total_num_frames=2, fps=2.0, duration=2.0) for _ in videos ] return result class _Tokenizer: pad_token_id = 0 padding_side = "right" _token_ids = { "<|time_start|>": TIME_START_TOKEN_ID, "<|time_end|>": TIME_END_TOKEN_ID, "<|im_end|>": IM_END_TOKEN_ID, } def convert_tokens_to_ids(self, token): return self._token_ids[token] def pad(self, features, padding, max_length, pad_to_multiple_of, return_tensors): del padding, max_length, return_tensors sequence_length = max(len(feature["input_ids"]) for feature in features) if pad_to_multiple_of is not None: sequence_length = ((sequence_length + pad_to_multiple_of - 1) // pad_to_multiple_of) * pad_to_multiple_of padded = {"input_ids": [], "attention_mask": []} for feature in features: pad_length = sequence_length - len(feature["input_ids"]) if self.padding_side == "right": padded["input_ids"].append(feature["input_ids"] + [self.pad_token_id] * pad_length) padded["attention_mask"].append(feature["attention_mask"] + [0] * pad_length) else: padded["input_ids"].append([self.pad_token_id] * pad_length + feature["input_ids"]) padded["attention_mask"].append([0] * pad_length + feature["attention_mask"]) return {key: torch.tensor(value) for key, value in padded.items()} class _Processor: image_token_id = IMAGE_TOKEN_ID video_token_id = VIDEO_TOKEN_ID vision_start_token_id = VISION_START_TOKEN_ID vision_end_token_id = VISION_END_TOKEN_ID def __init__(self): self.image_processor = _ImageProcessor() self.video_processor = _VideoProcessor() self.tokenizer = _Tokenizer() @staticmethod def _calculate_timestamps(*args, **kwargs): del args, kwargs return [0.0, 1.0] def _get_plugin(): return get_mm_plugin( name="moss_vl", image_token="<|image_pad|>", video_token="<|video_pad|>", vision_bos_token="<|vision_start|>", vision_eos_token="<|vision_end|>", time_bos_token="<|time_start|>", time_eos_token="<|time_end|>", ) def _video_ids(seed): return [ VISION_START_TOKEN_ID, TIME_START_TOKEN_ID, seed, TIME_END_TOKEN_ID, IMAGE_TOKEN_ID, TIME_START_TOKEN_ID, seed + 1, TIME_END_TOKEN_ID, IMAGE_TOKEN_ID, VISION_END_TOKEN_ID, ] def _left_pad(sequences, pad_value): max_len = max(map(len, sequences)) return torch.tensor([[pad_value] * (max_len - len(sequence)) + sequence for sequence in sequences]) def test_moss_vl_process_messages_expands_video_frames(): plugin = _get_plugin() processor = _Processor() messages = [ {"role": "user", "content": "First , then