OpenMOSS-Team/MOSS-Music-8B-Instruct
372.7k
1import importlib.util2import os3import re4import sys5import types6from dataclasses import dataclass7from typing import List, Optional, Sequence, Union8 9import numpy as np10import torch11import torchaudio # noqa: F40112from transformers import AutoTokenizer, BatchEncoding13 14 15@dataclass16class MelConfig:17 mel_sr: int = 1600018 mel_dim: int = 12819 mel_n_fft: int = 40020 mel_hop_length: int = 16021 mel_dtype: torch.dtype = torch.bfloat1622 use_whisper_feature_extractor: bool = True23 24 25def load_chat_template(template_path: str, mossflux_path: str = None) -> List:26 if mossflux_path is None:27 template_dir = os.path.dirname(os.path.abspath(template_path))28 current = template_dir29 while current and os.path.basename(current) != "mossLite":30 parent = os.path.dirname(current)31 if parent == current:32 break33 current = parent34 if os.path.basename(current) == "mossLite":35 mossflux_path = os.path.join(current, "mossflux")36 37 if mossflux_path and mossflux_path not in sys.path:38 sys.path.insert(0, mossflux_path)39 40 spec = importlib.util.spec_from_file_location("chat_template_module", template_path)41 module = importlib.util.module_from_spec(spec)42 sys.modules["chat_template_module"] = module43 spec.loader.exec_module(module)44 return module.chat_template45 46 47class MossMusicProcessor:48 _AUDIO_SPAN_RE = re.compile(r"<\|audio_bos\|>(?:<\|AUDIO\|>)+<\|audio_eos\|>")49 _auto_class = None50 51 @classmethod52 def register_for_auto_class(cls, auto_class="AutoProcessor"):53 if not isinstance(auto_class, str):54 auto_class = auto_class.__name__55 cls._auto_class = auto_class56 57 def __init__(58 self,59 tokenizer,60 *,61 mel_config: Optional[MelConfig] = None,62 template_path: Optional[str] = None,63 enable_time_marker: bool = True,64 audio_token_id: int = 151654,65 audio_start_id: int = 151669,66 audio_end_id: int = 151670,67 ):68 self._base_tokenizer = tokenizer69 self.tokenizer = tokenizer70 self.audio_token_id = int(audio_token_id)71 self.audio_start_id = int(audio_start_id)72 self.audio_end_id = int(audio_end_id)73 self.chat_template = (74 None if template_path is None else load_chat_template(template_path)75 )76 self.custom_texts = {}77 self.enable_time_marker = bool(enable_time_marker)78 self.config = mel_config or MelConfig()79 self._whisper_feature_extractor = None80 81 alias_map = {82 "<|AUDIO|>": self.audio_token_id,83 "<|audio_bos|>": self.audio_start_id,84 "<|audio_eos|>": self.audio_end_id,85 }86 orig_convert_tokens_to_ids = self.tokenizer.convert_tokens_to_ids87 88 def _patched_convert_tokens_to_ids(tokenizer_self, tokens):89 if isinstance(tokens, (list, tuple)):90 converted = [91 _patched_convert_tokens_to_ids(tokenizer_self, token)92 for token in tokens93 ]94 return converted if isinstance(tokens, list) else tuple(converted)95 if isinstance(tokens, str) and tokens in alias_map:96 return alias_map[tokens]97 return orig_convert_tokens_to_ids(tokens)98 99 self.tokenizer.convert_tokens_to_ids = types.MethodType(100 _patched_convert_tokens_to_ids, self.tokenizer101 )102 103 self._digit_token_ids = {104 "0": 15,105 "1": 16,106 "2": 17,107 "3": 18,108 "4": 19,109 "5": 20,110 "6": 21,111 "7": 22,112 "8": 23,113 "9": 24,114 }115 self.audio_tokens_per_second = 12.5116 self.time_marker_every_seconds = 2117 self.time_marker_every_audio_tokens = int(118 self.audio_tokens_per_second * self.time_marker_every_seconds119 )120 self.model_input_names = [121 "input_ids",122 "attention_mask",123 "audio_data",124 "audio_data_seqlens",125 ]126 127 @classmethod128 def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):129 tokenizer_kwargs = {}130 for key in ["cache_dir", "revision", "token", "local_files_only"]:131 if key in kwargs:132 tokenizer_kwargs[key] = kwargs[key]133 134 tokenizer = AutoTokenizer.from_pretrained(135 pretrained_model_name_or_path,136 use_fast=False,137 **tokenizer_kwargs,138 )139 140 mel_config = kwargs.pop("mel_config", None)141 template_path = kwargs.pop("template_path", None)142 enable_time_marker = kwargs.pop("enable_time_marker", False)143 audio_token_id = kwargs.pop("audio_token_id", 151654)144 audio_start_id = kwargs.pop("audio_start_id", 151669)145 audio_end_id = kwargs.pop("audio_end_id", 151670)146 147 return cls(148 tokenizer,149 mel_config=mel_config,150 template_path=template_path,151 enable_time_marker=enable_time_marker,152 audio_token_id=audio_token_id,153 audio_start_id=audio_start_id,154 audio_end_id=audio_end_id,155 )156 157 def load_template(self, template_path: str):158 self.chat_template = load_chat_template(template_path)159 return self160 161 def set_custom_text(self, key: str, text: str):162 self.custom_texts[key] = text163 return self164 165 def clear_custom_text(self, key: Optional[str] = None):166 if key is None:167 self.custom_texts.clear()168 else:169 self.custom_texts.pop(key, None)170 return self171 172 def _template_requires_audio(self) -> bool:173 if self.chat_template is None:174 return False175 for segment in self.chat_template:176 if segment.type in {"audio_contiguous", "audio_token"}:177 return True178 return False179 180 @staticmethod181 def _conv3_downsample_len(raw_mel_len: int) -> int:182 def conv_out_len(length: int) -> int:183 return (length - 1) // 2 + 1184 185 length1 = conv_out_len(int(raw_mel_len))186 length2 = conv_out_len(length1)187 length3 = conv_out_len(length2)188 return int(length3)189 190 def _get_whisper_feature_extractor(self):191 if self._whisper_feature_extractor is not None:192 return self._whisper_feature_extractor193 194 from transformers.models.whisper.feature_extraction_whisper import (195 WhisperFeatureExtractor,196 )197 198 self._whisper_feature_extractor = WhisperFeatureExtractor(199 feature_size=int(self.config.mel_dim),200 sampling_rate=int(self.config.mel_sr),201 hop_length=int(self.config.mel_hop_length),202 n_fft=int(self.config.mel_n_fft),203 )204 return self._whisper_feature_extractor205 206 def _extract_mel(self, audio: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:207 if isinstance(audio, np.ndarray):208 wav = torch.from_numpy(audio)209 else:210 wav = audio211 wav = wav.to(dtype=torch.float32)212 if wav.dim() == 1:213 wav = wav.unsqueeze(0)214 215 if bool(getattr(self.config, "use_whisper_feature_extractor", False)):216 fe = self._get_whisper_feature_extractor()217 wav_np = wav.detach().to("cpu", torch.float32).contiguous().numpy()218 if wav_np.ndim == 2:219 wav_np = wav_np[0]220 feats = fe._np_extract_fbank_features(wav_np[None, ...], device="cpu")221 mel = torch.from_numpy(feats[0])222 223 return mel.to(dtype=self.config.mel_dtype)224 225 def _get_time_marker_token_ids(self, second: int) -> List[int]:226 return [self._digit_token_ids[digit] for digit in str(second)]227 228 def _build_audio_tokens_with_time_markers(self, audio_seq_len: int) -> List[int]:229 total_duration_seconds = audio_seq_len / self.audio_tokens_per_second230 num_full_seconds = int(total_duration_seconds)231 232 token_ids: List[int] = []233 audio_tokens_consumed = 0234 for second in range(235 self.time_marker_every_seconds,236 num_full_seconds + 1,237 self.time_marker_every_seconds,238 ):239 marker_pos = (240 second // self.time_marker_every_seconds241 ) * self.time_marker_every_audio_tokens242 audio_segment_len = marker_pos - audio_tokens_consumed243 if audio_segment_len > 0:244 token_ids.extend([self.audio_token_id] * audio_segment_len)245 audio_tokens_consumed += audio_segment_len246 token_ids.extend(self._get_time_marker_token_ids(second))247 248 remaining = audio_seq_len - audio_tokens_consumed249 if remaining > 0:250 token_ids.extend([self.audio_token_id] * remaining)251 return token_ids252 253 def _build_audio_placeholder_ids(self, num_audio_tokens: int) -> List[int]:254 if self.enable_time_marker:255 return self._build_audio_tokens_with_time_markers(num_audio_tokens)256 return [self.audio_token_id] * num_audio_tokens257 258 def _build_input_from_template(259 self, num_audio_tokens: int, include_answer: bool = False260 ) -> List[int]:261 if self.chat_template is None:262 raise ValueError("Chat template not loaded.")263 264 input_ids: List[int] = []265 for segment in self.chat_template:266 seg_type = segment.type267 if seg_type == "constant_text_token":268 input_ids.extend(segment.text_ids.tolist())269 elif seg_type in {"audio_contiguous", "audio_token"}:270 input_ids.extend(self._build_audio_placeholder_ids(num_audio_tokens))271 elif seg_type == "text_token":272 text_token_key = segment.text_token_key273 if "answer" in text_token_key.lower() and not include_answer:274 break275 if text_token_key not in self.custom_texts:276 break277 text_ids = self._base_tokenizer.encode(278 self.custom_texts[text_token_key], add_special_tokens=False279 )280 input_ids.extend(text_ids)281 282 return input_ids283 284 def _build_default_prompt(self, text: str, has_audio: bool) -> str:285 if has_audio:286 return (287 "<|im_start|>system\n"288 "You are a helpful assistant.<|im_end|>\n"289 "<|im_start|>user\n"290 "<|audio_bos|><|AUDIO|><|audio_eos|>\n"291 f"{text}<|im_end|>\n"292 "<|im_start|>assistant\n"293 )294 return (295 "<|im_start|>system\n"296 "You are a helpful assistant.<|im_end|>\n"297 "<|im_start|>user\n"298 f"{text}<|im_end|>\n"299 "<|im_start|>assistant\n"300 )301 302 def _build_input_from_prompt(self, prompt: str, token_lens: List[int]) -> List[int]:303 spans = list(self._AUDIO_SPAN_RE.finditer(prompt))304 if len(spans) != len(token_lens):305 raise ValueError(306 f"Audio placeholder count mismatch: found {len(spans)} spans in text, "307 f"but got {len(token_lens)} audio inputs."308 )309 310 input_ids: List[int] = []311 cursor = 0312 for index, match in enumerate(spans):313 prefix = prompt[cursor : match.start()]314 if prefix:315 input_ids.extend(316 self._base_tokenizer.encode(prefix, add_special_tokens=False)317 )318 319 input_ids.append(self.audio_start_id)320 input_ids.extend(self._build_audio_placeholder_ids(int(token_lens[index])))321 input_ids.append(self.audio_end_id)322 cursor = match.end()323 324 suffix = prompt[cursor:]325 if suffix:326 input_ids.extend(327 self._base_tokenizer.encode(suffix, add_special_tokens=False)328 )329 return input_ids330 331 def __call__(332 self,333 *,334 text: Union[str, Sequence[str], None] = None,335 audios: Optional[Sequence[Union[np.ndarray, torch.Tensor]]] = None,336 audio: Optional[Sequence[Union[np.ndarray, torch.Tensor]]] = None,337 return_tensors: str = "pt",338 **kwargs,339 ):340 if isinstance(text, (list, tuple)):341 if len(text) != 1:342 raise ValueError(f"Expected text batch size 1, got {len(text)}")343 prompt_text = text[0]344 else:345 prompt_text = text346 347 audio_list = audios if audios is not None else audio348 audio_list = [] if audio_list is None else list(audio_list)349 350 mels: List[torch.Tensor] = []351 raw_lengths: List[int] = []352 token_lens: List[int] = []353 for one_audio in audio_list:354 mel = self._extract_mel(one_audio)355 raw_len = int(mel.shape[-1])356 mels.append(mel)357 raw_lengths.append(raw_len)358 token_lens.append(self._conv3_downsample_len(raw_len))359 360 if mels:361 max_length = max(raw_lengths)362 audio_batch = torch.zeros(363 (len(mels), self.config.mel_dim, max_length),364 dtype=self.config.mel_dtype,365 )366 for index, mel in enumerate(mels):367 audio_batch[index, :, : mel.shape[-1]] = mel368 seqlens_tensor = torch.tensor(raw_lengths, dtype=torch.long)369 else:370 audio_batch = None371 seqlens_tensor = None372 373 if prompt_text is not None:374 if self._AUDIO_SPAN_RE.search(prompt_text) is None and audio_list:375 prompt_text = self._build_default_prompt(prompt_text, has_audio=True)376 elif self._AUDIO_SPAN_RE.search(prompt_text) is None and not audio_list:377 prompt_text = self._build_default_prompt(prompt_text, has_audio=False)378 input_ids_list = self._build_input_from_prompt(prompt_text, token_lens)379 elif self.chat_template is not None:380 input_ids_list = self._build_input_from_template(381 token_lens[0] if token_lens else 0382 )383 else:384 raise ValueError(385 "Either provide text or load a chat_template before calling the processor."386 )387 388 input_ids_tensor = torch.tensor([input_ids_list], dtype=torch.long)389 attention_mask_tensor = torch.ones_like(input_ids_tensor)390 391 data = {392 "input_ids": input_ids_tensor,393 "attention_mask": attention_mask_tensor,394 }395 if audio_batch is not None:396 data["audio_data"] = audio_batch397 data["audio_data_seqlens"] = seqlens_tensor398 return BatchEncoding(data=data, tensor_type=return_tensors)399 400 def batch_decode(self, *args, **kwargs):401 return self._base_tokenizer.batch_decode(*args, **kwargs)402 403 def decode(self, *args, **kwargs):404 return self._base_tokenizer.decode(*args, **kwargs)405 406 407__all__ = ["MelConfig", "MossMusicProcessor"]408 