File size: 32,504 Bytes
e5c09aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# Copyright 2025 the LlamaFactory team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import defaultdict
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional

from ...extras import logging
from ...extras.constants import IGNORE_INDEX
from .processor_utils import DatasetProcessor, greedy_knapsack, infer_seqlen


if TYPE_CHECKING:
    from ..mm_plugin import AudioInput, ImageInput, VideoInput


logger = logging.get_logger(__name__)


@dataclass
class SupervisedDatasetProcessor(DatasetProcessor):
    SIL_TOKENS: list[int] | None = None
    def _encode_data_example(

        self,

        task: str,

        prompt: list[dict[str, str]],

        response: list[dict[str, str]],

        system: Optional[str],

        tools: Optional[str],

        images: list["ImageInput"],

        videos: list["VideoInput"],

        audios: list["AudioInput"],

    ) -> tuple[list[int], list[int]]:
        cls = type(self)
        sil = type(self).SIL_TOKENS
        if sil is None:
            # "<|silence|>" 的 token 序列(不是 special token)
            sil = self.tokenizer.encode("<|silence|>", add_special_tokens=False)
            type(self).SIL_TOKENS = sil
        messages = self.template.mm_plugin.process_messages([[prompt,response]], images, videos, audios, self.processor) # Qwen2_5OmniProcessor #### 需要改!!!
        input_ids, labels = self.template.mm_plugin.process_token_ids(
            [], [], images, videos, audios, self.tokenizer, self.processor
        )
        encoded_pairs = self.template.encode_multiturn(self.tokenizer, messages, system, tools) #[{'content': '<|vision_bos|><|audio_bos|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|VIDEO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|AUDIO|><|audio_eos|><|vision_eos|>What is the video describing?', 'role': 'user'}, {'content': 'A girl who is drawing a picture of a guitar and feel nervous.', 'role': 'assistant'}]
        total_length = len(input_ids) + (1 if self.template.efficient_eos else 0)
        if self.data_args.mask_history: #false
            encoded_pairs = encoded_pairs[::-1]  # high priority for last turns
        input_ids = []
        labels = []         
        anchor_idx_list = []    
        gate_label_list = []    
        def startswith_silence(toks, sil=sil):
            return len(toks) >= len(sil) and toks[:len(sil)] == sil
