File size: 11,884 Bytes
6e802a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# dataset.py: Text + think block + speech tokens -> one training sequence with per-region loss weights; collator.

import json
import logging
import torch
from torch.utils.data import Dataset
from dataclasses import dataclass
from typing import List, Dict, Any

from tokenizer import TokenRegistry, NUM_SPEECH_TOKENS

logger = logging.getLogger(__name__)

IGNORE_INDEX = -100


class MambaCoTTTSDataset(Dataset):
    def __init__(
        self,
        data_path: str,
        tokenizer,
        registry: TokenRegistry,
        stage: int = 1,
        max_seq_len: int = 2048,
        cfg_dropout: float = 0.0,
        cot_mode: str = "prose",
    ):
        assert stage in (1, 2, "auto"), f"stage must be 1, 2 or 'auto', got {stage}"

        self.tokenizer = tokenizer
        self.registry = registry
        self.stage = stage
        self.max_seq_len = max_seq_len
        self.cfg_dropout = cfg_dropout

        self.data: List[dict] = []
        with open(data_path, "r") as f:
            for line in f:
                line = line.strip()
                if line:
                    self.data.append(json.loads(line))

        assert cot_mode in ("prose", "tags", "none"), (
            f"cot_mode={cot_mode!r} is not supported by the flat "
            f"MambaCoTTTSDataset. The P²-CoT plan tiers (plan / tags+plan / "
            f"full) carry a per-position injection schedule and run on the "
            f"delay/depth path only — launch with --depth_module (or "
            f"--delay_pattern), which routes to delay_dataset.DelayMimiDataset."
            if cot_mode in ("plan", "tags+plan", "full") else cot_mode)
        self.cot_mode = cot_mode
        if cot_mode != "prose":
            for row in self.data:
                if cot_mode == "none":
                    row["reasoning"] = ""
                else:
                    row["reasoning"] = (
                        f"EMO={row.get('emotion_label') or 'neutral'} "
                        f"PACE={row.get('pace') or 'normal'} "
                        f"PITCH={row.get('pitch') or 'normal'}")

        self.bos_id = registry.bos_id
        self.eos_id = registry.eos_id
        self.user_prompt_id = registry.user_prompt_id
        self.think_start_id = registry.think_start_id
        self.think_end_id = registry.think_end_id
        self.audio_start_id = registry.audio_start_id
        self.audio_end_id = registry.audio_end_id

        logger.info(
            f"Loaded {len(self.data)} samples from {data_path} (stage={stage}, "
            f"cot_mode={cot_mode}, max_seq_len={max_seq_len})"
        )

    def __len__(self) -> int:
        return len(self.data)

    def __getitem__(self, idx: int) -> dict:
        item = self.data[idx]

        if self.stage == "auto":
            row_stage = 2 if str(item.get("reasoning", "") or "").strip() else 1
        else:
            row_stage = self.stage

        prompt_text: str = item["prompt"]
        speech_values: List[int] = item["speech_tokens"]
        residual_codes = item.get("residual_codes", None)
        speaker_id = item.get("speaker_id", None)

        clamped = False
        for i, v in enumerate(speech_values):
            if v < 0 or v >= NUM_SPEECH_TOKENS:
                speech_values[i] = max(0, min(v, NUM_SPEECH_TOKENS - 1))
                clamped = True
        if clamped:
            logger.warning(f"Sample {idx}: speech_tokens contained out-of-range values, clamped to [0, {NUM_SPEECH_TOKENS - 1}]")

        speech_ids = [self.registry.speech_value_to_id(v) for v in speech_values]

        if self.cfg_dropout > 0.0 and torch.rand(1).item() < self.cfg_dropout:
            prompt_ids = []
        else:
            prompt_ids = self.tokenizer.encode(prompt_text, add_special_tokens=False)

        reasoning_ids = []
        if row_stage == 2:
            reasoning_text = item.get("reasoning", "")
            if reasoning_text:
                reasoning_clean = reasoning_text.strip()
                if reasoning_clean.startswith("<THINK>"):
                    reasoning_clean = reasoning_clean[len("<THINK>"):].strip()
                if reasoning_clean.endswith("</THINK>"):
                    reasoning_clean = reasoning_clean[:-len("</THINK>")].strip()
                if reasoning_clean:
                    reasoning_ids = self.tokenizer.encode(reasoning_clean, add_special_tokens=False)

        if row_stage == 1:
            sequence = (
                [self.bos_id, self.user_prompt_id]
                + prompt_ids
                + [self.audio_start_id]
                + speech_ids
                + [self.audio_end_id, self.eos_id]
            )
            prompt_end = 2 + len(prompt_ids)
            reasoning_start = prompt_end
            reasoning_end = prompt_end
            speech_start = prompt_end + 1
            speech_end = speech_start + len(speech_ids)
        else:
            sequence = (
                [self.bos_id, self.user_prompt_id]
                + prompt_ids
                + [self.think_start_id]
                + reasoning_ids
                + [self.think_end_id]
                + [self.audio_start_id]
                + speech_ids
                + [self.audio_end_id, self.eos_id]
            )
            prompt_end = 2 + len(prompt_ids)
            reasoning_start = prompt_end + 1
            reasoning_end = reasoning_start + len(reasoning_ids)
            speech_start = reasoning_end + 2
            speech_end = speech_start + len(speech_ids)

        if len(sequence) > self.max_seq_len:
            overflow = len(sequence) - self.max_seq_len

            max_speech_trim = len(speech_ids)
            speech_trim = min(overflow, max_speech_trim)
            speech_ids_trimmed = speech_ids[:len(speech_ids) - speech_trim]
            overflow -= speech_trim

            reasoning_ids_trimmed = reasoning_ids
            if overflow > 0 and row_stage == 2:
                max_reasoning_trim = len(reasoning_ids)
                reasoning_trim = min(overflow, max_reasoning_trim)
                reasoning_ids_trimmed = reasoning_ids[:len(reasoning_ids) - reasoning_trim]
                overflow -= reasoning_trim

            if overflow > 0:
                logger.warning(
                    f"Sample {idx}: sequence still {overflow} tokens over max_seq_len "
                    f"after truncating speech and reasoning"
                )

            if row_stage == 1:
                sequence = (
                    [self.bos_id, self.user_prompt_id]
                    + prompt_ids
                    + [self.audio_start_id]
                    + speech_ids_trimmed
                    + [self.audio_end_id, self.eos_id]
                )
                prompt_end = 2 + len(prompt_ids)
                reasoning_start = prompt_end
                reasoning_end = prompt_end
                speech_start = prompt_end + 1
                speech_end = speech_start + len(speech_ids_trimmed)
            else:
                sequence = (
                    [self.bos_id, self.user_prompt_id]
                    + prompt_ids
                    + [self.think_start_id]
                    + reasoning_ids_trimmed
                    + [self.think_end_id]
                    + [self.audio_start_id]
                    + speech_ids_trimmed
                    + [self.audio_end_id, self.eos_id]
                )
                prompt_end = 2 + len(prompt_ids)
                reasoning_start = prompt_end + 1
                reasoning_end = reasoning_start + len(reasoning_ids_trimmed)
                speech_start = reasoning_end + 2
                speech_end = speech_start + len(speech_ids_trimmed)

        return {
            "input_ids": torch.tensor(sequence, dtype=torch.long),
            "prompt_end": prompt_end,
            "reasoning_start": reasoning_start,
            "reasoning_end": reasoning_end,
            "speech_start": speech_start,
            "speech_end": speech_end,
            "residual_codes": residual_codes,
            "speaker_id": speaker_id,
        }


