pinecoresystems commited on
Commit
892a034
·
verified ·
1 Parent(s): 31cd268

Mirror processing_moss_tts.py

Browse files
Files changed (1) hide show
  1. processing_moss_tts.py +914 -0
processing_moss_tts.py ADDED
@@ -0,0 +1,914 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ """Processor for the MOSS-TTS-Local-Transformer-v1.5 HuggingFace release."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast
11
+
12
+ import torch
13
+ import torchaudio
14
+ from transformers import (
15
+ AutoConfig,
16
+ AutoModel,
17
+ AutoTokenizer,
18
+ BatchFeature,
19
+ PreTrainedTokenizerBase,
20
+ ProcessorMixin,
21
+ logging,
22
+ processing_utils,
23
+ )
24
+
25
+ from .configuration_moss_tts import MossTTSLocalConfig
26
+
27
+
28
+ if hasattr(processing_utils, "MODALITY_TO_BASE_CLASS_MAPPING"):
29
+ processing_utils.MODALITY_TO_BASE_CLASS_MAPPING["audio_tokenizer"] = "PreTrainedModel"
30
+ else:
31
+ processing_utils.AUTO_TO_BASE_CLASS_MAPPING["AutoModel"] = "PreTrainedModel"
32
+ logger = logging.get_logger(__name__)
33
+
34
+ AUDIO_PLACEHOLDER = "<|audio|>"
35
+ USER_ROLE_PREFIX = "user\n"
36
+ USER_TEMPLATE_REFERENCE_PREFIX = (
37
+ "<user_inst>\n"
38
+ "- Reference(s):\n"
39
+ )
40
+ USER_TEMPLATE_AFTER_REFERENCE_SUFFIX = (
41
+ "\n"
42
+ "- Text:\n"
43
+ )
44
+ USER_TEMPLATE_SUFFIX = "\n</user_inst>"
45
+ ASSISTANT_TURN_PREFIX = "\n"
46
+ ASSISTANT_ROLE_PREFIX = "assistant\n"
47
+ USER_MESSAGE_FIELDS = (
48
+ "text",
49
+ "reference",
50
+ "instruction",
51
+ "tokens",
52
+ "quality",
53
+ "sound_event",
54
+ "ambient_sound",
55
+ "language",
56
+ )
57
+
58
+
59
+ def _normalize_template_value(value: Any) -> str:
60
+ if value is None:
61
+ return "None"
62
+ resolved = str(value).strip()
63
+ return resolved or "None"
64
+
65
+
66
+ def _render_user_prompt_after_reference(
67
+ language_code: object | None = None,
68
+ prompt_fields: Optional[Dict[str, Any]] = None,
69
+ ) -> str:
70
+ fields = dict(prompt_fields or {})
71
+ return (
72
+ "\n- Instruction:\n"
73
+ + _normalize_template_value(fields.get("instruction"))
74
+ + "\n- Tokens:\n"
75
+ + _normalize_template_value(fields.get("tokens"))
76
+ + "\n- Quality:\n"
77
+ + _normalize_template_value(fields.get("quality"))
78
+ + "\n- Sound Event:\n"
79
+ + _normalize_template_value(fields.get("sound_event"))
80
+ + "\n- Ambient Sound:\n"
81
+ + _normalize_template_value(fields.get("ambient_sound"))
82
+ + "\n- Language:\n"
83
+ + _normalize_template_value(fields.get("language", language_code))
84
+ + USER_TEMPLATE_AFTER_REFERENCE_SUFFIX
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class Message:
90
+ def to_dict(self) -> Dict[str, Any]:
91
+ raise NotImplementedError
92
+
93
+
94
+ @dataclass
95
+ class UserMessage(Message):
96
+ text: Optional[str] = None
97
+ reference: Optional[List[Optional[Union[str, os.PathLike, torch.Tensor]]]] = None
98
+ instruction: Optional[str] = None
99
+ tokens: Optional[int] = None
100
+ quality: Optional[str] = None
101
+ sound_event: Optional[str] = None
102
+ ambient_sound: Optional[str] = None
103
+ language: Optional[str] = None
104
+
105
+ def __post_init__(self) -> None:
106
+ template = """<user_inst>
107
+ - Reference(s):
108
+ {reference}
109
+ - Instruction:
110
+ {instruction}
111
+ - Tokens:
112
+ {tokens}
113
+ - Quality:
114
+ {quality}
115
+ - Sound Event:
116
+ {sound_event}
117
+ - Ambient Sound:
118
+ {ambient_sound}
119
+ - Language:
120
+ {language}
121
+ - Text:
122
+ {text}
123
+ </user_inst>"""
124
+
125
+ audio_codes_list: list[Union[str, os.PathLike, torch.Tensor]] = []
126
+ if self.reference is None:
127
+ reference = "None"
128
+ else:
129
+ reference_items: list[str] = []
130
+ for speaker_idx, speaker_reference in enumerate(self.reference):
131
+ if speaker_reference is None:
132
+ continue
133
+ # Keep raw audio placeholders directly under "- Reference(s):".
134
+ # Speaker labels such as "[S1]:" change the token sequence and
135
+ # can affect voice-clone conditioning.
136
+ reference_items.append(AUDIO_PLACEHOLDER)
137
+ audio_codes_list.append(speaker_reference)
138
+ reference = "\n".join(reference_items) if reference_items else "None"
139
+
140
+ self._content = (
141
+ template.replace("{reference}", str(reference))
142
+ .replace("{instruction}", str(self.instruction))
143
+ .replace("{tokens}", str(self.tokens))
144
+ .replace("{quality}", str(self.quality))
145
+ .replace("{sound_event}", str(self.sound_event))
146
+ .replace("{ambient_sound}", str(self.ambient_sound))
147
+ .replace("{language}", str(self.language))
148
+ .replace("{text}", str(self.text))
149
+ )
150
+ self._audio_codes_list = audio_codes_list
151
+
152
+ def to_dict(self) -> Dict[str, Any]:
153
+ return {
154
+ "role": "user",
155
+ "content": self._content,
156
+ "audio_codes_list": self._audio_codes_list,
157
+ "text": self.text,
158
+ "instruction": self.instruction,
159
+ "tokens": self.tokens,
160
+ "quality": self.quality,
161
+ "sound_event": self.sound_event,
162
+ "ambient_sound": self.ambient_sound,
163
+ "language": self.language,
164
+ }
165
+
166
+
167
+ @dataclass
168
+ class AssistantMessage(Message):
169
+ audio_codes_list: List[Union[str, os.PathLike, torch.Tensor]]
170
+ content: str = AUDIO_PLACEHOLDER
171
+
172
+ def to_dict(self) -> Dict[str, Any]:
173
+ return {
174
+ "role": "assistant",
175
+ "content": self.content,
176
+ "audio_codes_list": self.audio_codes_list,
177
+ }
178
+
179
+
180
+ class MossTTSLocalProcessor(ProcessorMixin):
181
+ attributes = ["tokenizer"]
182
+ tokenizer_class = "AutoTokenizer"
183
+ audio_tokenizer_class = "AutoModel"
184
+
185
+ tokenizer: PreTrainedTokenizerBase
186
+ audio_tokenizer: Any
187
+
188
+ def __init__(
189
+ self,
190
+ tokenizer: PreTrainedTokenizerBase,
191
+ audio_tokenizer: Any = None,
192
+ model_config: Optional[MossTTSLocalConfig] = None,
193
+ **kwargs,
194
+ ) -> None:
195
+ super().__init__(tokenizer=tokenizer, audio_tokenizer=audio_tokenizer, **kwargs)
196
+ self.tokenizer = tokenizer
197
+ self.audio_tokenizer = audio_tokenizer
198
+ self.model_config = model_config or MossTTSLocalConfig()
199
+
200
+ def _id_to_token(token_id: int) -> str:
201
+ token = tokenizer.convert_ids_to_tokens(int(token_id))
202
+ if isinstance(token, list):
203
+ return token[0] if token else ""
204
+ return cast(str, token)
205
+
206
+ self.audio_user_slot_token = _id_to_token(self.model_config.audio_user_slot_token_id)
207
+ self.audio_assistant_slot_token = _id_to_token(self.model_config.audio_assistant_slot_token_id)
208
+ self.audio_start_token = _id_to_token(self.model_config.audio_start_token_id)
209
+ self.audio_end_token = _id_to_token(self.model_config.audio_end_token_id)
210
+
211
+ @classmethod
212
+ def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
213
+ trust_remote_code = kwargs.pop("trust_remote_code", True)
214
+ kwargs.pop("_from_auto", None)
215
+ codec_path = kwargs.pop("codec_path", None)
216
+ codec_weight_dtype = kwargs.pop("codec_weight_dtype", "bf16")
217
+ codec_compute_dtype = kwargs.pop("codec_compute_dtype", None)
218
+ codec_attention_implementation = kwargs.pop("codec_attention_implementation", None)
219
+
220
+ codec_kwargs: Dict[str, Any] = {}
221
+ if codec_weight_dtype is not None:
222
+ # Default to bf16 codec weights; callers can pass codec_weight_dtype="fp32" to override.
223
+ codec_kwargs["codec_weight_dtype"] = codec_weight_dtype
224
+
225
+ model_ref = Path(str(pretrained_model_name_or_path))
226
+ model_ref_or_name = model_ref if model_ref.exists() else pretrained_model_name_or_path
227
+ model_config = cast(
228
+ MossTTSLocalConfig,
229
+ AutoConfig.from_pretrained(
230
+ model_ref_or_name,
231
+ *args,
232
+ trust_remote_code=trust_remote_code,
233
+ **kwargs,
234
+ ),
235
+ )
236
+
237
+ if codec_path is None:
238
+ try:
239
+ processor_dict, _ = cls.get_processor_dict(
240
+ pretrained_model_name_or_path,
241
+ **dict(kwargs),
242
+ )
243
+ codec_path = processor_dict.get("audio_tokenizer_name_or_path")
244
+ audio_tokenizer_dict = processor_dict.get("audio_tokenizer", {})
245
+ if isinstance(audio_tokenizer_dict, dict):
246
+ codec_path = audio_tokenizer_dict.get("audio_tokenizer_name_or_path") or codec_path
247
+ except Exception:
248
+ codec_path = None
249
+ if codec_path is None:
250
+ codec_path = getattr(model_config, "audio_tokenizer_name_or_path", None)
251
+ if codec_path is None:
252
+ codec_path = "OpenMOSS-Team/MOSS-Audio-Tokenizer-v2"
253
+
254
+ tokenizer = AutoTokenizer.from_pretrained(
255
+ model_ref_or_name,
256
+ *args,
257
+ trust_remote_code=trust_remote_code,
258
+ **kwargs,
259
+ )
260
+ audio_tokenizer = AutoModel.from_pretrained(
261
+ codec_path,
262
+ trust_remote_code=trust_remote_code,
263
+ **kwargs,
264
+ **codec_kwargs,
265
+ )
266
+ if codec_compute_dtype is not None and hasattr(audio_tokenizer, "set_compute_dtype"):
267
+ audio_tokenizer.set_compute_dtype(codec_compute_dtype)
268
+ if codec_attention_implementation is not None and hasattr(
269
+ audio_tokenizer, "set_attention_implementation"
270
+ ):
271
+ audio_tokenizer.set_attention_implementation(codec_attention_implementation)
272
+ return cls(
273
+ tokenizer=tokenizer,
274
+ audio_tokenizer=audio_tokenizer,
275
+ model_config=model_config,
276
+ **kwargs,
277
+ )
278
+
279
+ @staticmethod
280
+ def build_user_message(
281
+ text: Optional[str] = None,
282
+ reference: Optional[List[Optional[Union[str, os.PathLike, torch.Tensor]]]] = None,
283
+ instruction: Optional[str] = None,
284
+ tokens: Optional[int] = None,
285
+ quality: Optional[str] = None,
286
+ sound_event: Optional[str] = None,
287
+ ambient_sound: Optional[str] = None,
288
+ language: Optional[str] = None,
289
+ ) -> Dict[str, Any]:
290
+ if reference is not None and not isinstance(reference, list):
291
+ reference = [reference]
292
+ return UserMessage(
293
+ text=text,
294
+ reference=reference,
295
+ instruction=instruction,
296
+ tokens=tokens,
297
+ quality=quality,
298
+ sound_event=sound_event,
299
+ ambient_sound=ambient_sound,
300
+ language=language,
301
+ ).to_dict()
302
+
303
+ @staticmethod
304
+ def build_assistant_message(
305
+ audio_codes_list: List[Union[str, os.PathLike, torch.Tensor]],
306
+ content: str = AUDIO_PLACEHOLDER,
307
+ ) -> Dict[str, Any]:
308
+ return AssistantMessage(audio_codes_list=audio_codes_list, content=content).to_dict()
309
+
310
+ def _assert_fixed_nq(self, n_vq: Optional[int]) -> int:
311
+ config_nq = int(self.model_config.n_vq)
312
+ if n_vq is not None and int(n_vq) != config_nq:
313
+ raise ValueError(
314
+ "This MOSS-TTS-Local-Transformer-v1.5 release uses the RVQ depth stored in the model config. "
315
+ f"Expected n_vq={config_nq}, got {int(n_vq)}."
316
+ )
317
+ return config_nq
318
+
319
+ def _encode_text(self, text: str) -> list[int]:
320
+ try:
321
+ return list(self.tokenizer.encode(text, add_special_tokens=False))
322
+ except TypeError:
323
+ return list(self.tokenizer.encode(text))
324
+
325
+ def _build_text_rows(self, token_ids: Sequence[int], *, device: Optional[torch.device] = None) -> torch.Tensor:
326
+ rows = torch.full(
327
+ (len(token_ids), int(self.model_config.n_vq) + 1),
328
+ int(self.model_config.audio_pad_token_id),
329
+ dtype=torch.long,
330
+ device=device,
331
+ )
332
+ if token_ids:
333
+ rows[:, 0] = torch.tensor([int(token_id) for token_id in token_ids], dtype=torch.long, device=rows.device)
334
+ return rows
335
+
336
+ def _build_audio_rows(self, audio_tokens: torch.Tensor, slot_token_id: int) -> torch.Tensor:
337
+ rows = torch.full(
338
+ (int(audio_tokens.shape[0]), int(self.model_config.n_vq) + 1),
339
+ int(self.model_config.audio_pad_token_id),
340
+ dtype=torch.long,
341
+ device=audio_tokens.device,
342
+ )
343
+ if rows.shape[0] > 0:
344
+ rows[:, 0] = int(slot_token_id)
345
+ rows[:, 1:] = audio_tokens.to(dtype=torch.long)
346
+ return rows
347
+
348
+ def _user_prompt_prefix_ids(self) -> list[int]:
349
+ return (
350
+ [int(self.model_config.im_start_token_id)]
351
+ + self._encode_text(USER_ROLE_PREFIX)
352
+ + self._encode_text(USER_TEMPLATE_REFERENCE_PREFIX)
353
+ )
354
+
355
+ def _user_prompt_after_reference_ids(
356
+ self,
357
+ language_code: object | None,
358
+ prompt_fields: Optional[Dict[str, Any]],
359
+ ) -> list[int]:
360
+ return self._encode_text(
361
+ _render_user_prompt_after_reference(
362
+ language_code=language_code,
363
+ prompt_fields=prompt_fields,
364
+ )
365
+ )
366
+
367
+ def _assistant_prompt_prefix_ids(self) -> list[int]:
368
+ return (
369
+ self._encode_text(USER_TEMPLATE_SUFFIX)
370
+ + [int(self.model_config.im_end_token_id)]
371
+ + self._encode_text(ASSISTANT_TURN_PREFIX)
372
+ + [int(self.model_config.im_start_token_id)]
373
+ + self._encode_text(ASSISTANT_ROLE_PREFIX)
374
+ )
375
+
376
+ def _prompt_fields_from_user_message(self, message: Dict[str, Any]) -> dict[str, Any]:
377
+ fields = {}
378
+ for key in ("instruction", "tokens", "quality", "sound_event", "ambient_sound"):
379
+ if key in message and message.get(key) is not None:
380
+ fields[key] = message.get(key)
381
+ if "language" in message and message.get("language") is not None:
382
+ fields["language"] = message.get("language")
383
+ return fields
384
+
385
+ def _build_generation_or_voice_clone_codes(
386
+ self,
387
+ message: Dict[str, Any],
388
+ n_vq: int,
389
+ ) -> torch.Tensor:
390
+ if "text" not in message:
391
+ raise ValueError("Direct MOSS-TTS-Local-Transformer-v1.5 generation requires messages built by build_user_message(...).")
392
+ text = "" if message.get("text") is None else str(message.get("text"))
393
+ prompt_fields = self._prompt_fields_from_user_message(message)
394
+ language_code = message.get("language")
395
+ audio_codes_list = self._resolve_audio_items(message.get("audio_codes_list", []), n_vq)
396
+ text_token_ids = self._encode_text(text)
397
+
398
+ if audio_codes_list:
399
+ parts: list[torch.Tensor] = [self._build_text_rows(
400
+ self._user_prompt_prefix_ids(),
401
+ device=audio_codes_list[0].device,
402
+ )]
403
+ for reference_codes in audio_codes_list:
404
+ parts.append(self._build_text_rows([int(self.model_config.audio_start_token_id)], device=reference_codes.device))
405
+ parts.append(self._build_audio_rows(reference_codes, int(self.model_config.audio_user_slot_token_id)))
406
+ parts.append(self._build_text_rows([int(self.model_config.audio_end_token_id)], device=reference_codes.device))
407
+ parts.append(
408
+ self._build_text_rows(
409
+ self._user_prompt_after_reference_ids(language_code, prompt_fields)
410
+ + text_token_ids
411
+ + self._assistant_prompt_prefix_ids()
412
+ + [int(self.model_config.audio_start_token_id)],
413
+ device=audio_codes_list[0].device,
414
+ )
415
+ )
416
+ return torch.cat(parts, dim=0)
417
+
418
+ prompt_token_ids = (
419
+ self._user_prompt_prefix_ids()
420
+ + self._encode_text("None")
421
+ + self._user_prompt_after_reference_ids(language_code, prompt_fields)
422
+ + text_token_ids
423
+ + self._assistant_prompt_prefix_ids()
424
+ + [int(self.model_config.audio_start_token_id)]
425
+ )
426
+ return self._build_text_rows(prompt_token_ids)
427
+
428
+ def _build_continuation_codes(
429
+ self,
430
+ conversation: list[Dict[str, Any]],
431
+ n_vq: int,
432
+ ) -> torch.Tensor:
433
+ if len(conversation) < 2:
434
+ raise ValueError("continuation mode requires a user message followed by an assistant audio message.")
435
+ user_message = conversation[-2]
436
+ assistant_message = conversation[-1]
437
+ if user_message.get("role") != "user" or assistant_message.get("role") != "assistant":
438
+ raise ValueError("continuation mode requires the last two messages to be user, assistant.")
439
+ if "text" not in user_message:
440
+ raise ValueError("Direct MOSS-TTS-Local-Transformer-v1.5 continuation requires user messages built by build_user_message(...).")
441
+
442
+ text = "" if user_message.get("text") is None else str(user_message.get("text"))
443
+ prompt_fields = self._prompt_fields_from_user_message(user_message)
444
+ language_code = user_message.get("language")
445
+ prompt_token_ids = (
446
+ self._user_prompt_prefix_ids()
447
+ + self._encode_text("None")
448
+ + self._user_prompt_after_reference_ids(language_code, prompt_fields)
449
+ + self._encode_text(text)
450
+ + self._assistant_prompt_prefix_ids()
451
+ + [int(self.model_config.audio_start_token_id)]
452
+ )
453
+ audio_codes_list = self._resolve_audio_items(assistant_message.get("audio_codes_list", []), n_vq)
454
+ if not audio_codes_list:
455
+ return self._build_text_rows(prompt_token_ids)
456
+ if len(audio_codes_list) != 1:
457
+ raise ValueError("The MOSS-TTS-Local-Transformer-v1.5 continuation path expects exactly one prompt audio item.")
458
+ prompt_audio_codes = audio_codes_list[0]
459
+ return torch.cat(
460
+ [
461
+ self._build_text_rows(prompt_token_ids, device=prompt_audio_codes.device),
462
+ self._build_audio_rows(prompt_audio_codes, int(self.model_config.audio_assistant_slot_token_id)),
463
+ ],
464
+ dim=0,
465
+ )
466
+
467
+ def _try_build_direct_codes(
468
+ self,
469
+ conversation: list[Dict[str, Any]],
470
+ mode: str,
471
+ n_vq: int,
472
+ ) -> Optional[torch.Tensor]:
473
+ if mode == "generation" and len(conversation) == 1 and conversation[-1].get("role") == "user":
474
+ if "text" in conversation[-1]:
475
+ return self._build_generation_or_voice_clone_codes(conversation[-1], n_vq)
476
+ return None
477
+ if mode == "continuation" and len(conversation) >= 2:
478
+ if "text" in conversation[-2]:
479
+ return self._build_continuation_codes(conversation, n_vq)
480
+ return None
481
+ return None
482
+
483
+ def __call__(self, *args, **kwargs) -> BatchFeature:
484
+ conversations = args[0] if args else kwargs.pop("conversations")
485
+ mode: str = kwargs.pop("mode", "generation")
486
+ apply_chat_template: bool = kwargs.pop("apply_chat_template", True)
487
+ n_vq = self._assert_fixed_nq(kwargs.pop("n_vq", None))
488
+
489
+ kwargs.pop("return_tensors", None)
490
+ kwargs.pop("padding", None)
491
+ kwargs.pop("truncation", None)
492
+
493
+ if mode not in {"generation", "continuation", "computing_loss"}:
494
+ raise ValueError(f"Unsupported mode: {mode}")
495
+ if isinstance(conversations, (Message, dict)):
496
+ conversations = [conversations]
497
+ elif isinstance(conversations, list) and conversations and all(
498
+ isinstance(item, (Message, dict)) for item in conversations
499
+ ):
500
+ conversations = [conversations]
501
+
502
+ input_ids_list: list[torch.Tensor] = []
503
+ for conversation in conversations:
504
+ if isinstance(conversation, (Message, dict)):
505
+ conversation = [conversation]
506
+ conversation = [self._normalize_message(message) for message in conversation]
507
+
508
+ if (mode == "generation") ^ (conversation[-1]["role"] == "user"):
509
+ raise ValueError("generation mode must end with a user message.")
510
+ if mode == "continuation" and conversation[-1]["role"] != "assistant":
511
+ raise ValueError("continuation mode must end with an assistant message.")
512
+
513
+ direct_codes = self._try_build_direct_codes(conversation, mode, n_vq)
514
+ if direct_codes is not None:
515
+ input_ids_list.append(direct_codes)
516
+ continue
517
+
518
+ unified_parts = []
519
+ for message_idx, message in enumerate(conversation):
520
+ content = str(message["content"])
521
+ if apply_chat_template:
522
+ add_generation_prompt = mode == "generation" and message_idx == len(conversation) - 1
523
+ try:
524
+ content = self.tokenizer.apply_chat_template(
525
+ [{"role": message["role"], "content": content}],
526
+ add_generation_prompt=add_generation_prompt,
527
+ tokenize=False,
528
+ )
529
+ except Exception:
530
+ logger.warning("apply_chat_template failed; falling back to raw message content.")
531
+
532
+ raw_audio_items = message.get("audio_codes_list", [])
533
+ audio_codes_list = self._resolve_audio_items(raw_audio_items, n_vq)
534
+ unified_parts.append(
535
+ self._get_unified_codes(
536
+ role=message["role"],
537
+ content=content,
538
+ audio_codes_list=audio_codes_list,
539
+ truncation=(mode == "continuation"),
540
+ )
541
+ )
542
+
543
+ unified_codes = torch.cat(unified_parts, dim=0)
544
+ if mode == "generation":
545
+ audio_start_row = torch.full(
546
+ (1, n_vq + 1),
547
+ int(self.model_config.audio_pad_token_id),
548
+ dtype=unified_codes.dtype,
549
+ device=unified_codes.device,
550
+ )
551
+ audio_start_row[:, 0] = int(self.model_config.audio_start_token_id)
552
+ unified_codes = torch.cat([unified_codes, audio_start_row], dim=0)
553
+ input_ids_list.append(unified_codes)
554
+
555
+ return BatchFeature(data=self._pad(input_ids_list))
556
+
557
+ def _normalize_message(self, message: Union[Message, Dict[str, Any]]) -> Dict[str, Any]:
558
+ if isinstance(message, Message):
559
+ return message.to_dict()
560
+ if not isinstance(message, dict):
561
+ raise TypeError("Each message must be a Message or dict.")
562
+ if "content" in message and "audio_codes_list" in message:
563
+ return message
564
+ role = message.get("role")
565
+ if role == "user":
566
+ return self.build_user_message(**{key: message.get(key) for key in USER_MESSAGE_FIELDS})
567
+ if role == "assistant":
568
+ return self.build_assistant_message(
569
+ audio_codes_list=message.get("audio_codes_list", []),
570
+ content=message.get("content", AUDIO_PLACEHOLDER),
571
+ )
572
+ raise ValueError(f"Unsupported role: {role}")
573
+
574
+ def _resolve_audio_items(
575
+ self,
576
+ raw_audio_items: list[Any],
577
+ n_vq: int,
578
+ ) -> list[torch.Tensor]:
579
+ if not raw_audio_items:
580
+ return []
581
+ resolved: list[Optional[torch.Tensor]] = [None] * len(raw_audio_items)
582
+ paths: list[str] = []
583
+ path_positions: list[int] = []
584
+ for index, item in enumerate(raw_audio_items):
585
+ if isinstance(item, torch.Tensor):
586
+ if item.ndim != 2 or int(item.shape[1]) != n_vq:
587
+ raise ValueError(f"audio code tensor must have shape [T, {n_vq}], got {tuple(item.shape)}.")
588
+ resolved[index] = item.to(dtype=torch.long).cpu()
589
+ elif isinstance(item, (str, os.PathLike)):
590
+ paths.append(str(item))
591
+ path_positions.append(index)
592
+ else:
593
+ raise TypeError("Audio items must be tensors or path-like values.")
594
+ if paths:
595
+ encoded = self.encode_audios_from_path(paths, n_vq=n_vq)
596
+ for position, codes in zip(path_positions, encoded):
597
+ resolved[position] = codes
598
+ return [cast(torch.Tensor, item) for item in resolved]
599
+
600
+ def _pad(self, input_ids_list: list[torch.Tensor]) -> Dict[str, torch.Tensor]:
601
+ device = input_ids_list[0].device
602
+ lengths = torch.tensor([item.shape[0] for item in input_ids_list], device=device)
603
+ padded = torch.nn.utils.rnn.pad_sequence(
604
+ input_ids_list,
605
+ batch_first=True,
606
+ padding_value=int(self.model_config.audio_pad_token_id),
607
+ padding_side="left",
608
+ )
609
+ left_pad_mask = (padded.shape[1] - lengths).unsqueeze(1) > torch.arange(
610
+ padded.shape[1],
611
+ device=device,
612
+ ).unsqueeze(0)
613
+ padded[..., 0][left_pad_mask] = int(self.model_config.pad_token_id)
614
+ attention_mask = torch.zeros(padded.shape[:2], dtype=torch.bool, device=device)
615
+ attention_mask[~left_pad_mask] = True
616
+ return {"input_ids": padded, "attention_mask": attention_mask}
617
+
618
+ @staticmethod
619
+ def _replace_audio_placeholders(
620
+ content: str,
621
+ lengths: list[int],
622
+ slot_token: str,
623
+ audio_start_token: str,
624
+ audio_end_token: str,
625
+ ) -> str:
626
+ placeholder_count = content.count(AUDIO_PLACEHOLDER)
627
+ if placeholder_count != len(lengths):
628
+ raise ValueError(
629
+ f"Number of {AUDIO_PLACEHOLDER} ({placeholder_count}) does not match "
630
+ f"audio item count ({len(lengths)})."
631
+ )
632
+ lengths_iter = iter(lengths)
633
+
634
+ def replacer(_: re.Match) -> str:
635
+ length = int(next(lengths_iter))
636
+ if length <= 0:
637
+ return f"{audio_start_token}{audio_end_token}"
638
+ return f"{audio_start_token}{slot_token * length}{audio_end_token}"
639
+
640
+ return re.sub(re.escape(AUDIO_PLACEHOLDER), replacer, content)
641
+
642
+ def _get_unified_codes(
643
+ self,
644
+ role: str,
645
+ content: str,
646
+ audio_codes_list: list[torch.Tensor],
647
+ truncation: bool,
648
+ ) -> torch.Tensor:
649
+ n_vq = int(self.model_config.n_vq)
650
+ slot_token = self.audio_user_slot_token if role == "user" else self.audio_assistant_slot_token
651
+ content = self._replace_audio_placeholders(
652
+ content=content,
653
+ lengths=[int(codes.shape[0]) for codes in audio_codes_list],
654
+ slot_token=slot_token,
655
+ audio_start_token=self.audio_start_token,
656
+ audio_end_token=self.audio_end_token,
657
+ )
658
+ text_codes = torch.tensor(
659
+ self.tokenizer.encode(content),
660
+ dtype=torch.long,
661
+ device=audio_codes_list[0].device if audio_codes_list else None,
662
+ )
663
+
664
+ audio_start_indices = torch.where(text_codes == int(self.model_config.audio_start_token_id))[0]
665
+ audio_end_indices = torch.where(text_codes == int(self.model_config.audio_end_token_id))[0]
666
+ if len(audio_start_indices) != len(audio_codes_list) or len(audio_end_indices) != len(audio_codes_list):
667
+ raise ValueError("Audio placeholders do not match the encoded audio spans.")
668
+
669
+ if not audio_codes_list:
670
+ audio_codes = torch.full(
671
+ (len(text_codes), n_vq),
672
+ int(self.model_config.audio_pad_token_id),
673
+ dtype=torch.long,
674
+ device=text_codes.device,
675
+ )
676
+ else:
677
+ pieces: list[torch.Tensor] = []
678
+ prefix_idx = 0
679
+ for start_t, end_t, codes in zip(audio_start_indices, audio_end_indices, audio_codes_list):
680
+ start_idx = int(start_t.item())
681
+ end_idx = int(end_t.item())
682
+ pad_before = torch.full(
683
+ (start_idx - prefix_idx + 1, n_vq),
684
+ int(self.model_config.audio_pad_token_id),
685
+ dtype=torch.long,
686
+ device=codes.device,
687
+ )
688
+ pieces.extend([pad_before, codes.to(dtype=torch.long)])
689
+ prefix_idx = end_idx
690
+ if truncation:
691
+ trailing = torch.zeros(
692
+ (0, n_vq),
693
+ dtype=torch.long,
694
+ device=audio_codes_list[0].device,
695
+ )
696
+ else:
697
+ last_end = int(audio_end_indices[-1].item())
698
+ trailing = torch.full(
699
+ (len(text_codes) - last_end, n_vq),
700
+ int(self.model_config.audio_pad_token_id),
701
+ dtype=torch.long,
702
+ device=audio_codes_list[0].device,
703
+ )
704
+ pieces.append(trailing)
705
+ audio_codes = torch.cat(pieces, dim=0)
706
+
707
+ if text_codes.shape[0] != audio_codes.shape[0]:
708
+ min_len = min(text_codes.shape[0], audio_codes.shape[0])
709
+ text_codes = text_codes[:min_len]
710
+ audio_codes = audio_codes[:min_len]
711
+ return torch.cat([text_codes.unsqueeze(1), audio_codes], dim=1)
712
+
713
+ def _parse_text_codes(self, start_length: int, text_codes: torch.LongTensor) -> str:
714
+ text = cast(str, self.tokenizer.decode(text_codes))
715
+ prefix = cast(str, self.tokenizer.decode(text_codes[:start_length]))
716
+ text = text[len(prefix):]
717
+ audio_pattern = re.compile(
718
+ rf"(?:{re.escape(self.audio_start_token)})?"
719
+ rf"(?:{re.escape(self.audio_assistant_slot_token)})*"
720
+ rf"{re.escape(self.audio_end_token)}"
721
+ )
722
+ return audio_pattern.sub(
723
+ lambda match: AUDIO_PLACEHOLDER if self.audio_assistant_slot_token in match.group(0) else "",
724
+ text,
725
+ )
726
+
727
+ def _parse_audio_codes(
728
+ self,
729
+ start_length: int,
730
+ audio_codes: torch.LongTensor,
731
+ *,
732
+ return_stereo: bool = True,
733
+ ) -> list[torch.Tensor]:
734
+ is_pad = audio_codes.eq(int(self.model_config.audio_pad_token_id)).all(dim=1)
735
+ non_pad = ~is_pad
736
+ if not bool(non_pad.any().item()):
737
+ return []
738
+ idx = torch.nonzero(non_pad).squeeze(1)
739
+ breaks = torch.where(idx[1:] != idx[:-1] + 1)[0] + 1
740
+ segment_indices = [idx] if breaks.numel() == 0 else list(torch.tensor_split(idx, breaks.cpu().tolist()))
741
+ code_segments = [audio_codes[segment] for segment in segment_indices]
742
+ decoded = self.decode_audio_codes(code_segments, return_stereo=return_stereo)
743
+
744
+ if start_length > 0 and code_segments and decoded:
745
+ first_code_length = int(code_segments[0].shape[0])
746
+ if first_code_length > 0:
747
+ trim_ratio = max(0.0, min(float(start_length) / float(first_code_length), 1.0))
748
+ if trim_ratio >= 1.0:
749
+ decoded = decoded[1:]
750
+ elif trim_ratio > 0.0:
751
+ trim_samples = int(decoded[0].shape[-1] * trim_ratio)
752
+ decoded[0] = decoded[0][..., trim_samples:]
753
+ return decoded
754
+
755
+ def decode(self, output: Any, *, return_stereo: bool = True) -> list[Optional[AssistantMessage]]:
756
+ generated_messages: list[Optional[AssistantMessage]] = []
757
+ for start_length, generation_ids in output:
758
+ content = self._parse_text_codes(int(start_length), generation_ids[:, 0])
759
+ audio_codes_list = self._parse_audio_codes(
760
+ int(start_length),
761
+ generation_ids[:, 1:],
762
+ return_stereo=return_stereo,
763
+ )
764
+ if content == "":
765
+ generated_messages.append(None)
766
+ else:
767
+ generated_messages.append(
768
+ AssistantMessage(
769
+ content=content,
770
+ audio_codes_list=cast(list[Union[str, torch.Tensor]], audio_codes_list),
771
+ )
772
+ )
773
+ return generated_messages
774
+
775
+ @staticmethod
776
+ def loudness_normalize(
777
+ wav: torch.Tensor,
778
+ target_dbfs: float = -20.0,
779
+ gain_range: tuple[float, float] = (-3.0, 3.0),
780
+ ) -> torch.Tensor:
781
+ wav = wav.to(torch.float32)
782
+ if wav.numel() == 0:
783
+ return wav
784
+ current_dbfs = 10.0 * torch.log10(torch.mean(wav**2) + 1e-9)
785
+ gain = max(gain_range[0], min(float(target_dbfs - current_dbfs), gain_range[1]))
786
+ return wav * (10.0 ** (gain / 20.0))
787
+
788
+ def _get_audio_tokenizer_device(self) -> torch.device:
789
+ audio_tokenizer = getattr(self, "audio_tokenizer", None)
790
+ if audio_tokenizer is None:
791
+ raise RuntimeError("audio_tokenizer is not set.")
792
+ try:
793
+ return next(audio_tokenizer.parameters()).device
794
+ except StopIteration:
795
+ return torch.device("cpu")
796
+
797
+ def encode_audios_from_wav(
798
+ self,
799
+ wav_list: Union[torch.Tensor, list[torch.Tensor]],
800
+ sampling_rate: int,
801
+ n_vq: Optional[int] = None,
802
+ ) -> list[torch.Tensor]:
803
+ n_vq = self._assert_fixed_nq(n_vq)
804
+ if self.audio_tokenizer is None:
805
+ raise RuntimeError("audio_tokenizer is not set.")
806
+ if isinstance(wav_list, torch.Tensor):
807
+ wav_list = [wav_list]
808
+ target_sr = int(self.model_config.sampling_rate)
809
+ device = self._get_audio_tokenizer_device()
810
+ prepared = []
811
+ for wav in wav_list:
812
+ if wav.ndim == 1:
813
+ wav = wav.unsqueeze(0)
814
+ if wav.shape[0] == 1:
815
+ wav = wav.repeat(2, 1)
816
+ elif wav.shape[0] > 2:
817
+ wav = wav[:2]
818
+ if int(sampling_rate) != target_sr:
819
+ wav = torchaudio.functional.resample(wav, int(sampling_rate), target_sr)
820
+ prepared.append(self.loudness_normalize(wav).to(device))
821
+
822
+ if hasattr(self.audio_tokenizer, "batch_encode"):
823
+ encoded = self.audio_tokenizer.batch_encode(prepared, num_quantizers=n_vq)
824
+ audio_codes = encoded.audio_codes
825
+ audio_lengths = encoded.audio_codes_lengths
826
+ else:
827
+ max_len = max(int(wav.shape[-1]) for wav in prepared)
828
+ input_values = torch.zeros(len(prepared), 1, max_len, dtype=torch.float32, device=device)
829
+ padding_mask = torch.zeros(len(prepared), max_len, dtype=torch.bool, device=device)
830
+ for index, wav in enumerate(prepared):
831
+ input_values[index, 0, : wav.shape[-1]] = wav
832
+ padding_mask[index, : wav.shape[-1]] = True
833
+ encoded = self.audio_tokenizer.encode(
834
+ input_values,
835
+ padding_mask=padding_mask,
836
+ num_quantizers=n_vq,
837
+ return_dict=True,
838
+ )
839
+ audio_codes = encoded.audio_codes
840
+ audio_lengths = encoded.audio_codes_lengths
841
+
842
+ if audio_codes is None or audio_lengths is None:
843
+ raise RuntimeError("audio_tokenizer did not return audio_codes/audio_codes_lengths.")
844
+ result = []
845
+ for index in range(int(audio_codes.shape[1])):
846
+ length = int(audio_lengths[index].item())
847
+ result.append(audio_codes[:, index, :length].transpose(0, 1).contiguous().cpu().long())
848
+ return result
849
+
850
+ def encode_audios_from_path(
851
+ self,
852
+ wav_path_list: Union[str, os.PathLike, list[Union[str, os.PathLike]]],
853
+ n_vq: Optional[int] = None,
854
+ ) -> list[torch.Tensor]:
855
+ if isinstance(wav_path_list, (str, os.PathLike)):
856
+ wav_path_list = [wav_path_list]
857
+ wavs = []
858
+ target_sr = int(self.model_config.sampling_rate)
859
+ for wav_path in wav_path_list:
860
+ wav, sr = torchaudio.load(str(wav_path))
861
+ if int(sr) != target_sr:
862
+ wav = torchaudio.functional.resample(wav, int(sr), target_sr)
863
+ wavs.append(wav)
864
+ return self.encode_audios_from_wav(wavs, target_sr, n_vq=n_vq)
865
+
866
+ def decode_audio_codes(
867
+ self,
868
+ audio_tokens_list: Union[torch.Tensor, list[torch.Tensor]],
869
+ *,
870
+ return_stereo: bool = True,
871
+ ) -> list[torch.Tensor]:
872
+ if self.audio_tokenizer is None:
873
+ raise RuntimeError("audio_tokenizer is not set.")
874
+ if isinstance(audio_tokens_list, torch.Tensor):
875
+ audio_tokens_list = [audio_tokens_list]
876
+ if not audio_tokens_list:
877
+ return []
878
+
879
+ n_vq = int(self.model_config.n_vq)
880
+ device = self._get_audio_tokenizer_device()
881
+ codes_list = [
882
+ codes[:, :n_vq].transpose(0, 1).contiguous().to(device=device, dtype=torch.long)
883
+ for codes in audio_tokens_list
884
+ ]
885
+ max_len = max(int(codes.shape[1]) for codes in codes_list)
886
+ audio_codes = torch.zeros(n_vq, len(codes_list), max_len, device=device, dtype=torch.long)
887
+ padding_mask = torch.zeros(len(codes_list), max_len, device=device, dtype=torch.bool)
888
+ for index, codes in enumerate(codes_list):
889
+ length = int(codes.shape[1])
890
+ audio_codes[:, index, :length] = codes
891
+ padding_mask[index, :length] = True
892
+
893
+ decoded = self.audio_tokenizer.decode(
894
+ audio_codes,
895
+ padding_mask=padding_mask,
896
+ num_quantizers=n_vq,
897
+ return_dict=True,
898
+ chunk_duration=8,
899
+ )
900
+ audio = decoded.audio
901
+ audio_lengths = decoded.audio_lengths
902
+ if audio is None or audio_lengths is None:
903
+ raise RuntimeError("audio_tokenizer.decode did not return audio/audio_lengths.")
904
+ wavs = []
905
+ for index in range(int(audio.shape[0])):
906
+ length = int(audio_lengths[index].item())
907
+ wav = audio[index, :, :length].contiguous().cpu().to(torch.float32)
908
+ if not return_stereo:
909
+ if wav.shape[0] == 1:
910
+ wav = wav.squeeze(0)
911
+ else:
912
+ wav = wav.mean(dim=0)
913
+ wavs.append(wav)
914
+ return wavs