# # ################# 混合 ################
        if cls.SIL_TOKENS is None:
            cls.SIL_TOKENS = self.tokenizer.encode("<|silence|>", add_special_tokens=False)
        SIL = cls.SIL_TOKENS

        if getattr(cls, "VISION_EOS_TOKENS", None) is None:
            cls.VISION_EOS_TOKENS = self.tokenizer.encode("<|vision_eos|>", add_special_tokens=False)
        VEOS = cls.VISION_EOS_TOKENS
        VEOS_L = len(VEOS)

        if getattr(cls, "ASSIST_TOKENS", None) is None:
            cls.ASSIST_TOKENS = self.tokenizer.encode(
                "<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False
            )
        ASSIST = cls.ASSIST_TOKENS  

        if getattr(cls, "USER_TOKENS", None) is None:
            cls.USER_TOKENS = self.tokenizer.encode("<|im_start|>user\n", add_special_tokens=False)
        USER = cls.USER_TOKENS 
        def find_last_veos_position(source_ids: list[int], veos: list[int]) -> int:
            """只返回最后一次出现的“pattern 末尾”索引;找不到则返回 -1"""
            L = len(veos)
            if L == 0 or len(source_ids) < L:
                return -1
            # 反向扫描,第一次命中就是最后一次出现
            for i in range(len(source_ids) - L, -1, -1):
                if source_ids[i:i+L] == veos:
                    return i + L - 1
            return -1    
        def is_alert_example(prompt_msg, resp_msg) -> bool:
            if len(resp_msg) > 0 and isinstance(resp_msg[0].get("text", ""), str):
                return resp_msg[0]["text"].lstrip().startswith("alert")
            return False

        is_alert = is_alert_example(prompt, response)
        chunk_gate_labels: list[float] = []
        for _src_ids, tgt_ids in encoded_pairs:
            t = list(tgt_ids)
            should_speak = (len(t) > 0) and (not startswith_silence(t))
            chunk_gate_labels.append(1.0 if should_speak else 0.0)

        if is_alert:
            new_pairs: list[tuple[list[int], list[int]]] = []
            for i, (src_ids, tgt_ids) in enumerate(encoded_pairs):
                src = list(src_ids)
                tgt = list(tgt_ids)

                if chunk_gate_labels[i] == 1.0 and len(tgt) > 0:
                    if ASSIST and len(src) >= len(ASSIST):
                        if src[-len(ASSIST) :] == ASSIST:
                            src = src[: -len(ASSIST)]

                    tgt = []

                    if i + 1 < len(encoded_pairs):
                        next_src, next_tgt = encoded_pairs[i + 1]
                        next_src = list(next_src)
                        if USER and len(next_src) >= len(USER):
                            if next_src[: len(USER)] == USER:
                                next_src = next_src[len(USER) :]
                        encoded_pairs[i + 1] = (next_src, list(next_tgt))

                new_pairs.append((src, tgt))

            encoded_pairs = new_pairs
        for turn_idx, (source_ids, target_ids) in enumerate(encoded_pairs):
            if total_length >= self.data_args.cutoff_len:
                break

            # 截断长度
            source_len, target_len = infer_seqlen(
                len(source_ids), len(target_ids), self.data_args.cutoff_len - total_length
            )
            source_ids = list(source_ids[:source_len])
            target_ids = list(target_ids[:target_len])

            # ---------- source_label ----------
            if self.data_args.train_on_prompt:
                source_label = source_ids
            elif self.template.efficient_eos:
                source_label = [self.tokenizer.eos_token_id] + [IGNORE_INDEX] * (source_len - 1)
            else:
                source_label = [IGNORE_INDEX] * source_len

            # ---------- target_label----------
            if self.data_args.mask_history and turn_idx != 0:
                target_label = [IGNORE_INDEX] * target_len
            # else:
            #     # gate-only:无论 alert 还是 narration,都不训 LM
            #     target_label = [IGNORE_INDEX] * target_len
            else:
                if 151643 in target_ids: 
                    if task == "turn-taking":
                        target_label = target_ids[:-3] + [IGNORE_INDEX] * 3
                    else:
                        target_label = [IGNORE_INDEX] * target_len
                else:
                    # Narration:若以 <|silence|> 开头,该轮不训练 LM
                    ##target_label = [IGNORE_INDEX] * target_len if startswith_silence(target_ids) else target_ids
                    if task == "turn-taking":
                        target_label = [IGNORE_INDEX] * target_len if startswith_silence(target_ids) else target_ids
                    else:
                        target_label = [IGNORE_INDEX] * target_len

            # ---------- flatten ----------
            base_len = len(input_ids)
            input_ids += source_ids + target_ids
            labels += source_label + target_label
            total_length += len(source_ids) + len(target_ids)

            # ---------- time head:anchor + gate ----------
            veos_pos = find_last_veos_position(source_ids, VEOS)
            if veos_pos != -1:
                anchor_abs = base_len + veos_pos
                anchor_idx_list.append(anchor_abs)
                gate_label_list.append(chunk_gate_labels[turn_idx])

        # efficient_eos 收尾
        if self.template.efficient_eos:
            input_ids += [self.tokenizer.eos_token_id]
            labels += [self.tokenizer.eos_token_id]

        if task == "turn-taking":
            pos_anchor_idxs = [a for a, y in zip(anchor_idx_list, gate_label_list) if y == 1.0]
            pos_gate_label_list = [1.0] * len(pos_anchor_idxs)
            return input_ids, labels, pos_anchor_idxs, pos_gate_label_list, [[prompt, response]]

        return input_ids, labels, anchor_idx_list, gate_label_list, [[prompt, response]]

