A continuation of kimi source tree.txt. The complete source identity, every recorded occurrence, and linked equations are on its first page.
Source transcript · continued
Download complete source text ↓6,675 lines · 275.4 KB
r"""
Constructs a KimiK25 processor which wraps a KimiK25 image processor and a tokenizer into a single processor.
[`KimiK25Processor`] offers all the functionalities of [`KimiK25ImageProcessor`] and [`TikTokenTokenizer`]. See the
[`~KimiK25Processor.__call__`] and [`~KimiK25Processor.decode`] for more information.
Args:
image_processor ([`KimiK25ImageProcessor`], *optional*):
The image processor is a required input.
tokenizer ([`TikTokenTokenizer`], *optional*):
The tokenizer is a required input.
chat_template (`str`, *optional*): A Jinja template which will be used to convert lists of messages
in a chat into a tokenizable string.
"""
attributes = ["image_processor", "tokenizer"]
valid_kwargs = ["chat_template"]
image_processor_class = "AutoImageProcessor"
tokenizer_class = "AutoTokenizer"
def __init__(
self,
image_processor=None,
tokenizer=None,
chat_template=None,
**kwargs,
):
super().__init__(image_processor,
tokenizer,
chat_template=chat_template)
self.media_processor = image_processor
# A special temporal placeholder to be replaced by actual video placeholders
self.video_placeholder = "<|kimi_k25_video_placeholder|>"
def update_raw_text(self, text: str, video_prompts: list[str]) -> str:
# replace video prompt in text with video chunk prompts
video_count = text.count(self.video_placeholder)
if video_count == 0:
return text
assert video_count == len(video_prompts)
text_parts = text.split(self.video_placeholder)
assert len(text_parts) == len(video_prompts) + 1
text = "".join([
text_parts[i] + video_prompts[i] for i in range(len(video_prompts))
])
text += text_parts[-1]
return text
def preprocess_medias(self, medias: list[dict]) -> list[dict]:
updated_medias = []
video_prompts = []
for media in medias:
if media['type'] == 'image':
updated_medias.append(media)
elif media['type'] == 'video':
video_chunks = self.media_processor.split_video_chunks(
media['video'])
updated_medias.extend(video_chunks)
video_prompts.append("".join(
[vc['prompt'] for vc in video_chunks]))
else:
raise ValueError(f"unsupported media type: {media['type']}")
return updated_medias, video_prompts
def __call__(self,
messages: list[dict] = None,
medias: list[dict] = None,
text: str = None,
return_tensors: str = "pt",
**kwargs) -> BatchFeature:
"""
Process multimodal inputs for Kimi-K2.5 model.
This processor accepts ordered messages and extracts both media and text in a single pass.
text will be automatically updated if video input detected in messages
Args:
messages: List of message dicts with 'role' and 'content' fields.
If provided, medias and text will be extracted automatically.
medias: Pre-extracted list of media dicts. If None, extracted from messages.
text: Pre-formatted text string. If None, generated via apply_chat_template.
return_tensors: Format of returned tensors ('pt', 'np', 'tf'). Default: 'pt'.
**kwargs: Additional arguments passed to tokenizer.apply_chat_template.
Returns:
BatchFeature with fields: input_ids, attention_mask, pixel_values, grid_thws.
"""
if messages is None and (medias is None or text is None):
raise ValueError(
"Provide either 'messages' or both 'medias' and 'text'")
if medias is not None and text is not None:
updated_medias, video_prompts = self.preprocess_medias(medias)
preprocessed = self.media_processor.preprocess(
updated_medias, return_tensors=return_tensors)
text = self.update_raw_text(text, video_prompts)
text_inputs = self.tokenizer(text, return_tensors=return_tensors)
return BatchFeature(data={**text_inputs, **preprocessed.data})
if medias is None:
medias = self._extract_medias_from_messages(messages)
updated_medias, video_prompts = self.preprocess_medias(medias)
preprocessed = self.media_processor.preprocess(
updated_medias, return_tensors=return_tensors)
# Generate text if not provided
if text is None:
text = self.tokenizer.apply_chat_template(messages, **kwargs)
text = self.update_raw_text(text, video_prompts)
text_inputs = self.tokenizer(text, return_tensors=return_tensors)
return BatchFeature(data={**text_inputs, **preprocessed.data})
@staticmethod
def _extract_medias_from_messages(messages: list[dict]) -> list[dict]:
"""
Extract media items from messages in a single pass.
This is an optimized version that processes messages only once.
Kept as internal method since external callers should use __call__.
"""
medias = []
for msg in messages:
if msg['role'] != 'user' or not msg.get('content'):
continue
for content_part in msg['content']:
if not isinstance(content_part, dict):
continue
content_type = content_part.get('type')
if content_type in ['video_url', 'video']:
medias.append({
'type': 'video',
'video': content_part['video_url']['url'],
'first_frame_timestamp': 0.0
})
elif content_type in ['image_url', 'image']:
medias.append({
'type': 'image',
'image': content_part['image_url'],
})
return medias
def apply_chat_template(self, messages, **kwargs):
return self.tokenizer.apply_chat_template(messages, **kwargs)
def batch_decode(self, *args, **kwargs):
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
return self.tokenizer.decode(*args, **kwargs)
@property
def model_input_names(self):
return ['input_ids', 'attention_mask', 'pixel_values', 'grid_thws']
>>>>> FILE: kimi_k25_vision_processing.py (10019 bytes) <<<<<
"""Image processor class for Kimi-K2.5.
"""
import json
from typing import Any, Dict, Optional, Union
import numpy as np
import torch
from PIL import Image
from transformers.image_processing_utils import (BaseImageProcessor,
BatchFeature)
from transformers.utils import TensorType
from .media_utils import (MediaInput, VideoChunkInput, _to_tensor,
ensure_media_type, get_video_meta, image_to_np,
navit_patchify, navit_resize_image,
navit_resize_video, normalize,
real_sample_fps_and_max_num_frames, timestamp_as_str)
try:
from mecord import VideoReader
except ImportError:
VideoReader = None
def resampling(video_bytes: bytes,
sample_indices: list[int],
key_indices=None,
frame_time_info=None,
num_threads=4) -> str:
video = VideoReader(video_bytes,
num_threads=num_threads,
frame_time_info=frame_time_info,
key_indices=key_indices)
# extract target frames
frames = video[sample_indices]
frames = [Image.fromarray(frame) for frame in frames]
return frames
class KimiK25VisionProcessor(BaseImageProcessor):
model_type = "kimi_k25"
def __init__(
self,
media_proc_cfg: dict,
**kwargs,
):
super().__init__(**kwargs)
self.media_proc_cfg = media_proc_cfg
self.num_frames_per_chunk = media_proc_cfg[
'temporal_merge_kernel_size']
def media_tokens_calculator(self, media: MediaInput):
media = ensure_media_type(media)
ret = self.get_resize_config(media)
return ret['num_tokens']
@classmethod
def make_chunk_prompt(cls, timestamp_text: str) -> str:
return f"{timestamp_text}<|media_begin|>video<|media_content|><|media_pad|><|media_end|>"
def split_video_chunks(self,
video_url: str | bytes) -> list[list[Image.Image]]:
# video_url should be base64 str or bytes
video_spec = get_video_meta(video_url)
sample_fps = min(self.media_proc_cfg['sample_fps'], video_spec.fps)
sampled_nframes = max(
round(video_spec.num_frames * sample_fps / video_spec.fps), 1)
frame_inds = np.linspace(0, video_spec.num_frames - 1,
sampled_nframes).round().astype(int)
frame_inds = frame_inds.tolist()
sampled_frame_ids = []
temporal_merge_kernel_size = self.media_proc_cfg[
"temporal_merge_kernel_size"]
num_chunks = 0
chunk_timestamp = []
for i in range(0, len(frame_inds), temporal_merge_kernel_size):
sampled_frame_ids.extend(frame_inds[i:i +
temporal_merge_kernel_size])
start_time = frame_inds[i] / float(video_spec.fps)
timestamp_text = timestamp_as_str(
start_time, self.media_proc_cfg["timestamp_mode"])
chunk_timestamp.append(timestamp_text)
num_chunks += 1
sampled_frames = resampling(video_url, sampled_frame_ids)
chunks = []
for chunk_id in range(num_chunks):
chunk = sampled_frames[chunk_id *
temporal_merge_kernel_size:(chunk_id + 1) *
temporal_merge_kernel_size]
chunks.append(
VideoChunkInput(type="video_chunk",
video_chunk=chunk,
prompt=self.make_chunk_prompt(
chunk_timestamp[chunk_id])))
return chunks
def get_resize_config(self, media_input: MediaInput) -> dict:
if media_input['type'] == 'image':
w, h = media_input['image'].size
ret = navit_resize_image(
w, h, self.media_proc_cfg['patch_size'],
self.media_proc_cfg['merge_kernel_size'],
self.media_proc_cfg['in_patch_limit'],
self.media_proc_cfg['patch_limit_on_one_side'],
self.media_proc_cfg['fixed_output_tokens'])
return ret
elif media_input['type'] == 'video_chunk':
frame = media_input['video_chunk'][0]
width, height = frame.size
num_frames = len(media_input["video_chunk"])
fps = 1.0
sample_fps, max_num_frames_each_video = real_sample_fps_and_max_num_frames(
media_input["type"],
self.media_proc_cfg['sample_fps'],
self.media_proc_cfg['max_num_frames_each_video'],
)
in_patch_limit_each_frame = self.media_proc_cfg[
'in_patch_limit_each_frame']
if in_patch_limit_each_frame is None:
in_patch_limit_each_frame = self.media_proc_cfg[
'in_patch_limit']
ret = navit_resize_video(
width,
height,
num_frames,
fps,
sample_fps,
self.media_proc_cfg['patch_size'],
self.media_proc_cfg['merge_kernel_size'],
in_patch_limit_each_frame,
self.media_proc_cfg['patch_limit_on_one_side'],
self.media_proc_cfg['in_patch_limit_video'],
max_num_frames_each_video,
self.media_proc_cfg['fixed_output_tokens'],
)
return ret
else:
raise ValueError("Unsupported type: {}".format(
media_input['type']))
def resize_image(self, image: Image.Image, new_width: int, new_height: int,
pad_width: int, pad_height: int) -> np.ndarray:
image_np = image_to_np(image, (new_width, new_height), "resize")
image_np = np.pad(
image_np,
((0, pad_height), (0, pad_width), (0, 0)),
mode="constant",
constant_values=0,
)
return image_np
def preprocess(
self,
medias: list[MediaInput],
return_tensors: Optional[Union[str, TensorType]] = None,
) -> BatchFeature:
"""
Preprocess a atom vision input (images/video_chunk) into model-ready tensors.
Args:
medias: List of MediaInput.
return_tensors: Desired output format ('pt', 'np', 'tf', or None).
Returns:
BatchFeature containing 'pixel_values' and 'grid_thws' tensors.
"""
if not isinstance(medias, list):
medias = [medias]
if medias:
pixel_values = []
for item in medias:
item = ensure_media_type(item)
resize_config = self.get_resize_config(item)
new_width, new_height, pad_width, pad_height = resize_config[
'new_width'], resize_config['new_height'], resize_config[
'pad_width'], resize_config['pad_height']
if item['type'] == 'image':
image = item['image']
image_np = self.resize_image(image, new_width, new_height,
pad_width, pad_height)
pixel_values.append(np.expand_dims(image_np, axis=0))
elif item['type'] == 'video_chunk':
pixels = []
for frame in item['video_chunk']:
frame_np = self.resize_image(frame, new_width,
new_height, pad_width,
pad_height)
pixels.append(frame_np)
pixel_values.append(np.stack(pixels, axis=0))
else:
raise ValueError("Unsupported type: {}".format(
item['type']))
normalized_pixel_values = []
image_std_inv = 1.0 / np.array(self.media_proc_cfg['image_std'])
image_mean = np.array(self.media_proc_cfg['image_mean'])
for pixels in pixel_values:
pixels = normalize(pixels, image_mean, image_std_inv)
pixels_and_thw = navit_patchify(
pixels,
self.media_proc_cfg['patch_size'],
)
normalized_pixel_values.append(pixels_and_thw)
pixel_values = torch.cat([
_to_tensor(pixel_value['pixel_values'])
for pixel_value in normalized_pixel_values
])
grid_thws = torch.cat([
_to_tensor(pixel_value['grid_thw'],
dtype=torch.int64).unsqueeze(0)
for pixel_value in normalized_pixel_values
])
data = {
'pixel_values': pixel_values,
'grid_thws': grid_thws,
}
else:
data = {}
return BatchFeature(data=data, tensor_type=return_tensors)
def __repr__(self):
return f"KimiK25VisionProcessor(media_proc_cfg={self.media_proc_cfg})"
def to_dict(self) -> Dict[str, Any]:
output = super().to_dict()
output["media_proc_cfg"] = self.media_proc_cfg
if "media_processor" in output:
del output["media_processor"]
return output
@classmethod
def from_dict(cls, config_dict: Dict[str, Any], **kwargs):
config = config_dict.copy()
media_proc_cfg = config.pop("media_proc_cfg", {})
return cls(media_proc_cfg=media_proc_cfg, **config, **kwargs)
def to_json_string(self):
dictionary = self.to_dict()
for key, value in dictionary.items():
if hasattr(value, 'tolist'):
dictionary[key] = value.tolist()
return json.dumps(dictionary, indent=2, sort_keys=True) + "\n"
>>>>> FILE: media_utils.py (13179 bytes) <<<<<
import base64
import io
import math
import os
from datetime import datetime, timezone
from typing import List, Literal, Optional, TypedDict
import numpy as np
from PIL import Image
from pydantic import BaseModel, Field
try:
from mecord import VideoReader
except ImportError:
VideoReader = None
class VideoSpec(BaseModel):
media_type: str = Literal['video']
height: int = Field(..., gt=0, description="video frame height")
width: int = Field(..., gt=0, description="video frame width")
num_frames: int = Field(..., gt=0, description="num frames")
fps: float = Field(..., gt=0, description="average fps")
# optional, help to accelerate video reading
key_indices: list[int] = Field(None, description="key indices")
frame_time_info: dict = Field(None, description="frame time info")
class ImageInput(TypedDict):
type: Literal['image']
image: Image.Image
class VideoChunkInput(TypedDict):
type: Literal['video_chunk']
video_chunk: List[Image.Image]
prompt: Optional[str] = None
MediaInput = ImageInput | VideoChunkInput
def get_video_meta(video_src: bytes | str | os.PathLike,
accurate: bool = True) -> dict:
"""Get the dimensions of a video."""
if isinstance(video_src, os.PathLike):
video_src = str(video_src)
# if b64 string, decode to bytes
if isinstance(video_src,
str) and video_src.startswith('data:video/mp4;base64,'):
video_src = base64.b64decode(video_src.split(',')[1])
video = VideoReader(video_src, auto_init=accurate, num_threads=1)
assert video.num_frames > 0, "Invalid video format."
assert video.original_width > 0 and video.original_height > 0, (
"Invalid video format.")
assert video.avg_fps > 0, "Invalid video format."
return VideoSpec(media_type='video',
height=video.original_height,
width=video.original_width,
num_frames=video.num_frames,
fps=video.avg_fps,
key_indices=video.key_indices,
frame_time_info=video.frame_time_info)
def timestamp_as_str(timestamp: float,
timestamp_mode: str = "hh:mm:ss.fff") -> str:
"""Convert a timestamp to a string in the format of HH:MM:SS.mmm."""
if timestamp_mode == "hh:mm:ss.fff":
return (datetime.fromtimestamp(timestamp,
tz=timezone.utc).strftime("%H:%M:%S") +
f".{int((timestamp % 1) * 1000):03d}")
elif timestamp_mode == "mm:ss.fff":
return (datetime.fromtimestamp(timestamp,
tz=timezone.utc).strftime("%M:%S") +
f".{int((timestamp % 1) * 1000):03d}")
elif timestamp_mode == "mm:ss":
return datetime.fromtimestamp(timestamp,
tz=timezone.utc).strftime("%M:%S")
else:
raise ValueError(f"Invalid timestamp mode: {timestamp_mode}")
def navit_resize_image(
width: int,
height: int,
patch_size: int,
merge_kernel_size: int,
in_patch_limit: int,
patch_limit_on_one_side: int,
fixed_output_tokens: int | None,
):
# Apply the patch limits.
s1 = math.sqrt(
in_patch_limit /
(max(1.0, width // patch_size) * max(1.0, height // patch_size)))
s2 = patch_limit_on_one_side * patch_size / width
s3 = patch_limit_on_one_side * patch_size / height
scale = min(1.0, s1, s2, s3)
new_w, new_h = max(1, int(width * scale)), max(1, int(height * scale))
new_w = min(new_w, patch_limit_on_one_side * patch_size)
new_h = min(new_h, patch_limit_on_one_side * patch_size)
# Calculate the padding to make the height and width divisible by the merge kernel size and patch size.
factor = merge_kernel_size * patch_size
pad_height = (factor - new_h % factor) % factor
pad_width = (factor - new_w % factor) % factor
if fixed_output_tokens is not None:
num_tokens = fixed_output_tokens
else:
# Calculate new dimensions after padding and patching
token_height = (new_h + pad_height) // factor
token_width = (new_w + pad_width) // factor
assert token_height * merge_kernel_size <= patch_limit_on_one_side, (
f"token_height {token_height} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
)
assert token_width * merge_kernel_size <= patch_limit_on_one_side, (
f"token_width {token_width} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
)
num_tokens = token_height * token_width
return {
"num_tokens": num_tokens,
"new_width": new_w,
"new_height": new_h,
"pad_width": pad_width,
"pad_height": pad_height,
"sampled_nframes": 1,
}
def navit_resize_video(
width: int,
height: int,
nframes: int,
avg_fps: float,
sample_fps: float,
patch_size: int,
merge_kernel_size: int,
in_patch_limit_each_frame: int,
patch_limit_on_one_side: int,
in_patch_limit_total: int | None,
max_num_frames_each_video: int | None,
fixed_output_tokens_each_frame: int | None,
):
sample_fps = min(sample_fps, avg_fps)
# Calculate the number of frames to sample based on target FPS
sampled_nframes = max(round(nframes * sample_fps / avg_fps), 1)
if max_num_frames_each_video is not None:
sampled_nframes = min(sampled_nframes, max_num_frames_each_video)
if in_patch_limit_total is not None:
in_patch_limit_each_frame = min(
round(in_patch_limit_total / sampled_nframes),
in_patch_limit_each_frame)
ret = navit_resize_image(
width,
height,
patch_size,
merge_kernel_size,
in_patch_limit_each_frame,
patch_limit_on_one_side,
fixed_output_tokens_each_frame,
)
ret["sampled_nframes"] = sampled_nframes
return ret
def real_sample_fps_and_max_num_frames(
type_name: Literal["video", "video_chunk"],
sample_fps: float,
max_num_frames_each_video: int | None,
) -> tuple[int, int | None]:
if type_name == "video":
return sample_fps, max_num_frames_each_video
elif type_name == "video_chunk":
max_num_frames_each_video = None
sample_fps = math.inf
return sample_fps, max_num_frames_each_video
else:
return math.inf, None
def _to_pil(data: str | bytes):
if isinstance(data, Image.Image):
return data.convert("RGB")
elif isinstance(data, str):
if data.startswith("data:"):
raw_base64 = data.split(",")[1]
return Image.open(io.BytesIO(
base64.b64decode(raw_base64))).convert("RGB")
else:
return Image.open(data).convert("RGB")
elif isinstance(data, bytes):
return Image.open(io.BytesIO(data)).convert("RGB")
else:
raise ValueError(f"Unsupported data type: {type(data)}")
def ensure_media_type(media: MediaInput) -> MediaInput:
if media['type'] == 'image':
media['image'] = _to_pil(media['image'])
return media
elif media['type'] == 'video_chunk':
media['video_chunk'] = [
_to_pil(frame) for frame in media['video_chunk']
]
return media
else:
raise ValueError(f"Unsupported media type: {media['type']}")
def image_to_np(
image: Image.Image,
resize_to: tuple[int, int] | None = None,
mode: str = "resize",
raise_error_for_ill_resize: bool = True,
) -> np.ndarray:
"""Convert an image to a numpy array.
Args:
content: The image to convert.
resize_to: The size to resize the image to.
mode: The mode to resize the image to.
raise_error_for_ill_resize: Whether to raise an error for ill-sized resize.
Returns:
A numpy array.
"""
assert isinstance(image, Image.Image), "image must be a PIL Image"
if resize_to is not None:
if mode == "resize":
image = image.resize(resize_to, resample=Image.Resampling.BICUBIC)
elif mode == "rescale_and_pad_to_center":
scale = min(resize_to[0] / image.width,
resize_to[1] / image.height, 1.0)
new_width = round(image.width * scale)
new_height = round(image.height * scale)
if new_width == 0 or new_height == 0:
if raise_error_for_ill_resize:
raise ValueError(
f"Invalid resize to: {resize_to}, from image size: {image.size}"
)
else:
return np.zeros((resize_to[1], resize_to[0], 3),
dtype=np.uint8)
image = image.resize((new_width, new_height),
resample=Image.Resampling.BICUBIC)
padding_left = (resize_to[0] - new_width) // 2
padding_right = resize_to[0] - new_width - padding_left
padding_top = (resize_to[1] - new_height) // 2
padding_bottom = resize_to[1] - new_height - padding_top
image = np.asarray(image)
image = np.pad(
image,
((padding_top, padding_bottom), (padding_left, padding_right),
(0, 0)),
mode="constant",
constant_values=0,
)
assert image.shape == (resize_to[1], resize_to[0], 3)
elif mode == "rescale_and_pad_to_rightbottom":
scale = min(resize_to[0] / image.width,
resize_to[1] / image.height, 1.0)
new_width = round(image.width * scale)
new_height = round(image.height * scale)
if new_width == 0 or new_height == 0:
if raise_error_for_ill_resize:
raise ValueError(
f"Invalid resize to: {resize_to}, from image size: {image.size}"
)
else:
return np.zeros((resize_to[1], resize_to[0], 3),
dtype=np.uint8)
image = image.resize((new_width, new_height),
resample=Image.Resampling.BICUBIC)
padding_right = resize_to[0] - new_width
padding_bottom = resize_to[1] - new_height
image = np.asarray(image)
image = np.pad(
image,
((0, padding_bottom), (0, padding_right), (0, 0)),
mode="constant",
constant_values=0,
)
assert image.shape == (resize_to[1], resize_to[0], 3)
else:
raise ValueError(f"Invalid mode: {mode}")
if isinstance(image, Image.Image):
return np.asarray(image)
else:
return image
def navit_patchify(pixel_values: np.ndarray,
patch_size: int) -> dict[str, np.ndarray]:
"""Reshape the pixel values to a navit shape.
Args:
pixel_values: np.ndarray, shape (t, h, w, c)
patch_size: int
Returns:
dict[str, np.ndarray]
- patches: np.ndarray, shape (t * h//patch_size * w//patch_size, c, patch_size, patch_size)
- grid_thw: np.ndarray, (t, h//patch_size, w//patch_size)
"""
T, H, W, C = pixel_values.shape
assert C == 3, "pixel_values must have 3 channels"
patches = pixel_values.reshape(T, H // patch_size, patch_size,
W // patch_size, patch_size, C)
# (T, H//patch_size, W//patch_size, C, patch_size, patch_size)
patches = patches.transpose(0, 1, 3, 5, 2, 4)
patches = patches.reshape(-1, C, patch_size, patch_size)
grid_thw = np.array([T, H // patch_size, W // patch_size])
return {"pixel_values": patches, "grid_thw": grid_thw}
def normalize(x: np.ndarray,
mean,
std_inv,
pixels_dtype: np.dtype = np.float32) -> np.ndarray:
"""Normalize the image.
Args:
x: The image to normalize. The shape is (..., 3). The dtype is uint8. The range is [0, 255].
mean: The mean of the image.
std_inv: The inverse of the std of the image.
pixels_dtype: The dtype of the image.
Returns:
The normalized image. The shape is (..., 3). The dtype is determined by the pixels_dtype.
"""
x = (x / 255.0).astype(pixels_dtype)
x -= mean
x *= std_inv
return x
def _to_tensor(data, **kwargs):
import torch
if isinstance(data, np.ndarray):
return torch.from_numpy(data).to(**kwargs)
elif isinstance(data, torch.Tensor):
return data.to(**kwargs)
elif isinstance(data, list):
return [_to_tensor(item, **kwargs) for item in data]
elif isinstance(data, tuple):
return tuple(_to_tensor(item, **kwargs) for item in data)
elif isinstance(data, dict):
return {k: _to_tensor(v, **kwargs) for k, v in data.items()}
elif data is None:
return None
else:
raise ValueError(f"Unsupported data type: {type(data)}")
>>>>> FILE: tool_declaration_ts.py (16517 bytes) <<<<<
"""
Encode structured tool declaration to typescript style string.
"""
import dataclasses
import json
import logging
from collections.abc import Sequence
from typing import Any
logger = logging.getLogger(__name__)
_TS_INDENT = " "
_TS_FIELD_DELIMITER = ",\n"
class _SchemaRegistry:
"""Registry for schema definitions to handle $ref resolution"""
def __init__(self):
self.definitions = {}
self.has_self_ref = False
def register_definitions(self, defs: dict[str, Any]):
"""Register schema definitions from $defs section"""
if not defs:
return
for def_name, def_schema in defs.items():
self.definitions[def_name] = def_schema
def resolve_ref(self, ref: str) -> dict[str, Any]:
"""Resolve a reference to its schema definition"""
if ref == "#":
self.has_self_ref = True
return {"$self_ref": True}
elif ref.startswith("#/$defs/"):
def_name = ref.split("/")[-1]
if def_name not in self.definitions:
raise ValueError(f"Reference not found: {ref}")
return self.definitions[def_name]
else:
raise ValueError(f"Unsupported reference format: {ref}")
def _format_description(description: str, indent: str = "") -> str:
return "\n".join([
f"{indent}// {line}" if line else ""
for line in description.split("\n")
])
class _BaseType:
description: str
constraints: dict[str, Any]
def __init__(
self,
extra_props: dict[str, Any],
*,
allowed_constraint_keys: Sequence[str] = (),
):
self.description = extra_props.get("description", "")
self.constraints = {
k: v
for k, v in extra_props.items() if k in allowed_constraint_keys
}
def to_typescript_style(self, indent: str = "") -> str:
raise NotImplementedError
def format_docstring(self, indent: str) -> str:
lines = []
if self.description:
lines.append(_format_description(self.description, indent))
if self.constraints:
constraints_str = ", ".join(f"{k}: {v}" for k, v in sorted(
self.constraints.items(), key=lambda kv: kv[0]))
lines.append(f"{indent}// {constraints_str}")
return "".join(x + "\n" for x in lines)
class _ParameterTypeScalar(_BaseType):
type: str
def __init__(self, type: str, extra_props: dict[str, Any] | None = None):
self.type = type
allowed_constraint_keys: list[str] = []
if self.type == "string":
allowed_constraint_keys = ["maxLength", "minLength", "pattern"]
elif self.type in ("number", "integer"):
allowed_constraint_keys = ["maximum", "minimum"]
super().__init__(extra_props or {},
allowed_constraint_keys=allowed_constraint_keys)
def to_typescript_style(self, indent: str = "") -> str:
# Map integer to number in TypeScript
if self.type == "integer":
return "number"
return self.type
class _ParameterTypeObject(_BaseType):
properties: list["_Parameter"]
additional_properties: Any | None = None
def __init__(self,
json_schema_object: dict[str, Any],
registry: _SchemaRegistry | None = None):
super().__init__(json_schema_object)
self.properties = []
self.additional_properties = None
if not json_schema_object:
return
if "$defs" in json_schema_object and registry:
registry.register_definitions(json_schema_object["$defs"])
self.additional_properties = json_schema_object.get(
"additionalProperties")
if isinstance(self.additional_properties, dict):
self.additional_properties = _parse_parameter_type(
self.additional_properties, registry)
if "properties" not in json_schema_object:
return
required_parameters = json_schema_object.get("required", [])
optional_parameters = set(
json_schema_object["properties"].keys()) - set(required_parameters)
self.properties = [
_Parameter(
name=name,
type=_parse_parameter_type(prop, registry),
optional=name in optional_parameters,
default=prop.get("default")
if isinstance(prop, dict) else None,
) for name, prop in json_schema_object["properties"].items()
]
def to_typescript_style(self, indent: str = "") -> str:
# sort by optional, make the required parameters first
parameters = [p for p in self.properties if not p.optional]
opt_params = [p for p in self.properties if p.optional]
parameters = sorted(parameters, key=lambda p: p.name)
parameters.extend(sorted(opt_params, key=lambda p: p.name))
param_strs = []
for p in parameters:
one = p.to_typescript_style(indent=indent + _TS_INDENT)
param_strs.append(one)
if self.additional_properties is not None:
ap_type_str = "any"
if self.additional_properties is True:
ap_type_str = "any"
elif self.additional_properties is False:
ap_type_str = "never"
elif isinstance(self.additional_properties, _ParameterType):
ap_type_str = self.additional_properties.to_typescript_style(
indent=indent + _TS_INDENT)
else:
raise ValueError(
f"Unknown additionalProperties: {self.additional_properties}"
)
param_strs.append(
f"{indent + _TS_INDENT}[k: string]: {ap_type_str}")
if not param_strs:
return "{}"
params_str = _TS_FIELD_DELIMITER.join(param_strs)
if params_str:
# add new line before and after
params_str = f"\n{params_str}\n"
# always wrap with object
return f"{{{params_str}{indent}}}"
class _ParameterTypeArray(_BaseType):
item: "_ParameterType"
def __init__(self,
json_schema_object: dict[str, Any],
registry: _SchemaRegistry | None = None):
super().__init__(json_schema_object,
allowed_constraint_keys=("minItems", "maxItems"))
if json_schema_object.get("items"):
self.item = _parse_parameter_type(json_schema_object["items"],
registry)
else:
self.item = _ParameterTypeScalar(type="any")
def to_typescript_style(self, indent: str = "") -> str:
item_docstring = self.item.format_docstring(indent + _TS_INDENT)
if item_docstring:
return ("Array<\n" + item_docstring + indent + _TS_INDENT +
self.item.to_typescript_style(indent=indent + _TS_INDENT) +
"\n" + indent + ">")
else:
return f"Array<{self.item.to_typescript_style(indent=indent)}>"
class _ParameterTypeEnum(_BaseType):
# support scalar types only
enum: list[str | int | float | bool | None]
def __init__(self, json_schema_object: dict[str, Any]):
super().__init__(json_schema_object)
self.enum = json_schema_object["enum"]
# Validate enum values against declared type if present
if "type" in json_schema_object:
typ = json_schema_object["type"]
if isinstance(typ, list):
if len(typ) == 1:
typ = typ[0]
elif len(typ) == 2:
if "null" not in typ:
raise ValueError(f"Enum type {typ} is not supported")
else:
typ = typ[0] if typ[0] != "null" else typ[1]
else:
raise ValueError(f"Enum type {typ} is not supported")
for val in self.enum:
if val is None:
continue
if typ == "string" and not isinstance(val, str):
raise ValueError(f"Enum value {val} is not a string")
elif typ == "number" and not isinstance(val, (int, float)):
raise ValueError(f"Enum value {val} is not a number")
elif typ == "integer" and not isinstance(val, int):
raise ValueError(f"Enum value {val} is not an integer")
elif typ == "boolean" and not isinstance(val, bool):
raise ValueError(f"Enum value {val} is not a boolean")
def to_typescript_style(self, indent: str = "") -> str:
return " | ".join(
[f'"{e}"' if isinstance(e, str) else str(e) for e in self.enum])
class _ParameterTypeAnyOf(_BaseType):
types: list["_ParameterType"]
def __init__(
self,
json_schema_object: dict[str, Any],
registry: _SchemaRegistry | None = None,
):
super().__init__(json_schema_object)
self.types = [
_parse_parameter_type(t, registry)
for t in json_schema_object["anyOf"]
]
def to_typescript_style(self, indent: str = "") -> str:
return " | ".join(
[t.to_typescript_style(indent=indent) for t in self.types])
class _ParameterTypeUnion(_BaseType):
types: list[str]
def __init__(self, json_schema_object: dict[str, Any]):
super().__init__(json_schema_object)
mapping = {
"string": "string",
"number": "number",
"integer": "number",
"boolean": "boolean",
"null": "null",
"object": "{}",
"array": "Array<any>",
}
self.types = [mapping[t] for t in json_schema_object["type"]]
def to_typescript_style(self, indent: str = "") -> str:
return " | ".join(self.types)
class _ParameterTypeRef(_BaseType):
ref_name: str
is_self_ref: bool = False
def __init__(self, json_schema_object: dict[str, Any],
registry: _SchemaRegistry):
super().__init__(json_schema_object)
ref = json_schema_object["$ref"]
resolved_schema = registry.resolve_ref(ref)
if resolved_schema.get("$self_ref", False):
self.ref_name = "parameters"
self.is_self_ref = True
else:
self.ref_name = ref.split("/")[-1]
def to_typescript_style(self, indent: str = "") -> str:
return self.ref_name
_ParameterType = (_ParameterTypeScalar
| _ParameterTypeObject
| _ParameterTypeArray
| _ParameterTypeEnum
| _ParameterTypeAnyOf
| _ParameterTypeUnion
| _ParameterTypeRef)
@dataclasses.dataclass
class _Parameter:
"""
A parameter in a function, or a field in a object.
It consists of the type as well as the name.
"""
type: _ParameterType
name: str = "_"
optional: bool = True
default: Any | None = None
@classmethod
def parse_extended(cls, attributes: dict[str, Any]) -> "_Parameter":
if not attributes:
raise ValueError("attributes is empty")
return cls(
name=attributes.get("name", "_"),
type=_parse_parameter_type(attributes),
optional=attributes.get("optional", False),
default=attributes.get("default"),
)
def to_typescript_style(self, indent: str = "") -> str:
comments = self.type.format_docstring(indent)
if self.default is not None:
default_repr = (json.dumps(self.default, ensure_ascii=False)
if not isinstance(self.default, (int, float, bool))
else repr(self.default))
comments += f"{indent}// Default: {default_repr}\n"
return (
comments +
f"{indent}{self.name}{'?' if self.optional else ''}: {self.type.to_typescript_style(indent=indent)}"
)
def _parse_parameter_type(
json_schema_object: dict[str, Any] | bool,
registry: _SchemaRegistry | None = None) -> _ParameterType:
if isinstance(json_schema_object, bool):
if json_schema_object:
return _ParameterTypeScalar(type="any")
else:
logger.warning(
f"Warning: Boolean value {json_schema_object} is not supported, use null instead."
)
return _ParameterTypeScalar(type="null")
if "$ref" in json_schema_object and registry:
return _ParameterTypeRef(json_schema_object, registry)
if "anyOf" in json_schema_object:
return _ParameterTypeAnyOf(json_schema_object, registry)
elif "enum" in json_schema_object:
return _ParameterTypeEnum(json_schema_object)
elif "type" in json_schema_object:
typ = json_schema_object["type"]
if isinstance(typ, list):
return _ParameterTypeUnion(json_schema_object)
elif typ == "object":
return _ParameterTypeObject(json_schema_object, registry)
elif typ == "array":
return _ParameterTypeArray(json_schema_object, registry)
else:
return _ParameterTypeScalar(typ, json_schema_object)
elif json_schema_object == {}:
return _ParameterTypeScalar(type="any")
else:
raise ValueError(f"Invalid JSON Schema object: {json_schema_object}")
def _openai_function_to_typescript_style(function: dict[str, Any], ) -> str:
"""Convert OpenAI function definition (dict) to TypeScript style string."""
registry = _SchemaRegistry()
parameters = function.get("parameters") or {}
parsed = _ParameterTypeObject(parameters, registry)
interfaces = []
root_interface_name = None
if registry.has_self_ref:
root_interface_name = "parameters"
params_str = _TS_FIELD_DELIMITER.join([
p.to_typescript_style(indent=_TS_INDENT) for p in parsed.properties
])
params_str = f"\n{params_str}\n" if params_str else ""
interface_def = f"interface {root_interface_name} {{{params_str}}}"
interfaces.append(interface_def)
definitions_copy = dict(registry.definitions)
for def_name, def_schema in definitions_copy.items():
obj_type = _parse_parameter_type(def_schema, registry)
params_str = obj_type.to_typescript_style()
description_part = ""
if obj_description := def_schema.get("description", ""):
description_part = _format_description(obj_description) + "\n"
interface_def = f"{description_part}interface {def_name} {params_str}"
interfaces.append(interface_def)
interface_str = "\n".join(interfaces)
function_name = function.get("name", "function")
if root_interface_name:
type_def = f"type {function_name} = (_: {root_interface_name}) => any;"
else:
params_str = parsed.to_typescript_style()
type_def = f"type {function_name} = (_: {params_str}) => any;"
description = function.get("description")
return "\n".join(
filter(
bool,
[
interface_str,
((description and _format_description(description)) or ""),
type_def,
],
))
def encode_tools_to_typescript_style(tools: list[dict[str, Any]], ) -> str:
"""
Convert tools (list of dict) to TypeScript style string.
Supports OpenAI format: {"type": "function", "function": {...}}
Args:
tools: List of tool definitions in dict format
Returns:
TypeScript style string representation of the tools
"""
if not tools:
return ""
functions = []
for tool in tools:
tool_type = tool.get("type")
if tool_type == "function":
func_def = tool.get("function", {})
if func_def:
functions.append(
_openai_function_to_typescript_style(func_def))
else:
# Skip unsupported tool types (like "_plugin")
continue
if not functions:
return ""
functions_str = "\n".join(functions)
result = "# Tools\n\n"
if functions_str:
result += "## functions\nnamespace functions {\n"
result += functions_str + "\n"
result += "}\n"
return result
================================================================================
PART 6 - DOCS / LICENSE (verbatim)
================================================================================
>>>>> FILE: README.md <<<<<
---
tags:
- compressed-tensors
license: other
license_name: modified-mit
library_name: transformers
pipeline_tag: image-text-to-text
paper: arxiv.org/abs/2602.02276
---
<div align="center">
<picture>
<img src="figures/kimi-logo.png" width="30%" alt="Kimi K2.5">
</picture>
</div>
<hr>
<div align="center" style="line-height:1">
<a href="https://www.kimi.com" target="_blank"><img alt="Chat" src="https://img.shields.io/badge/🤖%20Chat-Kimi%20K2.5-ff6b6b?color=1783ff&logoColor=white"/></a>
<a href="https://github.com/moonshotai/Kimi-K2.5"><img alt="github" src="https://img.shields.io/badge/Github-Kimi%20K2.5-181717?logo=github&color=1783ff&logoColor=white"/></a>
<a href="https://www.moonshot.ai" target="_blank"><img alt="Homepage" src="https://img.shields.io/badge/Homepage-Moonshot%20AI-white?logo=Kimi&logoColor=white"/></a>
</div>
<div align="center" style="line-height: 1;">
<a href="https://huggingface.co/moonshotai" target="_blank"><img alt="Hugging Face" src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Moonshot%20AI-ffc107?color=ffc107&logoColor=white"/></a>
<a href="https://twitter.com/kimi_moonshot" target="_blank"><img alt="Twitter Follow" src="https://img.shields.io/badge/Twitter-Kimi.ai-white?logo=x&logoColor=white"/></a>
<a href="https://discord.gg/TYU2fdJykW" target="_blank"><img alt="Discord" src="https://img.shields.io/badge/Discord-Kimi.ai-white?logo=discord&logoColor=white"/></a>
</div>
<div align="center" style="line-height: 1;">
<a href="https://huggingface.co/moonshotai/Kimi-K2.5/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Modified_MIT-f5de53?&color=f5de53"/></a>
</div>
<p align="center">
<b>📰 <a href="https://www.kimi.com/blog/kimi-k2-5.html">Tech Blog</a></b> | <b>📄 <a href="https://arxiv.org/abs/2602.02276">Paper</a></b>
</p>
</p>
## 0. Changelog
- 2026.1.29:
- The default system prompt might cause confusion to users and unexpected behaviours, so we remove it.
- The token `<|media_start|>` is incorrect; it has been replaced with `<|media_begin|>` in the chat template.
## 1. Model Introduction
Kimi K2.5 is an open-source, native multimodal agentic model built through continual pretraining on approximately 15 trillion mixed visual and text tokens atop Kimi-K2-Base. It seamlessly integrates vision and language understanding with advanced agentic capabilities, instant and thinking modes, as well as conversational and agentic paradigms.
### Key Features
- **Native Multimodality**: Pre-trained on vision–language tokens, K2.5 excels in visual knowledge, cross-modal reasoning, and agentic tool use grounded in visual inputs.
- **Coding with Vision**: K2.5 generates code from visual specifications (UI designs, video workflows) and autonomously orchestrates tools for visual data processing.
- **Agent Swarm**: K2.5 transitions from single-agent scaling to a self-directed, coordinated swarm-like execution scheme. It decomposes complex tasks into parallel sub-tasks executed by dynamically instantiated, domain-specific agents.
## 2. Model Summary
<div align="center">
| | |
|:---:|:---:|
| **Architecture** | Mixture-of-Experts (MoE) |
| **Total Parameters** | 1T |
| **Activated Parameters** | 32B |
| **Number of Layers** (Dense layer included) | 61 |
| **Number of Dense Layers** | 1 |
| **Attention Hidden Dimension** | 7168 |
| **MoE Hidden Dimension** (per Expert) | 2048 |
| **Number of Attention Heads** | 64 |
| **Number of Experts** | 384 |
| **Selected Experts per Token** | 8 |
| **Number of Shared Experts** | 1 |
| **Vocabulary Size** | 160K |
| **Context Length** | 256K |
| **Attention Mechanism** | MLA |
| **Activation Function** | SwiGLU |
| **Vision Encoder** | MoonViT |
| **Parameters of Vision Encoder** | 400M |
</div>
## 3. Evaluation Results
<div align="center">
<table>
<thead>
<tr>
<th align="center">Benchmark</th>
<th align="center"><sup>Kimi K2.5<br><sup>(Thinking)</sup></sup></th>
<th align="center"><sup>GPT-5.2 <br><sup>(xhigh)</sup></sup></th>
<th align="center"><sup>Claude 4.5 Opus <br><sup>(Extended Thinking)</sup></sup></th>
<th align="center"><sup>Gemini 3 Pro <br><sup>(High Thinking Level)</sup></sup></th>
<th align="center"><sup>DeepSeek V3.2 <br><sup>(Thinking)</sup></sup></th>
<th align="center"><sup>Qwen3-VL-<br>235B-A22B-<br>Thinking</sup></th>
</tr>
</thead>
<tbody>
<tr>
<td align="center" colspan=8><strong>Reasoning & Knowledge</strong></td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">HLE-Full</td>
<td align="center" style="vertical-align: middle">30.1</td>
<td align="center" style="vertical-align: middle">34.5</td>
<td align="center" style="vertical-align: middle">30.8</td>
<td align="center" style="vertical-align: middle">37.5</td>
<td align="center" style="vertical-align: middle">25.1<sup>†</sup></td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">HLE-Full<br>(w/ tools)</td>
<td align="center" style="vertical-align: middle">50.2</td>
<td align="center" style="vertical-align: middle">45.5</td>
<td align="center" style="vertical-align: middle">43.2</td>
<td align="center" style="vertical-align: middle">45.8</td>
<td align="center" style="vertical-align: middle">40.8<sup>†</sup></td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">AIME 2025</td>
<td align="center" style="vertical-align: middle">96.1</td>
<td align="center" style="vertical-align: middle">100</td>
<td align="center" style="vertical-align: middle">92.8</td>
<td align="center" style="vertical-align: middle">95.0</td>
<td align="center" style="vertical-align: middle">93.1</td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">HMMT 2025 (Feb)</td>
<td align="center" style="vertical-align: middle">95.4</td>
<td align="center" style="vertical-align: middle">99.4</td>
<td align="center" style="vertical-align: middle">92.9*</td>
<td align="center" style="vertical-align: middle">97.3*</td>
<td align="center" style="vertical-align: middle">92.5</td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">IMO-AnswerBench</td>
<td align="center" style="vertical-align: middle">81.8</td>
<td align="center" style="vertical-align: middle">86.3</td>
<td align="center" style="vertical-align: middle">78.5*</td>
<td align="center" style="vertical-align: middle">83.1*</td>
<td align="center" style="vertical-align: middle">78.3</td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">GPQA-Diamond</td>
<td align="center" style="vertical-align: middle">87.6</td>
<td align="center" style="vertical-align: middle">92.4</td>
<td align="center" style="vertical-align: middle">87.0</td>
<td align="center" style="vertical-align: middle">91.9</td>
<td align="center" style="vertical-align: middle">82.4</td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">MMLU-Pro</td>
<td align="center" style="vertical-align: middle">87.1</td>
<td align="center" style="vertical-align: middle">86.7*</td>
<td align="center" style="vertical-align: middle">89.3*</td>
<td align="center" style="vertical-align: middle">90.1</td>
<td align="center" style="vertical-align: middle">85.0</td>
<td align="center" style="vertical-align: middle">-</td>
</tr>
<tr>
<td align="center" colspan=8><strong>Image & Video</strong></td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">MMMU-Pro</td>
<td align="center" style="vertical-align: middle">78.5</td>
<td align="center" style="vertical-align: middle">79.5*</td>
<td align="center" style="vertical-align: middle">74.0</td>
<td align="center" style="vertical-align: middle">81.0</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">69.3</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">CharXiv (RQ)</td>
<td align="center" style="vertical-align: middle">77.5</td>
<td align="center" style="vertical-align: middle">82.1</td>
<td align="center" style="vertical-align: middle">67.2*</td>
<td align="center" style="vertical-align: middle">81.4</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">66.1</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">MathVision</td>
<td align="center" style="vertical-align: middle">84.2</td>
<td align="center" style="vertical-align: middle">83.0</td>
<td align="center" style="vertical-align: middle">77.1*</td>
<td align="center" style="vertical-align: middle">86.1*</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">74.6</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">MathVista (mini)</td>
<td align="center" style="vertical-align: middle">90.1</td>
<td align="center" style="vertical-align: middle">82.8*</td>
<td align="center" style="vertical-align: middle">80.2*</td>
<td align="center" style="vertical-align: middle">89.8*</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">85.8</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">ZeroBench</td>
<td align="center" style="vertical-align: middle">9</td>
<td align="center" style="vertical-align: middle">9*</td>
<td align="center" style="vertical-align: middle">3*</td>
<td align="center" style="vertical-align: middle">8*</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">4*</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">ZeroBench<br>(w/ tools)</td>
<td align="center" style="vertical-align: middle">11</td>
<td align="center" style="vertical-align: middle">7*</td>
<td align="center" style="vertical-align: middle">9*</td>
<td align="center" style="vertical-align: middle">12*</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">3*</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">OCRBench</td>
<td align="center" style="vertical-align: middle">92.3</td>
<td align="center" style="vertical-align: middle">80.7*</td>
<td align="center" style="vertical-align: middle">86.5*</td>
<td align="center" style="vertical-align: middle">90.3*</td>
<td align="center" style="vertical-align: middle">-</td>
<td align="center" style="vertical-align: middle">87.5</td>
</tr>
<tr>
<td align="center" style="vertical-align: middle">OmniDocBench 1.5</td>
<td align="center" style="vertical-align: middle">88.8</td>
<td align="center" style="vertical-align: middle">85.7</td>
<td align="center" style="vertical-align: middle">87.7*</td>