erik-at-boson commited on
Commit
2fa4648
·
verified ·
1 Parent(s): c658265

fix: dual-mode imports + complete ChatMLDatasetSample + transcribe output parsing

Browse files
Files changed (1) hide show
  1. higgs_audio_collator.py +722 -0
higgs_audio_collator.py ADDED
@@ -0,0 +1,722 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import librosa
2
+ import torch
3
+ import torch.nn.functional as F
4
+ import math
5
+ import numpy as np
6
+ from typing import List, Tuple, Dict, Optional
7
+
8
+ from dataclasses import dataclass
9
+ from transformers.models.whisper.processing_whisper import WhisperProcessor
10
+ from transformers import __version__ as _tf_version
11
+ from packaging.version import parse as _V
12
+ def get_tf_version(): return _V(_tf_version)
13
+ from packaging.version import parse as V
14
+ try:
15
+ from .modeling_higgs_audio_xcodec import XCodecProcessor
16
+ from .utils import build_delay_pattern_mask
17
+ except ImportError:
18
+ from modeling_higgs_audio_xcodec import XCodecProcessor
19
+ from utils import build_delay_pattern_mask
20
+
21
+ # Minimal dataclass — full chatml_dataset not needed for inference
22
+ from dataclasses import dataclass, field
23
+ from typing import Optional
24
+ import torch
25
+
26
+ @dataclass
27
+ class ChatMLDatasetSample:
28
+ input_ids: torch.Tensor
29
+ label_ids: torch.Tensor
30
+ audio_ids_concat: Optional[torch.Tensor] = None
31
+ audio_ids_start: Optional[torch.Tensor] = None
32
+ audio_waveforms_concat: Optional[torch.Tensor] = None
33
+ audio_waveforms_start: Optional[torch.Tensor] = None
34
+ audio_sample_rate: Optional[torch.Tensor] = None
35
+ audio_speaker_indices: Optional[torch.Tensor] = None
36
+ audio_label_ids_concat: Optional[torch.Tensor] = None
37
+ audio_cuts: Optional[list] = None
38
+ # Training-only fields (None for inference)
39
+ reward: Optional[float] = None
40
+ group_id: Optional[int] = None
41
+ group_score_sample: Optional[torch.Tensor] = None
42
+ max_score_sample: Optional[torch.Tensor] = None
43
+ min_score_sample: Optional[torch.Tensor] = None
44
+
45
+ def get_wv(self, idx):
46
+ """Extract the idx-th waveform from the concatenated buffer."""
47
+ wv_start = self.audio_waveforms_start[idx]
48
+ sr = self.audio_sample_rate[idx]
49
+ if idx < len(self.audio_waveforms_start) - 1:
50
+ wv_end = self.audio_waveforms_start[idx + 1]
51
+ else:
52
+ wv_end = self.audio_waveforms_concat.shape[-1]
53
+ return self.audio_waveforms_concat[wv_start:wv_end], sr
54
+
55
+ def get_audio_cuts(self, idx):
56
+ """Get VAD cuts for the idx-th audio segment."""
57
+ if self.audio_cuts is None:
58
+ return None
59
+ return self.audio_cuts[idx]
60
+
61
+ def num_audios(self):
62
+ """Number of audio segments in this sample."""
63
+ if self.audio_waveforms_start is None:
64
+ return 0
65
+ return len(self.audio_waveforms_start)
66
+
67
+ def get_audio_codes(self, idx):
68
+ """Get audio codes for the idx-th segment (not used for inference)."""
69
+ return None
70
+
71
+ def get_audio_codes_labels(self, idx):
72
+ """Get audio code labels for the idx-th segment (not used for inference)."""
73
+ return None
74
+
75
+ RankedChatMLDatasetSampleTuple = None # not used for inference
76
+
77
+
78
+ def _ceil_to_nearest(n, round_to):
79
+ if round_to <= 1: return n # avoid division by zero
80
+ return (n + round_to - 1) // round_to * round_to
81
+
82
+ def _ceil_to_next_power_of_two(x):
83
+ return 1 if x == 0 else 2 ** (x - 1).bit_length()
84
+
85
+ def fix_and_round_time_dim(
86
+ audio_features: torch.Tensor, # (B, C, T_feat)
87
+ attention_mask: torch.Tensor | None, # (B, T_mask) or None
88
+ round_to: int = 4,
89
+ pad_value: float = 0.0,
90
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
91
+ """
92
+ Make feature & mask time dims match and pad to a multiple of `round_to`.
93
+ Also resolves Whisper's 'longest' oddity where T_mask = T_feat +/- 1.
94
+
95
+ Returns: (features_fixed, mask_fixed_or_none)
96
+ """
97
+ assert audio_features.dim() == 3, "audio_features must be (B, C, T)"
98
+ B, C, T_feat = audio_features.shape
99
+ dev = audio_features.device
100
+ dtype_feat = audio_features.dtype
101
+ if attention_mask is not None:
102
+ assert attention_mask.dim() == 2, "attention_mask must be (B, T)"
103
+ assert attention_mask.size(0) == B, "batch size mismatch"
104
+ T_mask = attention_mask.size(1)
105
+ else:
106
+ T_mask = T_feat
107
+ # 1) First, equalize T between features and mask (crop/pad conservatively).
108
+ T = max(T_feat, T_mask)
109
+ if attention_mask is None:
110
+ with torch.no_grad():
111
+ col_nonpad = (audio_features != pad_value).any(dim=1) # (B, T_feat)
112
+ attention_mask = col_nonpad.to(dtype=torch.int32)
113
+ # Now reconcile off-by-one cases first (common Whisper bug).
114
+ if T_mask == T_feat + 1:
115
+ if (attention_mask[:, -1] == 0).all():
116
+ attention_mask = attention_mask[:, :-1]
117
+ T_mask -= 1
118
+ else:
119
+ audio_features = F.pad(audio_features, (0, 1), value=pad_value)
120
+ T_feat += 1
121
+ elif T_feat == T_mask + 1:
122
+ if (audio_features[:, :, -1] == pad_value).all():
123
+ audio_features = audio_features[:, :, :-1]
124
+ T_feat -= 1
125
+ else:
126
+ attention_mask = F.pad(attention_mask, (0, 1), value=0)
127
+ T_mask += 1
128
+ T = max(T_feat, T_mask)
129
+ if T_feat < T:
130
+ audio_features = F.pad(audio_features, (0, T - T_feat), value=pad_value)
131
+ T_feat = T
132
+ if T_mask < T:
133
+ attention_mask = F.pad(attention_mask, (0, T - T_mask), value=0)
134
+ T_mask = T
135
+ # 2) Round up to multiple of `round_to`
136
+ if round_to is not None and round_to > 1:
137
+ T_rounded = _ceil_to_nearest(T, round_to)
138
+ if T_rounded != T:
139
+ audio_features = F.pad(audio_features, (0, T_rounded - T), value=pad_value)
140
+ attention_mask = F.pad(attention_mask, (0, T_rounded - T), value=0)
141
+ T = T_rounded
142
+ audio_features = audio_features.to(dtype=dtype_feat, device=dev)
143
+ attention_mask = attention_mask.to(dtype=torch.int32, device=dev)
144
+ assert audio_features.size(-1) == attention_mask.size(-1), "time dims should now match"
145
+ return audio_features, attention_mask
146
+
147
+ from accelerate.logging import get_logger
148
+ logger = get_logger(__name__)
149
+
150
+ @dataclass
151
+ class HiggsAudioBatchInput:
152
+ input_ids: torch.LongTensor # shape (bsz, seq_len).
153
+ attention_mask: torch.Tensor # shape (bsz, seq_len).
154
+ audio_features: Optional[torch.Tensor] # shape (num_audio_in, feature_dim, max_mel_seq_len).
155
+ audio_feature_attention_mask: Optional[torch.Tensor] # shape (num_audio_in, max_mel_seq_len).
156
+ audio_out_ids: Optional[torch.LongTensor] # shape (num_codebooks, audio_out_total_length)
157
+ audio_out_ids_start: Optional[torch.LongTensor] # shape (num_audio_out,)
158
+ # The audio_out_ids_start_group_loc has the same length as audio_out_ids_start. It is used to recover group location in a batch for an audio segment
159
+ # Currently, we concatenante audio segments along dim 0 to handle variadic audio segment length. However, in the alignment stage, we need the location information
160
+ # For example,
161
+ # audio_out_ids_start = [0, 2, 4, 8]; and the first two audio segments come from the same sample in a batch, and other two come from different samples.
162
+ # This is a batch of 3 samples, then we will have the group location as:
163
+ # audio_out_ids_start_group_loc = [0, 0, 1, 2]
164
+ audio_out_ids_start_group_loc: Optional[torch.LongTensor] # shape (num_audio_out,), specify which a sample's group location in the batch
165
+ audio_in_ids: Optional[torch.LongTensor] # shape (num_codebooks, audio_in_total_length)
166
+ audio_in_ids_start: Optional[torch.LongTensor] # shape (num_audio_in,)
167
+ label_ids: Optional[torch.LongTensor] # shape (bsz, seq_len)
168
+ label_audio_ids: Optional[torch.LongTensor] # shape (num_codebooks, audio_out_total_length)
169
+ reward: Optional[float] = None
170
+ group_id: Optional[torch.LongTensor] = None
171
+ audio_wv_lengths: Optional[torch.LongTensor] = None # shape (num_audio_in,) - true 24k waveform lengths per audio chunk (XCodec path)
172
+
173
+ class HiggsAudioSampleCollator:
174
+ """Sample collator for Higgs-Audio model.
175
+
176
+ Args:
177
+ whisper_processor (WhisperProcessor): The whisper processor.
178
+ audio_in_token_id (int): The token id for audio-in.
179
+ audio_out_token_id (int): The token id for audio-out.
180
+ pad_token_id (int): The token id for padding.
181
+ audio_stream_bos_id (int): The token id for audio-stream beginning of sentence.
182
+ audio_stream_eos_id (int): The token id for audio-stream end of sentence.
183
+ round_to (int): The round-to value.
184
+ pad_left (bool): Whether to pad left.
185
+ return_audio_in_tokens (bool): Whether to return audio-in tokens.
186
+ use_delay_pattern (bool): Whether to use delay pattern.
187
+ disable_audio_codes_transform (bool): Whether to add bos and eos tokens to audio codes.
188
+ chunk_size_seconds (int): The chunk size in seconds.
189
+ add_new_bos_eos_for_long_chunk (bool): Whether to add new bos and eos tokens for long chunks.
190
+ mask_audio_out_token_label (bool): Whether to always mask the label associated with <|AUDIO_OUT|> token. Since we will always have `<|AUDIO_OUT|>` after `<|audio_bos|>`, we can safely mask <|AUDIO_OUT|>.
191
+
192
+ """
193
+ def __init__(
194
+ self,
195
+ whisper_processor: WhisperProcessor | XCodecProcessor, # FIXME respect previous signature but allow XCodecProcessor
196
+ audio_in_token_id,
197
+ audio_out_token_id,
198
+ pad_token_id,
199
+ audio_stream_bos_id,
200
+ audio_stream_eos_id,
201
+ round_to=8,
202
+ pad_left=False,
203
+ encode_whisper_embed=True, # FIXME respect previous signature, use encoder_backend to determine if we use whisper or xcodec
204
+ return_audio_in_tokens=True,
205
+ audio_num_codebooks=None,
206
+ use_delay_pattern=False,
207
+ disable_audio_codes_transform=False,
208
+ chunk_size_seconds=30, # Maximum duration for each chunk
209
+ add_new_bos_eos_for_long_chunk=True,
210
+ mask_audio_out_token_label=True,
211
+ encoder_padding_method='max_length',
212
+ duplicate_audio_triplet: bool = False,
213
+ vad_cut: bool = False,
214
+ split_vads: bool = False,
215
+ ):
216
+ self.encoder_processor = whisper_processor
217
+ if isinstance(self.encoder_processor, WhisperProcessor):
218
+ self.encoder_backend = "whisper"
219
+ elif isinstance(self.encoder_processor, XCodecProcessor):
220
+ self.encoder_backend = "xcodec"
221
+ else:
222
+ raise ValueError(f"Invalid encoder processor: {self.encoder_processor}")
223
+ self.round_to = round_to
224
+ self.pad_left = pad_left
225
+ self.audio_in_token_id = audio_in_token_id
226
+ self.audio_out_token_id = audio_out_token_id
227
+ self.audio_stream_bos_id = audio_stream_bos_id
228
+ self.audio_stream_eos_id = audio_stream_eos_id
229
+ self.pad_token_id = pad_token_id
230
+ self.encode_whisper_embed = encode_whisper_embed
231
+ self.return_audio_in_tokens = return_audio_in_tokens
232
+ self.audio_num_codebooks = audio_num_codebooks
233
+ self.use_delay_pattern = use_delay_pattern
234
+ self.encoder_padding_method = encoder_padding_method
235
+ self.duplicate_audio_triplet = duplicate_audio_triplet
236
+ if encode_whisper_embed:
237
+ self.chunk_size_seconds = chunk_size_seconds
238
+ self.chunk_size_samples = int(chunk_size_seconds * self.encoder_processor.feature_extractor.sampling_rate)
239
+ else:
240
+ self.chunk_size_seconds = None
241
+ self.chunk_size_samples = None
242
+ self.disable_audio_codes_transform = disable_audio_codes_transform
243
+ self.add_new_bos_eos_for_long_chunk = add_new_bos_eos_for_long_chunk
244
+ self.mask_audio_out_token_label = mask_audio_out_token_label
245
+ self.vad_cut = vad_cut
246
+ self.split_vads = split_vads
247
+
248
+ def _duplicate_audio_tokens_helper(self, input_ids, audio_idx, labels, num_chunks) -> Tuple[torch.Tensor, torch.Tensor]:
249
+ """Duplicate audio tokens helper."""
250
+ if self.duplicate_audio_triplet:
251
+ # ----- OLD behavior: repeat the triplet -----
252
+ triplet = input_ids[audio_idx-1:audio_idx+2] # <|audio_bos|><|AUDIO|><|audio_eos|>
253
+ duplicated = triplet.repeat(num_chunks) # length = 3 * num_chunks
254
+ new_input_ids = torch.cat([input_ids[:audio_idx - 1], duplicated, input_ids[audio_idx + 2:]], dim=0)
255
+ new_labels = None
256
+ if labels is not None:
257
+ triplet_lbl = labels[audio_idx-1:audio_idx+2]
258
+ duplicated_labels = triplet_lbl.repeat(num_chunks)
259
+ new_labels = torch.cat([labels[:audio_idx - 1], duplicated_labels, labels[audio_idx + 2:]], dim=0)
260
+ else:
261
+ # ----- NEW behavior: duplicate only <|AUDIO|> -----
262
+ audio_tok = input_ids[audio_idx: audio_idx + 1] # <|AUDIO|>
263
+ duplicated = audio_tok.repeat(num_chunks) # length = num_chunks
264
+ new_input_ids = torch.cat([input_ids[:audio_idx], duplicated, input_ids[audio_idx + 1:]], dim=0)
265
+ new_labels = None
266
+ if labels is not None:
267
+ audio_lbl = labels[audio_idx: audio_idx + 1]
268
+ duplicated_labels = audio_lbl.repeat(num_chunks)
269
+ new_labels = torch.cat([labels[:audio_idx], duplicated_labels, labels[audio_idx + 1:]], dim=0)
270
+ return new_input_ids, new_labels
271
+
272
+ def _chunk_post_process_helper(
273
+ self,
274
+ ranges: List[Tuple[int, int]],
275
+ ) -> List[Tuple[int, int]]:
276
+ """
277
+ Given (start, end) ranges on the original waveform, split each so that every output range has length <= self.chunk_size_samples.
278
+ """
279
+ out: List[Tuple[int, int]] = []
280
+ max_len = int(self.chunk_size_samples)
281
+ for s, e in ranges:
282
+ L = e - s
283
+ if L == 0: continue # skip any degenerate chunk
284
+ pos = s
285
+ while pos < e:
286
+ nxt = min(e, pos + max_len)
287
+ out.append((pos, nxt))
288
+ pos = nxt
289
+ return out
290
+
291
+ def _process_and_duplicate_audio_tokens(self, input_ids: torch.Tensor, audio_idx: int, wv: torch.Tensor, sr: int, labels: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, torch.Tensor, int]:
292
+ """Process long audio and duplicate corresponding audio tokens.
293
+
294
+ Args:
295
+ input_ids: Input token ids
296
+ audio_idx: Index of the audio token in the sequence
297
+ wv: Audio waveform
298
+ sr: Sample rate
299
+ labels: Optional label ids to be duplicated alongside input ids
300
+
301
+ Returns:
302
+ Tuple of:
303
+ - New input ids with duplicated audio tokens
304
+ - New label ids (if labels were provided) or None
305
+ - Number of chunks created
306
+ """
307
+ # Calculate number of chunks needed
308
+ total_samples = len(wv)
309
+ num_chunks = math.ceil(total_samples / self.chunk_size_samples)
310
+ if num_chunks <= 1:
311
+ return input_ids, labels, 1
312
+ new_input_ids, new_labels = self._duplicate_audio_tokens_helper(input_ids, audio_idx, labels, num_chunks)
313
+ return new_input_ids, new_labels, num_chunks
314
+
315
+ def _vad_process_audio_chunks(self, input_ids: torch.Tensor, audio_idx: int, wv: torch.Tensor, sr: int, labels: torch.Tensor, audio_cuts: List[Tuple[int, int]]) -> Tuple[torch.Tensor, torch.Tensor, int, List[torch.Tensor]]:
316
+ """Process long audio and duplicate corresponding audio tokens.
317
+
318
+ Similar to _process_and_duplicate_audio_tokens, but use VAD to cut the audio into chunks and respects the VAD boundaries.
319
+ It also directly return the audio chunks while duplicating the audio tokens.
320
+
321
+ If self.duplicate_audio_triplet == True (default / OLD):
322
+ - repeat the full triplet <|audio_bos|><|AUDIO|><|audio_eos|> for each chunk
323
+ Else (NEW):
324
+ - keep one pair of <|audio_bos|> ... <|audio_eos|> and duplicate only <|AUDIO|> in between
325
+ """
326
+ total_samples = len(wv)
327
+ if self.split_vads:
328
+ # if vad is [0,1] [2,4] [5,8] then return [0,1] [2,4] [5,8] aka respect vad completely and drop audios in between
329
+ wv_chunks = audio_cuts
330
+ else:
331
+ # if vad is [0,1] [2,4] [5,8] then return [0,1] [1,4] [4,8] aka respect the start only and use audios in between
332
+ wv_chunks = []
333
+ prev_e = 0
334
+ for idx, (start_t, end_t) in enumerate(audio_cuts):
335
+ s, e = min(prev_e, start_t), end_t
336
+ if idx == len(audio_cuts) - 1: # last chunk
337
+ e = total_samples
338
+ wv_chunks.append((s, e))
339
+ prev_e = e
340
+ wv_chunks = self._chunk_post_process_helper(wv_chunks)
341
+ new_input_ids, new_labels = self._duplicate_audio_tokens_helper(input_ids, audio_idx, labels, len(wv_chunks))
342
+ return new_input_ids, new_labels, len(wv_chunks), wv_chunks
343
+
344
+ def __call__(self, batch: List[ChatMLDatasetSample]):
345
+ """Collate the input data with support for long audio processing."""
346
+
347
+ label_ids = None
348
+ label_audio_ids = None
349
+ if all([ele.label_ids is None for ele in batch]):
350
+ return_labels = False
351
+ else:
352
+ return_labels = True
353
+
354
+ if self.encode_whisper_embed:
355
+ # Process each sample in the batch to handle long audio
356
+ # TODO(?) The implementation here can be optimized.
357
+ processed_batch = []
358
+ audio_in_wv_l_in_batch = []
359
+ for i in range(len(batch)):
360
+ sample = batch[i]
361
+ audio_in_mask = sample.input_ids == self.audio_in_token_id
362
+ audio_in_indices = torch.where(audio_in_mask)[0]
363
+ audio_out_mask = sample.input_ids == self.audio_out_token_id
364
+
365
+ # Process each audio token and duplicate if needed
366
+ modified_input_ids = sample.input_ids
367
+ modified_labels = sample.label_ids if return_labels else None
368
+ modified_waveforms_concat = []
369
+ modified_waveforms_start = []
370
+ modified_sample_rate = []
371
+ offset = 0 # Track position changes from duplicating tokens
372
+ curr_wv_offset = 0
373
+
374
+ # Process input audio tokens
375
+ for idx, audio_idx in enumerate(audio_in_indices):
376
+ # Get the audio for this token
377
+ wv, sr = sample.get_wv(idx) # Use idx since we want the original audio index
378
+ # Get the audio vad cuts for this token - fallback to full audio if not available (trigger default behavior)
379
+ audio_cuts = sample.get_audio_cuts(idx) if self.vad_cut else None
380
+ if audio_cuts is None or len(audio_cuts) == 0:
381
+ audio_cuts = [(0, len(wv))]
382
+ # No resample should happen in collator, it should be done in dataloader
383
+ assert sr == self.encoder_processor.feature_extractor.sampling_rate, f"Sample rate mismatch: {sr} != {self.encoder_processor.feature_extractor.sampling_rate}"
384
+ wv = wv.contiguous()
385
+ # Process and duplicate tokens if necessary
386
+ token_pos = audio_idx + offset
387
+ if audio_cuts is not None:
388
+ modified_input_ids, modified_labels, num_chunks, chunks_ranges = self._vad_process_audio_chunks(
389
+ modified_input_ids,
390
+ token_pos,
391
+ wv,
392
+ sr,
393
+ modified_labels,
394
+ audio_cuts,
395
+ )
396
+ # Update audio data
397
+ for chunk_idx in range(num_chunks):
398
+ chunk_start, chunk_end = chunks_ranges[chunk_idx]
399
+ chunk_wv = wv[chunk_start:chunk_end]
400
+ audio_in_wv_l_in_batch.append(chunk_wv)
401
+ modified_waveforms_concat.append(chunk_wv)
402
+ modified_waveforms_start.append(curr_wv_offset)
403
+ curr_wv_offset += len(chunk_wv)
404
+ modified_sample_rate.append(sr)
405
+ else:
406
+ raise ValueError(f"Never expected to reach here")
407
+ # Update offset for next iteration - each extra chunk inserts this many new tokens at the splice point
408
+ per_chunk_insert = 3 if self.duplicate_audio_triplet else 1
409
+ offset += (num_chunks - 1) * per_chunk_insert
410
+
411
+ # Create new sample with modified tokens and audio data
412
+ processed_sample = ChatMLDatasetSample(
413
+ input_ids=modified_input_ids,
414
+ label_ids=modified_labels if return_labels else sample.label_ids,
415
+ audio_ids_concat=sample.audio_ids_concat,
416
+ audio_ids_start=sample.audio_ids_start,
417
+ audio_waveforms_concat=torch.cat(modified_waveforms_concat) if modified_waveforms_concat else sample.audio_waveforms_concat,
418
+ audio_waveforms_start=torch.tensor(modified_waveforms_start, dtype=torch.long) if modified_waveforms_start else sample.audio_waveforms_start,
419
+ audio_sample_rate=torch.tensor(modified_sample_rate) if modified_sample_rate else sample.audio_sample_rate,
420
+ audio_speaker_indices=torch.tensor([]),
421
+ # FIXME(sxjscience): The logic here is not correct for audio_label_ids_concat.
422
+ audio_label_ids_concat=sample.audio_label_ids_concat,
423
+ )
424
+ # audio_in_chunk_len = len(torch.where(modified_input_ids == self.audio_in_token_id)[0])
425
+ # assert audio_in_chunk_len == processed_sample.num_audios(), f"Mismatch: audio_in_chunk_len={audio_in_chunk_len}, processed_sample.num_audios()={processed_sample.num_audios()}"
426
+ processed_batch.append(processed_sample)
427
+ else:
428
+ processed_batch = batch
429
+
430
+ # Get the max sequence length based on processed batch
431
+ max_seq_length = _ceil_to_nearest(max([len(sample.input_ids) for sample in processed_batch]), self.round_to)
432
+
433
+ # Get the ids for audio-in and audio-out for each batch
434
+ audio_in_wv_l = []
435
+ audio_in_ids_l = []
436
+ audio_out_ids_l = []
437
+ audio_out_ids_group_loc_l = []
438
+ audio_in_label_ids_l = None
439
+ audio_out_label_ids_l = None
440
+ reward_l = []
441
+ group_id_l = []
442
+
443
+ if return_labels:
444
+ audio_out_no_train_flag = [] # Whether the audio-out data should be trained on or not.
445
+
446
+ # Process the audio inputs and outputs
447
+ for i in range(len(processed_batch)):
448
+ audio_in_mask = processed_batch[i].input_ids == self.audio_in_token_id
449
+ audio_out_mask = processed_batch[i].input_ids == self.audio_out_token_id
450
+ audio_ids = torch.ones_like(processed_batch[i].input_ids)
451
+ audio_ids[audio_in_mask ^ audio_out_mask] = torch.cumsum(audio_ids[audio_in_mask ^ audio_out_mask], 0) - 1
452
+ audio_in_ids = audio_ids[audio_in_mask]
453
+ audio_out_ids = audio_ids[audio_out_mask]
454
+
455
+ if return_labels:
456
+ audio_out_no_train_flag.append(processed_batch[i].label_ids[audio_out_mask] < 0)
457
+ if self.mask_audio_out_token_label:
458
+ processed_batch[i].label_ids[audio_out_mask] = -100
459
+
460
+ # Process audio inputs
461
+ if self.return_audio_in_tokens:
462
+ audio_in_ids_l.extend([processed_batch[i].get_audio_codes(idx)[:self.audio_num_codebooks, :] for idx in audio_in_ids])
463
+ if processed_batch[i].audio_label_ids_concat is not None:
464
+ if audio_in_label_ids_l is None:
465
+ audio_in_label_ids_l = []
466
+ audio_in_label_ids_l.extend([processed_batch[i].get_audio_codes_labels(idx)[:self.audio_num_codebooks, :] for idx in audio_in_ids])
467
+
468
+ audio_out_ids_l.extend([processed_batch[i].get_audio_codes(idx)[:self.audio_num_codebooks, :] for idx in audio_out_ids])
469
+ audio_out_ids_group_loc_l.append(i)
470
+ if processed_batch[i].reward is not None:
471
+ reward_l.append(processed_batch[i].reward)
472
+ if processed_batch[i].group_id is not None:
473
+ group_id_l.append(processed_batch[i].group_id)
474
+ if processed_batch[i].audio_label_ids_concat is not None:
475
+ if audio_out_label_ids_l is None:
476
+ audio_out_label_ids_l = []
477
+ audio_out_label_ids_l.extend([processed_batch[i].get_audio_codes_labels(idx)[:self.audio_num_codebooks, :] for idx in audio_out_ids])
478
+
479
+ if self.encode_whisper_embed:
480
+ for chunk in audio_in_wv_l_in_batch: # this is for the whole batch
481
+ audio_in_wv_l.append(chunk.cpu().numpy())
482
+
483
+ if return_labels:
484
+ audio_out_no_train_flag = torch.cat(audio_out_no_train_flag, dim=0)
485
+
486
+ # Process all audio features
487
+ audio_wv_lengths = None
488
+ audio_feature_attention_mask = None
489
+ if len(audio_in_wv_l) > 0:
490
+ if self.encoder_backend == "whisper":
491
+ feature_ret = self.encoder_processor.feature_extractor(
492
+ audio_in_wv_l,
493
+ sampling_rate=self.encoder_processor.feature_extractor.sampling_rate,
494
+ return_attention_mask=True,
495
+ padding=self.encoder_padding_method,
496
+ )
497
+ audio_features = torch.from_numpy(feature_ret["input_features"])
498
+ audio_feature_attention_mask = torch.from_numpy(feature_ret["attention_mask"])
499
+ if self.encoder_padding_method == "longest": # for max_length, we don't need to fix the time dim
500
+ audio_features, audio_feature_attention_mask = fix_and_round_time_dim(
501
+ audio_features,
502
+ audio_feature_attention_mask,
503
+ round_to=16, # FIXME - hard code to 16 to avoid SP issue later
504
+ pad_value=0.0,
505
+ )
506
+ elif self.encoder_backend == "xcodec":
507
+ feature_ret = self.encoder_processor.feature_extractor(
508
+ audio_in_wv_l,
509
+ sampling_rate=self.encoder_processor.feature_extractor.sampling_rate,
510
+ return_attention_mask=False,
511
+ padding=self.encoder_padding_method,
512
+ )
513
+ audio_features = torch.from_numpy(feature_ret["input_features"]).to(dtype=torch.bfloat16)
514
+ audio_wv_lengths = torch.from_numpy(feature_ret["audio_wv_lengths"])
515
+ else:
516
+ raise ValueError(f"Invalid encoder backend: {self.encoder_backend}")
517
+ else:
518
+ if self.encode_whisper_embed:
519
+ if self.encoder_backend == "whisper":
520
+ audio_features = torch.zeros(
521
+ (0, self.encoder_processor.feature_extractor.feature_size,
522
+ self.encoder_processor.feature_extractor.nb_max_frames),
523
+ dtype=torch.float32
524
+ )
525
+ audio_feature_attention_mask = torch.zeros(
526
+ (0, self.encoder_processor.feature_extractor.nb_max_frames),
527
+ dtype=torch.int32
528
+ )
529
+ elif self.encoder_backend == "xcodec": # xcodec processor can handle empty input
530
+ feature_ret = self.encoder_processor.feature_extractor(
531
+ audio_in_wv_l,
532
+ sampling_rate=self.encoder_processor.feature_extractor.sampling_rate,
533
+ return_attention_mask=False,
534
+ padding=self.encoder_padding_method,
535
+ )
536
+ audio_features = torch.from_numpy(feature_ret["input_features"])
537
+ audio_wv_lengths = torch.from_numpy(feature_ret["audio_wv_lengths"])
538
+ else:
539
+ raise ValueError(f"Invalid encoder backend: {self.encoder_backend}")
540
+ else:
541
+ audio_features = None
542
+ audio_feature_attention_mask = None
543
+
544
+ # Process audio input tokens
545
+ if len(audio_in_ids_l) > 0:
546
+ # Append audio-stream-bos and eos tokens
547
+ new_audio_in_ids_l = []
548
+ for ele in audio_in_ids_l:
549
+ if self.disable_audio_codes_transform:
550
+ # Do not add audio-stream-bos or eos tokens.
551
+ # This may indicate that the sample comes from ConstantLengthDatasetWithBuffer.
552
+ audio_codes = ele
553
+ else:
554
+ audio_codes = torch.cat(
555
+ [
556
+ torch.full((ele.shape[0], 1), self.audio_stream_bos_id, dtype=torch.long),
557
+ ele,
558
+ torch.full((ele.shape[0], 1), self.audio_stream_eos_id, dtype=torch.long)
559
+ ], dim=1
560
+ )
561
+ if self.use_delay_pattern:
562
+ audio_codes = build_delay_pattern_mask(
563
+ audio_codes.unsqueeze(0),
564
+ bos_token_id=self.audio_stream_bos_id,
565
+ pad_token_id=self.audio_stream_eos_id
566
+ )[0].squeeze(0)
567
+ new_audio_in_ids_l.append(audio_codes)
568
+ audio_in_ids = torch.cat(new_audio_in_ids_l, dim=1).long()
569
+ audio_in_ids_start = torch.cumsum(torch.tensor([0] + [audio_codes.shape[1] for audio_codes in new_audio_in_ids_l[:-1]]), dim=0)
570
+ else:
571
+ audio_in_ids = torch.zeros((0, 0), dtype=torch.long)
572
+ audio_in_ids_start = torch.zeros(0, dtype=torch.long)
573
+
574
+ # Process audio output tokens
575
+ audio_out_ids_start_group_loc = None
576
+ if len(audio_out_ids_l) > 0:
577
+ new_audio_out_ids_l = []
578
+ label_audio_ids_l = []
579
+ for idx, ele in enumerate(audio_out_ids_l):
580
+ if self.disable_audio_codes_transform:
581
+ # Do not add audio-stream-bos or eos tokens.
582
+ # This may indicate that the sample comes from ConstantLengthDatasetWithBuffer.
583
+ audio_codes = ele
584
+ if return_labels:
585
+ label_audio_ids = audio_out_label_ids_l[idx]
586
+ else:
587
+ audio_codes = torch.cat(
588
+ [
589
+ torch.full((ele.shape[0], 1), self.audio_stream_bos_id, dtype=torch.long),
590
+ ele,
591
+ torch.full((ele.shape[0], 1), self.audio_stream_eos_id, dtype=torch.long)
592
+ ], dim=1
593
+ )
594
+ if return_labels:
595
+ label_audio_ids = torch.cat(
596
+ [
597
+ torch.full((ele.shape[0], 1), -100, dtype=torch.long),
598
+ ele,
599
+ torch.full((ele.shape[0], 1), self.audio_stream_eos_id, dtype=torch.long)
600
+ ], dim=1
601
+ )
602
+ if self.use_delay_pattern:
603
+ audio_codes = build_delay_pattern_mask(
604
+ audio_codes.unsqueeze(0),
605
+ bos_token_id=self.audio_stream_bos_id,
606
+ pad_token_id=self.audio_stream_eos_id
607
+ )[0].squeeze(0)
608
+ if return_labels:
609
+ label_audio_ids = build_delay_pattern_mask(
610
+ label_audio_ids.unsqueeze(0),
611
+ bos_token_id=-100,
612
+ pad_token_id=-100,
613
+ )[0].squeeze(0)
614
+ new_audio_out_ids_l.append(audio_codes)
615
+
616
+ if return_labels:
617
+ if audio_out_no_train_flag[idx]:
618
+ label_audio_ids[:] = -100
619
+ label_audio_ids_l.append(label_audio_ids)
620
+
621
+ audio_out_ids = torch.cat(new_audio_out_ids_l, dim=1).long()
622
+ if return_labels:
623
+ label_audio_ids = torch.cat(label_audio_ids_l, dim=1).long()
624
+ audio_out_ids_start = torch.cumsum(
625
+ torch.tensor([0] + [audio_codes.shape[1] for audio_codes in new_audio_out_ids_l[:-1]]),
626
+ dim=0
627
+ )
628
+ audio_out_ids_start_group_loc = torch.tensor(audio_out_ids_group_loc_l, dtype=torch.long)
629
+ else:
630
+ audio_out_ids = torch.zeros((0, 0), dtype=torch.long)
631
+ audio_out_ids_start = torch.zeros(0, dtype=torch.long)
632
+ if return_labels:
633
+ label_audio_ids = torch.zeros((0, 0), dtype=torch.long)
634
+
635
+ reward = torch.tensor(reward_l, dtype=torch.float32)
636
+ group_id = torch.tensor(group_id_l, dtype=torch.long)
637
+
638
+ # Handle padding for input ids and attention mask
639
+ if self.pad_left:
640
+ input_ids = torch.stack(
641
+ [F.pad(ele.input_ids, (max_seq_length - len(ele.input_ids), 0), value=self.pad_token_id) for ele in processed_batch]
642
+ )
643
+ if return_labels:
644
+ label_ids = torch.stack(
645
+ [F.pad(ele.label_ids, (max_seq_length - len(ele.label_ids), 0), value=-100) for ele in processed_batch]
646
+ )
647
+ attention_mask = torch.stack(
648
+ [F.pad(torch.ones_like(ele.input_ids), (max_seq_length - len(ele.input_ids), 0), value=0) for ele in processed_batch]
649
+ )
650
+ else:
651
+ input_ids = torch.stack(
652
+ [F.pad(ele.input_ids, (0, max_seq_length - len(ele.input_ids)), value=self.pad_token_id) for ele in processed_batch]
653
+ )
654
+ if return_labels:
655
+ label_ids = torch.stack(
656
+ [F.pad(ele.label_ids, (0, max_seq_length - len(ele.label_ids)), value=-100) for ele in processed_batch]
657
+ )
658
+ attention_mask = torch.stack(
659
+ [F.pad(torch.ones_like(ele.input_ids), (0, max_seq_length - len(ele.input_ids)), value=0) for ele in processed_batch]
660
+ )
661
+
662
+ if not self.return_audio_in_tokens:
663
+ audio_in_ids = None
664
+ audio_in_ids_start = None
665
+
666
+ # Apply audio_num_codebooks limit if specified
667
+ if self.audio_num_codebooks is not None:
668
+ if audio_in_ids is not None:
669
+ audio_in_ids = audio_in_ids[:self.audio_num_codebooks]
670
+ if audio_out_ids is not None:
671
+ audio_out_ids = audio_out_ids[:self.audio_num_codebooks]
672
+ if label_audio_ids is not None:
673
+ label_audio_ids = label_audio_ids[:self.audio_num_codebooks]
674
+
675
+ return HiggsAudioBatchInput(
676
+ input_ids=input_ids,
677
+ attention_mask=attention_mask,
678
+ audio_features=audio_features,
679
+ audio_feature_attention_mask=audio_feature_attention_mask,
680
+ audio_out_ids=audio_out_ids,
681
+ audio_out_ids_start=audio_out_ids_start,
682
+ audio_out_ids_start_group_loc=audio_out_ids_start_group_loc,
683
+ audio_in_ids=audio_in_ids,
684
+ audio_in_ids_start=audio_in_ids_start,
685
+ label_ids=label_ids,
686
+ label_audio_ids=label_audio_ids,
687
+ reward=reward,
688
+ group_id=group_id,
689
+ audio_wv_lengths=audio_wv_lengths,
690
+ )
691
+
692
+
693
+ class HiggsAudioDPOSamplesCollator(HiggsAudioSampleCollator):
694
+ def __init__(self, *args, **kwargs):
695
+ super().__init__(*args, **kwargs)
696
+
697
+ def __call__(self, batch: List[RankedChatMLDatasetSampleTuple]) -> HiggsAudioBatchInput:
698
+ # flatten ranked chatml samples
699
+ chosen = []
700
+ rejected = []
701
+
702
+ for sample in batch:
703
+ chosen.append(sample.max_score_sample())
704
+ rejected.append(sample.min_score_sample())
705
+
706
+ merged = chosen
707
+ merged.extend(rejected)
708
+
709
+ return super().__call__(batch=merged)
710
+
711
+
712
+ class HiggsAudioGRPOSamplesCollator(HiggsAudioSampleCollator):
713
+ def __init__(self, *args, **kwargs):
714
+ super().__init__(*args, **kwargs)
715
+
716
+ def __call__(self, batch: List[RankedChatMLDatasetSampleTuple]) -> HiggsAudioBatchInput:
717
+ merged = []
718
+
719
+ for idx, sample in enumerate(batch):
720
+ merged.extend(sample.group_score_sample(idx))
721
+
722
+ return super().__call__(batch=merged)