################# 训 narration 的###################        
        # # "<|silence|>" 的 token 序列(非 special token,按文本编码)
        # if getattr(cls, "SIL_TOKENS", None) is None:
        #     cls.SIL_TOKENS = self.tokenizer.encode("<|silence|>", add_special_tokens=False)
        # SIL = cls.SIL_TOKENS

        # # "<|vision_eos|>" 的 token 序列
        # if getattr(cls, "VISION_EOS_TOKENS", None) is None:
        #     cls.VISION_EOS_TOKENS = self.tokenizer.encode("<|vision_eos|>", add_special_tokens=False)
        # VEOS = cls.VISION_EOS_TOKENS
        # VEOS_L = len(VEOS)

        # if self.data_args.mask_history:
        #     encoded_pairs = encoded_pairs[::-1]
        # def find_last_veos_position(source_ids: list[int], veos: list[int]) -> int:
        #     """只返回最后一次出现的“pattern 末尾”索引;找不到则返回 -1"""
        #     L = len(veos)
        #     if L == 0 or len(source_ids) < L:
        #         return -1
        #     # 反向扫描,第一次命中就是最后一次出现
        #     for i in range(len(source_ids) - L, -1, -1):
        #         if source_ids[i:i+L] == veos:
        #             return i + L - 1
        #     return -1
        
        # for turn_idx, (source_ids, target_ids) in enumerate(encoded_pairs):
        #     if total_length >= self.data_args.cutoff_len:
        #         break

        #     # 截断本轮可用长度
        #     source_len, target_len = infer_seqlen(
        #         len(source_ids), len(target_ids), self.data_args.cutoff_len - total_length
        #     )
        #     source_ids = list(source_ids[:source_len])
        #     target_ids = list(target_ids[:target_len])

        #     # -------- SFT: 构造 labels --------
        #     if self.data_args.train_on_prompt:
        #         source_label = source_ids
        #     elif self.template.efficient_eos:
        #         source_label = [self.tokenizer.eos_token_id] + [IGNORE_INDEX] * (source_len - 1)
        #     else:
        #         source_label = [IGNORE_INDEX] * source_len

        #     if self.data_args.mask_history and turn_idx != 0:
        #         # 仅训练最后一轮(若启用)
        #         target_label = [IGNORE_INDEX] * target_len
        #     else:
        #         # 一些你的自定义规则保留
        #         if 151643 in target_ids:  # 你之前的 “没生成完句子” 特判
        #             #target_label = target_ids[:-3] + [IGNORE_INDEX] * 3
        #             target_label = [IGNORE_INDEX] * target_len
        #         # elif turn_idx == 0 and (messages[0]["content"] == "Narration History"):
        #         #     target_label = [IGNORE_INDEX] * target_len
        #         else:
        #             # Narration:若以 <|silence|> 开头,该轮不训练 LM
        #             ##target_label = [IGNORE_INDEX] * target_len if startswith_silence(target_ids) else target_ids
        #             target_label = [IGNORE_INDEX] * target_len

        #     # 将本轮拼到扁平序列
        #     base_len = len(input_ids)
        #     input_ids += source_ids + target_ids
        #     labels += source_label + target_label
        #     total_length += len(source_ids) + len(target_ids)

        #     # -------- Time Head: 记录锚点与标签 --------
        #     # 该 turn 是否“应该说话”:target 非空 且 不以 <|silence|> 开头
        #     should_speak = (target_len > 0) and (not startswith_silence(target_ids))

        #     # 找出本轮 source 内最后一个 <|vision_eos|> 的位置(相对 source_ids)
        #     veos_pos = find_last_veos_position(source_ids,VEOS)

        #     # 将这些位置映射到扁平后的 input_ids 绝对下标
        #     anchor_abs = base_len + veos_pos  # pos 落在 source 段
        #     anchor_idx_list.append(anchor_abs)
        #     gate_label_list.append(1.0 if should_speak else 0.0)

        # # 可选:efficient_eos 末尾补 eos
        # if self.template.efficient_eos:
        #     input_ids += [self.tokenizer.eos_token_id]
        #     labels += [self.tokenizer.eos_token_id]

        # # 返回给上层(collator 负责把 anchor/label pad 成等长,并生成 gate_mask)
        # return input_ids, labels, anchor_idx_list, gate_label_list, [[prompt, response]]        
