kernelpool commited on
Commit
72d65b9
·
verified ·
1 Parent(s): 1ce8e96

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +59 -0
  2. added_tokens.json +9 -0
  3. config.json +0 -0
  4. configuration_kimi_k3.py +285 -0
  5. encoding_k3.py +647 -0
  6. generation_config.json +4 -0
  7. kimi_k3_processor.py +187 -0
  8. kimi_k3_vision_processing.py +179 -0
  9. media_utils.py +376 -0
  10. model-00001-of-00185.safetensors +3 -0
  11. model-00002-of-00185.safetensors +3 -0
  12. model-00003-of-00185.safetensors +3 -0
  13. model-00004-of-00185.safetensors +3 -0
  14. model-00005-of-00185.safetensors +3 -0
  15. model-00006-of-00185.safetensors +3 -0
  16. model-00007-of-00185.safetensors +3 -0
  17. model-00008-of-00185.safetensors +3 -0
  18. model-00009-of-00185.safetensors +3 -0
  19. model-00010-of-00185.safetensors +3 -0
  20. model-00011-of-00185.safetensors +3 -0
  21. model-00012-of-00185.safetensors +3 -0
  22. model-00013-of-00185.safetensors +3 -0
  23. model-00014-of-00185.safetensors +3 -0
  24. model-00015-of-00185.safetensors +3 -0
  25. model-00016-of-00185.safetensors +3 -0
  26. model-00017-of-00185.safetensors +3 -0
  27. model-00018-of-00185.safetensors +3 -0
  28. model-00019-of-00185.safetensors +3 -0
  29. model-00020-of-00185.safetensors +3 -0
  30. model-00021-of-00185.safetensors +3 -0
  31. model-00022-of-00185.safetensors +3 -0
  32. model-00023-of-00185.safetensors +3 -0
  33. model-00024-of-00185.safetensors +3 -0
  34. model-00025-of-00185.safetensors +3 -0
  35. model-00026-of-00185.safetensors +3 -0
  36. model-00027-of-00185.safetensors +3 -0
  37. model-00028-of-00185.safetensors +3 -0
  38. model-00029-of-00185.safetensors +3 -0
  39. model-00030-of-00185.safetensors +3 -0
  40. model-00031-of-00185.safetensors +3 -0
  41. model-00032-of-00185.safetensors +3 -0
  42. model-00033-of-00185.safetensors +3 -0
  43. model-00034-of-00185.safetensors +3 -0
  44. model-00035-of-00185.safetensors +3 -0
  45. model-00036-of-00185.safetensors +3 -0
  46. model-00037-of-00185.safetensors +3 -0
  47. model-00038-of-00185.safetensors +3 -0
  48. model-00039-of-00185.safetensors +3 -0
  49. model-00040-of-00185.safetensors +3 -0
  50. model-00041-of-00185.safetensors +3 -0
README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ library_name: mlx
4
+ pipeline_tag: image-text-to-text
5
+ tags:
6
+ - mlx
7
+ license: other
8
+ license_name: kimi-k3
9
+ license_link: https://huggingface.co/moonshotai/Kimi-K3/blob/main/LICENSE
10
+ base_model: moonshotai/Kimi-K3
11
+ base_model_relation: quantized
12
+ ---
13
+
14
+ # kernelpool/Kimi-K3-2bit-UVMAX
15
+
16
+ Mixed-precision (UVMAX) quantization of [moonshotai/Kimi-K3](https://huggingface.co/moonshotai/Kimi-K3).
17
+
18
+ ## What is UVMAX?
19
+
20
+ UVMAX is a mixed-precision scheme: bit widths are assigned per tensor class
21
+ from measured round-trip quantization error, rather than uniformly.
22
+
23
+ | Tensor class | Bits | Parameters | Size | Share |
24
+ |---|---|---|---|---|
25
+ | Expert FFNs (routed, latent space) | 2 (gs 128) | 2.72 T | 713.2 GiB | 93.8% |
26
+ | Shared experts, MoE latent projections, dense MLP | 8 (gs 64) | 17.5 B | 17.4 GiB | 2.3% |
27
+ | Attention (KDA + MLA, all projections) | 6 (gs 64) | 36 B | 27.4 GiB | 3.6% |
28
+ | Embeddings, `lm_head` | 4 (gs 64) | 2.4 B | 1.2 GiB | 0.2% |
29
+ | MoE routers | 8 (gs 64) | 0.6 B | 0.6 GiB | 0.1% |
30
+ | Vision tower + projector (unquantized bf16) | — | 0.4 B | 0.8 GiB | 0.1% |
31
+ | Norms, AttnRes projections, gate params (unquantized) | — | — | 0.1 GiB | <0.1% |
32
+
33
+ ## Use with mlx
34
+
35
+ This model requires Kimi K3 support from mlx-lm PR #TBD, which has not yet
36
+ been merged. Until it is included in an mlx-lm release, install mlx-lm from
37
+ the PR branch:
38
+
39
+ ```bash
40
+ pip install git+https://github.com/ml-explore/mlx-lm.git@refs/pull/TBD/head
41
+ pip install tiktoken
42
+ ```
43
+
44
+ ```python
45
+ from mlx_lm import load, generate
46
+
47
+ model, tokenizer = load(
48
+ "kernelpool/Kimi-K3-2bit-UVMAX",
49
+ tokenizer_config={"trust_remote_code": True},
50
+ trust_remote_code=True,
51
+ )
52
+
53
+ prompt = "hello"
54
+
55
+ messages = [{"role": "user", "content": prompt}]
56
+ prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
57
+
58
+ response = generate(model, tokenizer, prompt=prompt, verbose=True)
59
+ ```
added_tokens.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "<|end_header_id|>": 163844,
3
+ "<|im_assistant|>": 163842,
4
+ "<|im_end|>": 163840,
5
+ "<|im_middle|>": 163846,
6
+ "<|im_system|>": 163845,
7
+ "<|im_user|>": 163841,
8
+ "<|start_header_id|>": 163843
9
+ }
config.json ADDED
The diff for this file is too large to render. See raw diff
 