@dataclass
class MambaCoTDataCollator:
    pad_token_id: int
    registry: TokenRegistry
    stage: int = 1
    lambda_r: float = 0.3
    lambda_s: float = 1.0
    semantic_weight: float = 1.0
    eos_weight: float = 1.0
    level_cycle: int = 0

    def __call__(self, batch: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
        input_ids_list = [item["input_ids"] for item in batch]

        input_ids = torch.nn.utils.rnn.pad_sequence(
            input_ids_list, batch_first=True, padding_value=self.pad_token_id
        )
        B, T_max = input_ids.shape

        labels = input_ids.clone()

        loss_weights = torch.ones(B, T_max, dtype=torch.float32)

        for i, item in enumerate(batch):
            seq_len = len(item["input_ids"])
            prompt_end = item["prompt_end"]

            labels[i, :prompt_end] = IGNORE_INDEX

            if seq_len < T_max:
                labels[i, seq_len:] = IGNORE_INDEX

            loss_weights[i, :prompt_end] = 0.0
            if seq_len < T_max:
                loss_weights[i, seq_len:] = 0.0

            if self.stage == 2 or self.stage == "auto":
                r_start = item["reasoning_start"]
                r_end = item["reasoning_end"]
                s_start = item["speech_start"]
                s_end = item["speech_end"]

                think_delim_start = max(prompt_end, r_start - 1)
                think_delim_end = min(seq_len, r_end + 1)
                loss_weights[i, think_delim_start:think_delim_end] = self.lambda_r

                loss_weights[i, s_start - 1:seq_len] = self.lambda_s

            if self.semantic_weight != 1.0 and self.level_cycle > 0:
                s_start = item["speech_start"]
                s_end = min(item["speech_end"], seq_len)
                for pos in range(s_start, s_end, self.level_cycle):
                    loss_weights[i, pos] *= self.semantic_weight
            if self.eos_weight != 1.0:
                s_end = item["speech_end"]
                for pos in (s_end, s_end + 1):
                    if pos < seq_len:
                        loss_weights[i, pos] *= self.eos_weight

        speech_mask = self.registry.build_speech_mask(input_ids) & (labels != IGNORE_INDEX)

        attention_mask = (input_ids != self.pad_token_id).long()

        has_residual = any(item.get("residual_codes") is not None for item in batch)
        residual_codes_tensor = None
        if has_residual:
            num_layers = 0
            for item in batch:
                if item.get("residual_codes") is not None:
                    num_layers = len(item["residual_codes"])
                    break
            if num_layers > 0:
                residual_codes_tensor = torch.full((B, num_layers, T_max), -100, dtype=torch.long)
                for i, item in enumerate(batch):
                    rc = item.get("residual_codes")
                    if rc is not None:
                        s_start = item["speech_start"]
                        for layer_idx, layer_codes in enumerate(rc):
                            for t_idx, code in enumerate(layer_codes):
                                pos = s_start + t_idx
                                if pos < T_max:
                                    residual_codes_tensor[i, layer_idx, pos] = code

        speaker_ids = None
        if any(item.get("speaker_id") is not None for item in batch):
            speaker_ids = torch.tensor(
                [item.get("speaker_id", 0) for item in batch], dtype=torch.long
            )

        return {
            "input_ids": input_ids,
            "labels": labels,
            "attention_mask": attention_mask,
            "speech_mask": speech_mask,
            "loss_weights": loss_weights,
            "residual_codes": residual_codes_tensor,
            "speaker_ids": speaker_ids,
        }