################# 训 narration 的###################

################# 训 time head 的(proactive)###################
        # 1) 先记录“原始”每个 chunk 是否有 target(有 target = 需要开口)
        # chunk_gate_labels: list[float] = []
        # for (src_ids, tgt_ids) in encoded_pairs:
        #     chunk_gate_labels.append(1.0 if (len(tgt_ids) > 0 and not startswith_silence(tgt_ids)) else 0.0)

        # cls = type(self)
        # # 用 tokenizer 直接拿模板 token 序列,避免硬编码 ID
        # if getattr(cls, "ASSIST_TOKENS", None) is None:
        #     # 对应 "<|im_end|>\n<|im_start|>assistant\n"
        #     cls.ASSIST_TOKENS = self.tokenizer.encode(
        #         "<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False
        #     )
        # if getattr(cls, "USER_TOKENS", None) is None:
        #     # 对应 "<|im_start|>user\n"
        #     cls.USER_TOKENS = self.tokenizer.encode(
        #         "<|im_start|>user\n", add_special_tokens=False
        #     )
        # if getattr(cls, "VISION_EOS_TOKENS", None) is None:
        #     # 对应 "<|vision_eos|>",用来确定 anchor 的位置
        #     cls.VISION_EOS_TOKENS = self.tokenizer.encode(
        #         "<|vision_eos|>", add_special_tokens=False
        #     )

        # assist_tokens = cls.ASSIST_TOKENS
        # user_tokens = cls.USER_TOKENS
        # vision_tokens = cls.VISION_EOS_TOKENS

        # # 2) 对 encoded_pairs 做一次 pass:
        # #    - 对于原来有 target 的 chunk:
        # #         * 从 source 末尾切掉 "<|im_end|>\n<|im_start|>assistant\n"
        # #         * 把 target_ids 清空(不训练 LM)
        # #         * 把下一条 source 开头的 "<|im_start|>user\n" 切掉
        # new_pairs: list[tuple[list[int], list[int]]] = []
        # for i, (src_ids, tgt_ids) in enumerate(encoded_pairs):
        #     src_ids = list(src_ids)
        #     tgt_ids = list(tgt_ids)

        #     #if chunk_gate_labels[i] == 1.0 and tgt_ids:  # 原本在这个 chunk 有 assistant 文本 → label=1
        #         # 切掉 source 末尾的 "<|im_end|>\n<|im_start|>assistant\n"
        #         # if assist_tokens and len(src_ids) >= len(assist_tokens):
        #         #     if src_ids[-len(assist_tokens):] == assist_tokens:
        #         #         src_ids = src_ids[:-len(assist_tokens)]

        #         # 只训练 time head,不训练 LM head → target 清空
        #         # tgt_ids = []

        #         # 下一条 pair[i+1],如果以 "<|im_start|>user\n" 开头,就切掉这个前缀
        #         # if i + 1 < len(encoded_pairs):
        #         #     next_src, next_tgt = encoded_pairs[i + 1]
        #         #     next_src = list(next_src)
        #         #     if user_tokens and len(next_src) >= len(user_tokens):
        #         #         if next_src[:len(user_tokens)] == user_tokens:
        #         #             next_src = next_src[len(user_tokens):]
        #         #     encoded_pairs[i + 1] = (next_src, list(next_tgt))

        #     new_pairs.append((src_ids, tgt_ids))

        # encoded_pairs = new_pairs

        # # ===============================
        # #       flatten + label 构造
        # # ===============================
        # for turn_idx, (source_ids, target_ids) in enumerate(encoded_pairs):
        #     if total_length >= self.data_args.cutoff_len:
        #         break

        #     source_len, target_len = infer_seqlen(
        #         len(source_ids), len(target_ids), self.data_args.cutoff_len - total_length
        #     )
        #     source_ids = source_ids[:source_len]
        #     target_ids = target_ids[:target_len]

        #     if self.data_args.train_on_prompt:  # prompt部分也要预测,也就是预训练的那种模式
        #         source_label = source_ids
        #     elif self.template.efficient_eos:
        #         source_label = [self.tokenizer.eos_token_id] + [IGNORE_INDEX] * (source_len - 1)
        #     else:
        #         source_label = [IGNORE_INDEX] * source_len

        #     if self.data_args.mask_history and turn_idx != 0:  # train on the last turn only
        #         target_label = [IGNORE_INDEX] * target_len
        #     else:
        #         if 151643 in target_ids:  ##### 没生成完句子。
        #             target_label = target_ids[:-3] + [IGNORE_INDEX] * 3
        #         elif turn_idx == 0 and (messages[0]["content"] == "Narration History"):
        #             target_label = [IGNORE_INDEX] * target_len
        #         else:
        #             # 这里即便 startswith_silence 逻辑还在,对 proactive_gate 来说,
        #             # 上面我们已经把所有有文本的 target 清空了,所以不会再训练 LM。
        #             target_label = target_ids   #[IGNORE_INDEX] * target_len if startswith_silence(target_ids) else target_ids

        #     total_length += len(source_ids) + len(target_ids)

        #     if self.data_args.mask_history:  # false # reversed sequences
        #         base_len = len(input_ids)
        #         input_ids = source_ids + target_ids + input_ids
        #         labels = source_label + target_label + labels
        #     else:
        #         base_len = len(input_ids)
        #         input_ids += source_ids + target_ids
        #         labels += source_label + target_label

        #     # # ===============================
        #     # #       Time head anchor & label
        #     # # ===============================
        #     # # 在当前 chunk 的 source 里找到最后一个 "<|vision_eos|>",以它作为 anchor;
        #     # # label 由 chunk_gate_labels[turn_idx] 决定:
        #     # #   - 原来有 target 的 chunk → 1.0(需要开口)
        #     # #   - 原来没有 target 的 chunk → 0.0(不需要开口)
        #     # if vision_tokens and source_len >= len(vision_tokens):
        #     #     L = len(vision_tokens)
        #     #     pos = -1
        #     #     # 从后往前找,可以保证拿到最后一个 vision_eos
        #     #     for idx in range(source_len - L, -1, -1):
        #     #         if source_ids[idx : idx + L] == vision_tokens:
        #     #             pos = idx + L - 1  # pattern 最后一个 token 的位置
        #     #             break

        #     #     if pos != -1:
        #     #         anchor_idx = base_len + pos
        #     #         anchor_idx_list.append(anchor_idx)
        #     #         gate_label_list.append(chunk_gate_labels[turn_idx])

        # if self.template.efficient_eos:  # false
        #     input_ids += [self.tokenizer.eos_token_id]
        #     labels += [self.tokenizer.eos_token_id]

        # return input_ids, labels, [[prompt, response]] #anchor_idx_list, gate_label_list,