configuration_kimi_k3.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from transformers.configuration_utils import PretrainedConfig
4
+
5
+
6
+ class KimiLinearConfig(PretrainedConfig):
7
+ model_type = "kimi_linear"
8
+ keys_to_ignore_at_inference = ["past_key_values"]
9
+
10
+ def __init__(
11
+ self,
12
+ model_type="kimi_linear",
13
+ vocab_size=163840,
14
+ hidden_size=4096,
15
+ head_dim=None,
16
+ intermediate_size=11008,
17
+ num_hidden_layers=32,
18
+ num_attention_heads=32,
19
+ num_key_value_heads=None,
20
+ hidden_act="silu",
21
+ initializer_range=0.02,
22
+ rms_norm_eps=1e-6,
23
+ use_cache=True,
24
+ pad_token_id=0,
25
+ bos_token_id=1,
26
+ eos_token_id=2,
27
+ rope_theta=10000.0,
28
+ rope_scaling=None,
29
+ tie_word_embeddings=False,
30
+ moe_intermediate_size: Optional[int] = None,
31
+ moe_renormalize: bool = True,
32
+ moe_router_activation_func: str = "sigmoid",
33
+ num_experts: Optional[int] = None,
34
+ num_experts_per_token: Optional[int] = None,
35
+ num_shared_experts: int = 0,
36
+ routed_scaling_factor: float = 1.0,
37
+ first_k_dense_replace: int = 0,
38
+ moe_layer_freq: int = 1,
39
+ use_grouped_topk: bool = True,
40
+ num_expert_group: int = 1,
41
+ topk_group: int = 1,
42
+ q_lora_rank: Optional[int] = None,
43
+ kv_lora_rank: Optional[int] = None,
44
+ qk_nope_head_dim: Optional[int] = None,
45
+ qk_rope_head_dim: Optional[int] = None,
46
+ v_head_dim: Optional[int] = None,
47
+ mla_use_nope: Optional[bool] = False,
48
+ mla_use_output_gate: Optional[bool] = False,
49
+ num_nextn_predict_layers: int = 0,
50
+ linear_attn_config: Optional[dict] = None,
51
+ attn_res_block_size: Optional[int] = None,
52
+ latent_moe_use_norm: bool = False,
53
+ activation_situ_beta: Optional[float] = None,
54
+ activation_situ_linear_beta: Optional[float] = None,
55
+ max_position_embeddings: int = 4096,
56
+ routed_expert_hidden_size: Optional[int] = None,
57
+ topk_method: str = "noaux_tc",
58
+ **kwargs,
59
+ ):
60
+ self.model_type = model_type
61
+ self.vocab_size = vocab_size
62
+ self.hidden_size = hidden_size
63
+ self.head_dim = (
64
+ head_dim if head_dim is not None else hidden_size // num_attention_heads
65
+ )
66
+ self.intermediate_size = intermediate_size
67
+ self.num_hidden_layers = num_hidden_layers
68
+ self.num_attention_heads = num_attention_heads
69
+
70
+ # for backward compatibility
71
+ if num_key_value_heads is None:
72
+ num_key_value_heads = num_attention_heads
73
+
74
+ self.num_key_value_heads = num_key_value_heads
75
+ self.hidden_act = hidden_act
76
+ self.initializer_range = initializer_range
77
+ self.rms_norm_eps = rms_norm_eps
78
+ self.use_cache = use_cache
79
+ self.rope_theta = rope_theta
80
+ self.rope_scaling = rope_scaling
81
+
82
+ self.q_lora_rank = q_lora_rank
83
+ self.kv_lora_rank = kv_lora_rank
84
+ self.qk_nope_head_dim = qk_nope_head_dim
85
+ self.qk_rope_head_dim = qk_rope_head_dim
86
+ self.v_head_dim = v_head_dim
87
+ self.mla_use_nope = mla_use_nope
88
+ self.mla_use_output_gate = mla_use_output_gate
89
+ # moe config
90
+ self.num_experts = num_experts
91
+ self.num_experts_per_token = num_experts_per_token
92
+ self.moe_renormalize = moe_renormalize
93
+ self.num_shared_experts = num_shared_experts
94
+ self.routed_scaling_factor = routed_scaling_factor
95
+ self.moe_router_activation_func = moe_router_activation_func
96
+ assert self.moe_router_activation_func in ("softmax", "sigmoid")
97
+ self.moe_intermediate_size = moe_intermediate_size
98
+ self.first_k_dense_replace = first_k_dense_replace
99
+ self.moe_layer_freq = moe_layer_freq
100
+ self.use_grouped_topk = use_grouped_topk
101
+ self.num_expert_group = num_expert_group
102
+ self.topk_group = topk_group
103
+ self.num_nextn_predict_layers = num_nextn_predict_layers
104
+
105
+ self.attn_res_block_size = attn_res_block_size
106
+ self.latent_moe_use_norm = latent_moe_use_norm
107
+ self.activation_situ_beta = activation_situ_beta
108
+ self.activation_situ_linear_beta = activation_situ_linear_beta
109
+ self.max_position_embeddings = max_position_embeddings
110
+ self.routed_expert_hidden_size = routed_expert_hidden_size
111
+ self.topk_method = topk_method
112
+
113
+ if linear_attn_config is not None:
114
+ assert linear_attn_config["kda_layers"] is not None
115
+ assert linear_attn_config["full_attn_layers"] is not None
116
+ self.linear_attn_config = linear_attn_config
117
+
118
+ super().__init__(
119
+ pad_token_id=pad_token_id,
120
+ bos_token_id=bos_token_id,
121
+ eos_token_id=eos_token_id,
122
+ tie_word_embeddings=tie_word_embeddings,
123
+ **kwargs,
124
+ )
125
+
126
+ @property
127
+ def is_mla(self):
128
+ return (
129
+ self.q_lora_rank is not None
130
+ or self.kv_lora_rank is not None
131
+ or self.qk_nope_head_dim is not None
132
+ or self.qk_rope_head_dim is not None
133
+ or self.v_head_dim is not None
134
+ or self.mla_use_nope is True
135
+ )
136
+
137
+ @property
138
+ def is_moe(self):
139
+ return self.num_experts is not None
140
+
141
+ @property
142
+ def is_linear_attn(self) -> bool:
143
+ return not (
144
+ self.linear_attn_config is None
145
+ or (
146
+ isinstance(self.linear_attn_config, dict)
147
+ and self.linear_attn_config["kda_layers"] is not None
148
+ and len(self.linear_attn_config["kda_layers"]) == 0
149
+ )
150
+ )
151
+
152
+ def is_kda_layer(self, layer_idx: int):
153
+ return (
154
+ self.linear_attn_config is not None
155
+ and (layer_idx + 1) in self.linear_attn_config["kda_layers"]
156
+ )
157
+
158
+
159
+ class KimiK3VisionConfig(PretrainedConfig):
160
+
161
+ def __init__(
162
+ self,
163
+ patch_size: int = 14,
164
+ init_pos_emb_height: int = 64,
165
+ init_pos_emb_width: int = 64,
166
+ init_pos_emb_time: int = 4,
167
+ pos_emb_type: str = 'divided_fixed',
168
+ vt_num_attention_heads: int = 12,
169
+ vt_num_hidden_layers: int = 27,
170
+ vt_hidden_size: int = 1024,
171
+ vt_intermediate_size: int = 4096,
172
+ merge_kernel_size: tuple = (2, 2),
173
+ merge_type: str = 'sd2_tpool',
174
+ _attn_implementation: str = 'flash_attention_2',
175
+ # MM Projector parameters
176
+ mm_projector_type: str = 'patchmergerv2',
177
+ mm_hidden_size: int | None = None,
178
+ projector_hidden_act: str = "gelu",
179
+ projector_ln_eps: float = 1e-5,
180
+ # vision tower parameters
181
+ qkv_hidden_size: int = 1536,
182
+ norm_type: str = 'rmsnorm',
183
+ attn_bias: bool = False,
184
+ patch_embed_proj_bias: bool = False,
185
+ mlp_type: str = 'mlp2',
186
+ linear_bias: bool = False,
187
+ activation_func: str = 'gelu_pytorch_tanh',
188
+ pos_emb_interpolation_mode: str = 'bilinear',
189
+ # Other parameters
190
+ ignore_index: int = -100,
191
+ media_placeholder_token_id: int = 163605,
192
+ pad_token_id: int = 0,
193
+ text_hidden_size=7168,
194
+ **kwargs):
195
+
196
+ self.patch_size = patch_size
197
+ self.init_pos_emb_height = init_pos_emb_height
198
+ self.init_pos_emb_width = init_pos_emb_width
199
+ self.init_pos_emb_time = init_pos_emb_time
200
+ self.pos_emb_type = pos_emb_type
201
+ self.vt_num_attention_heads = vt_num_attention_heads
202
+ self.vt_num_hidden_layers = vt_num_hidden_layers
203
+ self.vt_hidden_size = vt_hidden_size
204
+ self.vt_intermediate_size = vt_intermediate_size
205
+ self.merge_kernel_size = merge_kernel_size
206
+ self.merge_type = merge_type
207
+ self._attn_implementation = _attn_implementation
208
+
209
+ # MM Projector config
210
+ self.mm_projector_type = mm_projector_type
211
+ self.mm_hidden_size = mm_hidden_size if mm_hidden_size is not None else vt_hidden_size
212
+ self.projector_hidden_act = projector_hidden_act
213
+ self.projector_ln_eps = projector_ln_eps
214
+ self.text_hidden_size = text_hidden_size
215
+
216
+ # vision tower parameters
217
+ self.qkv_hidden_size = qkv_hidden_size
218
+ self.norm_type = norm_type
219
+ self.attn_bias = attn_bias
220
+ self.patch_embed_proj_bias = patch_embed_proj_bias
221
+ self.mlp_type = mlp_type
222
+ self.linear_bias = linear_bias
223
+ self.activation_func = activation_func
224
+ self.pos_emb_interpolation_mode = pos_emb_interpolation_mode
225
+
226
+ super().__init__(**kwargs)
227
+
228
+
229
+ class KimiK3Config(PretrainedConfig):
230
+ """Kimi-K3 model configuration.
231
+
232
+ Args:
233
+ text_config (dict | KimiLinearConfig): Configuration for the text model.
234
+
235
+ Vision Tower Parameters (from MoonViT3dConfig):
236
+ patch_size (int): Patch size for vision tower.
237
+ init_pos_emb_height (int): Initial position embedding height.
238
+ init_pos_emb_width (int): Initial position embedding width.
239
+ init_pos_emb_time (int): Initial position embedding time dimension.
240
+ pos_emb_type (str): Type of position embedding.
241
+ vt_num_attention_heads (int): Number of attention heads in vision tower.
242
+ vt_num_hidden_layers (int): Number of hidden layers in vision tower.
243
+ vt_hidden_size (int): Hidden size of vision tower.
244
+ vt_intermediate_size (int): Intermediate size in vision tower FFN.
245
+ merge_kernel_size (tuple): Kernel size for patch merging.
246
+ merge_type (str): Type of merge operation.
247
+ _attn_implementation (str): Attention implementation type.
248
+
249
+ MM Projector Parameters (from MultiModalProjectorConfig):
250
+ mm_projector_type (str): Type of multimodal projector.
251
+ mm_hidden_size (int): Hidden size from vision tower (should match vt_hidden_size).
252
+ projector_hidden_act (str): Activation function for projector.
253
+ projector_ln_eps (float): Layer norm epsilon for projector.
254
+
255
+ Other Parameters:
256
+ ignore_index (int): The ignore index for the loss function.
257
+ media_placeholder_token_id (int): The token ID to use for media placeholders.
258
+ pad_token_id (int): The token ID to use for padding.
259
+ """
260
+
261
+ model_type = "kimi_k3"
262
+
263
+ def __init__(
264
+ self,
265
+ text_config: dict | KimiLinearConfig = None,
266
+ vision_config: dict | KimiK3VisionConfig = None,
267
+ # Other parameters
268
+ ignore_index: int = -100,
269
+ media_placeholder_token_id: int = 163605,
270
+ pad_token_id: int = 0,
271
+ **kwargs,
272
+ ):
273
+ if isinstance(text_config, dict):
274
+ text_config = KimiLinearConfig(**text_config)
275
+ if isinstance(vision_config, dict):
276
+ vision_config = KimiK3VisionConfig(**vision_config)
277
+ self.text_config = text_config
278
+ self.vision_config = vision_config
279
+ # Other config
280
+ self.ignore_index = ignore_index
281
+ self.media_placeholder_token_id = media_placeholder_token_id
282
+ if getattr(self.text_config, "quantization_config", None) is not None:
283
+ self.quantization_config = self.text_config.quantization_config
284
+
285
+ super().__init__(pad_token_id=pad_token_id, **kwargs)
encoding_k3.py ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kimi K3 XTML encoding helpers.
2
+
3
+ This module keeps chat rendering in Python.
4
+ Callers that need token IDs should consume ``EncodeSegment`` objects directly:
5
+ structural markers may be encoded as tiktoken special tokens, while user/tool
6
+ text and attribute values are encoded as ordinary text.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass
13
+ from typing import Any, Iterable, Optional
14
+
15
+ OPEN_TOKEN = "<|open|>"
16
+ CLOSE_TOKEN = "<|close|>"
17
+ SEP_TOKEN = "<|sep|>"
18
+ END_OF_MSG_TOKEN = "<|end_of_msg|>"
19
+ IMAGE_PLACEHOLDER = "<|kimi_image_placeholder|>"
20
+
21
+ _VALID_THINKING_EFFORTS = {"low", "high", "max"}
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class EncodeSegment:
26
+ text: str
27
+ allow_special: bool = False
28
+
29
+
30
+ class _ImagePromptState:
31
+ def __init__(self, image_prompts: Optional[list[str]] = None):
32
+ self.image_prompts = image_prompts
33
+ self.index = 0
34
+
35
+ def next_prompt(self) -> str:
36
+ if self.image_prompts is None:
37
+ return IMAGE_PLACEHOLDER
38
+ if self.index >= len(self.image_prompts):
39
+ raise ValueError("More image placeholders than image prompts.")
40
+ prompt = self.image_prompts[self.index]
41
+ self.index += 1
42
+ return prompt
43
+
44
+ def assert_consumed(self) -> None:
45
+ if self.image_prompts is None:
46
+ return
47
+ if self.index != len(self.image_prompts):
48
+ raise ValueError(
49
+ f"image prompt count {len(self.image_prompts)} != "
50
+ f"consumed placeholder count {self.index}"
51
+ )
52
+
53
+
54
+ def _segment(text: Any, *, allow_special: bool = False) -> list[EncodeSegment]:
55
+ text = str(text)
56
+ if not text:
57
+ return []
58
+ return [EncodeSegment(text, allow_special=allow_special)]
59
+
60
+
61
+ def _control(text: str) -> list[EncodeSegment]:
62
+ return _segment(text, allow_special=True)
63
+
64
+
65
+ def _text(text: Any) -> list[EncodeSegment]:
66
+ return _segment(text, allow_special=False)
67
+
68
+
69
+ def _append_text(
70
+ segments: list[EncodeSegment],
71
+ text: Any,
72
+ image_state: _ImagePromptState,
73
+ ) -> None:
74
+ text = str(text)
75
+ if text == "":
76
+ return
77
+ if image_state.image_prompts is None or IMAGE_PLACEHOLDER not in text:
78
+ segments.extend(_text(text))
79
+ return
80
+
81
+ parts = text.split(IMAGE_PLACEHOLDER)
82
+ for i, part in enumerate(parts):
83
+ segments.extend(_text(part))
84
+ if i < len(parts) - 1:
85
+ segments.extend(_segment(image_state.next_prompt(),
86
+ allow_special=True))
87
+
88
+
89
+ def _escape_attr_value(value: Any) -> str:
90
+ return str(value).replace("&", "&amp;").replace('"', "&quot;")
91
+
92
+
93
+ def _attr(key: str, value: Any) -> list[EncodeSegment]:
94
+ return (
95
+ _text(f" {key}")
96
+ + _text('="')
97
+ + _text(_escape_attr_value(value))
98
+ + _text('"')
99
+ )
100
+
101
+
102
+ def _open_tag(tag: str, attrs: Iterable[tuple[str, Any]] = ()) -> list[EncodeSegment]:
103
+ segments: list[EncodeSegment] = []
104
+ segments.extend(_control(OPEN_TOKEN))
105
+ segments.extend(_text(tag))
106
+ for key, value in attrs:
107
+ segments.extend(_attr(key, value))
108
+ segments.extend(_control(SEP_TOKEN))
109
+ return segments
110
+
111
+ def _close_tag(tag: str) -> list[EncodeSegment]:
112
+ segments: list[EncodeSegment] = []
113
+ segments.extend(_control(CLOSE_TOKEN))
114
+ segments.extend(_text(tag))
115
+ segments.extend(_control(SEP_TOKEN))
116
+ return segments
117
+
118
+
119
+ def _end_of_msg() -> list[EncodeSegment]:
120
+ return _control(END_OF_MSG_TOKEN)
121
+
122
+
123
+ def _json_compact(value: Any) -> str:
124
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
125
+
126
+
127
+ def _is_mapping(value: Any) -> bool:
128
+ return isinstance(value, dict)
129
+
130
+
131
+ def _xtml_type(value: Any) -> str:
132
+ if isinstance(value, bool):
133
+ return "boolean"
134
+ if value is None:
135
+ return "null"
136
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
137
+ return "number"
138
+ if isinstance(value, str):
139
+ return "string"
140
+ if _is_mapping(value):
141
+ return "object"
142
+ return "array"
143
+
144
+
145
+ def _xtml_value(value: Any) -> str:
146
+ if isinstance(value, str):
147
+ return value
148
+ return json.dumps(value, ensure_ascii=False)
149
+
150
+
151
+ def _get_value(obj: Any, key: str, default: Any = None) -> Any:
152
+ if isinstance(obj, dict):
153
+ return obj.get(key, default)
154
+ return getattr(obj, key, default)
155
+
156
+
157
+ def extract_response_schema(response_format: Any) -> Any:
158
+ if response_format is None:
159
+ return None
160
+
161
+ json_schema = _get_value(response_format, "json_schema")
162
+ if json_schema is None:
163
+ return None
164
+
165
+ if isinstance(json_schema, dict):
166
+ return json_schema.get(
167
+ "schema",
168
+ json_schema.get("json_schema", json_schema),
169
+ )
170
+
171
+ schema = _get_value(json_schema, "schema")
172
+ if schema is not None:
173
+ return schema
174
+
175
+ schema = _get_value(json_schema, "json_schema")
176
+ if schema is not None:
177
+ return schema
178
+
179
+ return json_schema
180
+
181
+
182
+ def deep_sort_dict(obj: Any) -> Any:
183
+ if isinstance(obj, dict):
184
+ return {k: deep_sort_dict(v) for k, v in sorted(obj.items())}
185
+ if isinstance(obj, list):
186
+ return [deep_sort_dict(item) for item in obj]
187
+ return obj
188
+
189
+
190
+ def normalize_tool_arguments(arguments: Any) -> tuple[dict[str, Any], Optional[str]]:
191
+ if arguments is None:
192
+ return {}, None
193
+ if isinstance(arguments, dict):
194
+ return arguments, None
195
+ if isinstance(arguments, str):
196
+ if not arguments.strip():
197
+ return {}, None
198
+ try:
199
+ parsed = json.loads(arguments)
200
+ except json.JSONDecodeError:
201
+ return {}, arguments
202
+ if not isinstance(parsed, dict):
203
+ raise ValueError("Kimi K3 tool call arguments must be a JSON object.")
204
+ return parsed, None
205
+ raise TypeError(
206
+ "Kimi K3 tool call arguments must be a dict or a JSON object string."
207
+ )
208
+
209
+
210
+ def normalize_message(message: Any) -> Any:
211
+ if not isinstance(message, dict):
212
+ return message
213
+
214
+ normalized = dict(message)
215
+
216
+ tools = normalized.get("tools")
217
+ if tools is not None:
218
+ normalized["tools"] = deep_sort_dict(tools)
219
+
220
+ tool_calls = normalized.get("tool_calls")
221
+ if not tool_calls:
222
+ return normalized
223
+
224
+ normalized_calls = []
225
+ for tool_call in tool_calls:
226
+ if not isinstance(tool_call, dict):
227
+ normalized_calls.append(tool_call)
228
+ continue
229
+
230
+ tc = dict(tool_call)
231
+ function = tc.get("function")
232
+ if isinstance(function, dict):
233
+ fn = dict(function)
234
+ arguments, json_block = normalize_tool_arguments(fn.get("arguments"))
235
+ fn["arguments"] = arguments
236
+ if json_block is None:
237
+ fn.pop("_xtml_json_block", None)
238
+ else:
239
+ fn["_xtml_json_block"] = json_block
240
+ tc["function"] = fn
241
+ else:
242
+ arguments, json_block = normalize_tool_arguments(tc.get("arguments"))
243
+ tc["arguments"] = arguments
244
+ if json_block is None:
245
+ tc.pop("_xtml_json_block", None)
246
+ else:
247
+ tc["_xtml_json_block"] = json_block
248
+ normalized_calls.append(tc)
249
+
250
+ normalized["tool_calls"] = normalized_calls
251
+ return normalized
252
+
253
+
254
+ def normalize_conversation(conversation: Any) -> Any:
255
+ if not isinstance(conversation, list):
256
+ return conversation
257
+
258
+ def normalize_messages(messages: list[Any]) -> list[Any]:
259
+ return [normalize_message(message) for message in messages]
260
+
261
+ if conversation and isinstance(conversation[0], list):
262
+ return [normalize_messages(messages) for messages in conversation]
263
+ return normalize_messages(conversation)
264
+
265
+
266
+ def _tool_call_id_index(tool_calls: Any) -> dict:
267
+ """Map assistant ``tool_calls[].id`` to ``(1-based position, function name)``.
268
+
269
+ The position mirrors the chat template's enumeration over ``tool_calls``
270
+ (every entry advances the position, even an id-less one). Duplicate ids keep
271
+ their first occurrence.
272
+ """
273
+ index: dict = {}
274
+ if not isinstance(tool_calls, list):
275
+ return index
276
+ for position, tool_call in enumerate(tool_calls, start=1):
277
+ if not isinstance(tool_call, dict):
278
+ continue
279
+ call_id = tool_call.get("id")
280
+ if call_id is None:
281
+ continue
282
+ key = str(call_id)
283
+ if key in index:
284
+ continue
285
+ function = tool_call.get("function")
286
+ name = (
287
+ function.get("name") if isinstance(function, dict) else tool_call.get("name")
288
+ )
289
+ index[key] = (position, name)
290
+ return index
291
+
292
+
293
+ def normalize_xtml_tool_result_messages(messages: list[Any]) -> list[Any]:
294
+ """Re-sort K3 XTML tool results into assistant ``tool_calls`` order.
295
+
296
+ Serving frameworks generally deliver tool results already in call order. A
297
+ direct Transformers caller, however, may pass OpenAI-style tool messages in any
298
+ order, so each run of consecutive tool messages is matched against the most
299
+ recent preceding assistant ``tool_calls`` by opaque ``tool_call_id`` ==
300
+ ``tool_calls[].id`` (K3 drops the ``func:index`` format requirement) and
301
+ sorted by the matched 1-based position. The matched call is authoritative,
302
+ so each matched message's ``tool`` is set to that call's function name --
303
+ this keeps an explicit (and possibly stale) ``tool``/``name`` from drifting
304
+ out of sync with the reordered position. ``index`` is still derived from the
305
+ rendered position by the chat template. A run that cannot be fully matched is
306
+ left untouched. Re-running is idempotent.
307
+
308
+ This function is side-effect free: matched tool messages are shallow-copied
309
+ before their ``tool``/``name`` is rewritten, and every other message is
310
+ appended to the output as-is. The input list and its message objects are
311
+ never mutated.
312
+ """
313
+ if not isinstance(messages, list):
314
+ return messages
315
+
316
+ output: list[Any] = []
317
+ current_index: dict = {}
318
+ i = 0
319
+ n = len(messages)
320
+
321
+ while i < n:
322
+ message = messages[i]
323
+
324
+ if isinstance(message, dict) and message.get("role") == "assistant":
325
+ tool_calls = message.get("tool_calls")
326
+ current_index = _tool_call_id_index(tool_calls) if tool_calls else {}
327
+ output.append(message)
328
+ i += 1
329
+ continue
330
+
331
+ if not isinstance(message, dict) or message.get("role") != "tool":
332
+ output.append(message)
333
+ i += 1
334
+ continue
335
+
336
+ run: list[tuple] = [] # (position, original_offset, message, name)
337
+ unresolved = False
338
+ offset = 0
339
+ while (
340
+ i < n and isinstance(messages[i], dict) and messages[i].get("role") == "tool"
341
+ ):
342
+ tool_message = messages[i]
343
+ call_id = tool_message.get("tool_call_id", tool_message.get("id"))
344
+ matched = current_index.get(str(call_id)) if call_id is not None else None
345
+ if matched is None:
346
+ unresolved = True
347
+ run.append((None, offset, tool_message, None))
348
+ else:
349
+ position, name = matched
350
+ run.append((position, offset, tool_message, name))
351
+ offset += 1
352
+ i += 1
353
+
354
+ if unresolved:
355
+ output.extend(item[2] for item in run)
356
+ else:
357
+ run.sort(key=lambda item: (item[0], item[1]))
358
+ for _, _, tool_message, name in run:
359
+ if name is None:
360
+ output.append(tool_message)
361
+ continue
362
+ # The id-matched call is authoritative: align tool (and any
363
+ # explicit name) so the rendered XTML tool attribute cannot
364
+ # disagree with the reordered position. Copy first so the
365
+ # caller's message object is never mutated.
366
+ resolved = dict(tool_message)
367
+ resolved["tool"] = name
368
+ if "name" in resolved:
369
+ resolved["name"] = name
370
+ output.append(resolved)
371
+
372
+ return output
373
+
374
+
375
+ def is_batched_conversation(conversation: Any) -> bool:
376
+ return (
377
+ isinstance(conversation, list)
378
+ and bool(conversation)
379
+ and isinstance(conversation[0], list)
380
+ )
381
+
382
+
383
+ def _render_content_segments(
384
+ content: Any,
385
+ image_state: _ImagePromptState,
386
+ ) -> list[EncodeSegment]:
387
+ segments: list[EncodeSegment] = []
388
+ if isinstance(content, str):
389
+ _append_text(segments, content, image_state)
390
+ elif content is not None:
391
+ for part in content:
392
+ if part["type"] in ["image", "image_url"]:
393
+ segments.extend(
394
+ _segment(image_state.next_prompt(), allow_special=True))
395
+ else:
396
+ _append_text(segments, part["text"], image_state)
397
+ return segments
398
+
399
+
400
+ def _internal_system_message(message_type: str, body: str) -> list[EncodeSegment]:
401
+ segments: list[EncodeSegment] = []
402
+ segments.extend(_open_tag("message", [("role", "system"), ("type", message_type)]))
403
+ segments.extend(_text(body.strip()))
404
+ segments.extend(_close_tag("message"))
405
+ segments.extend(_end_of_msg())
406
+ return segments
407
+
408
+
409
+ def _render_assistant_segments(
410
+ message: dict[str, Any],
411
+ image_state: _ImagePromptState,
412
+ thinking: bool = True,
413
+ ) -> list[EncodeSegment]:
414
+ segments: list[EncodeSegment] = []
415
+ # The <think> channel is structural: in thinking mode every assistant
416
+ # message carries the open/close tags even when there is no reasoning
417
+ # content to fill in. In non-thinking mode the channel is dropped
418
+ # entirely.
419
+ if thinking:
420
+ reasoning_content = message.get("reasoning_content") or message.get(
421
+ "reasoning"
422
+ )
423
+ segments.extend(_open_tag("think"))
424
+ if reasoning_content is not None and str(reasoning_content).strip():
425
+ _append_text(segments, reasoning_content, image_state)
426
+ segments.extend(_close_tag("think"))
427
+
428
+ segments.extend(_open_tag("response"))
429
+ segments.extend(_render_content_segments(message.get("content"), image_state))
430
+ segments.extend(_close_tag("response"))
431
+
432
+ tool_calls = message.get("tool_calls")
433
+ if tool_calls:
434
+ segments.extend(_open_tag("tools"))
435
+ for index, tool_call in enumerate(tool_calls, start=1):
436
+ fn = tool_call.get("function", tool_call)
437
+ segments.extend(
438
+ _open_tag("call", [("tool", fn["name"]), ("index", index)])
439
+ )
440
+ args = fn.get("arguments", {})
441
+ json_block = fn.get("_xtml_json_block")
442
+ if json_block is not None:
443
+ segments.extend(_open_tag("json", [("type", "object")]))
444
+ _append_text(segments, json_block, image_state)
445
+ segments.extend(_close_tag("json"))
446
+ elif _is_mapping(args):
447
+ for key, value in args.items():
448
+ segments.extend(
449
+ _open_tag(
450
+ "argument",
451
+ [("key", key), ("type", _xtml_type(value))],
452
+ )
453
+ )
454
+ _append_text(segments, _xtml_value(value), image_state)
455
+ segments.extend(_close_tag("argument"))
456
+ segments.extend(_close_tag("call"))
457
+ segments.extend(_close_tag("tools"))
458
+
459
+ return segments
460
+
461
+
462
+ def _render_tool_declare(tools: Any, *, dynamic: bool = False) -> list[EncodeSegment]:
463
+ if dynamic:
464
+ body = (
465
+ "## New Tools Available\n"
466
+ "The system dynamically extends the toolset via lazy-loading.\n"
467
+ "You have access to all existing and extended tools.\n"
468
+ "Here are the specs for the extended tools.\n\n"
469
+ "```json\n"
470
+ f"{_json_compact(tools)}\n"
471
+ "```"
472
+ )
473
+ else:
474
+ body = (
475
+ "# Tools\n"
476
+ "Here are the available tools, described in JSONSchema.\n\n"
477
+ "```json\n"
478
+ f"{_json_compact(tools)}\n"
479
+ "```"
480
+ )
481
+ segments: list[EncodeSegment] = []
482
+ segments.extend(_open_tag("message", [("role", "system"), ("type", "tool-declare")]))
483
+ segments.extend(_text(body))
484
+ segments.extend(_close_tag("message"))
485
+ segments.extend(_end_of_msg())
486
+ return segments
487
+
488
+
489
+ def build_chat_segments(
490
+ messages: list[Any],
491
+ tools: Optional[list[dict]] = None,
492
+ *,
493
+ add_generation_prompt: bool = True,
494
+ thinking: bool = True,
495
+ image_prompts: Optional[list[str]] = None,
496
+ **kwargs: Any,
497
+ ) -> list[EncodeSegment]:
498
+ # Re-sort tool results by tool_call_id at the lowest layer so every caller
499
+ # (processor or direct tokenizer) gets correctly ordered XTML. The helper is
500
+ # side-effect free, so the caller's message objects are left untouched.
501
+ messages = normalize_xtml_tool_result_messages(messages)
502
+ messages = normalize_conversation(messages)
503
+ tools = deep_sort_dict(tools)
504
+
505
+ kwargs = dict(kwargs)
506
+ response_format = kwargs.get("response_format")
507
+ if "response_schema" not in kwargs:
508
+ response_schema = extract_response_schema(response_format)
509
+ if response_schema is not None:
510
+ kwargs["response_schema"] = response_schema
511
+ if kwargs.get("response_schema") is not None:
512
+ kwargs["response_schema"] = deep_sort_dict(kwargs["response_schema"])
513
+
514
+ image_state = _ImagePromptState(image_prompts)
515
+ segments: list[EncodeSegment] = []
516
+
517
+ tool_calls = None
518
+ tool_index = 0
519
+
520
+ if tools:
521
+ segments.extend(_render_tool_declare(tools))
522
+
523
+ thinking_effort = kwargs.get("thinking_effort")
524
+ if thinking and thinking_effort is not None:
525
+ assert thinking_effort in _VALID_THINKING_EFFORTS, (
526
+ f"Unsupported thinking_effort={thinking_effort!r}; "
527
+ f"supported values are {sorted(_VALID_THINKING_EFFORTS)}."
528
+ )
529
+ if thinking and thinking_effort in _VALID_THINKING_EFFORTS:
530
+ segments.extend(
531
+ _internal_system_message(
532
+ "thinking-effort",
533
+ "`thinking_effort` guides on how much to think in your "
534
+ "thinking channel (not including the response channel), "
535
+ "supported values include `low`, `medium`, `high`, and `max`.\n"
536
+ f"Now the system is invoked with `thinking_effort={thinking_effort}`.",
537
+ )
538
+ )
539
+
540
+ for message_index, message in enumerate(messages):
541
+ if not isinstance(message, dict):
542
+ continue
543
+
544
+ role = message["role"]
545
+ if role == "user":
546
+ attrs = [("role", "user")]
547
+ if message.get("name"):
548
+ attrs.append(("name", message["name"]))
549
+ segments.extend(_open_tag("message", attrs))
550
+ segments.extend(_render_content_segments(message.get("content"), image_state))
551
+ segments.extend(_close_tag("message"))
552
+ segments.extend(_end_of_msg())
553
+ elif role == "system" and message.get("tools"):
554
+ segments.extend(_render_tool_declare(message["tools"], dynamic=True))
555
+ elif role == "system":
556
+ attrs = [("role", "system")]
557
+ if message.get("name"):
558
+ attrs.append(("name", message["name"]))
559
+ segments.extend(_open_tag("message", attrs))
560
+ segments.extend(_render_content_segments(message.get("content"), image_state))
561
+ segments.extend(_close_tag("message"))
562
+ segments.extend(_end_of_msg())
563
+ elif role == "tool":
564
+ tool_index += 1
565
+ tool_name = message.get("tool", message.get("name"))
566
+ if (
567
+ tool_name is None
568
+ and tool_calls is not None
569
+ and tool_index <= len(tool_calls)
570
+ ):
571
+ tc = tool_calls[tool_index - 1]
572
+ fn = tc.get("function", tc)
573
+ tool_name = fn["name"]
574
+ if tool_name is None:
575
+ raise ValueError(
576
+ "Kimi K3 tool messages need a resolvable tool name: "
577
+ "carry `tool`/`name`, or match a preceding assistant "
578
+ "tool_call by order."
579
+ )
580
+ segments.extend(
581
+ _open_tag(
582
+ "message",
583
+ [("role", "tool"), ("tool", tool_name), ("index", tool_index)],
584
+ )
585
+ )
586
+ segments.extend(_render_content_segments(message.get("content"), image_state))
587
+ segments.extend(_close_tag("message"))
588
+ segments.extend(_end_of_msg())
589
+ elif role == "assistant":
590
+ tool_calls = message.get("tool_calls")
591
+ tool_index = 0
592
+ attrs = [("role", "assistant")]
593
+ if message.get("name"):
594
+ attrs.append(("name", message["name"]))
595
+ segments.extend(_open_tag("message", attrs))
596
+ segments.extend(_render_assistant_segments(message, image_state, thinking))
597
+ segments.extend(_close_tag("message"))
598
+ segments.extend(_end_of_msg())
599
+
600
+ tool_choice = kwargs.get("tool_choice")
601
+ if tool_choice == "required":
602
+ segments.extend(
603
+ _internal_system_message(
604
+ "tool-choice",
605
+ "The system is invoked with `tool_choice=required`.\n"
606
+ "You MUST call tools in the next message.",
607
+ )
608
+ )
609
+ elif tool_choice == "none":
610
+ segments.extend(
611
+ _internal_system_message(
612
+ "tool-choice",
613
+ "The system is invoked with `tool_choice=none`.\n"
614
+ "You MUST NOT call any tools in the next message.",
615
+ )
616
+ )
617
+
618
+ rf = kwargs.get("response_format")
619
+ rf_type = _get_value(rf, "type", rf) if isinstance(rf, dict) else rf
620
+ if rf_type == "json_object":
621
+ segments.extend(
622
+ _internal_system_message(
623
+ "response-format",
624
+ "The system is invoked with `response_format=json_object`.\n"
625
+ "Your response must be raw JSON data without markdown code "
626
+ "blocks (```json) or any additional formatting.",
627
+ )
628
+ )
629
+ elif rf_type == "json_schema":
630
+ schema = _json_compact(kwargs.get("response_schema"))
631
+ segments.extend(
632
+ _internal_system_message(
633
+ "response-format",
634
+ "The system is invoked with `response_format=json_schema`.\n"
635
+ "Your response must be raw JSON data without markdown code "
636
+ "blocks (```json) or any additional formatting.\n"
637
+ "The JSON data must match the following schema:\n"
638
+ f"```json\n{schema}\n```",
639
+ )
640
+ )
641
+
642
+ if add_generation_prompt:
643
+ segments.extend(_open_tag("message", [("role", "assistant")]))
644
+ segments.extend(_open_tag("think" if thinking else "response"))
645
+
646
+ image_state.assert_consumed()
647
+ return segments
generation_config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "max_length": 1048576,
3
+ "eos_token_id": 163586
4
+ }
kimi_k3_processor.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kimi-K3 processor: wraps vision processor + tokenizer into a single interface.
2
+
3
+ Chat rendering (including XTML tool-result ordering) is handled by the
4
+ tokenizer's Python encoder; this processor adds multimodal media preprocessing.
5
+ """
6
+
7
+ from transformers.feature_extraction_utils import BatchFeature
8
+ from transformers.processing_utils import ProcessorMixin
9
+ from transformers.utils import logging
10
+
11
+ from .media_utils import ensure_media_type
12
+
13
+ logger = logging.get_logger(__name__)
14
+
15
+ # ── KimiK3Processor ───────────────────────────────────────────────────
16
+
17
+
18
+ class KimiK3Processor(ProcessorMixin):
19
+ r"""
20
+ Constructs a KimiK3 processor which wraps a KimiK3 image processor
21
+ and a tokenizer into a single processor.
22
+
23
+ [`KimiK3Processor`] offers all the functionalities of
24
+ [`KimiK3VisionProcessor`] and [`TikTokenTokenizer`].
25
+
26
+ Args:
27
+ image_processor ([`KimiK3VisionProcessor`], *optional*):
28
+ The image processor is a required input.
29
+ tokenizer ([`TikTokenTokenizer`], *optional*):
30
+ The tokenizer is a required input.
31
+ chat_template (`str`, *optional*): Kept for ProcessorMixin
32
+ compatibility. Kimi K3 chat encoding is implemented in Python by
33
+ the tokenizer.
34
+ """
35
+
36
+ attributes = ["image_processor", "tokenizer"]
37
+ valid_kwargs = ["chat_template"]
38
+ image_processor_class = "AutoImageProcessor"
39
+ tokenizer_class = "AutoTokenizer"
40
+
41
+ def __init__(
42
+ self,
43
+ image_processor=None,
44
+ tokenizer=None,
45
+ chat_template=None,
46
+ **kwargs,
47
+ ):
48
+ super().__init__(image_processor,
49
+ tokenizer,
50
+ chat_template=chat_template)
51
+ self.media_processor = image_processor
52
+ self.image_placeholder = "<|kimi_image_placeholder|>"
53
+
54
+ # ── Media preprocessing ────────────────────────────────────────────
55
+
56
+ def update_raw_text(self, text: str, image_prompts: list[str]) -> str:
57
+ # Replace image placeholders
58
+ image_count = text.count(self.image_placeholder)
59
+ if image_count > 0:
60
+ assert image_count == len(image_prompts), (
61
+ f"image placeholder count {image_count} != "
62
+ f"image_prompts count {len(image_prompts)}")
63
+ text_parts = text.split(self.image_placeholder)
64
+ assert len(text_parts) == len(image_prompts) + 1
65
+ text = "".join([
66
+ text_parts[i] + image_prompts[i]
67
+ for i in range(len(image_prompts))
68
+ ])
69
+ text += text_parts[-1]
70
+
71
+ return text
72
+
73
+ def preprocess_medias(self,
74
+ medias: list[dict]) -> tuple[list[dict], list[str]]:
75
+ """Process media items and generate corresponding prompts.
76
+
77
+ Returns:
78
+ A tuple of (updated_medias, image_prompts).
79
+ """
80
+ updated_medias = []
81
+ image_prompts = []
82
+ for media in medias:
83
+ if media['type'] == 'image':
84
+ updated_medias.append(media)
85
+ img = ensure_media_type(
86
+ media,
87
+ transparent_bg_config=self.media_processor.
88
+ _transparent_bg_config,
89
+ transparent_bg_fill_stage=self.media_processor.
90
+ _transparent_bg_fill_stage,
91
+ )['image']
92
+ w, h = img.size
93
+ image_prompts.append(
94
+ self.media_processor.make_image_prompt(w, h))
95
+ else:
96
+ raise ValueError(f"unsupported media type: {media['type']}")
97
+ return updated_medias, image_prompts
98
+
99
+ # ── Main entry points ──────────────────────────────────────────────
100
+
101
+ def __call__(self,
102
+ messages: list[dict] = None,
103
+ medias: list[dict] = None,
104
+ text: str = None,
105
+ return_tensors: str = "pt",
106
+ **kwargs) -> BatchFeature:
107
+ """
108
+ Process multimodal inputs for Kimi-K3 model.
109
+
110
+ Args:
111
+ messages: List of message dicts with 'role' and 'content' fields.
112
+ If provided, medias and text will be extracted automatically.
113
+ medias: Pre-extracted list of media dicts.
114
+ text: Pre-formatted text string.
115
+ return_tensors: Format of returned tensors. Default: 'pt'.
116
+ **kwargs: Additional arguments passed to apply_chat_template.
117
+
118
+ Returns:
119
+ BatchFeature with fields: input_ids, attention_mask,
120
+ pixel_values, grid_thws.
121
+ """
122
+ if messages is None and (medias is None or text is None):
123
+ raise ValueError(
124
+ "Provide either 'messages' or both 'medias' and 'text'")
125
+
126
+ if medias is not None and text is not None:
127
+ updated_medias, image_prompts = (self.preprocess_medias(medias))
128
+ preprocessed = self.media_processor.preprocess(
129
+ updated_medias, return_tensors=return_tensors)
130
+ text = self.update_raw_text(text, image_prompts)
131
+ text_inputs = self.tokenizer(text, return_tensors=return_tensors)
132
+ return BatchFeature(data={**text_inputs, **preprocessed.data})
133
+
134
+ if medias is None:
135
+ medias = self._extract_medias_from_messages(messages)
136
+ updated_medias, image_prompts = (self.preprocess_medias(medias))
137
+ preprocessed = self.media_processor.preprocess(
138
+ updated_medias, return_tensors=return_tensors)
139
+
140
+ if text is None:
141
+ text_inputs = self.tokenizer.apply_chat_template(
142
+ messages,
143
+ tokenize=True,
144
+ return_tensors=return_tensors,
145
+ return_dict=True,
146
+ image_prompts=image_prompts,
147
+ **kwargs)
148
+ return BatchFeature(data={**text_inputs, **preprocessed.data})
149
+
150
+ text = self.update_raw_text(text, image_prompts)
151
+ text_inputs = self.tokenizer(text, return_tensors=return_tensors)
152
+ return BatchFeature(data={**text_inputs, **preprocessed.data})
153
+
154
+ @staticmethod
155
+ def _extract_medias_from_messages(messages: list[dict]) -> list[dict]:
156
+ """Extract media items from messages in a single pass."""
157
+ medias = []
158
+ for msg in messages:
159
+ if msg['role'] != 'user' or not msg.get('content'):
160
+ continue
161
+
162
+ for content_part in msg['content']:
163
+ if not isinstance(content_part, dict):
164
+ continue
165
+
166
+ content_type = content_part.get('type')
167
+ if content_type in ['image_url', 'image']:
168
+ image_data = content_part.get(content_type)
169
+ assert image_data is not None, f"image data is missing for content part: {content_part}"
170
+ medias.append({
171
+ 'type': 'image',
172
+ 'image': image_data,
173
+ })
174
+ return medias
175
+
176
+ def apply_chat_template(self, messages, **kwargs):
177
+ return self.tokenizer.apply_chat_template(messages, **kwargs)
178
+
179
+ def batch_decode(self, *args, **kwargs):
180
+ return self.tokenizer.batch_decode(*args, **kwargs)
181
+
182
+ def decode(self, *args, **kwargs):
183
+ return self.tokenizer.decode(*args, **kwargs)
184
+
185
+ @property
186
+ def model_input_names(self):
187
+ return ['input_ids', 'attention_mask', 'pixel_values', 'grid_thws']
kimi_k3_vision_processing.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image processor class for Kimi-K3.
2
+ """
3
+
4
+ import json
5
+ from typing import Any, Dict, Optional, Union
6
+
7
+ import numpy as np
8
+ import torch
9
+ from PIL import Image
10
+ from transformers.image_processing_utils import (BaseImageProcessor,
11
+ BatchFeature)
12
+ from transformers.utils import TensorType
13
+
14
+ from .media_utils import (MediaInput, TransparentBgConfig, _to_tensor,
15
+ ensure_media_type, image_to_np, navit_patchify,
16
+ navit_resize_image, normalize)
17
+
18
+
19
+ class KimiK3VisionProcessor(BaseImageProcessor):
20
+ model_type = "kimi_k3"
21
+
22
+ def __init__(
23
+ self,
24
+ media_proc_cfg: dict,
25
+ **kwargs,
26
+ ):
27
+ super().__init__(**kwargs)
28
+ self.media_proc_cfg = media_proc_cfg
29
+
30
+ @property
31
+ def _transparent_bg_config(self) -> Optional[TransparentBgConfig]:
32
+ cfg = self.media_proc_cfg.get("transparent_bg_config")
33
+ if cfg is None:
34
+ return None
35
+ if isinstance(cfg, TransparentBgConfig):
36
+ return cfg
37
+ return TransparentBgConfig(**cfg)
38
+
39
+ @property
40
+ def _transparent_bg_fill_stage(self) -> str:
41
+ return self.media_proc_cfg.get("transparent_bg_fill_stage",
42
+ "before_resize")
43
+
44
+ def media_tokens_calculator(self, media: MediaInput):
45
+ media = ensure_media_type(
46
+ media,
47
+ transparent_bg_config=self._transparent_bg_config,
48
+ transparent_bg_fill_stage=self._transparent_bg_fill_stage,
49
+ )
50
+ ret = self.get_resize_config(media)
51
+ return ret['num_tokens']
52
+
53
+ @classmethod
54
+ def make_image_prompt(cls, width: int, height: int) -> str:
55
+ """Build the K3 image placeholder with resolution info."""
56
+ return (f"<|media_begin|>image {width}x{height}"
57
+ f"<|media_content|><|media_pad|><|media_end|>")
58
+
59
+ def get_resize_config(self, media_input: MediaInput) -> dict:
60
+ if media_input['type'] == 'image':
61
+ w, h = media_input['image'].size
62
+ ret = navit_resize_image(
63
+ w, h, self.media_proc_cfg['patch_size'],
64
+ self.media_proc_cfg['merge_kernel_size'],
65
+ self.media_proc_cfg['in_patch_limit'],
66
+ self.media_proc_cfg['patch_limit_on_one_side'],
67
+ self.media_proc_cfg['fixed_output_tokens'])
68
+ return ret
69
+ else:
70
+ raise ValueError("Unsupported type: {}".format(
71
+ media_input['type']))
72
+
73
+ def resize_image(self, image: Image.Image, new_width: int, new_height: int,
74
+ pad_width: int, pad_height: int) -> np.ndarray:
75
+ image_np = image_to_np(
76
+ image,
77
+ (new_width, new_height),
78
+ "resize",
79
+ transparent_bg_config=self._transparent_bg_config,
80
+ transparent_bg_fill_stage=self._transparent_bg_fill_stage,
81
+ )
82
+ image_np = np.pad(
83
+ image_np,
84
+ ((0, pad_height), (0, pad_width), (0, 0)),
85
+ mode="constant",
86
+ constant_values=0,
87
+ )
88
+ return image_np
89
+
90
+ def preprocess(
91
+ self,
92
+ medias: list[MediaInput],
93
+ return_tensors: Optional[Union[str, TensorType]] = None,
94
+ ) -> BatchFeature:
95
+ """
96
+ Preprocess a atom vision input (images) into model-ready tensors.
97
+
98
+ Args:
99
+ medias: List of MediaInput.
100
+ return_tensors: Desired output format ('pt', 'np', 'tf', or None).
101
+
102
+ Returns:
103
+ BatchFeature containing 'pixel_values' and 'grid_thws' tensors.
104
+ """
105
+ if not isinstance(medias, list):
106
+ medias = [medias]
107
+ if medias:
108
+ pixel_values = []
109
+ for item in medias:
110
+ item = ensure_media_type(
111
+ item,
112
+ transparent_bg_config=self._transparent_bg_config,
113
+ transparent_bg_fill_stage=self._transparent_bg_fill_stage,
114
+ )
115
+ resize_config = self.get_resize_config(item)
116
+ new_width, new_height, pad_width, pad_height = resize_config[
117
+ 'new_width'], resize_config['new_height'], resize_config[
118
+ 'pad_width'], resize_config['pad_height']
119
+ if item['type'] == 'image':
120
+ image = item['image']
121
+ image_np = self.resize_image(image, new_width, new_height,
122
+ pad_width, pad_height)
123
+ pixel_values.append(np.expand_dims(image_np, axis=0))
124
+ else:
125
+ raise ValueError("Unsupported type: {}".format(
126
+ item['type']))
127
+ normalized_pixel_values = []
128
+ image_std_inv = 1.0 / np.array(self.media_proc_cfg['image_std'])
129
+ image_mean = np.array(self.media_proc_cfg['image_mean'])
130
+ for pixels in pixel_values:
131
+ pixels = normalize(pixels, image_mean, image_std_inv)
132
+ pixels_and_thw = navit_patchify(
133
+ pixels,
134
+ self.media_proc_cfg['patch_size'],
135
+ )
136
+ normalized_pixel_values.append(pixels_and_thw)
137
+
138
+ pixel_values = torch.cat([
139
+ _to_tensor(pixel_value['pixel_values'])
140
+ for pixel_value in normalized_pixel_values
141
+ ])
142
+ grid_thws = torch.cat([
143
+ _to_tensor(pixel_value['grid_thw'],
144
+ dtype=torch.int64).unsqueeze(0)
145
+ for pixel_value in normalized_pixel_values
146
+ ])
147
+
148
+ data = {
149
+ 'pixel_values': pixel_values,
150
+ 'grid_thws': grid_thws,
151
+ }
152
+
153
+ else:
154
+ data = {}
155
+
156
+ return BatchFeature(data=data, tensor_type=return_tensors)
157
+
158
+ def __repr__(self):
159
+ return f"KimiK3VisionProcessor(media_proc_cfg={self.media_proc_cfg})"
160
+
161
+ def to_dict(self) -> Dict[str, Any]:
162
+ output = super().to_dict()
163
+ output["media_proc_cfg"] = self.media_proc_cfg
164
+ if "media_processor" in output:
165
+ del output["media_processor"]
166
+ return output
167
+
168
+ @classmethod
169
+ def from_dict(cls, config_dict: Dict[str, Any], **kwargs):
170
+ config = config_dict.copy()
171
+ media_proc_cfg = config.pop("media_proc_cfg", {})
172
+ return cls(media_proc_cfg=media_proc_cfg, **config, **kwargs)
173
+
174
+ def to_json_string(self):
175
+ dictionary = self.to_dict()
176
+ for key, value in dictionary.items():
177
+ if hasattr(value, 'tolist'):
178
+ dictionary[key] = value.tolist()
179
+ return json.dumps(dictionary, indent=2, sort_keys=True) + "\n"
media_utils.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import functools
3
+ import io
4
+ import math
5
+ from dataclasses import dataclass
6
+ from typing import Literal, TypedDict
7
+
8
+ import numpy as np
9
+ from PIL import Image
10
+
11
+
12
+ class ImageInput(TypedDict):
13
+ type: Literal['image']
14
+ image: Image.Image
15
+
16
+
17
+ MediaInput = ImageInput
18
+
19
+
20
+ @dataclass
21
+ class TransparentBgConfig:
22
+ """The config of the transparent background."""
23
+
24
+ pattern: Literal["white", "black", "gray", "chessboard"] = "black"
25
+ """The pattern of the transparent background."""
26
+
27
+ chessboard_square_size: int = 16
28
+ """The size of the squares in the chessboard background."""
29
+
30
+ chessboard_square_on_top_left: bool = True
31
+ """Whether to start the chessboard with a white square on the top left."""
32
+
33
+ chessboard_white_value: int = 255
34
+ """The value of the white pixels in the background."""
35
+
36
+ chessboard_gray_value: int = 200
37
+ """The value of the gray pixels in the background."""
38
+
39
+
40
+ @functools.lru_cache(maxsize=256)
41
+ def _create_chessboard_background(
42
+ height: int,
43
+ width: int,
44
+ square_size: int,
45
+ square_on_top_left: bool,
46
+ white_value: int,
47
+ gray_value: int,
48
+ ) -> np.ndarray:
49
+ """Create a chessboard background."""
50
+ bg = np.ones((height, width, 3), dtype=np.uint8) * white_value
51
+ for y in range(0, height, square_size):
52
+ for x in range(0, width, square_size):
53
+ if (y // square_size + x // square_size) % 2 == (
54
+ 1 if square_on_top_left else 0):
55
+ bg[y:y + square_size, x:x + square_size] = gray_value
56
+ return bg
57
+
58
+
59
+ def fill_transparent_bg_with(
60
+ image: Image.Image,
61
+ transparent_bg_config: TransparentBgConfig | None = None,
62
+ ) -> Image.Image:
63
+ """Composite a (possibly) transparent image onto a configured background.
64
+
65
+ When ``transparent_bg_config`` is ``None``, the image is simply converted
66
+ to RGB (preserving the historical behavior). Otherwise the alpha channel
67
+ is alpha-composited over a background generated according to the config.
68
+ """
69
+ if transparent_bg_config is None:
70
+ return image.convert("RGB")
71
+
72
+ if image.mode == "RGB":
73
+ return image
74
+
75
+ has_alpha = "A" in image.getbands() or "transparency" in image.info
76
+ if not has_alpha:
77
+ return image.convert("RGB")
78
+
79
+ img = np.array(image.convert("RGBA"))
80
+ height, width = img.shape[:2]
81
+ bg_pattern = transparent_bg_config.pattern
82
+ if bg_pattern == "white":
83
+ bg = np.full((height, width, 3), 255, dtype=np.uint8)
84
+ elif bg_pattern == "black":
85
+ bg = np.zeros((height, width, 3), dtype=np.uint8)
86
+ elif bg_pattern == "gray":
87
+ bg = np.full((height, width, 3), 128, dtype=np.uint8)
88
+ elif bg_pattern == "chessboard":
89
+ bg = _create_chessboard_background(
90
+ height,
91
+ width,
92
+ transparent_bg_config.chessboard_square_size,
93
+ transparent_bg_config.chessboard_square_on_top_left,
94
+ transparent_bg_config.chessboard_white_value,
95
+ transparent_bg_config.chessboard_gray_value,
96
+ )
97
+ else:
98
+ raise ValueError(f"Invalid background pattern: {bg_pattern}")
99
+
100
+ alpha = img[:, :, 3]
101
+ img_rgb = img[:, :, :3]
102
+ alpha_normalized = alpha.astype(np.float32) / 255.0
103
+ alpha_3d = np.stack([alpha_normalized] * 3, axis=2)
104
+ result = alpha_3d * img_rgb + (1 - alpha_3d) * bg
105
+ result = result.astype(np.uint8)
106
+ return Image.fromarray(result)
107
+
108
+
109
+ def navit_resize_image(
110
+ width: int,
111
+ height: int,
112
+ patch_size: int,
113
+ merge_kernel_size: int,
114
+ in_patch_limit: int,
115
+ patch_limit_on_one_side: int,
116
+ fixed_output_tokens: int | None,
117
+ ):
118
+ # Apply the patch limits.
119
+ s1 = math.sqrt(
120
+ in_patch_limit /
121
+ (max(1.0, width // patch_size) * max(1.0, height // patch_size)))
122
+ s2 = patch_limit_on_one_side * patch_size / width
123
+ s3 = patch_limit_on_one_side * patch_size / height
124
+ scale = min(1.0, s1, s2, s3)
125
+ new_w, new_h = max(1, int(width * scale)), max(1, int(height * scale))
126
+ new_w = min(new_w, patch_limit_on_one_side * patch_size)
127
+ new_h = min(new_h, patch_limit_on_one_side * patch_size)
128
+
129
+ # Calculate the padding to make the height and width divisible by the merge kernel size and patch size.
130
+ factor = merge_kernel_size * patch_size
131
+
132
+ pad_height = (factor - new_h % factor) % factor
133
+ pad_width = (factor - new_w % factor) % factor
134
+
135
+ if fixed_output_tokens is not None:
136
+ num_tokens = fixed_output_tokens
137
+ else:
138
+ # Calculate new dimensions after padding and patching
139
+ token_height = (new_h + pad_height) // factor
140
+ token_width = (new_w + pad_width) // factor
141
+
142
+ assert token_height * merge_kernel_size <= patch_limit_on_one_side, (
143
+ f"token_height {token_height} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
144
+ )
145
+ assert token_width * merge_kernel_size <= patch_limit_on_one_side, (
146
+ f"token_width {token_width} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
147
+ )
148
+
149
+ num_tokens = token_height * token_width
150
+ return {
151
+ "num_tokens": num_tokens,
152
+ "new_width": new_w,
153
+ "new_height": new_h,
154
+ "pad_width": pad_width,
155
+ "pad_height": pad_height,
156
+ "sampled_nframes": 1,
157
+ }
158
+
159
+
160
+ def _to_pil(
161
+ data: str | bytes | Image.Image,
162
+ transparent_bg_config: TransparentBgConfig | None = None,
163
+ to_rgb: bool = True,
164
+ ) -> Image.Image:
165
+ """Load an image and (optionally) composite its transparent background.
166
+
167
+ Args:
168
+ data: A PIL Image, a base64 ``data:`` URL, a file path, or raw bytes.
169
+ transparent_bg_config: The config used to fill the transparent
170
+ background. ``None`` keeps the historical behavior of converting
171
+ to RGB without compositing.
172
+ to_rgb: If ``False`` the image is returned as-is (the
173
+ ``transparent_bg_config`` is ignored). The caller is then
174
+ expected to call :func:`fill_transparent_bg_with` later — e.g.
175
+ after a resize.
176
+ """
177
+ if isinstance(data, Image.Image):
178
+ image = data
179
+ elif isinstance(data, str):
180
+ if data.startswith("data:"):
181
+ raw_base64 = data.split(",")[1]
182
+ image = Image.open(io.BytesIO(base64.b64decode(raw_base64)))
183
+ else:
184
+ image = Image.open(data)
185
+ elif isinstance(data, bytes):
186
+ image = Image.open(io.BytesIO(data))
187
+ else:
188
+ raise ValueError(f"Unsupported data type: {type(data)}")
189
+
190
+ if not to_rgb:
191
+ return image
192
+
193
+ return fill_transparent_bg_with(image, transparent_bg_config)
194
+
195
+
196
+ def ensure_media_type(
197
+ media: MediaInput,
198
+ transparent_bg_config: TransparentBgConfig | None = None,
199
+ transparent_bg_fill_stage: Literal["before_resize",
200
+ "after_resize"] = "before_resize",
201
+ ) -> MediaInput:
202
+ if media['type'] == 'image':
203
+ media['image'] = _to_pil(
204
+ media['image'],
205
+ transparent_bg_config=transparent_bg_config,
206
+ to_rgb=transparent_bg_fill_stage == "before_resize",
207
+ )
208
+ return media
209
+ else:
210
+ raise ValueError(f"Unsupported media type: {media['type']}")
211
+
212
+
213
+ def image_to_np(
214
+ image: Image.Image,
215
+ resize_to: tuple[int, int] | None = None,
216
+ mode: str = "resize",
217
+ raise_error_for_ill_resize: bool = True,
218
+ transparent_bg_config: TransparentBgConfig | None = None,
219
+ transparent_bg_fill_stage: Literal["before_resize",
220
+ "after_resize"] = "before_resize",
221
+ ) -> np.ndarray:
222
+ """Convert an image to a numpy array.
223
+
224
+ Args:
225
+ content: The image to convert.
226
+ resize_to: The size to resize the image to.
227
+ mode: The mode to resize the image to.
228
+ raise_error_for_ill_resize: Whether to raise an error for ill-sized resize.
229
+ transparent_bg_config: The config of the transparent background. Only
230
+ used when ``transparent_bg_fill_stage == "after_resize"`` (the
231
+ caller is responsible for filling before resize otherwise).
232
+ transparent_bg_fill_stage: When to composite the transparent
233
+ background — before or after the resize step.
234
+
235
+ Returns:
236
+ A numpy array.
237
+ """
238
+ assert isinstance(image, Image.Image), "image must be a PIL Image"
239
+ if resize_to is not None:
240
+ if mode == "resize":
241
+ image = image.resize(resize_to, resample=Image.Resampling.BICUBIC)
242
+ if transparent_bg_fill_stage == "after_resize":
243
+ image = fill_transparent_bg_with(image, transparent_bg_config)
244
+
245
+ elif mode == "rescale_and_pad_to_center":
246
+ scale = min(resize_to[0] / image.width,
247
+ resize_to[1] / image.height, 1.0)
248
+ new_width = round(image.width * scale)
249
+ new_height = round(image.height * scale)
250
+ if new_width == 0 or new_height == 0:
251
+ if raise_error_for_ill_resize:
252
+ raise ValueError(
253
+ f"Invalid resize to: {resize_to}, from image size: {image.size}"
254
+ )
255
+ else:
256
+ return np.zeros((resize_to[1], resize_to[0], 3),
257
+ dtype=np.uint8)
258
+
259
+ image = image.resize((new_width, new_height),
260
+ resample=Image.Resampling.BICUBIC)
261
+ if transparent_bg_fill_stage == "after_resize":
262
+ image = fill_transparent_bg_with(image, transparent_bg_config)
263
+ padding_left = (resize_to[0] - new_width) // 2
264
+ padding_right = resize_to[0] - new_width - padding_left
265
+ padding_top = (resize_to[1] - new_height) // 2
266
+ padding_bottom = resize_to[1] - new_height - padding_top
267
+ image = np.asarray(image)
268
+ image = np.pad(
269
+ image,
270
+ ((padding_top, padding_bottom), (padding_left, padding_right),
271
+ (0, 0)),
272
+ mode="constant",
273
+ constant_values=0,
274
+ )
275
+ assert image.shape == (resize_to[1], resize_to[0], 3)
276
+
277
+ elif mode == "rescale_and_pad_to_rightbottom":
278
+ scale = min(resize_to[0] / image.width,
279
+ resize_to[1] / image.height, 1.0)
280
+ new_width = round(image.width * scale)
281
+ new_height = round(image.height * scale)
282
+ if new_width == 0 or new_height == 0:
283
+ if raise_error_for_ill_resize:
284
+ raise ValueError(
285
+ f"Invalid resize to: {resize_to}, from image size: {image.size}"
286
+ )
287
+ else:
288
+ return np.zeros((resize_to[1], resize_to[0], 3),
289
+ dtype=np.uint8)
290
+
291
+ image = image.resize((new_width, new_height),
292
+ resample=Image.Resampling.BICUBIC)
293
+ if transparent_bg_fill_stage == "after_resize":
294
+ image = fill_transparent_bg_with(image, transparent_bg_config)
295
+ padding_right = resize_to[0] - new_width
296
+ padding_bottom = resize_to[1] - new_height
297
+ image = np.asarray(image)
298
+ image = np.pad(
299
+ image,
300
+ ((0, padding_bottom), (0, padding_right), (0, 0)),
301
+ mode="constant",
302
+ constant_values=0,
303
+ )
304
+ assert image.shape == (resize_to[1], resize_to[0], 3)
305
+
306
+ else:
307
+ raise ValueError(f"Invalid mode: {mode}")
308
+
309
+ if isinstance(image, Image.Image):
310
+ return np.asarray(image)
311
+ else:
312
+ return image
313
+
314
+
315
+ def navit_patchify(pixel_values: np.ndarray,
316
+ patch_size: int) -> dict[str, np.ndarray]:
317
+ """Reshape the pixel values to a navit shape.
318
+
319
+ Args:
320
+ pixel_values: np.ndarray, shape (t, h, w, c)
321
+ patch_size: int
322
+
323
+ Returns:
324
+ dict[str, np.ndarray]
325
+ - patches: np.ndarray, shape (t * h//patch_size * w//patch_size, c, patch_size, patch_size)
326
+ - grid_thw: np.ndarray, (t, h//patch_size, w//patch_size)
327
+ """
328
+ T, H, W, C = pixel_values.shape
329
+ assert C == 3, "pixel_values must have 3 channels"
330
+
331
+ patches = pixel_values.reshape(T, H // patch_size, patch_size,
332
+ W // patch_size, patch_size, C)
333
+ # (T, H//patch_size, W//patch_size, C, patch_size, patch_size)
334
+ patches = patches.transpose(0, 1, 3, 5, 2, 4)
335
+ patches = patches.reshape(-1, C, patch_size, patch_size)
336
+ grid_thw = np.array([T, H // patch_size, W // patch_size])
337
+ return {"pixel_values": patches, "grid_thw": grid_thw}
338
+
339
+
340
+ def normalize(x: np.ndarray,
341
+ mean,
342
+ std_inv,
343
+ pixels_dtype: np.dtype = np.float32) -> np.ndarray:
344
+ """Normalize the image.
345
+
346
+ Args:
347
+ x: The image to normalize. The shape is (..., 3). The dtype is uint8. The range is [0, 255].
348
+ mean: The mean of the image.
349
+ std_inv: The inverse of the std of the image.
350
+ pixels_dtype: The dtype of the image.
351
+ Returns:
352
+ The normalized image. The shape is (..., 3). The dtype is determined by the pixels_dtype.
353
+ """
354
+ x = (x / 255.0).astype(pixels_dtype)
355
+ x -= mean
356
+ x *= std_inv
357
+ return x
358
+
359
+
360
+ def _to_tensor(data, **kwargs):
361
+ import torch
362
+
363
+ if isinstance(data, np.ndarray):
364
+ return torch.from_numpy(data).to(**kwargs)
365
+ elif isinstance(data, torch.Tensor):
366
+ return data.to(**kwargs)
367
+ elif isinstance(data, list):
368
+ return [_to_tensor(item, **kwargs) for item in data]
369
+ elif isinstance(data, tuple):
370
+ return tuple(_to_tensor(item, **kwargs) for item in data)
371
+ elif isinstance(data, dict):
372
+ return {k: _to_tensor(v, **kwargs) for k, v in data.items()}
373
+ elif data is None:
374
+ return None
375
+ else:
376
+ raise ValueError(f"Unsupported data type: {type(data)}")
model-00001-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d1a60ccb44d2c70085717a0a0100fe2a950460ae95810a53e5a4324f057f9bd9
3
+ size 4936231310
model-00002-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64d80ecd05bd201eae321c4586090340d42ade4b9442668b02e063afb8e95d95
3
+ size 5240783442
model-00003-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:726818a99125faa1d487a620f2c305a73510d05b6520a1ac73412a34706a00f5
3
+ size 3645759823
model-00004-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:93fb29164cc8cbbc280a737a9f48e0f96f6ff01a476d9c952e59e088a9ee19e7
3
+ size 5240783440
model-00005-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bfcbf3605e7342e25b905352424e15b10edf3f84712da07c59f48305d9670c09
3
+ size 3473372860
model-00006-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ee31b98a9f4cfe1613546d5765a1035a1df5153e6fb1ab193bb62b63cc28215
3
+ size 5240783442
model-00007-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dddba61cee4c9d21983a860c8307e1c0b7a1e4fb3ec99c20397b64ca7052a3f9
3
+ size 3645759841
model-00008-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48c711e0a5e1f564ec80a3f4c04223a5854ac6748098bb1242fb3c1612b143cf
3
+ size 5240783442
model-00009-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fc8a5cce236a8e74d99e1db3a7001183ab75d1fcc25254432b29294e55e6426c
3
+ size 3645759791
model-00010-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bbab4be97bff48cac50ef176f410fb00e67dc6a6856bb2b7c791a12d270aaf09
3
+ size 5240783442
model-00011-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f3531c5d8094cd2f4f47a08fd6fb9a7d5b0120b0cbb7d39dbd5261875c3e757
3
+ size 3645759843
model-00012-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:04f0a9182d8ece493a78d0f24f2ba76c92bd63c5773e4a64f3c489894aee30db
3
+ size 5240783442
model-00013-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2dd1fce85d0092395b468b1ea9317814b8a16ea484e1052e58178e121017d4d5
3
+ size 3473372798
model-00014-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b5cfbeba263044db8170129b3f81b489e23541f6fcc89e59e4f7cec5dd82b1b7
3
+ size 5240783442
model-00015-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ddc5381e8674b0da7d51109b48e7407a9ebc2b6192421a0739cd579ac43ea865
3
+ size 3645759831
model-00016-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f912afd3d01adf32a7e7732a6b89ca22b63eccd9584b93115bae5bb180ff0109
3
+ size 5240783442
model-00017-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:68fe5808be766e7b87bfb11ac8da91d1941b76e5996aaed7b6df2a17e5f65c01
3
+ size 3645759841
model-00018-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5b4a892622abfed3c92bab16461de495cfc5d51cbbf395a3cd35980b4b204245
3
+ size 5240783442
model-00019-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf62390fbc5fae3a9073ca8d50b35380721374f912da372c8ce0fde92caab870
3
+ size 3645759871
model-00020-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:555bd270de301a64e523758785b192c16a65d8d26e4d294592b755e2f18c36f9
3
+ size 5240783444
model-00021-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a599037e73adcadaac5cff3719c8be06264b7c6aecf41a0c532a2e3b834c634
3
+ size 3473372912
model-00022-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:12c245aec45b5047995b29c3bb8eb62e80a6654340cd5b52edb031999b5c4b4f
3
+ size 5240783446
model-00023-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf701db169d3522a2b40d5af86ed9c1c6208c8607697d7b0032467e66467852e
3
+ size 3645759894
model-00024-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f0d67ce61bc274ffa289a3f07136bfb751a49747ed2c6cf47dbd624c407c5398
3
+ size 5240783446
model-00025-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96ef21f857bba47bddef11522183a58ab8309f236ed711dd7d71b58eb749626b
3
+ size 3645759820
model-00026-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a1b0f53d75af4a74e24c5181cc92822a49a9166c1333d49d2725393d81d7510
3
+ size 5240783446
model-00027-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b7da5b618e2f5efc06033eedfef58943e1bf5b4271698b590f2c3129631321d7
3
+ size 3645759816
model-00028-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e4466b8b7b5beabfd21af28a387e0cf6b194cecd845dcdab9e323cfe8ae15802
3
+ size 5240783442
model-00029-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:76335537d6c7bc6d5e3c8ced7e7bdbee6a66bca0a241e3816f521bdfb402f85e
3
+ size 3473372910
model-00030-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:221364c03a3f92a55ad44180269993b6e423fd99403ce1d35d812c56d89203c7
3
+ size 5240783446
model-00031-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91eeff8ee44d5e6389b7b9fe730b1f880ae53e17b9d2d087d295460f44be07d5
3
+ size 3645759808
model-00032-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1bddbb57aedc66efc1d63b79bd2d0c203f8086ee6215862ce63e5b5046d6e30f
3
+ size 5240783446
model-00033-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:911bc1259e5672b7dc4d565f1ef5c9b9229019fa0044334113850727396a43cb
3
+ size 3645759898
model-00034-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e59bd8c1ffecdf3effba49609389b6cce2246b54464ee3f1ae481a046be16c6c
3
+ size 5240783446
model-00035-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f5b86f3b55bff7f005c2911e92bebe70a7e4c0cac3a076758f45f0131529bf5a
3
+ size 3645759894
model-00036-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0e58c766c07e49851dab73a896fe530dc8dabac60d73b2ff637c7c7495cb85f9
3
+ size 5240783446
model-00037-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13b79e949543ad129f22db48c59e68edd1b4260142051bac165f196d499e1a47
3
+ size 3473372912
model-00038-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ef490d649a1e97523688ba123b3caa589c883a9df2614f325fde96cd25cdd360
3
+ size 5240783446
model-00039-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d40e9e62be243c7bddf5c37b5d4b379a490a5b88b1cc850dff7d554bf46394f
3
+ size 3645759814
model-00040-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1daa5d84bc3d5b568509ac86c477c8f02b45bc7d7e4e305b0b034ca3288bacf9
3
+ size 5240783446
model-00041-of-00185.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7aeac53f439fb92aee6a380def5513bee59653549b42a83baf4ee19fc7a1ffdd
3
+ size 3645759896