import json import logging import re import torch import random import traceback import numpy as np from funasr.register import tables from funasr.utils.load_utils import extract_fbank, load_audio_text_image_video @tables.register("dataset_classes", "FunASR") class FunASR(torch.utils.data.Dataset): """ FunASR dataset """ def __init__( self, path, index_ds: str = None, frontend=None, tokenizer=None, int_pad_value: int = -1, float_pad_value: float = 0.0, **kwargs, ): """Initialize FunASR. Args: path: TODO. index_ds: TODO. frontend: Audio frontend for feature extraction. tokenizer: Tokenizer instance for text encoding/decoding. int_pad_value: TODO. float_pad_value: TODO. **kwargs: Additional keyword arguments. """ super().__init__() index_ds_class = tables.index_ds_classes.get(index_ds) self.index_ds = index_ds_class(path, **kwargs) preprocessor_speech = kwargs.get("preprocessor_speech", None) if preprocessor_speech: preprocessor_speech_class = tables.preprocessor_classes.get(preprocessor_speech) preprocessor_speech = preprocessor_speech_class( **kwargs.get("preprocessor_speech_conf") ) self.preprocessor_speech = preprocessor_speech preprocessor_noise = kwargs.get("preprocessor_noise", None) if preprocessor_noise: preprocessor_noise_class = tables.preprocessor_classes.get(preprocessor_noise) preprocessor_noise = preprocessor_noise_class(**kwargs.get("preprocessor_noise_conf")) self.preprocessor_noise = preprocessor_noise prompt_classes_text = kwargs.get("prompt_classes", None) if prompt_classes_text is not None: prompt_classes = tables.prompt_classes.get(prompt_classes_text) prompt_classes = prompt_classes(**kwargs.get("prompt_conf")) else: prompt_classes = None self.prompt_classes = prompt_classes self.frontend = frontend self.fs = 16000 if frontend is None else frontend.fs self.data_type = "sound" self.tokenizer = tokenizer self.int_pad_value = int_pad_value self.float_pad_value = float_pad_value self.sos = kwargs.get("sos", "<|startoftranscript|>") self.eos = kwargs.get("eos", "<|endoftext|>") self.batch_size = kwargs.get("batch_size") self.batch_type = kwargs.get("batch_type") self.prompt_ids_len = 0 self.retry = kwargs.get("retry", 100) self.pattern = re.compile(r"(<\|startofspeech\|>.*?<\|endofspeech\|>)") # self.kwargs = kwargs self.max_token_length = kwargs.get("max_token_length", 1500) self.batch_size_scale_ratio_max = kwargs.get("batch_size_scale_ratio_max", 1.5) self.batch_size_token_max = kwargs.get("batch_size_token_max", 2500) self.multiturn_num_max = kwargs.get("multiturn_num_max", 5) self.max_source_length = kwargs.get("max_source_length", 3000) self.max_target_length = kwargs.get("max_target_length", 1024) self.do_think = kwargs.get("do_think", True) self.sys_prompt = kwargs.get("sys_prompt", True) # used for dynamic output alignment self.use_dynamic_output_ratio = kwargs.get("use_dynamic_output_ratio", 0.0) self.min_output_mask_token_len = kwargs.get("min_mask_token_len", 1) self.min_output_non_mask_token_len = kwargs.get("min_non_mask_token_len", 6) # [eos] def get_source_len(self, index): """Get source len. Args: index: TODO. """ item = self.index_ds[index] return self.index_ds.get_source_len(item) def get_target_len(self, index): """Get target len. Args: index: TODO. """ item = self.index_ds[index] return self.index_ds.get_target_len(item) def get_random_user_prompt(self, item, user_prompt): """Get random user prompt. Args: item: TODO. user_prompt: TODO. """ tasks = ["语音转写:", "Speech transcription:"] language = item.get("language", None) # LID in distill data is fake language = None if language is not None: if language.lower() == "zh": tasks.append("语音转写成中文:") tasks.append("Transcribe speech into Chinese:") elif language.lower() == "en": tasks.append("语音转写成英文:") tasks.append("Transcribe speech into English:") if len(tasks) == 2: task = random.choice(tasks) elif len(tasks) == 4: task = random.choices(tasks, weights=[0.4, 0.4, 0.1, 0.1])[0] if "语音转写:<|startofspeech|>" in user_prompt: user_prompt = user_prompt.replace("语音转写:<|startofspeech|>", task + "<|startofspeech|>") elif "Speech transcription:<|startofspeech|>" in user_prompt: user_prompt = user_prompt.replace("Speech transcription:<|startofspeech|>", task + "<|startofspeech|>") return user_prompt def __len__(self): """Internal: len .""" return len(self.index_ds) def __getitem__(self, index): """Internal: getitem . Args: index: TODO. """ output = None for idx in range(self.retry): if idx > 0: logging.info(f"retry: {idx}") badcase_flag = False if idx == 0: index_cur = index else: index_cur = torch.randint(0, len(self.index_ds), ()).item() item = self.index_ds[index_cur] system = item["system"] user = item["user"] assistant = item["assistant"] is_noised = item.get("noised", False) if len(user) < 1 or len(assistant) < 1: logging.warning(f"item is error: {item}") continue input_ids, labels, fbank, fbank_lens, fbank_mask, fbank_beg, fake_token_len = ( [], [], [], [], [], [], [], ) for i, (system_prompt, user_prompt, target_out) in enumerate( zip(system, user, assistant) ): if i >= self.multiturn_num_max: break if len(input_ids) > self.max_token_length: logging.info( f"input_ids > max_token_length: {len(input_ids)}>{self.max_token_length}, {item}" ) break if self.prompt_classes is not None: asr_prompt = user_prompt.split("<|startofspeech|>")[0] language = self.prompt_classes.detect_language(asr_prompt) user_prompt_all_context = self.prompt_classes.get_prompt(item, language) else: user_prompt_all_context = "" if i == 0: source_input = f"<|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n{user_prompt_all_context}{user_prompt}<|im_end|>\n<|im_start|>assistant\n" if not self.sys_prompt: source_input = f"<|im_start|>user\n{user_prompt_all_context}{user_prompt}<|im_end|>\n<|im_start|>assistant\n" else: source_input = ( f"<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n" ) if not self.do_think: source_input += "\n\n\n\n" splits = self.pattern.split(source_input) source_ids = [] fbank_i = [] fake_token_len_i = 0 fbank_beg_i = -1 fbank_lens_i = [] speech = [] speech_lengths = [] for k, sub_str in enumerate(splits): if not sub_str.startswith("<|startofspeech|>"): sub_token = self.tokenizer.encode(sub_str) source_ids += sub_token else: sub_str = sub_str.replace("<|startofspeech|>", "").replace( "<|endofspeech|>", "" ) if sub_str.startswith("!"): try: data_src = load_audio_text_image_video(sub_str[1:], fs=self.fs) if self.preprocessor_noise is not None and not is_noised: try: data_src = self.preprocessor_noise(data_src.numpy()) except Exception as e: logging.error(f"Generate noise audio failed: {e}") speech, speech_lengths = extract_fbank( data_src, data_type=self.data_type, frontend=self.frontend, is_final=True, ) # speech: [b, T, d] if speech_lengths > self.max_source_length: logging.info( f"speech_lengths > max_source_length: {speech_lengths}>{self.max_source_length}, {item}" ) badcase_flag = True except Exception as e: logging.warning( f"Loading wav failed! {str(e)}, {traceback.format_exc()}\n{item}" ) badcase_flag = True continue if True: olens = 1 + (speech_lengths[0].item() - 3 + 2 * 1) // 2 olens = 1 + (olens - 3 + 2 * 1) // 2 fake_token_len_i = (olens - 1) // 2 + 1 else: fake_token_len_i = speech_lengths[0].item() fake_token = [0] * fake_token_len_i fbank_beg_i = len(source_ids) source_ids += fake_token if badcase_flag: continue if fbank_beg_i > 0: fbank_beg += [fbank_beg_i + len(input_ids)] fake_token_len += [fake_token_len_i] else: fbank_beg += [-1] fake_token_len += [0] if target_out is not None and any( isinstance(item, dict) and "prev_content" in item for item in target_out ): prev_value = next( ( item["prev_content"] for item in target_out if isinstance(item, dict) and "prev_content" in item ), None, ) source_ids += self.tokenizer.encode(prev_value) source_mask = [-100] * len(source_ids) target_out = f"{target_out[0]}<|im_end|>" else: source_mask = [-100] * len(source_ids) target_out = f"{target_out}<|im_end|>" target_ids = self.tokenizer.encode(target_out) if len(target_ids) > self.max_target_length: logging.info( f"text_length: {len(target_ids)} > {self.max_target_length}, drop it: {item}" ) # simulate prev-token fixed output target_labels = target_ids.copy() if np.random.rand() < self.use_dynamic_output_ratio: max_len = len(target_labels) min_output_mask_token_len = min(self.min_output_mask_token_len, max_len) min_output_non_mask_token_len = min(self.min_output_non_mask_token_len, max_len) if max_len - min_output_non_mask_token_len > min_output_mask_token_len: end_index = np.random.randint(min_output_mask_token_len, max_len - min_output_non_mask_token_len) else: end_index = max_len - min_output_non_mask_token_len if end_index > 0: target_labels[:end_index] = [-100] * end_index input_ids += source_ids + target_ids labels += source_mask + target_labels if len(speech) > 0: fbank.append(speech[0, :, :]) fbank_lens.append(speech_lengths) if badcase_flag: continue input_ids = torch.tensor(input_ids, dtype=torch.int64) # [: self.max_token_length] attention_mask = torch.tensor([1] * len(input_ids), dtype=torch.int32) labels = torch.tensor(labels, dtype=torch.int64) # [: self.max_token_length] fbank_beg = torch.tensor(fbank_beg, dtype=torch.int32) fake_token_len = torch.tensor(fake_token_len, dtype=torch.int32) output = { "fbank_beg": fbank_beg, "fake_token_len": fake_token_len, "input_ids": input_ids, "attention_mask": attention_mask, "labels_ids": labels, } output["item"] = item if len(fbank) > 0: output["speech"] = fbank output["speech_lengths"] = fbank_lens if len(input_ids) > self.max_token_length: logging.warning( f"len(input_ids): {len(input_ids)} > max_token_length: {self.max_token_length}, item: {item}" ) continue break return output def collator(self, samples: list = None): """Collator. Args: samples: TODO. """ for idx in range(self.retry): badcase_flag = False outputs = {} for sample in samples: if sample is None: continue for key in sample.keys(): if key not in outputs: outputs[key] = [] if isinstance(sample[key], (list, tuple)): outputs[key].extend(sample[key]) else: outputs[key].append(sample[key]) for key, data_list in outputs.items(): if isinstance(data_list[0], torch.Tensor): if data_list[0].dtype == torch.int64 or data_list[0].dtype == torch.int32: pad_value = self.int_pad_value else: pad_value = self.float_pad_value outputs[key] = torch.nn.utils.rnn.pad_sequence( data_list, batch_first=True, padding_value=pad_value ) if self.batch_type != "example": b, t = outputs["input_ids"].shape if b > 1 and b * t > self.batch_size_token_max: logging.info( f"Warning, {idx}th, b*t: {b}*{t}={b * t} > batch_size_sample_max: {self.batch_size_token_max}, drop last data" ) samples = samples[:-1] continue break return outputs @tables.register("index_ds_classes", "FunASR") class FunASR(torch.utils.data.Dataset): # torch.utils.data.Dataset def __init__(self, path: str, **kwargs): """Initialize FunASR. Args: path: TODO. **kwargs: Additional keyword arguments. """ super().__init__() self.max_source_length = kwargs.get("max_source_length", 8000) self.min_source_length = kwargs.get("min_source_length", 10) self.max_target_length = kwargs.get("max_target_length", 2048) self.min_target_length = kwargs.get("min_target_length", 0) # self.max_token_length = kwargs.get("max_token_length", 2200)+ audio_downsample_rate = int(kwargs.get("audio_downsample_rate", 8)) is_training = kwargs.get("is_training", True) if not (path.endswith(".jsonl") or path.endswith(".json")): # jsonl list file data_split_num = kwargs.get("data_split_num", 1) data_split_i = kwargs.get("data_split_i", 0) if not is_training: data_split_num = 1 data_split_i = 0 with open(path, encoding="utf-8") as fin: file_list_all = fin.readlines() num_per_slice = (len(file_list_all) - 1) // data_split_num + 1 # 16 file_list = file_list_all[ data_split_i * num_per_slice: (data_split_i + 1) * num_per_slice ] logging.info( f"is_training: {is_training}, data_split_num: {data_split_num}, data_split_i: {data_split_i}, \nfile_list: {file_list}, \nfile_list_all: {file_list_all}" ) else: file_list = [path] contents = [] total_whrs = 0.0 total_token_for_llm_B = 0.0 for file_json in file_list: with open(file_json.strip(), encoding="utf-8") as fin: for line in fin: try: data_dict = json.loads(line.strip()) except Exception as e: logging.error( f"drop it, json error: {e}, line: {line}, file_json: {file_json}" ) continue data = data_dict["messages"] if isinstance(data_dict.get("speech_length", 0), (list, tuple)): speech_length = int(data_dict.get("speech_length", [0])[0]) text_length = int(data_dict.get("text_length", 0)[0]) else: speech_length = int(data_dict.get("speech_length", 0)) text_length = int(data_dict.get("text_length", 0)) speech_length = int(speech_length) text_length = int(text_length) if speech_length > 0 and speech_length < 1: continue if text_length < 1: logging.warning( f"speech_length: {speech_length}, text_length: {text_length}, data: {data}, file_json: {file_json}" ) if len(data) > 2: text_length = len(data[2]['content']) continue if speech_length > self.max_source_length: continue if speech_length < self.min_source_length: continue if text_length > self.max_target_length: continue system, user, assistant = [], [], [] for i, item in enumerate(data): try: role = item["role"] content = item["content"] except KeyError: logging.error( f"drop it, KeyError: {item}, file_json: {file_json}" ) continue if role == "system": system.append(content) elif role == "user": user.append(content) elif role == "assistant": if "prev_content" in item: prev_content = item["prev_content"] assistant.append([content, {"prev_content": prev_content}]) else: assistant.append(content) if len(system) == 0: system = ["You are a helpful assistant."] system = system * len(user) contents_i = { "system": system, "user": user, "assistant": assistant, "source_len": speech_length + text_length, } if "key" in data_dict: contents_i["key"] = data_dict["key"] if not isinstance(data_dict.get("key", "key_01234"), (list, tuple)) else data_dict["key"][0] if "hist_context" in data_dict: contents_i["hist_context"] = data_dict["hist_context"] if "hotwords" in data_dict: contents_i["hotwords"] = data_dict["hotwords"] if "asr_hotwords" in data_dict: contents_i["asr_hotwords"] = data_dict["asr_hotwords"] if "vad_segs" in data_dict: contents_i["vad_segs"] = data_dict["vad_segs"] if "word_list" in data_dict: contents_i["word_list"] = data_dict["word_list"] if "one_pass_result" in data_dict: contents_i["one_pass_result"] = data_dict["one_pass_result"] if "one_pass_wer" in data_dict: contents_i["one_pass_wer"] = data_dict["one_pass_wer"] if "noised" in data_dict: contents_i["noised"] = data_dict["noised"] if kwargs.get("save_meta", False): contents_i["meta"] = data_dict total_whrs += speech_length / 100.0 / 3600 / 10000 * audio_downsample_rate total_token_for_llm_B += (text_length + speech_length / 8) / 1000 / 1000 / 1000 contents.append(contents_i) self.contents = contents logging.info( f"\n\ntotal_num of samplers: {len(self.contents)}, total_whrs: {total_whrs:.5f}, total_token_for_llm_B: {total_token_for_llm_B:.5g}, {path}, {file_list}\n\n") def __len__(self): """Internal: len .""" return len(self.contents) def __getitem__(self, index): """Internal: getitem . Args: index: TODO. """ data = self.contents[index] return data def get_source_len(self, data_dict): """Get source len. Args: data_dict: TODO. """ source_len = data_dict.get("source_len", -1) if source_len < 0: source_len = len(data_dict["system"]) + len(data_dict["user"]) return source_len def get_target_len(self, data_dict): """Get target len. Args: data_dict: TODO. """ return 0