################# 训 time head 的###################

################# 训正常SFT的(turn-taking)###################
        # for turn_idx, (source_ids, target_ids) in enumerate(encoded_pairs):
        #     if total_length >= self.data_args.cutoff_len:
        #         break

        #     source_len, target_len = infer_seqlen(
        #         len(source_ids), len(target_ids), self.data_args.cutoff_len - total_length
        #     )
        #     source_ids = source_ids[:source_len]
        #     target_ids = target_ids[:target_len]
        #     ####total_length += source_len + target_len

        #     if self.data_args.train_on_prompt: #prompt部分也要预测,也就是预训练的那种模式
        #         source_label = source_ids
        #     elif self.template.efficient_eos:
        #         source_label = [self.tokenizer.eos_token_id] + [IGNORE_INDEX] * (source_len - 1)
        #     else:
        #         source_label = [IGNORE_INDEX] * source_len #######

        #     if self.data_args.mask_history and turn_idx != 0:  # train on the last turn only
        #         target_label = [IGNORE_INDEX] * target_len
        #     else:
        #         if 151643 in target_ids: ##### 没生成完句子。
        #             target_label = target_ids[:-3]+[IGNORE_INDEX]*3
        #         else:
        #             target_label = target_ids #[IGNORE_INDEX] * target_len if startswith_silence(target_ids) else target_ids 
        #     total_length += len(source_ids)+len(target_ids)
        #     if self.data_args.mask_history:  # false # reversed sequences
        #         input_ids = source_ids + target_ids + input_ids
        #         labels = source_label + target_label + labels
        #     else:
        #         base_len = len(input_ids)
        #         input_ids += source_ids + target_ids
        #         labels += source_label + target_label

        #     is_assistant_turn = (target_len > 0)
        #     if is_assistant_turn:
        #         # 锚点 = 本轮 source 的最后一个 token(assistant\n 的 '\n' 位)
        #         #anchor_idx = base_len + source_len - 1
        #         # 锚点 = 本轮source中最后一个<vision_eos>的位置
        #         anchor_idx = base_len + source_len - 6
        #         # gate 标签:仅当 target 以 <|silence|> 开头时视为沉默
        #         #is_sil = startswith_silence(target_ids)
        #         #anchor_idx_list.append(anchor_idx)
        #         #gate_label_list.append(0.0 if is_sil else 1.0)

        # if self.template.efficient_eos: #false
        #     input_ids += [self.tokenizer.eos_token_id]
        #     labels += [self.tokenizer.eos_token_id]
        # #print('prompt:',prompt,'input_ids:',input_ids)
        # return input_ids, labels, [[prompt,response]] #input_ids, labels, anchor_idx_list, gate_label_list, [[prompt,response]]
################# 训正常SFT的###################

    def preprocess_dataset(self, examples: dict[str, list[Any]]) -> dict[str, list[Any]]:
        # build inputs with format `<bos> X Y <eos>` and labels with format `<ignore> ... <ignore> Y <eos>`
        # for multiturn examples, we only mask the prompt part in each prompt-response pair.
        model_inputs = defaultdict(list)
        for i in range(len(examples["_prompt"])):
            # if len(examples["_prompt"][i]) % 2 != 1 or len(examples["_response"][i]) != 1:
            #     logger.warning_rank0(
            #         "Dropped invalid example: {}".format(examples["_prompt"][i] + examples["_response"][i])
            #     )
            #     continue

            input_ids, labels, anchor_idx_list, gate_label_list, messages = self._encode_data_example( ###### tokenize
                task=examples["_task"][i],
                prompt=examples["_prompt"][i], #query
                response=examples["_response"][i], #ans
                system=examples["_system"][i],
                tools=examples["_tools"][i],
                images=examples["_images"][i] or [],
                videos=examples["_videos"][i] or [],
                audios=examples["_audios"][i] or [],
            )
            model_inputs["input_ids"].append(input_ids) #[151544, 8948, xxxxx]
            model_inputs["attention_mask"].append([1] * len(input_ids)) #[[1,1,1,.....]]
            model_inputs["labels"].append(labels) #[-100,xxxxx]
            model_inputs["images"].append(examples["_images"][i])
            model_inputs["videos"].append(examples["_videos"][i])
            model_inputs["audios"].append(examples["_audios"][i])
            model_inputs['messages'].append(messages)
            model_inputs['anchor_idx_list'].append(anchor_idx_list)
            model_inputs['gate_label_list'].append(gate_label_list)
        return model_inputs

    def print_data_example(self, example: dict[str, list[int]]) -> None:
        valid_labels = list(filter(lambda x: x != IGNORE_INDEX, example["labels"]))
        print("input_ids:\n{}".format(example["input_ids"]))
        #print("inputs:\n{}".format(self.tokenizer.decode(example["input_ids"], skip_special_tokens=False)))
        #print("label_ids:\n{}".format(example["labels"]))
        #print(f"labels:\n{self.tokenizer.decode(valid_labels, skip_special_tokens=False)}")


@dataclass
class PackedSupervisedDatasetProcessor(SupervisedDatasetProcessor):
    def preprocess_dataset(self, examples: dict[str, list[Any]]) -> dict[str, list[Any]]:
        # TODO: use `position_ids` to achieve packing
        # build inputs with format `<bos> X1 Y1 <eos> <bos> X2 Y2 <eos>`
        # and labels with format `<ignore> ... <ignore> Y1 <eos> <ignore> ... <ignore> Y2 <eos>`
        print('PackedSupervisedDatasetProcessor')
        valid_num = 0
        batch_input_ids, batch_labels, batch_images, batch_videos, batch_audios = [], [], [], [], []
        lengths = []
        length2indexes = defaultdict(list)
        for i in range(len(examples["_prompt"])):
            if len(examples["_prompt"][i]) % 2 != 1 or len(examples["_response"][i]) != 1:
                logger.warning_rank0(
                    "Dropped invalid example: {}".format(examples["_prompt"][i] + examples["_response"][i])
                )
                continue

            input_ids, labels,_ = self._encode_data_example(
                prompt=examples["_prompt"][i],
                response=examples["_response"][i],
                system=examples["_system"][i],
                tools=examples["_tools"][i],
                images=examples["_images"][i] or [],
                videos=examples["_videos"][i] or [],
                audios=examples["_audios"][i] or [],
            )
            length = len(input_ids)
            if length > self.data_args.cutoff_len:
                logger.warning_rank0(f"Dropped lengthy example with length {length} > {self.data_args.cutoff_len}.")
            else:
                lengths.append(length)
                length2indexes[length].append(valid_num)
                batch_input_ids.append(input_ids)
                batch_labels.append(labels)
                batch_images.append(examples["_images"][i] or [])
                batch_videos.append(examples["_videos"][i] or [])
                batch_audios.append(examples["_audios"][i] or [])
                valid_num += 1

        model_inputs = defaultdict(list)
        knapsacks = greedy_knapsack(lengths, self.data_args.cutoff_len)
        for knapsack in knapsacks:
            packed_input_ids, packed_attention_masks, packed_position_ids, packed_labels = [], [], [], []
            packed_images, packed_videos, packed_audios = [], [], []
            for i, length in enumerate(knapsack):
                index = length2indexes[length].pop()
                packed_input_ids += batch_input_ids[index]
                packed_position_ids += list(range(len(batch_input_ids[index])))  # NOTE: pad_to_multiple_of ignore this
                packed_labels += batch_labels[index]
                packed_images += batch_images[index]
                packed_videos += batch_videos[index]
                packed_audios += batch_audios[index]
                if self.data_args.neat_packing:
                    packed_attention_masks += [i + 1] * len(batch_input_ids[index])  # start from 1
                else:
                    packed_attention_masks += [1] * len(batch_input_ids[index])

            if len(packed_input_ids) < self.data_args.cutoff_len + 1:  # avoid flash_attn drops attn mask
                pad_length = self.data_args.cutoff_len - len(packed_input_ids) + 1
                packed_input_ids += [self.tokenizer.pad_token_id] * pad_length
                packed_position_ids += [0] * pad_length
                packed_labels += [IGNORE_INDEX] * pad_length
                if self.data_args.neat_packing:
                    packed_attention_masks += [0] * pad_length
                else:
                    packed_attention_masks += [1] * pad_length  # more efficient flash_attn

            if len(packed_input_ids) != self.data_args.cutoff_len + 1:
                raise ValueError("The length of packed example should be identical to the cutoff length.")

            model_inputs["input_ids"].append(packed_input_ids)
            model_inputs["attention_mask"].append(packed_attention_masks)
            model_inputs["position_ids"].append(packed_position_ids)
            model_inputs["labels"].append(packed_labels)
            model_inputs["images"].append(packed_images or None)
            model_inputs["videos"].append(packed_videos or None)
            model_inputs["audios"].append(packed_audios or None)

        return model_inputs