diff --git a/genie_tts/Audio/Audio.py b/genie_tts/Audio/Audio.py new file mode 100644 index 0000000000000000000000000000000000000000..2c519b4da2c38bb9d88f2dfca96c6000ddb95a8a --- /dev/null +++ b/genie_tts/Audio/Audio.py @@ -0,0 +1,51 @@ +import os +import soundfile as sf +import soxr +import numpy as np +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +# 音频时长建议范围 (秒) +MIN_DURATION_S = 3 +MAX_DURATION_S = 10 +# 在音频末尾追加的静音时长 (秒) +SILENCE_TO_APPEND_S = 0.3 +# 模型期望的目标采样率 +TARGET_SAMPLING_RATE = 16000 + + +def load_audio( + audio_path: str, + target_sampling_rate: int = TARGET_SAMPLING_RATE +) -> Optional[np.ndarray]: + try: + wav, original_sr = sf.read(audio_path, dtype='float32') + if wav.ndim > 1: + wav = np.mean(wav, axis=1) # 多声道转单声道。 + if original_sr != target_sampling_rate: + wav = soxr.resample(wav, original_sr, target_sampling_rate, quality='hq') # 重采样。 + + except Exception as e: + logger.error(f"Failed to load reference audio: {audio_path}. Error: {e}") + return None + + # 检查音频长度是否在建议范围之外 + min_samples = int(MIN_DURATION_S * target_sampling_rate) + max_samples = int(MAX_DURATION_S * target_sampling_rate) + if not (min_samples <= wav.shape[0] <= max_samples): + duration = len(wav) / target_sampling_rate + logger.warning( + f"The reference audio '{os.path.basename(audio_path)}' has a duration of {duration:.2f} seconds, " + f"which is outside the recommended range of {MIN_DURATION_S} to {MAX_DURATION_S} seconds!" + ) + + # 创建并拼接静音 + silence_samples = int(SILENCE_TO_APPEND_S * target_sampling_rate) + silence_array = np.zeros(silence_samples, dtype=np.float32) + wav_processed = np.concatenate([wav, silence_array]) + + # 为模型输入增加批次维度 + # wav_processed = np.expand_dims(wav_processed, axis=0) + return wav_processed diff --git a/genie_tts/Audio/ReferenceAudio.py b/genie_tts/Audio/ReferenceAudio.py new file mode 100644 index 0000000000000000000000000000000000000000..a82842f354b92f16f502b8d86bbe5f444bbce44b --- /dev/null +++ b/genie_tts/Audio/ReferenceAudio.py @@ -0,0 +1,82 @@ +from ..Utils.Utils import LRUCacheDict +from ..GetPhonesAndBert import get_phones_and_bert +from ..Audio.Audio import load_audio +from ..ModelManager import model_manager + +from onnxruntime import InferenceSession +import os +import numpy as np +import soxr +from typing import Optional, Dict + + +class ReferenceAudio: + _prompt_cache: Dict[str, 'ReferenceAudio'] = LRUCacheDict( + capacity=int(os.getenv('Max_Cached_Reference_Audio', '10'))) + + def __new__(cls, prompt_wav: str, prompt_text: str, language: str): + if prompt_wav in cls._prompt_cache: + instance = cls._prompt_cache[prompt_wav] + if instance.text != prompt_text: # 如果文本与缓存内记录的不同,则更新。 + instance.set_text(prompt_text, language=language) + return instance + + instance = super().__new__(cls) + cls._prompt_cache[prompt_wav] = instance + return instance + + def __init__(self, prompt_wav: str, prompt_text: str, language: str): + if hasattr(self, '_initialized'): + return + + # 文本相关。 + self.text: str = prompt_text + self.phonemes_seq: Optional[np.ndarray] = None + self.text_bert: Optional[np.ndarray] = None + self.set_text(prompt_text, language=language) + + # 音频相关。 + self.audio_32k: Optional[np.ndarray] = load_audio( + audio_path=prompt_wav, + target_sampling_rate=32000 + ) + self.audio_16k: np.ndarray = soxr.resample(self.audio_32k, 32000, 16000, quality='hq') + + # 修复:添加 0.3 秒静音填充,防止参考音频内容泄露到生成结果中 + # 这是 GPT-SoVITS 的做法,帮助模型区分参考内容和目标内容的边界 + zero_padding_16k = np.zeros(int(16000 * 0.3), dtype=self.audio_16k.dtype) + audio_16k_padded = np.concatenate([self.audio_16k, zero_padding_16k]) + + self.audio_32k = np.expand_dims(self.audio_32k, axis=0) + self.audio_16k = np.expand_dims(self.audio_16k, axis=0) # 增加 Batch_Size 维度 + + if not model_manager.cn_hubert: + model_manager.load_cn_hubert() + # 使用添加了静音填充的音频提取 SSL 特征 + self.ssl_content: Optional[np.ndarray] = model_manager.cn_hubert.run( + None, {'input_values': np.expand_dims(audio_16k_padded, axis=0)} + )[0] + + self.global_emb: Optional[np.ndarray] = None + self.global_emb_advanced: Optional[np.ndarray] = None + + self._initialized = True + + def set_text(self, prompt_text: str, language: str) -> None: + self.text = prompt_text + self.phonemes_seq, self.text_bert = get_phones_and_bert(prompt_text, language=language) + + @classmethod + def clear_cache(cls) -> None: + """清空 ReferenceAudio 的缓存""" + cls._prompt_cache.clear() + + def update_global_emb(self, prompt_encoder: InferenceSession) -> None: + if self.global_emb is not None: + return + if model_manager.load_sv_model(): + sv_emb = model_manager.speaker_verification_model.run(None, {'waveform': self.audio_16k})[0] + self.global_emb, self.global_emb_advanced = prompt_encoder.run(None, { + 'ref_audio': self.audio_32k, + 'sv_emb': sv_emb, + }) diff --git a/genie_tts/Audio/__init__.py b/genie_tts/Audio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/Converter/Converter.py b/genie_tts/Converter/Converter.py new file mode 100644 index 0000000000000000000000000000000000000000..6951e71aedaf4d08338f12c277c0da3e7cf917bf --- /dev/null +++ b/genie_tts/Converter/Converter.py @@ -0,0 +1,11 @@ +from .v2.Converter import convert as convert_v2 +from .v2ProPlus.Converter import convert as convert_v2pp + +import os + + +def convert(torch_ckpt_path: str, torch_pth_path: str, output_dir: str) -> None: + if os.path.getsize(torch_pth_path) > 150 * 1024 * 1024: # 大于 150 MB + convert_v2pp(torch_ckpt_path, torch_pth_path, output_dir) + else: + convert_v2(torch_ckpt_path, torch_pth_path, output_dir) diff --git a/genie_tts/Converter/__init__.py b/genie_tts/Converter/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/Converter/load_state_dict.py b/genie_tts/Converter/load_state_dict.py new file mode 100644 index 0000000000000000000000000000000000000000..952d3580c094ca9b4c74b7ac94633b7817a3cc81 --- /dev/null +++ b/genie_tts/Converter/load_state_dict.py @@ -0,0 +1,26 @@ +import sys +import os + +sys.path.append(os.path.dirname(__file__)) + +import torch +from io import BytesIO +import utils + + +def load_sovits_model(pth_path: str, device: str = 'cpu'): + f = open(pth_path, "rb") + meta = f.read(2) + if meta != b"PK": + # noinspection PyTypeChecker + data = b"PK" + f.read() + bio = BytesIO() + # noinspection PyTypeChecker + bio.write(data) + bio.seek(0) + return torch.load(bio, map_location=device, weights_only=False) + return torch.load(pth_path, map_location=device, weights_only=False) + + +def load_gpt_model(ckpt_path: str, device: str = 'cpu'): + return torch.load(ckpt_path, map_location=device, weights_only=True) diff --git a/genie_tts/Converter/utils.py b/genie_tts/Converter/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..771e22f0bfc621725884db14c29ef22f3fa5fe42 --- /dev/null +++ b/genie_tts/Converter/utils.py @@ -0,0 +1,30 @@ +class HParams: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if type(v) == dict: + v = HParams(**v) + self[k] = v + + def keys(self): + return self.__dict__.keys() + + def items(self): + return self.__dict__.items() + + def values(self): + return self.__dict__.values() + + def __len__(self): + return len(self.__dict__) + + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, value): + return setattr(self, key, value) + + def __contains__(self, key): + return key in self.__dict__ + + def __repr__(self): + return self.__dict__.__repr__() diff --git a/genie_tts/Converter/v2/Converter.py b/genie_tts/Converter/v2/Converter.py new file mode 100644 index 0000000000000000000000000000000000000000..39910b97c9e6cb8aeda2a4d89594dbe1106af0af --- /dev/null +++ b/genie_tts/Converter/v2/Converter.py @@ -0,0 +1,146 @@ +from .VITSConverter import VITSConverter +from .T2SConverter import T2SModelConverter +from .EncoderConverter import EncoderConverter +from ...Utils.Constants import PACKAGE_NAME + +import logging +from typing import Optional, Tuple +import re +import os +import shutil +import traceback +import importlib.resources +import contextlib + +logger = logging.getLogger() + +CACHE_DIR = os.path.join(os.getcwd(), "Cache") +ENCODER_RESOURCE_PATH = "Data/v2/Models/t2s_encoder_fp32.onnx" +STAGE_DECODER_RESOURCE_PATH = "Data/v2/Models/t2s_stage_decoder_fp32.onnx" +FIRST_STAGE_DECODER_RESOURCE_PATH = "Data/v2/Models/t2s_first_stage_decoder_fp32.onnx" +VITS_RESOURCE_PATH = "Data/v2/Models/vits_fp32.onnx" +T2S_KEYS_RESOURCE_PATH = "Data/v2/Keys/t2s_onnx_keys.txt" +VITS_KEYS_RESOURCE_PATH = "Data/v2/Keys/vits_onnx_keys.txt" + + +def find_ckpt_and_pth(directory: str) -> Tuple[Optional[str], Optional[str]]: + """ + 在 directory(不递归子目录)里查找: + - .ckpt:从所有 .ckpt 文件名中搜索 'e{正整数}' 作为 epoch(找不到则视为 e0), + 选择 epoch 最大的那个文件(若无则为 None) + - .pth :从所有 .pth 文件名中搜索 'e{正整数}' 作为 epoch(找不到则视为 e0), + 选择 epoch 最大的那个文件(若无则为 None) + 若出现相同 epoch,选修改时间较新的文件以打破平手。 + """ + best_ckpt_path: Optional[str] = None + best_ckpt_epoch: int = -1 + + best_pth_path: Optional[str] = None + best_pth_epoch: int = -1 + + for filename in os.listdir(directory): + full_path = os.path.join(directory, filename) + + if not os.path.isfile(full_path): + continue + + # 提取 epoch + m = re.search(r"e(\d+)", filename, flags=re.IGNORECASE) + epoch = int(m.group(1)) if m else 0 + + # .ckpt 文件处理 + if filename.lower().endswith(".ckpt"): + if ( + epoch > best_ckpt_epoch + or ( + epoch == best_ckpt_epoch + and best_ckpt_path is not None + and os.path.getmtime(full_path) > os.path.getmtime(best_ckpt_path) + ) + ): + best_ckpt_epoch = epoch + best_ckpt_path = full_path + + # .pth 文件处理 + elif filename.lower().endswith(".pth"): + if ( + epoch > best_pth_epoch + or ( + epoch == best_pth_epoch + and best_pth_path is not None + and os.path.getmtime(full_path) > os.path.getmtime(best_pth_path) + ) + ): + best_pth_epoch = epoch + best_pth_path = full_path + + return best_ckpt_path, best_pth_path + + +def remove_folder(folder: str) -> None: + try: + if os.path.exists(folder): + shutil.rmtree(folder) + logger.info(f"🧹 Folder cleaned: {folder}") + except Exception as e: + logger.error(f"❌ Failed to clean folder {folder}: {e}") + + +def convert(torch_ckpt_path: str, + torch_pth_path: str, + output_dir: str): + # 确保缓存和输出目录存在 + os.makedirs(CACHE_DIR, exist_ok=True) + os.makedirs(output_dir, exist_ok=True) + + if len(os.listdir(output_dir)) > 0: + logger.warning(f"The output directory {output_dir} is not empty!") + + with contextlib.ExitStack() as stack: + files = importlib.resources.files(PACKAGE_NAME) + + def enter(p): + return stack.enter_context(importlib.resources.as_file(files.joinpath(p))) + + encoder_onnx_path = enter(ENCODER_RESOURCE_PATH) + stage_decoder_path = enter(STAGE_DECODER_RESOURCE_PATH) + first_stage_decoder_path = enter(FIRST_STAGE_DECODER_RESOURCE_PATH) + vits_onnx_path = enter(VITS_RESOURCE_PATH) + t2s_keys_path = enter(T2S_KEYS_RESOURCE_PATH) + vits_keys_path = enter(VITS_KEYS_RESOURCE_PATH) + + converter_1 = T2SModelConverter( + torch_ckpt_path=torch_ckpt_path, + stage_decoder_onnx_path=str(stage_decoder_path), + first_stage_decoder_onnx_path=str(first_stage_decoder_path), + key_list_file=str(t2s_keys_path), + output_dir=output_dir, + cache_dir=CACHE_DIR, + ) + converter_2 = VITSConverter( + torch_pth_path=torch_pth_path, + vits_onnx_path=str(vits_onnx_path), + key_list_file=str(vits_keys_path), + output_dir=output_dir, + cache_dir=CACHE_DIR, + ) + converter_3 = EncoderConverter( + ckpt_path=torch_ckpt_path, + pth_path=torch_pth_path, + onnx_input_path=str(encoder_onnx_path), + output_dir=output_dir, + ) + + try: + converter_1.run_full_process() + converter_2.run_full_process() + converter_3.run_full_process() + logger.info(f"🎉 Conversion successful! Saved to: {os.path.abspath(output_dir)}\n" + f"- Model Type: V2") + except Exception: + logger.error(f"❌ A critical error occurred during the conversion process") + logger.error(traceback.format_exc()) + remove_folder(output_dir) # 只在失败时清理输出目录 + finally: + # 无论成功还是失败,都尝试清理缓存目录 + remove_folder(CACHE_DIR) diff --git a/genie_tts/Converter/v2/EncoderConverter.py b/genie_tts/Converter/v2/EncoderConverter.py new file mode 100644 index 0000000000000000000000000000000000000000..33ed75ba3277ec535f9c2f88304c7c07cc2752b4 --- /dev/null +++ b/genie_tts/Converter/v2/EncoderConverter.py @@ -0,0 +1,106 @@ +import torch +import onnx +import os + +from ..load_state_dict import load_gpt_model, load_sovits_model + + +class EncoderConverter: + """ + 一个转换器,用于为 t2s_encoder 模型创建: + 1. 一个从 .ckpt 和 .pth 文件中合并而来的全精度 (fp32) .bin 权重文件。 + 2. 一个链接到该 .bin 文件的 ONNX 模型。 + """ + + def __init__(self, + ckpt_path: str, + pth_path: str, + onnx_input_path: str, + output_dir: str, + ): + self.ckpt_path: str = ckpt_path + self.pth_path: str = pth_path + self.onnx_input_path: str = onnx_input_path + self.output_dir: str = output_dir + + # 定义最终输出文件的路径 + self.output_bin_path: str = os.path.join(self.output_dir, "t2s_encoder_fp32.bin") + self.output_onnx_path: str = os.path.join(self.output_dir, "t2s_encoder_fp32.onnx") + + # 确保输出目录存在 + os.makedirs(self.output_dir, exist_ok=True) + + # 检查所有输入文件是否存在 + for path in [self.ckpt_path, self.pth_path, self.onnx_input_path]: + if not os.path.exists(path): + raise FileNotFoundError(f"Error: Input file not found! Path: {path}") + + def run_full_process(self): + # 1. 定义固定的 ONNX 权重键列表 (此顺序决定了 .bin 文件的布局) + onnx_keys = [ + "encoder.ar_text_embedding.word_embeddings.weight", + "encoder.bert_proj.weight", + "encoder.bert_proj.bias", + "encoder.ar_text_position.alpha", + "vits.ssl_proj.weight", + "vits.ssl_proj.bias", + "vits.quantizer.vq.layers.0._codebook.embed" + ] + + # 2. 加载所有必要的模型和权重 + ckpt_state_dict = load_gpt_model(self.ckpt_path)['weight'] + pth_state_dict = load_sovits_model(self.pth_path)['weight'] + model = onnx.load(self.onnx_input_path, load_external_data=False) + initializer_map = {init.name: init for init in model.graph.initializer} + current_offset = 0 + bin_filename = os.path.basename(self.output_bin_path) + + # 3. 生成 .bin 文件并同步修改 ONNX 模型 + with open(self.output_bin_path, 'wb') as f_bin: + for onnx_key in onnx_keys: + source_key = "" + source_dict = None + + if onnx_key.startswith("encoder."): + source_key = "model." + onnx_key[len("encoder."):] + source_dict = ckpt_state_dict + elif onnx_key.startswith("vits."): + source_key = onnx_key[len("vits."):] + source_dict = pth_state_dict + + if source_dict is None: + raise ValueError( + f"❌ Critical error: Unable to determine the weight source for ONNX key '{onnx_key}'.") + # 从源文件中提取张量 + tensor = source_dict.get(source_key) + if tensor is None: + raise ValueError( + f"❌ Critical error: Key '{source_key}' (corresponding to ONNX key '{onnx_key}') not found in the source file.") + + # 转换为 fp32 numpy 数组并获取字节 + numpy_array_fp32 = tensor.to(torch.float32).cpu().numpy() + tensor_bytes = numpy_array_fp32.tobytes() + tensor_length = len(tensor_bytes) + f_bin.write(tensor_bytes) + + # 在 ONNX 模型中找到对应的 initializer 并修改它 + if onnx_key in initializer_map: + tensor_proto = initializer_map[onnx_key] + + tensor_proto.ClearField('raw_data') + tensor_proto.data_location = onnx.TensorProto.EXTERNAL + del tensor_proto.external_data[:] + + keys_to_set = ["location", "offset", "length"] + values_to_set = [bin_filename, str(current_offset), str(tensor_length)] + + for k, v in zip(keys_to_set, values_to_set): + entry = tensor_proto.external_data.add() + entry.key = k + entry.value = v + + # 更新下一个权重的偏移量 + current_offset += tensor_length + + # 4. 保存修改后的 ONNX 模型 + onnx.save(model, self.output_onnx_path) diff --git a/genie_tts/Converter/v2/T2SConverter.py b/genie_tts/Converter/v2/T2SConverter.py new file mode 100644 index 0000000000000000000000000000000000000000..6a8de6925697ec911f6a22cfa4cdb31f4f73cbac --- /dev/null +++ b/genie_tts/Converter/v2/T2SConverter.py @@ -0,0 +1,125 @@ +import torch +import onnx +import numpy as np +import json +import os +from collections import OrderedDict + +from ..load_state_dict import load_gpt_model + + +class T2SModelConverter: + """ + 一个专门的转换器,用于处理 t2s (Text-to-Speech) 模型。 + - PyTorch 模型: .ckpt 文件 + - ONNX 模型: t2s_stage_decoder_fp32.onnx + - 遵循特定的键名映射规则。 + """ + + def __init__(self, + torch_ckpt_path: str, + stage_decoder_onnx_path: str, + first_stage_decoder_onnx_path: str, + key_list_file: str, + output_dir: str, + cache_dir: str, + ): + self.torch_ckpt_path: str = torch_ckpt_path + self.stage_decoder_onnx_path: str = stage_decoder_onnx_path + self.first_stage_decoder_onnx_path: str = first_stage_decoder_onnx_path + self.key_list_file: str = key_list_file + self.output_dir: str = output_dir + self.cache_dir: str = cache_dir + + os.makedirs(self.output_dir, exist_ok=True) + os.makedirs(self.output_dir, exist_ok=True) + + # 定义输出文件路径 + self.fp16_bin_path: str = os.path.join(self.output_dir, "t2s_shared_fp16.bin") + self.index_table_path: str = os.path.join(self.cache_dir, "t2s_weights_index_fp32.json") + self.relinked_encoder_path: str = os.path.join(self.output_dir, "t2s_encoder_fp32.onnx") + self.relinked_stage_decoder_path: str = os.path.join(self.output_dir, "t2s_stage_decoder_fp32.onnx") + self.relinked_first_stage_decoder_path: str = os.path.join(self.output_dir, "t2s_first_stage_decoder_fp32.onnx") + self.reconstructed_fp32_bin_path = os.path.join(self.output_dir, "t2s_shared_fp32.bin") + + def step1_create_fp16_bin_with_key_mapping(self): + """ + (1) 根据特定的键映射规则,从 .ckpt 创建 fp16 .bin 和 fp32 索引。 + (已根据用户验证脚本的正确逻辑进行最终修正) + """ + if not os.path.exists(self.key_list_file): + raise FileNotFoundError( + f"Error: Stage 1 requires the key list file, but it was not found: {self.key_list_file}") + + with open(self.key_list_file, 'r') as f: + onnx_keys = [line.strip() for line in f.readlines()] + + ckpt_data = load_gpt_model(self.torch_ckpt_path) + if 'weight' not in ckpt_data: + raise KeyError( + f"❌ Error: 'weight' key not found in the .ckpt file. Top-level keys in the file are: {list(ckpt_data.keys())}") + + torch_state_dict = ckpt_data['weight'] + + index_table = OrderedDict() + current_fp32_offset = 0 + + with open(self.fp16_bin_path, 'wb') as f_bin: + for onnx_key in onnx_keys: + transformed_onnx_key = onnx_key.replace('transformer_encoder', 'h') + torch_lookup_key = f"model.{transformed_onnx_key}" + torch_tensor = torch_state_dict.get(torch_lookup_key) + numpy_array_fp16 = torch_tensor.to(torch.float16).cpu().numpy() + f_bin.write(numpy_array_fp16.tobytes()) + tensor_length_fp32 = numpy_array_fp16.nbytes * 2 + index_table[onnx_key] = {'offset': current_fp32_offset, 'length': tensor_length_fp32} + current_fp32_offset += tensor_length_fp32 + + with open(self.index_table_path, 'w') as f_json: + json.dump(index_table, f_json, indent=4) # type: ignore + + def step2_relink_onnx_for_fp32(self, old_model: str, new_model: str): + """ + (2) 根据 fp32 索引表,修改 ONNX 模型,使其链接到未来的全精度 .bin。 + (使用与第一个脚本相同的、更稳定的底层方法) + """ + if not os.path.exists(self.index_table_path): + raise FileNotFoundError( + f"Error: Stage 2 requires the index file, but it was not found: {self.index_table_path}") + + # 加载描述 fp32 布局的索引表 + with open(self.index_table_path, 'r') as f: + index_table = json.load(f) + + model = onnx.load_model(old_model, load_external_data=False) + reconstructed_bin_filename = os.path.basename(self.reconstructed_fp32_bin_path) + + for tensor in model.graph.initializer: + if tensor.name in index_table: + tensor.ClearField('raw_data') + tensor.data_location = onnx.TensorProto.EXTERNAL + info = index_table[tensor.name] + del tensor.external_data[:] + keys = ["location", "offset", "length"] + values = [reconstructed_bin_filename, str(info['offset']), str(info['length'])] + + for k, v in zip(keys, values): + entry = tensor.external_data.add() + entry.key = k + entry.value = v + + onnx.save(model, new_model) + + @staticmethod + def step3_reconstruct_fp32_bin_from_fp16(fp16_bin_path: str, output_fp32_bin_path: str): + """ + (3) 静态工具函数:从半精度 .bin 文件还原出全精度 .bin 文件。 + """ + fp16_array = np.fromfile(fp16_bin_path, dtype=np.float16) + fp32_array = fp16_array.astype(np.float32) + fp32_array.tofile(output_fp32_bin_path) + + def run_full_process(self): + self.step1_create_fp16_bin_with_key_mapping() + self.step2_relink_onnx_for_fp32(self.stage_decoder_onnx_path, self.relinked_stage_decoder_path) + self.step2_relink_onnx_for_fp32(self.first_stage_decoder_onnx_path, self.relinked_first_stage_decoder_path) diff --git a/genie_tts/Converter/v2/VITSConverter.py b/genie_tts/Converter/v2/VITSConverter.py new file mode 100644 index 0000000000000000000000000000000000000000..8b04086a174d0edd3e0da75b7a76da9fc09491c6 --- /dev/null +++ b/genie_tts/Converter/v2/VITSConverter.py @@ -0,0 +1,129 @@ +import torch +import onnx +import numpy as np +import json +import os +from collections import OrderedDict + +from ..load_state_dict import load_sovits_model + + +class VITSConverter: + """ + 一个转换器,用于从 PyTorch 模型创建: + 1. 一个用于分发的半精度 (fp16) .bin 权重文件。 + 2. 一个与全精度 (fp32) 布局兼容的 ONNX 模型。 + 3. 一个可以将 fp16 .bin 文件还原为 fp32 .bin 的工具函数。 + """ + + def __init__(self, + torch_pth_path: str, + vits_onnx_path: str, + key_list_file: str, + output_dir: str, + cache_dir: str, + ): + self.torch_pth_path: str = torch_pth_path + self.vits_onnx_path: str = vits_onnx_path + self.key_list_file: str = key_list_file + self.output_dir: str = output_dir + self.cache_dir: str = cache_dir + # 定义输出文件路径 + self.fp16_bin_path: str = os.path.join(self.output_dir, "vits_fp16.bin") + self.index_table_path: str = os.path.join(self.cache_dir, "vits_weights_index_fp32.json") + self.relinked_fp32_onnx_path: str = os.path.join(self.output_dir, "vits_fp32.onnx") + self.reconstructed_fp32_bin_path: str = os.path.join(self.output_dir, "vits_fp32.bin") + + # 确保输出目录存在 + os.makedirs(self.cache_dir, exist_ok=True) + os.makedirs(self.output_dir, exist_ok=True) + + if not os.path.exists(self.key_list_file): + raise FileNotFoundError(f"Error: Key list file not found! Path: {self.key_list_file}") + + def step1_create_fp16_bin_and_fp32_index(self): + """ + (1) 创建一个半精度 (fp16) 的 .bin 文件,但生成一个 + 描述全精度 (fp32) 布局的索引表。 + """ + # 加载 key 列表 + with open(self.key_list_file, 'r') as f: + onnx_keys = [line.strip() for line in f.readlines()] + + # 加载 PyTorch 模型权重 + torch_state_dict = load_sovits_model(self.torch_pth_path)['weight'] + + index_table = OrderedDict() + current_fp32_offset = 0 + + with open(self.fp16_bin_path, 'wb') as f_bin: + for onnx_key in onnx_keys: + torch_key = onnx_key[len("vq_model."):] if onnx_key.startswith("vq_model.") else onnx_key + + torch_tensor = torch_state_dict.get(torch_key) + if torch_tensor is None: + raise ValueError(f"❌ Critical error: Key '{torch_key}' not found in the PyTorch weights") + + # 转换为 fp16 并写入文件 + torch_tensor_fp16 = torch_tensor.to(torch.float16) + numpy_array_fp16 = torch_tensor_fp16.cpu().numpy() + tensor_bytes_fp16 = numpy_array_fp16.tobytes() + f_bin.write(tensor_bytes_fp16) + tensor_length_fp32 = len(tensor_bytes_fp16) * 2 + index_table[onnx_key] = { + 'offset': current_fp32_offset, + 'length': tensor_length_fp32 + } + current_fp32_offset += tensor_length_fp32 + + # 保存描述 fp32 布局的索引表 + with open(self.index_table_path, 'w') as f_json: + json.dump(index_table, f_json, indent=4) # type: ignore + + def step2_relink_onnx_for_fp32(self): + """ + (2) 根据 fp32 索引表,修改 ONNX 模型,使其链接到一个 + 未来的、全精度的 .bin 文件。 + """ + # 加载描述 fp32 布局的索引表 + with open(self.index_table_path, 'r') as f: + index_table = json.load(f) + + model = onnx.load_model(self.vits_onnx_path, load_external_data=False) + reconstructed_bin_filename = os.path.basename(self.reconstructed_fp32_bin_path) + + for tensor in model.graph.initializer: + if tensor.name in index_table: + tensor.ClearField('raw_data') + tensor.data_location = onnx.TensorProto.EXTERNAL + info = index_table[tensor.name] + + del tensor.external_data[:] + + keys = ["location", "offset", "length"] + values = [reconstructed_bin_filename, str(info['offset']), str(info['length'])] + + for k, v in zip(keys, values): + entry = tensor.external_data.add() + entry.key = k + entry.value = v + + # 保存修改后的、链接到 fp32 权重的 ONNX 模型 + onnx.save(model, self.relinked_fp32_onnx_path) + + @staticmethod + def step3_reconstruct_fp32_bin_from_fp16(fp16_bin_path: str, output_fp32_bin_path: str): + """ + (3) 静态工具函数:从半精度 .bin 文件还原出全精度 .bin 文件。 + + Args: + fp16_bin_path (str): 输入的半精度 .bin 文件路径。 + output_fp32_bin_path (str): 输出的全精度 .bin 文件路径。 + """ + fp16_array = np.fromfile(fp16_bin_path, dtype=np.float16) + fp32_array = fp16_array.astype(np.float32) + fp32_array.tofile(output_fp32_bin_path) + + def run_full_process(self): + self.step1_create_fp16_bin_and_fp32_index() + self.step2_relink_onnx_for_fp32() diff --git a/genie_tts/Converter/v2/__init__.py b/genie_tts/Converter/v2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/Converter/v2ProPlus/Converter.py b/genie_tts/Converter/v2ProPlus/Converter.py new file mode 100644 index 0000000000000000000000000000000000000000..9fbac43d89704024911e8618a13413bce834ddc2 --- /dev/null +++ b/genie_tts/Converter/v2ProPlus/Converter.py @@ -0,0 +1,89 @@ +import logging +import traceback +import os +import contextlib +import importlib.resources + +from ...Utils.Constants import PACKAGE_NAME +from ..v2.VITSConverter import VITSConverter +from ..v2.T2SConverter import T2SModelConverter +from ..v2.EncoderConverter import EncoderConverter +from ..v2.Converter import (ENCODER_RESOURCE_PATH, STAGE_DECODER_RESOURCE_PATH, + FIRST_STAGE_DECODER_RESOURCE_PATH, T2S_KEYS_RESOURCE_PATH, CACHE_DIR, remove_folder) +from .PromptEncoderConverter import PromptEncoderConverter + +logger = logging.getLogger() + +# 使用 V2 ProPlus 的文件。 +VITS_RESOURCE_PATH = "Data/v2ProPlus/Models/vits_fp32.onnx" +PROMPT_ENCODER_RESOURCE_PATH = "Data/v2ProPlus/Models/prompt_encoder_fp32.onnx" +VITS_KEYS_RESOURCE_PATH = "Data/v2ProPlus/Keys/vits_weights.txt" +PROMPT_ENCODER_KEYS_RESOURCE_PATH = "Data/v2ProPlus/Keys/prompt_encoder_weights.txt" + + +def convert(torch_ckpt_path: str, torch_pth_path: str, output_dir: str) -> None: + # 确保缓存和输出目录存在 + os.makedirs(CACHE_DIR, exist_ok=True) + os.makedirs(output_dir, exist_ok=True) + + if len(os.listdir(output_dir)) > 0: + logger.warning(f"The output directory {output_dir} is not empty!") + + with contextlib.ExitStack() as stack: + files = importlib.resources.files(PACKAGE_NAME) + + def enter(p: str) -> str: + return str(stack.enter_context(importlib.resources.as_file(files.joinpath(p)))) + + encoder_onnx_path = enter(ENCODER_RESOURCE_PATH) + stage_decoder_path = enter(STAGE_DECODER_RESOURCE_PATH) + first_stage_decoder_path = enter(FIRST_STAGE_DECODER_RESOURCE_PATH) + vits_onnx_path = enter(VITS_RESOURCE_PATH) + t2s_keys_path = enter(T2S_KEYS_RESOURCE_PATH) + vits_keys_path = enter(VITS_KEYS_RESOURCE_PATH) + prompt_encoder_path = enter(PROMPT_ENCODER_RESOURCE_PATH) + prompt_encoder_keys_path = enter(PROMPT_ENCODER_KEYS_RESOURCE_PATH) + + converter_1 = T2SModelConverter( + torch_ckpt_path=torch_ckpt_path, + stage_decoder_onnx_path=stage_decoder_path, + first_stage_decoder_onnx_path=first_stage_decoder_path, + key_list_file=t2s_keys_path, + output_dir=output_dir, + cache_dir=CACHE_DIR, + ) + converter_2 = VITSConverter( + torch_pth_path=torch_pth_path, + vits_onnx_path=vits_onnx_path, + key_list_file=vits_keys_path, + output_dir=output_dir, + cache_dir=CACHE_DIR, + ) + converter_3 = EncoderConverter( + ckpt_path=torch_ckpt_path, + pth_path=torch_pth_path, + onnx_input_path=encoder_onnx_path, + output_dir=output_dir, + ) + converter_4 = PromptEncoderConverter( + torch_pth_path=torch_pth_path, + prompt_encoder_onnx_path=prompt_encoder_path, + key_list_file=prompt_encoder_keys_path, + output_dir=output_dir, + cache_dir=CACHE_DIR, + ) + + try: + converter_1.run_full_process() + converter_2.run_full_process() + converter_3.run_full_process() + converter_4.run_full_process() + logger.info(f"🎉 Conversion successful! Saved to: {os.path.abspath(output_dir)}\n" + f"- Model Type: V2ProPlus") + except Exception: + logger.error(f"❌ A critical error occurred during the conversion process") + logger.error(traceback.format_exc()) + remove_folder(output_dir) # 只在失败时清理输出目录 + finally: + # 无论成功还是失败,都尝试清理缓存目录 + remove_folder(CACHE_DIR) diff --git a/genie_tts/Converter/v2ProPlus/PromptEncoderConverter.py b/genie_tts/Converter/v2ProPlus/PromptEncoderConverter.py new file mode 100644 index 0000000000000000000000000000000000000000..55bac8664c15b2bbaae6fcfc71f13f4feb8b4b1d --- /dev/null +++ b/genie_tts/Converter/v2ProPlus/PromptEncoderConverter.py @@ -0,0 +1,128 @@ +import torch +import onnx +import json +import os +from collections import OrderedDict + +from ..load_state_dict import load_sovits_model + + +class PromptEncoderConverter: + """ + 一个转换器,用于从 PyTorch 模型创建: + 1. 一个用于分发的半精度 (fp16) .bin 权重文件。 + 2. 一个与全精度 (fp32) 布局兼容的 ONNX 模型。 + 3. 一个可以将 fp16 .bin 文件还原为 fp32 .bin 的工具函数。 + """ + + def __init__(self, + torch_pth_path: str, + prompt_encoder_onnx_path: str, + key_list_file: str, + output_dir: str, + cache_dir: str, + ): + self.torch_pth_path: str = torch_pth_path + self.vits_onnx_path: str = prompt_encoder_onnx_path + self.key_list_file: str = key_list_file + self.output_dir: str = output_dir + self.cache_dir: str = cache_dir + # 定义输出文件路径 + self.fp16_bin_path: str = os.path.join(self.output_dir, "prompt_encoder_fp16.bin") + self.index_table_path: str = os.path.join(self.cache_dir, "prompt_encoder_weights_index_fp32.json") + self.relinked_fp32_onnx_path: str = os.path.join(self.output_dir, "prompt_encoder_fp32.onnx") + self.reconstructed_fp32_bin_path: str = os.path.join(self.output_dir, "prompt_encoder_fp32.bin") + + # 确保输出目录存在 + os.makedirs(self.cache_dir, exist_ok=True) + os.makedirs(self.output_dir, exist_ok=True) + + if not os.path.exists(self.key_list_file): + raise FileNotFoundError(f"错误: Key 列表文件未找到! 路径: {self.key_list_file}") + + def step1_create_fp16_bin_and_fp32_index(self): + """ + (1) 创建一个半精度 (fp16) 的 .bin 文件,但生成一个 + 描述全精度 (fp32) 布局的索引表。 + """ + # 加载 key 列表 + with open(self.key_list_file, 'r') as f: + onnx_keys = [line.strip() for line in f.readlines()] + + # 加载 PyTorch 模型权重 + torch_state_dict = load_sovits_model(self.torch_pth_path)['weight'] + + index_table = OrderedDict() + # 这个偏移量将按照 fp32 的大小进行累加 + current_fp32_offset = 0 + + with open(self.fp16_bin_path, 'wb') as f_bin: + for onnx_key in onnx_keys: + torch_key = onnx_key[len("vq_model."):] if onnx_key.startswith("vq_model.") else onnx_key + + torch_tensor = torch_state_dict.get(torch_key) + if torch_tensor is None: + raise ValueError(f"❌ 严重错误: 在 PyTorch 权重中找不到 Key '{torch_key}'") + + # 转换为 fp16 并写入文件 + torch_tensor_fp16 = torch_tensor.to(torch.float16) + numpy_array_fp16 = torch_tensor_fp16.cpu().numpy() + tensor_bytes_fp16 = numpy_array_fp16.tobytes() + f_bin.write(tensor_bytes_fp16) + + # 关键步骤:计算并记录 fp32 的长度和偏移量 + # 一个 fp32 = 4 字节, 一个 fp16 = 2 字节。所以 fp32 长度是 fp16 的两倍。 + tensor_length_fp32 = len(tensor_bytes_fp16) * 2 + + index_table[onnx_key] = { + 'offset': current_fp32_offset, + 'length': tensor_length_fp32 + } + + # 偏移量也按照 fp32 的长度进行累加 + current_fp32_offset += tensor_length_fp32 + + # 保存描述 fp32 布局的索引表 + with open(self.index_table_path, 'w') as f_json: + json.dump(index_table, f_json, indent=4) # type: ignore + + def step2_relink_onnx_for_fp32(self): + """ + (2) 根据 fp32 索引表,修改 ONNX 模型,使其链接到一个 + 未来的、全精度的 .bin 文件。 + """ + # 加载描述 fp32 布局的索引表 + with open(self.index_table_path, 'r') as f: + index_table = json.load(f) + + # 加载 ONNX 模型结构 + model = onnx.load_model(self.vits_onnx_path, load_external_data=False) + + # 这个 ONNX 模型将要链接的 .bin 文件名 + reconstructed_bin_filename = os.path.basename(self.reconstructed_fp32_bin_path) + + for tensor in model.graph.initializer: + if tensor.name in index_table: + tensor.ClearField('raw_data') + tensor.data_location = onnx.TensorProto.EXTERNAL + info = index_table[tensor.name] + + del tensor.external_data[:] + + keys = ["location", "offset", "length"] + values = [reconstructed_bin_filename, str(info['offset']), str(info['length'])] + + for k, v in zip(keys, values): + entry = tensor.external_data.add() + entry.key = k + entry.value = v + + # 保存修改后的、链接到 fp32 权重的 ONNX 模型 + onnx.save(model, self.relinked_fp32_onnx_path) + + def run_full_process(self): + """ + 按顺序执行核心的转换步骤 (1 和 2)。 + """ + self.step1_create_fp16_bin_and_fp32_index() + self.step2_relink_onnx_for_fp32() diff --git a/genie_tts/Core/Inference.py b/genie_tts/Core/Inference.py new file mode 100644 index 0000000000000000000000000000000000000000..3486750f262fe8b56eddc322f394f630238a97cb --- /dev/null +++ b/genie_tts/Core/Inference.py @@ -0,0 +1,112 @@ +import onnxruntime as ort +import numpy as np +from typing import List, Optional +import threading + +from ..Audio.ReferenceAudio import ReferenceAudio +from ..GetPhonesAndBert import get_phones_and_bert + +MAX_T2S_LEN = 1000 + + +class GENIE: + def __init__(self): + self.stop_event: threading.Event = threading.Event() + + def tts( + self, + text: str, + prompt_audio: ReferenceAudio, + encoder: ort.InferenceSession, + first_stage_decoder: ort.InferenceSession, + stage_decoder: ort.InferenceSession, + vocoder: ort.InferenceSession, + prompt_encoder: Optional[ort.InferenceSession], + language: str = 'japanese', + ) -> Optional[np.ndarray]: + text = '。' + text # 防止漏第一句。 + text_seq, text_bert = get_phones_and_bert(text, language=language) + + semantic_tokens: np.ndarray = self.t2s_cpu( + ref_seq=prompt_audio.phonemes_seq, + ref_bert=prompt_audio.text_bert, + text_seq=text_seq, + text_bert=text_bert, + ssl_content=prompt_audio.ssl_content, + encoder=encoder, + first_stage_decoder=first_stage_decoder, + stage_decoder=stage_decoder, + ) + + eos_indices = np.where(semantic_tokens >= 1024) # 剔除不合法的元素,例如 EOS Token。 + if len(eos_indices[0]) > 0: + first_eos_index = eos_indices[-1][0] + semantic_tokens = semantic_tokens[..., :first_eos_index] + + if prompt_encoder is None: + return vocoder.run(None, { + "text_seq": text_seq, + "pred_semantic": semantic_tokens, + "ref_audio": prompt_audio.audio_32k + })[0] + else: + # V2ProPlus 新增。 + prompt_audio.update_global_emb(prompt_encoder=prompt_encoder) + audio_chunk = vocoder.run(None, { + "text_seq": text_seq, + "pred_semantic": semantic_tokens, + "ge": prompt_audio.global_emb, + "ge_advanced": prompt_audio.global_emb_advanced, + })[0] + return audio_chunk + + def t2s_cpu( + self, + ref_seq: np.ndarray, + ref_bert: np.ndarray, + text_seq: np.ndarray, + text_bert: np.ndarray, + ssl_content: np.ndarray, + encoder: ort.InferenceSession, + first_stage_decoder: ort.InferenceSession, + stage_decoder: ort.InferenceSession, + ) -> Optional[np.ndarray]: + """在CPU上运行T2S模型""" + # Encoder + x, prompts = encoder.run( + None, + { + "ref_seq": ref_seq, + "text_seq": text_seq, + "ref_bert": ref_bert, + "text_bert": text_bert, + "ssl_content": ssl_content, + }, + ) + + # First Stage Decoder + y, y_emb, *present_key_values = first_stage_decoder.run( + None, {"x": x, "prompts": prompts} + ) + + # Stage Decoder + input_names: List[str] = [inp.name for inp in stage_decoder.get_inputs()] + idx: int = 0 + for idx in range(0, 500): + if self.stop_event.is_set(): + return None + input_feed = { + name: data + for name, data in zip(input_names, [y, y_emb, *present_key_values]) + } + outputs = stage_decoder.run(None, input_feed) + y, y_emb, stop_condition_tensor, *present_key_values = outputs + + if stop_condition_tensor: + break + + y[0, -1] = 0 + return np.expand_dims(y[:, -idx:], axis=0) + + +tts_client: GENIE = GENIE() diff --git a/genie_tts/Core/Resources.py b/genie_tts/Core/Resources.py new file mode 100644 index 0000000000000000000000000000000000000000..d049cec7c68450ed5abc59f0188838583fa053ab --- /dev/null +++ b/genie_tts/Core/Resources.py @@ -0,0 +1,76 @@ +import os +from huggingface_hub import snapshot_download + + +def download_genie_data() -> None: + print(f"🚀 Starting download Genie-TTS resources… This may take a few moments. ⏳") + snapshot_download( + repo_id="High-Logic/Genie", + repo_type="model", + allow_patterns="GenieData/*", + local_dir=".", + local_dir_use_symlinks=True, # 软链接 + ) + print("✅ Genie-TTS resources downloaded successfully.") + + +def ensure_exists(path: str, name: str): + if not os.path.exists(path): + raise FileNotFoundError( + f"Required directory or file '{name}' was not found at: {path}\n" + f"Please download the pretrained models and place them under './GenieData', " + f"or set the environment variable GENIE_DATA_DIR to the correct directory." + ) + + +""" +文件结构与项目 Midori 同步。 +""" + +GENIE_DATA_DIR: str = os.getenv( + "GENIE_DATA_DIR", + "./GenieData" +) + +""" +Japanese_G2P_DIR: str = os.getenv( + "Japanese_G2P_DIR", + f"{GENIE_DATA_DIR}/G2P/JapaneseG2P" +) +""" + +English_G2P_DIR: str = os.getenv( + "English_G2P_DIR", + f"{GENIE_DATA_DIR}/G2P/EnglishG2P" +) + +Chinese_G2P_DIR: str = os.getenv( + "Chinese_G2P_DIR", + f"{GENIE_DATA_DIR}/G2P/ChineseG2P" +) + +HUBERT_MODEL_DIR: str = os.getenv( + "HUBERT_MODEL_DIR", + f"{GENIE_DATA_DIR}/chinese-hubert-base" +) + +SV_MODEL: str = os.getenv( + "SV_MODEL", + f"{GENIE_DATA_DIR}/speaker_encoder.onnx" +) + +ROBERTA_MODEL_DIR: str = os.getenv( + "ROBERTA_MODEL_DIR", + f"{GENIE_DATA_DIR}/RoBERTa" +) + +if not os.path.exists(GENIE_DATA_DIR): + print("⚠️ GenieData folder not found.") + choice = input("Would you like to download it automatically from HuggingFace? (y/N): ").strip().lower() + if choice == "y": + download_genie_data() + +# ---- Run directory checks ---- +ensure_exists(HUBERT_MODEL_DIR, "HUBERT_MODEL_DIR") +ensure_exists(SV_MODEL, "SV_MODEL") +# ensure_exists(ROBERTA_MODEL_DIR, "ROBERTA_MODEL_DIR") diff --git a/genie_tts/Core/TTSPlayer.py b/genie_tts/Core/TTSPlayer.py new file mode 100644 index 0000000000000000000000000000000000000000..6c70691e43622cfac3437122e91cef1270477468 --- /dev/null +++ b/genie_tts/Core/TTSPlayer.py @@ -0,0 +1,241 @@ +# 文件: .../Core/TTSPlayer.py + +import queue +import os +import threading + +import numpy as np +import wave +from typing import Optional, List, Callable +import logging + +from ..Utils.TextSplitter import TextSplitter +from ..Core.Inference import tts_client +from ..ModelManager import model_manager +from ..Utils.Shared import context +from ..Utils.Utils import clear_queue + +logger = logging.getLogger(__name__) + +STREAM_END = 'STREAM_END' # 这是一个特殊的标记,表示文本流结束 +AUDIO_STREAM_END = 'AUDIO_STREAM_END' # 新增:特殊的标记,表示音频流播放结束 + + +class TTSPlayer: + def __init__(self, sample_rate: int = 32000): + self._text_splitter = TextSplitter() + + self.sample_rate: int = sample_rate + self.channels: int = 1 + self.bytes_per_sample: int = 2 # 16-bit audio + + self._text_queue: queue.Queue = queue.Queue() + self._audio_queue: queue.Queue = queue.Queue() + + self._stop_event: threading.Event = threading.Event() + self._tts_done_event: threading.Event = threading.Event() + self._playback_done_event: threading.Event = threading.Event() # 新增:用于标记播放完成 + self._api_lock: threading.Lock = threading.Lock() + + self._tts_worker: Optional[threading.Thread] = None + self._playback_worker: Optional[threading.Thread] = None + + self._play: bool = False + self._current_save_path: Optional[str] = None + self._session_audio_chunks: List[np.ndarray] = [] + self._split: bool = False + + self._chunk_callback: Optional[Callable[[Optional[bytes]], None]] = None + + @staticmethod + def _preprocess_for_playback(audio_float: np.ndarray) -> bytes: + audio_int16 = (audio_float.squeeze() * 32767).astype(np.int16) + return audio_int16.tobytes() + + def _tts_worker_loop(self): + """从文本队列取句子,生成音频,并通过回调函数或音频队列分发。""" + while not self._stop_event.is_set(): + try: + sentence = self._text_queue.get(timeout=1) + if sentence is None or self._stop_event.is_set(): + break + except queue.Empty: + continue + + try: + if sentence is STREAM_END: + if self._current_save_path and self._session_audio_chunks: + self._save_session_audio() + + # 在TTS工作线程完成时,通过回调发送结束信号 + if self._chunk_callback: + self._chunk_callback(None) + + # 新增:如果开启了播放,通知音频队列流已结束 + if self._play: + self._audio_queue.put(AUDIO_STREAM_END) + + self._tts_done_event.set() + continue + + gsv_model = model_manager.get(context.current_speaker) + if not gsv_model or not context.current_prompt_audio: + logger.error("Missing model or reference audio.") + continue + + tts_client.stop_event.clear() + audio_chunk = tts_client.tts( + text=sentence, + prompt_audio=context.current_prompt_audio, + encoder=gsv_model.T2S_ENCODER, + first_stage_decoder=gsv_model.T2S_FIRST_STAGE_DECODER, + stage_decoder=gsv_model.T2S_STAGE_DECODER, + vocoder=gsv_model.VITS, + prompt_encoder=gsv_model.PROMPT_ENCODER, + language=gsv_model.LANGUAGE, + ) + + if audio_chunk is not None: + if self._play: + self._audio_queue.put(audio_chunk) + if self._current_save_path: + self._session_audio_chunks.append(audio_chunk) + + # 使用回调函数处理流式数据 + if self._chunk_callback: + audio_data = self._preprocess_for_playback(audio_chunk) + self._chunk_callback(audio_data) + + except Exception as e: + logger.error(f"A critical error occurred while processing the TTS task: {e}", exc_info=True) + # 发生错误时,也要确保发送结束信号 + if self._chunk_callback: + self._chunk_callback(None) + self._tts_done_event.set() + + def _playback_worker_loop(self): + try: + import sounddevice as sd + with sd.OutputStream(samplerate=self.sample_rate, + channels=self.channels, + dtype='float32') as stream: + while not self._stop_event.is_set(): + try: + audio_chunk = self._audio_queue.get(timeout=1) + if audio_chunk is None: + break + if audio_chunk is AUDIO_STREAM_END: + self._playback_done_event.set() + continue + stream.write(audio_chunk.squeeze()) + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error during audio playback: {e}", exc_info=True) + + except Exception as e: + logger.warning(f"Failed to initialize sounddevice: {e}. Audio playback will be skipped.") + # 如果音频设备初始化失败,即使不播放,也要消费队列中的结束信号,防止主线程死锁 + while not self._stop_event.is_set(): + try: + item = self._audio_queue.get(timeout=0.5) + if item is None: + break + if item is AUDIO_STREAM_END: + self._playback_done_event.set() + except queue.Empty: + continue + + def _save_session_audio(self): + try: + full_audio = np.concatenate(self._session_audio_chunks, axis=0) + with wave.open(self._current_save_path, 'wb') as wf: + wf.setnchannels(self.channels) + wf.setsampwidth(self.bytes_per_sample) + wf.setframerate(self.sample_rate) + wf.writeframes(self._preprocess_for_playback(full_audio)) + logger.info(f"Audio successfully saved to {os.path.abspath(self._current_save_path)}") + except Exception as e: + logger.error(f"Failed to save audio: {e}") + finally: + self._session_audio_chunks = [] + self._current_save_path = None + + def start_session( + self, + play: bool = False, + split: bool = False, + save_path: Optional[str] = None, + chunk_callback: Optional[Callable[[Optional[bytes]], None]] = None + ): + with self._api_lock: + self._tts_done_event.clear() + self._playback_done_event.clear() # 新增:重置播放完成事件 + self._chunk_callback = chunk_callback + self._stop_event.clear() + + if self._tts_worker is None or not self._tts_worker.is_alive(): + self._tts_worker = threading.Thread(target=self._tts_worker_loop, daemon=True) + self._tts_worker.start() + + if self._playback_worker is None or not self._playback_worker.is_alive(): + self._playback_worker = threading.Thread(target=self._playback_worker_loop, daemon=True) + self._playback_worker.start() + + clear_queue(self._text_queue) + clear_queue(self._audio_queue) + + self._play = play + self._split = split + self._current_save_path = save_path + self._session_audio_chunks = [] + + def feed(self, text_chunk: str): + with self._api_lock: + if not text_chunk: + return + if self._split: + sentences = self._text_splitter.split(text_chunk.strip()) + for sentence in sentences: + self._text_queue.put(sentence) + else: + self._text_queue.put(text_chunk) + + def end_session(self): + with self._api_lock: + self._text_queue.put(STREAM_END) + + def stop(self): + with self._api_lock: + if self._tts_worker is None and self._playback_worker is None: + return + if self._stop_event.is_set(): + return + tts_client.stop_event.set() + self._stop_event.set() + self._tts_done_event.set() + self._text_queue.put(None) + self._audio_queue.put(None) + if self._tts_worker and self._tts_worker.is_alive(): + self._tts_worker.join() + if self._playback_worker and self._playback_worker.is_alive(): + self._playback_worker.join() + self._tts_worker = None + self._playback_worker = None + + def wait_for_tts_completion(self): + if self._tts_done_event.is_set(): + return + self._tts_done_event.wait() + + def wait_for_playback_done(self): + # 1. 首先等待TTS生成全部完成 + self.wait_for_tts_completion() + + # 2. 如果开启了播放且没有被强制停止,则等待播放结束 + if self._play and not self._stop_event.is_set(): + if not self._playback_done_event.is_set(): + self._playback_done_event.wait() + + +tts_player: TTSPlayer = TTSPlayer() diff --git a/genie_tts/Core/__init__.py b/genie_tts/Core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/Data/v2/Keys/t2s_onnx_keys.txt b/genie_tts/Data/v2/Keys/t2s_onnx_keys.txt new file mode 100644 index 0000000000000000000000000000000000000000..0a0b35764b2f696e8b72b6c68809ac463d55987a --- /dev/null +++ b/genie_tts/Data/v2/Keys/t2s_onnx_keys.txt @@ -0,0 +1,291 @@ +ar_audio_embedding.word_embeddings.weight +ar_audio_position.alpha +transformer_encoder.layers.0.self_attn.in_proj_weight +transformer_encoder.layers.0.self_attn.in_proj_bias +transformer_encoder.layers.0.self_attn.out_proj.weight +transformer_encoder.layers.0.self_attn.out_proj.bias +transformer_encoder.layers.0.linear1.weight +transformer_encoder.layers.0.linear1.bias +transformer_encoder.layers.0.linear2.weight +transformer_encoder.layers.0.linear2.bias +transformer_encoder.layers.0.norm1.weight +transformer_encoder.layers.0.norm1.bias +transformer_encoder.layers.0.norm2.weight +transformer_encoder.layers.0.norm2.bias +transformer_encoder.layers.1.self_attn.in_proj_weight +transformer_encoder.layers.1.self_attn.in_proj_bias +transformer_encoder.layers.1.self_attn.out_proj.weight +transformer_encoder.layers.1.self_attn.out_proj.bias +transformer_encoder.layers.1.linear1.weight +transformer_encoder.layers.1.linear1.bias +transformer_encoder.layers.1.linear2.weight +transformer_encoder.layers.1.linear2.bias +transformer_encoder.layers.1.norm1.weight +transformer_encoder.layers.1.norm1.bias +transformer_encoder.layers.1.norm2.weight +transformer_encoder.layers.1.norm2.bias +transformer_encoder.layers.2.self_attn.in_proj_weight +transformer_encoder.layers.2.self_attn.in_proj_bias +transformer_encoder.layers.2.self_attn.out_proj.weight +transformer_encoder.layers.2.self_attn.out_proj.bias +transformer_encoder.layers.2.linear1.weight +transformer_encoder.layers.2.linear1.bias +transformer_encoder.layers.2.linear2.weight +transformer_encoder.layers.2.linear2.bias +transformer_encoder.layers.2.norm1.weight +transformer_encoder.layers.2.norm1.bias +transformer_encoder.layers.2.norm2.weight +transformer_encoder.layers.2.norm2.bias +transformer_encoder.layers.3.self_attn.in_proj_weight +transformer_encoder.layers.3.self_attn.in_proj_bias +transformer_encoder.layers.3.self_attn.out_proj.weight +transformer_encoder.layers.3.self_attn.out_proj.bias +transformer_encoder.layers.3.linear1.weight +transformer_encoder.layers.3.linear1.bias +transformer_encoder.layers.3.linear2.weight +transformer_encoder.layers.3.linear2.bias +transformer_encoder.layers.3.norm1.weight +transformer_encoder.layers.3.norm1.bias +transformer_encoder.layers.3.norm2.weight +transformer_encoder.layers.3.norm2.bias +transformer_encoder.layers.4.self_attn.in_proj_weight +transformer_encoder.layers.4.self_attn.in_proj_bias +transformer_encoder.layers.4.self_attn.out_proj.weight +transformer_encoder.layers.4.self_attn.out_proj.bias +transformer_encoder.layers.4.linear1.weight +transformer_encoder.layers.4.linear1.bias +transformer_encoder.layers.4.linear2.weight +transformer_encoder.layers.4.linear2.bias +transformer_encoder.layers.4.norm1.weight +transformer_encoder.layers.4.norm1.bias +transformer_encoder.layers.4.norm2.weight +transformer_encoder.layers.4.norm2.bias +transformer_encoder.layers.5.self_attn.in_proj_weight +transformer_encoder.layers.5.self_attn.in_proj_bias +transformer_encoder.layers.5.self_attn.out_proj.weight +transformer_encoder.layers.5.self_attn.out_proj.bias +transformer_encoder.layers.5.linear1.weight +transformer_encoder.layers.5.linear1.bias +transformer_encoder.layers.5.linear2.weight +transformer_encoder.layers.5.linear2.bias +transformer_encoder.layers.5.norm1.weight +transformer_encoder.layers.5.norm1.bias +transformer_encoder.layers.5.norm2.weight +transformer_encoder.layers.5.norm2.bias +transformer_encoder.layers.6.self_attn.in_proj_weight +transformer_encoder.layers.6.self_attn.in_proj_bias +transformer_encoder.layers.6.self_attn.out_proj.weight +transformer_encoder.layers.6.self_attn.out_proj.bias +transformer_encoder.layers.6.linear1.weight +transformer_encoder.layers.6.linear1.bias +transformer_encoder.layers.6.linear2.weight +transformer_encoder.layers.6.linear2.bias +transformer_encoder.layers.6.norm1.weight +transformer_encoder.layers.6.norm1.bias +transformer_encoder.layers.6.norm2.weight +transformer_encoder.layers.6.norm2.bias +transformer_encoder.layers.7.self_attn.in_proj_weight +transformer_encoder.layers.7.self_attn.in_proj_bias +transformer_encoder.layers.7.self_attn.out_proj.weight +transformer_encoder.layers.7.self_attn.out_proj.bias +transformer_encoder.layers.7.linear1.weight +transformer_encoder.layers.7.linear1.bias +transformer_encoder.layers.7.linear2.weight +transformer_encoder.layers.7.linear2.bias +transformer_encoder.layers.7.norm1.weight +transformer_encoder.layers.7.norm1.bias +transformer_encoder.layers.7.norm2.weight +transformer_encoder.layers.7.norm2.bias +transformer_encoder.layers.8.self_attn.in_proj_weight +transformer_encoder.layers.8.self_attn.in_proj_bias +transformer_encoder.layers.8.self_attn.out_proj.weight +transformer_encoder.layers.8.self_attn.out_proj.bias +transformer_encoder.layers.8.linear1.weight +transformer_encoder.layers.8.linear1.bias +transformer_encoder.layers.8.linear2.weight +transformer_encoder.layers.8.linear2.bias +transformer_encoder.layers.8.norm1.weight +transformer_encoder.layers.8.norm1.bias +transformer_encoder.layers.8.norm2.weight +transformer_encoder.layers.8.norm2.bias +transformer_encoder.layers.9.self_attn.in_proj_weight +transformer_encoder.layers.9.self_attn.in_proj_bias +transformer_encoder.layers.9.self_attn.out_proj.weight +transformer_encoder.layers.9.self_attn.out_proj.bias +transformer_encoder.layers.9.linear1.weight +transformer_encoder.layers.9.linear1.bias +transformer_encoder.layers.9.linear2.weight +transformer_encoder.layers.9.linear2.bias +transformer_encoder.layers.9.norm1.weight +transformer_encoder.layers.9.norm1.bias +transformer_encoder.layers.9.norm2.weight +transformer_encoder.layers.9.norm2.bias +transformer_encoder.layers.10.self_attn.in_proj_weight +transformer_encoder.layers.10.self_attn.in_proj_bias +transformer_encoder.layers.10.self_attn.out_proj.weight +transformer_encoder.layers.10.self_attn.out_proj.bias +transformer_encoder.layers.10.linear1.weight +transformer_encoder.layers.10.linear1.bias +transformer_encoder.layers.10.linear2.weight +transformer_encoder.layers.10.linear2.bias +transformer_encoder.layers.10.norm1.weight +transformer_encoder.layers.10.norm1.bias +transformer_encoder.layers.10.norm2.weight +transformer_encoder.layers.10.norm2.bias +transformer_encoder.layers.11.self_attn.in_proj_weight +transformer_encoder.layers.11.self_attn.in_proj_bias +transformer_encoder.layers.11.self_attn.out_proj.weight +transformer_encoder.layers.11.self_attn.out_proj.bias +transformer_encoder.layers.11.linear1.weight +transformer_encoder.layers.11.linear1.bias +transformer_encoder.layers.11.linear2.weight +transformer_encoder.layers.11.linear2.bias +transformer_encoder.layers.11.norm1.weight +transformer_encoder.layers.11.norm1.bias +transformer_encoder.layers.11.norm2.weight +transformer_encoder.layers.11.norm2.bias +transformer_encoder.layers.12.self_attn.in_proj_weight +transformer_encoder.layers.12.self_attn.in_proj_bias +transformer_encoder.layers.12.self_attn.out_proj.weight +transformer_encoder.layers.12.self_attn.out_proj.bias +transformer_encoder.layers.12.linear1.weight +transformer_encoder.layers.12.linear1.bias +transformer_encoder.layers.12.linear2.weight +transformer_encoder.layers.12.linear2.bias +transformer_encoder.layers.12.norm1.weight +transformer_encoder.layers.12.norm1.bias +transformer_encoder.layers.12.norm2.weight +transformer_encoder.layers.12.norm2.bias +transformer_encoder.layers.13.self_attn.in_proj_weight +transformer_encoder.layers.13.self_attn.in_proj_bias +transformer_encoder.layers.13.self_attn.out_proj.weight +transformer_encoder.layers.13.self_attn.out_proj.bias +transformer_encoder.layers.13.linear1.weight +transformer_encoder.layers.13.linear1.bias +transformer_encoder.layers.13.linear2.weight +transformer_encoder.layers.13.linear2.bias +transformer_encoder.layers.13.norm1.weight +transformer_encoder.layers.13.norm1.bias +transformer_encoder.layers.13.norm2.weight +transformer_encoder.layers.13.norm2.bias +transformer_encoder.layers.14.self_attn.in_proj_weight +transformer_encoder.layers.14.self_attn.in_proj_bias +transformer_encoder.layers.14.self_attn.out_proj.weight +transformer_encoder.layers.14.self_attn.out_proj.bias +transformer_encoder.layers.14.linear1.weight +transformer_encoder.layers.14.linear1.bias +transformer_encoder.layers.14.linear2.weight +transformer_encoder.layers.14.linear2.bias +transformer_encoder.layers.14.norm1.weight +transformer_encoder.layers.14.norm1.bias +transformer_encoder.layers.14.norm2.weight +transformer_encoder.layers.14.norm2.bias +transformer_encoder.layers.15.self_attn.in_proj_weight +transformer_encoder.layers.15.self_attn.in_proj_bias +transformer_encoder.layers.15.self_attn.out_proj.weight +transformer_encoder.layers.15.self_attn.out_proj.bias +transformer_encoder.layers.15.linear1.weight +transformer_encoder.layers.15.linear1.bias +transformer_encoder.layers.15.linear2.weight +transformer_encoder.layers.15.linear2.bias +transformer_encoder.layers.15.norm1.weight +transformer_encoder.layers.15.norm1.bias +transformer_encoder.layers.15.norm2.weight +transformer_encoder.layers.15.norm2.bias +transformer_encoder.layers.16.self_attn.in_proj_weight +transformer_encoder.layers.16.self_attn.in_proj_bias +transformer_encoder.layers.16.self_attn.out_proj.weight +transformer_encoder.layers.16.self_attn.out_proj.bias +transformer_encoder.layers.16.linear1.weight +transformer_encoder.layers.16.linear1.bias +transformer_encoder.layers.16.linear2.weight +transformer_encoder.layers.16.linear2.bias +transformer_encoder.layers.16.norm1.weight +transformer_encoder.layers.16.norm1.bias +transformer_encoder.layers.16.norm2.weight +transformer_encoder.layers.16.norm2.bias +transformer_encoder.layers.17.self_attn.in_proj_weight +transformer_encoder.layers.17.self_attn.in_proj_bias +transformer_encoder.layers.17.self_attn.out_proj.weight +transformer_encoder.layers.17.self_attn.out_proj.bias +transformer_encoder.layers.17.linear1.weight +transformer_encoder.layers.17.linear1.bias +transformer_encoder.layers.17.linear2.weight +transformer_encoder.layers.17.linear2.bias +transformer_encoder.layers.17.norm1.weight +transformer_encoder.layers.17.norm1.bias +transformer_encoder.layers.17.norm2.weight +transformer_encoder.layers.17.norm2.bias +transformer_encoder.layers.18.self_attn.in_proj_weight +transformer_encoder.layers.18.self_attn.in_proj_bias +transformer_encoder.layers.18.self_attn.out_proj.weight +transformer_encoder.layers.18.self_attn.out_proj.bias +transformer_encoder.layers.18.linear1.weight +transformer_encoder.layers.18.linear1.bias +transformer_encoder.layers.18.linear2.weight +transformer_encoder.layers.18.linear2.bias +transformer_encoder.layers.18.norm1.weight +transformer_encoder.layers.18.norm1.bias +transformer_encoder.layers.18.norm2.weight +transformer_encoder.layers.18.norm2.bias +transformer_encoder.layers.19.self_attn.in_proj_weight +transformer_encoder.layers.19.self_attn.in_proj_bias +transformer_encoder.layers.19.self_attn.out_proj.weight +transformer_encoder.layers.19.self_attn.out_proj.bias +transformer_encoder.layers.19.linear1.weight +transformer_encoder.layers.19.linear1.bias +transformer_encoder.layers.19.linear2.weight +transformer_encoder.layers.19.linear2.bias +transformer_encoder.layers.19.norm1.weight +transformer_encoder.layers.19.norm1.bias +transformer_encoder.layers.19.norm2.weight +transformer_encoder.layers.19.norm2.bias +transformer_encoder.layers.20.self_attn.in_proj_weight +transformer_encoder.layers.20.self_attn.in_proj_bias +transformer_encoder.layers.20.self_attn.out_proj.weight +transformer_encoder.layers.20.self_attn.out_proj.bias +transformer_encoder.layers.20.linear1.weight +transformer_encoder.layers.20.linear1.bias +transformer_encoder.layers.20.linear2.weight +transformer_encoder.layers.20.linear2.bias +transformer_encoder.layers.20.norm1.weight +transformer_encoder.layers.20.norm1.bias +transformer_encoder.layers.20.norm2.weight +transformer_encoder.layers.20.norm2.bias +transformer_encoder.layers.21.self_attn.in_proj_weight +transformer_encoder.layers.21.self_attn.in_proj_bias +transformer_encoder.layers.21.self_attn.out_proj.weight +transformer_encoder.layers.21.self_attn.out_proj.bias +transformer_encoder.layers.21.linear1.weight +transformer_encoder.layers.21.linear1.bias +transformer_encoder.layers.21.linear2.weight +transformer_encoder.layers.21.linear2.bias +transformer_encoder.layers.21.norm1.weight +transformer_encoder.layers.21.norm1.bias +transformer_encoder.layers.21.norm2.weight +transformer_encoder.layers.21.norm2.bias +transformer_encoder.layers.22.self_attn.in_proj_weight +transformer_encoder.layers.22.self_attn.in_proj_bias +transformer_encoder.layers.22.self_attn.out_proj.weight +transformer_encoder.layers.22.self_attn.out_proj.bias +transformer_encoder.layers.22.linear1.weight +transformer_encoder.layers.22.linear1.bias +transformer_encoder.layers.22.linear2.weight +transformer_encoder.layers.22.linear2.bias +transformer_encoder.layers.22.norm1.weight +transformer_encoder.layers.22.norm1.bias +transformer_encoder.layers.22.norm2.weight +transformer_encoder.layers.22.norm2.bias +transformer_encoder.layers.23.self_attn.in_proj_weight +transformer_encoder.layers.23.self_attn.in_proj_bias +transformer_encoder.layers.23.self_attn.out_proj.weight +transformer_encoder.layers.23.self_attn.out_proj.bias +transformer_encoder.layers.23.linear1.weight +transformer_encoder.layers.23.linear1.bias +transformer_encoder.layers.23.linear2.weight +transformer_encoder.layers.23.linear2.bias +transformer_encoder.layers.23.norm1.weight +transformer_encoder.layers.23.norm1.bias +transformer_encoder.layers.23.norm2.weight +transformer_encoder.layers.23.norm2.bias +ar_predict_layer.weight diff --git a/genie_tts/Data/v2/Keys/vits_onnx_keys.txt b/genie_tts/Data/v2/Keys/vits_onnx_keys.txt new file mode 100644 index 0000000000000000000000000000000000000000..d4f0b4bbe0d8fd259d320e2f2c7967e0a92660a1 --- /dev/null +++ b/genie_tts/Data/v2/Keys/vits_onnx_keys.txt @@ -0,0 +1,668 @@ +vq_model.dec.cond.bias +vq_model.dec.cond.weight +vq_model.dec.conv_post.weight +vq_model.dec.conv_pre.bias +vq_model.dec.conv_pre.weight +vq_model.dec.resblocks.0.convs1.0.bias +vq_model.dec.resblocks.0.convs1.0.weight_g +vq_model.dec.resblocks.0.convs1.0.weight_v +vq_model.dec.resblocks.0.convs1.1.bias +vq_model.dec.resblocks.0.convs1.1.weight_g +vq_model.dec.resblocks.0.convs1.1.weight_v +vq_model.dec.resblocks.0.convs1.2.bias +vq_model.dec.resblocks.0.convs1.2.weight_g +vq_model.dec.resblocks.0.convs1.2.weight_v +vq_model.dec.resblocks.0.convs2.0.bias +vq_model.dec.resblocks.0.convs2.0.weight_g +vq_model.dec.resblocks.0.convs2.0.weight_v +vq_model.dec.resblocks.0.convs2.1.bias +vq_model.dec.resblocks.0.convs2.1.weight_g +vq_model.dec.resblocks.0.convs2.1.weight_v +vq_model.dec.resblocks.0.convs2.2.bias +vq_model.dec.resblocks.0.convs2.2.weight_g +vq_model.dec.resblocks.0.convs2.2.weight_v +vq_model.dec.resblocks.1.convs1.0.bias +vq_model.dec.resblocks.1.convs1.0.weight_g +vq_model.dec.resblocks.1.convs1.0.weight_v +vq_model.dec.resblocks.1.convs1.1.bias +vq_model.dec.resblocks.1.convs1.1.weight_g +vq_model.dec.resblocks.1.convs1.1.weight_v +vq_model.dec.resblocks.1.convs1.2.bias +vq_model.dec.resblocks.1.convs1.2.weight_g +vq_model.dec.resblocks.1.convs1.2.weight_v +vq_model.dec.resblocks.1.convs2.0.bias +vq_model.dec.resblocks.1.convs2.0.weight_g +vq_model.dec.resblocks.1.convs2.0.weight_v +vq_model.dec.resblocks.1.convs2.1.bias +vq_model.dec.resblocks.1.convs2.1.weight_g +vq_model.dec.resblocks.1.convs2.1.weight_v +vq_model.dec.resblocks.1.convs2.2.bias +vq_model.dec.resblocks.1.convs2.2.weight_g +vq_model.dec.resblocks.1.convs2.2.weight_v +vq_model.dec.resblocks.10.convs1.0.bias +vq_model.dec.resblocks.10.convs1.0.weight_g +vq_model.dec.resblocks.10.convs1.0.weight_v +vq_model.dec.resblocks.10.convs1.1.bias +vq_model.dec.resblocks.10.convs1.1.weight_g +vq_model.dec.resblocks.10.convs1.1.weight_v +vq_model.dec.resblocks.10.convs1.2.bias +vq_model.dec.resblocks.10.convs1.2.weight_g +vq_model.dec.resblocks.10.convs1.2.weight_v +vq_model.dec.resblocks.10.convs2.0.bias +vq_model.dec.resblocks.10.convs2.0.weight_g +vq_model.dec.resblocks.10.convs2.0.weight_v +vq_model.dec.resblocks.10.convs2.1.bias +vq_model.dec.resblocks.10.convs2.1.weight_g +vq_model.dec.resblocks.10.convs2.1.weight_v +vq_model.dec.resblocks.10.convs2.2.bias +vq_model.dec.resblocks.10.convs2.2.weight_g +vq_model.dec.resblocks.10.convs2.2.weight_v +vq_model.dec.resblocks.11.convs1.0.bias +vq_model.dec.resblocks.11.convs1.0.weight_g +vq_model.dec.resblocks.11.convs1.0.weight_v +vq_model.dec.resblocks.11.convs1.1.bias +vq_model.dec.resblocks.11.convs1.1.weight_g +vq_model.dec.resblocks.11.convs1.1.weight_v +vq_model.dec.resblocks.11.convs1.2.bias +vq_model.dec.resblocks.11.convs1.2.weight_g +vq_model.dec.resblocks.11.convs1.2.weight_v +vq_model.dec.resblocks.11.convs2.0.bias +vq_model.dec.resblocks.11.convs2.0.weight_g +vq_model.dec.resblocks.11.convs2.0.weight_v +vq_model.dec.resblocks.11.convs2.1.bias +vq_model.dec.resblocks.11.convs2.1.weight_g +vq_model.dec.resblocks.11.convs2.1.weight_v +vq_model.dec.resblocks.11.convs2.2.bias +vq_model.dec.resblocks.11.convs2.2.weight_g +vq_model.dec.resblocks.11.convs2.2.weight_v +vq_model.dec.resblocks.12.convs1.0.bias +vq_model.dec.resblocks.12.convs1.0.weight_g +vq_model.dec.resblocks.12.convs1.0.weight_v +vq_model.dec.resblocks.12.convs1.1.bias +vq_model.dec.resblocks.12.convs1.1.weight_g +vq_model.dec.resblocks.12.convs1.1.weight_v +vq_model.dec.resblocks.12.convs1.2.bias +vq_model.dec.resblocks.12.convs1.2.weight_g +vq_model.dec.resblocks.12.convs1.2.weight_v +vq_model.dec.resblocks.12.convs2.0.bias +vq_model.dec.resblocks.12.convs2.0.weight_g +vq_model.dec.resblocks.12.convs2.0.weight_v +vq_model.dec.resblocks.12.convs2.1.bias +vq_model.dec.resblocks.12.convs2.1.weight_g +vq_model.dec.resblocks.12.convs2.1.weight_v +vq_model.dec.resblocks.12.convs2.2.bias +vq_model.dec.resblocks.12.convs2.2.weight_g +vq_model.dec.resblocks.12.convs2.2.weight_v +vq_model.dec.resblocks.13.convs1.0.bias +vq_model.dec.resblocks.13.convs1.0.weight_g +vq_model.dec.resblocks.13.convs1.0.weight_v +vq_model.dec.resblocks.13.convs1.1.bias +vq_model.dec.resblocks.13.convs1.1.weight_g +vq_model.dec.resblocks.13.convs1.1.weight_v +vq_model.dec.resblocks.13.convs1.2.bias +vq_model.dec.resblocks.13.convs1.2.weight_g +vq_model.dec.resblocks.13.convs1.2.weight_v +vq_model.dec.resblocks.13.convs2.0.bias +vq_model.dec.resblocks.13.convs2.0.weight_g +vq_model.dec.resblocks.13.convs2.0.weight_v +vq_model.dec.resblocks.13.convs2.1.bias +vq_model.dec.resblocks.13.convs2.1.weight_g +vq_model.dec.resblocks.13.convs2.1.weight_v +vq_model.dec.resblocks.13.convs2.2.bias +vq_model.dec.resblocks.13.convs2.2.weight_g +vq_model.dec.resblocks.13.convs2.2.weight_v +vq_model.dec.resblocks.14.convs1.0.bias +vq_model.dec.resblocks.14.convs1.0.weight_g +vq_model.dec.resblocks.14.convs1.0.weight_v +vq_model.dec.resblocks.14.convs1.1.bias +vq_model.dec.resblocks.14.convs1.1.weight_g +vq_model.dec.resblocks.14.convs1.1.weight_v +vq_model.dec.resblocks.14.convs1.2.bias +vq_model.dec.resblocks.14.convs1.2.weight_g +vq_model.dec.resblocks.14.convs1.2.weight_v +vq_model.dec.resblocks.14.convs2.0.bias +vq_model.dec.resblocks.14.convs2.0.weight_g +vq_model.dec.resblocks.14.convs2.0.weight_v +vq_model.dec.resblocks.14.convs2.1.bias +vq_model.dec.resblocks.14.convs2.1.weight_g +vq_model.dec.resblocks.14.convs2.1.weight_v +vq_model.dec.resblocks.14.convs2.2.bias +vq_model.dec.resblocks.14.convs2.2.weight_g +vq_model.dec.resblocks.14.convs2.2.weight_v +vq_model.dec.resblocks.2.convs1.0.bias +vq_model.dec.resblocks.2.convs1.0.weight_g +vq_model.dec.resblocks.2.convs1.0.weight_v +vq_model.dec.resblocks.2.convs1.1.bias +vq_model.dec.resblocks.2.convs1.1.weight_g +vq_model.dec.resblocks.2.convs1.1.weight_v +vq_model.dec.resblocks.2.convs1.2.bias +vq_model.dec.resblocks.2.convs1.2.weight_g +vq_model.dec.resblocks.2.convs1.2.weight_v +vq_model.dec.resblocks.2.convs2.0.bias +vq_model.dec.resblocks.2.convs2.0.weight_g +vq_model.dec.resblocks.2.convs2.0.weight_v +vq_model.dec.resblocks.2.convs2.1.bias +vq_model.dec.resblocks.2.convs2.1.weight_g +vq_model.dec.resblocks.2.convs2.1.weight_v +vq_model.dec.resblocks.2.convs2.2.bias +vq_model.dec.resblocks.2.convs2.2.weight_g +vq_model.dec.resblocks.2.convs2.2.weight_v +vq_model.dec.resblocks.3.convs1.0.bias +vq_model.dec.resblocks.3.convs1.0.weight_g +vq_model.dec.resblocks.3.convs1.0.weight_v +vq_model.dec.resblocks.3.convs1.1.bias +vq_model.dec.resblocks.3.convs1.1.weight_g +vq_model.dec.resblocks.3.convs1.1.weight_v +vq_model.dec.resblocks.3.convs1.2.bias +vq_model.dec.resblocks.3.convs1.2.weight_g +vq_model.dec.resblocks.3.convs1.2.weight_v +vq_model.dec.resblocks.3.convs2.0.bias +vq_model.dec.resblocks.3.convs2.0.weight_g +vq_model.dec.resblocks.3.convs2.0.weight_v +vq_model.dec.resblocks.3.convs2.1.bias +vq_model.dec.resblocks.3.convs2.1.weight_g +vq_model.dec.resblocks.3.convs2.1.weight_v +vq_model.dec.resblocks.3.convs2.2.bias +vq_model.dec.resblocks.3.convs2.2.weight_g +vq_model.dec.resblocks.3.convs2.2.weight_v +vq_model.dec.resblocks.4.convs1.0.bias +vq_model.dec.resblocks.4.convs1.0.weight_g +vq_model.dec.resblocks.4.convs1.0.weight_v +vq_model.dec.resblocks.4.convs1.1.bias +vq_model.dec.resblocks.4.convs1.1.weight_g +vq_model.dec.resblocks.4.convs1.1.weight_v +vq_model.dec.resblocks.4.convs1.2.bias +vq_model.dec.resblocks.4.convs1.2.weight_g +vq_model.dec.resblocks.4.convs1.2.weight_v +vq_model.dec.resblocks.4.convs2.0.bias +vq_model.dec.resblocks.4.convs2.0.weight_g +vq_model.dec.resblocks.4.convs2.0.weight_v +vq_model.dec.resblocks.4.convs2.1.bias +vq_model.dec.resblocks.4.convs2.1.weight_g +vq_model.dec.resblocks.4.convs2.1.weight_v +vq_model.dec.resblocks.4.convs2.2.bias +vq_model.dec.resblocks.4.convs2.2.weight_g +vq_model.dec.resblocks.4.convs2.2.weight_v +vq_model.dec.resblocks.5.convs1.0.bias +vq_model.dec.resblocks.5.convs1.0.weight_g +vq_model.dec.resblocks.5.convs1.0.weight_v +vq_model.dec.resblocks.5.convs1.1.bias +vq_model.dec.resblocks.5.convs1.1.weight_g +vq_model.dec.resblocks.5.convs1.1.weight_v +vq_model.dec.resblocks.5.convs1.2.bias +vq_model.dec.resblocks.5.convs1.2.weight_g +vq_model.dec.resblocks.5.convs1.2.weight_v +vq_model.dec.resblocks.5.convs2.0.bias +vq_model.dec.resblocks.5.convs2.0.weight_g +vq_model.dec.resblocks.5.convs2.0.weight_v +vq_model.dec.resblocks.5.convs2.1.bias +vq_model.dec.resblocks.5.convs2.1.weight_g +vq_model.dec.resblocks.5.convs2.1.weight_v +vq_model.dec.resblocks.5.convs2.2.bias +vq_model.dec.resblocks.5.convs2.2.weight_g +vq_model.dec.resblocks.5.convs2.2.weight_v +vq_model.dec.resblocks.6.convs1.0.bias +vq_model.dec.resblocks.6.convs1.0.weight_g +vq_model.dec.resblocks.6.convs1.0.weight_v +vq_model.dec.resblocks.6.convs1.1.bias +vq_model.dec.resblocks.6.convs1.1.weight_g +vq_model.dec.resblocks.6.convs1.1.weight_v +vq_model.dec.resblocks.6.convs1.2.bias +vq_model.dec.resblocks.6.convs1.2.weight_g +vq_model.dec.resblocks.6.convs1.2.weight_v +vq_model.dec.resblocks.6.convs2.0.bias +vq_model.dec.resblocks.6.convs2.0.weight_g +vq_model.dec.resblocks.6.convs2.0.weight_v +vq_model.dec.resblocks.6.convs2.1.bias +vq_model.dec.resblocks.6.convs2.1.weight_g +vq_model.dec.resblocks.6.convs2.1.weight_v +vq_model.dec.resblocks.6.convs2.2.bias +vq_model.dec.resblocks.6.convs2.2.weight_g +vq_model.dec.resblocks.6.convs2.2.weight_v +vq_model.dec.resblocks.7.convs1.0.bias +vq_model.dec.resblocks.7.convs1.0.weight_g +vq_model.dec.resblocks.7.convs1.0.weight_v +vq_model.dec.resblocks.7.convs1.1.bias +vq_model.dec.resblocks.7.convs1.1.weight_g +vq_model.dec.resblocks.7.convs1.1.weight_v +vq_model.dec.resblocks.7.convs1.2.bias +vq_model.dec.resblocks.7.convs1.2.weight_g +vq_model.dec.resblocks.7.convs1.2.weight_v +vq_model.dec.resblocks.7.convs2.0.bias +vq_model.dec.resblocks.7.convs2.0.weight_g +vq_model.dec.resblocks.7.convs2.0.weight_v +vq_model.dec.resblocks.7.convs2.1.bias +vq_model.dec.resblocks.7.convs2.1.weight_g +vq_model.dec.resblocks.7.convs2.1.weight_v +vq_model.dec.resblocks.7.convs2.2.bias +vq_model.dec.resblocks.7.convs2.2.weight_g +vq_model.dec.resblocks.7.convs2.2.weight_v +vq_model.dec.resblocks.8.convs1.0.bias +vq_model.dec.resblocks.8.convs1.0.weight_g +vq_model.dec.resblocks.8.convs1.0.weight_v +vq_model.dec.resblocks.8.convs1.1.bias +vq_model.dec.resblocks.8.convs1.1.weight_g +vq_model.dec.resblocks.8.convs1.1.weight_v +vq_model.dec.resblocks.8.convs1.2.bias +vq_model.dec.resblocks.8.convs1.2.weight_g +vq_model.dec.resblocks.8.convs1.2.weight_v +vq_model.dec.resblocks.8.convs2.0.bias +vq_model.dec.resblocks.8.convs2.0.weight_g +vq_model.dec.resblocks.8.convs2.0.weight_v +vq_model.dec.resblocks.8.convs2.1.bias +vq_model.dec.resblocks.8.convs2.1.weight_g +vq_model.dec.resblocks.8.convs2.1.weight_v +vq_model.dec.resblocks.8.convs2.2.bias +vq_model.dec.resblocks.8.convs2.2.weight_g +vq_model.dec.resblocks.8.convs2.2.weight_v +vq_model.dec.resblocks.9.convs1.0.bias +vq_model.dec.resblocks.9.convs1.0.weight_g +vq_model.dec.resblocks.9.convs1.0.weight_v +vq_model.dec.resblocks.9.convs1.1.bias +vq_model.dec.resblocks.9.convs1.1.weight_g +vq_model.dec.resblocks.9.convs1.1.weight_v +vq_model.dec.resblocks.9.convs1.2.bias +vq_model.dec.resblocks.9.convs1.2.weight_g +vq_model.dec.resblocks.9.convs1.2.weight_v +vq_model.dec.resblocks.9.convs2.0.bias +vq_model.dec.resblocks.9.convs2.0.weight_g +vq_model.dec.resblocks.9.convs2.0.weight_v +vq_model.dec.resblocks.9.convs2.1.bias +vq_model.dec.resblocks.9.convs2.1.weight_g +vq_model.dec.resblocks.9.convs2.1.weight_v +vq_model.dec.resblocks.9.convs2.2.bias +vq_model.dec.resblocks.9.convs2.2.weight_g +vq_model.dec.resblocks.9.convs2.2.weight_v +vq_model.dec.ups.0.bias +vq_model.dec.ups.0.weight_g +vq_model.dec.ups.0.weight_v +vq_model.dec.ups.1.bias +vq_model.dec.ups.1.weight_g +vq_model.dec.ups.1.weight_v +vq_model.dec.ups.2.bias +vq_model.dec.ups.2.weight_g +vq_model.dec.ups.2.weight_v +vq_model.dec.ups.3.bias +vq_model.dec.ups.3.weight_g +vq_model.dec.ups.3.weight_v +vq_model.dec.ups.4.bias +vq_model.dec.ups.4.weight_g +vq_model.dec.ups.4.weight_v +vq_model.enc_p.encoder2.attn_layers.0.conv_k.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_k.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_o.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_o.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_q.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_q.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_v.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_v.weight +vq_model.enc_p.encoder2.attn_layers.0.emb_rel_k +vq_model.enc_p.encoder2.attn_layers.0.emb_rel_v +vq_model.enc_p.encoder2.attn_layers.1.conv_k.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_k.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_o.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_o.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_q.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_q.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_v.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_v.weight +vq_model.enc_p.encoder2.attn_layers.1.emb_rel_k +vq_model.enc_p.encoder2.attn_layers.1.emb_rel_v +vq_model.enc_p.encoder2.attn_layers.2.conv_k.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_k.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_o.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_o.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_q.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_q.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_v.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_v.weight +vq_model.enc_p.encoder2.attn_layers.2.emb_rel_k +vq_model.enc_p.encoder2.attn_layers.2.emb_rel_v +vq_model.enc_p.encoder2.ffn_layers.0.conv_1.bias +vq_model.enc_p.encoder2.ffn_layers.0.conv_1.weight +vq_model.enc_p.encoder2.ffn_layers.0.conv_2.bias +vq_model.enc_p.encoder2.ffn_layers.0.conv_2.weight +vq_model.enc_p.encoder2.ffn_layers.1.conv_1.bias +vq_model.enc_p.encoder2.ffn_layers.1.conv_1.weight +vq_model.enc_p.encoder2.ffn_layers.1.conv_2.bias +vq_model.enc_p.encoder2.ffn_layers.1.conv_2.weight +vq_model.enc_p.encoder2.ffn_layers.2.conv_1.bias +vq_model.enc_p.encoder2.ffn_layers.2.conv_1.weight +vq_model.enc_p.encoder2.ffn_layers.2.conv_2.bias +vq_model.enc_p.encoder2.ffn_layers.2.conv_2.weight +vq_model.enc_p.encoder2.norm_layers_1.0.beta +vq_model.enc_p.encoder2.norm_layers_1.0.gamma +vq_model.enc_p.encoder2.norm_layers_1.1.beta +vq_model.enc_p.encoder2.norm_layers_1.1.gamma +vq_model.enc_p.encoder2.norm_layers_1.2.beta +vq_model.enc_p.encoder2.norm_layers_1.2.gamma +vq_model.enc_p.encoder2.norm_layers_2.0.beta +vq_model.enc_p.encoder2.norm_layers_2.0.gamma +vq_model.enc_p.encoder2.norm_layers_2.1.beta +vq_model.enc_p.encoder2.norm_layers_2.1.gamma +vq_model.enc_p.encoder2.norm_layers_2.2.beta +vq_model.enc_p.encoder2.norm_layers_2.2.gamma +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_k.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_k.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_o.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_o.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_q.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_q.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_v.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_v.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.emb_rel_k +vq_model.enc_p.encoder_ssl.attn_layers.0.emb_rel_v +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_k.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_k.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_o.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_o.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_q.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_q.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_v.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_v.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.emb_rel_k +vq_model.enc_p.encoder_ssl.attn_layers.1.emb_rel_v +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_k.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_k.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_o.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_o.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_q.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_q.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_v.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_v.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.emb_rel_k +vq_model.enc_p.encoder_ssl.attn_layers.2.emb_rel_v +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_1.bias +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_1.weight +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_2.bias +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_2.weight +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_1.bias +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_1.weight +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_2.bias +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_2.weight +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_1.bias +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_1.weight +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_2.bias +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_2.weight +vq_model.enc_p.encoder_ssl.norm_layers_1.0.beta +vq_model.enc_p.encoder_ssl.norm_layers_1.0.gamma +vq_model.enc_p.encoder_ssl.norm_layers_1.1.beta +vq_model.enc_p.encoder_ssl.norm_layers_1.1.gamma +vq_model.enc_p.encoder_ssl.norm_layers_1.2.beta +vq_model.enc_p.encoder_ssl.norm_layers_1.2.gamma +vq_model.enc_p.encoder_ssl.norm_layers_2.0.beta +vq_model.enc_p.encoder_ssl.norm_layers_2.0.gamma +vq_model.enc_p.encoder_ssl.norm_layers_2.1.beta +vq_model.enc_p.encoder_ssl.norm_layers_2.1.gamma +vq_model.enc_p.encoder_ssl.norm_layers_2.2.beta +vq_model.enc_p.encoder_ssl.norm_layers_2.2.gamma +vq_model.enc_p.encoder_text.attn_layers.0.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.0.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.0.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.1.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.1.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.1.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.2.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.2.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.2.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.3.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.3.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.3.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.4.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.4.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.4.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.5.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.5.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.5.emb_rel_v +vq_model.enc_p.encoder_text.ffn_layers.0.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.0.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.0.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.0.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.1.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.1.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.1.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.1.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.2.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.2.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.2.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.2.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.3.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.3.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.3.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.3.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.4.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.4.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.4.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.4.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.5.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.5.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.5.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.5.conv_2.weight +vq_model.enc_p.encoder_text.norm_layers_1.0.beta +vq_model.enc_p.encoder_text.norm_layers_1.0.gamma +vq_model.enc_p.encoder_text.norm_layers_1.1.beta +vq_model.enc_p.encoder_text.norm_layers_1.1.gamma +vq_model.enc_p.encoder_text.norm_layers_1.2.beta +vq_model.enc_p.encoder_text.norm_layers_1.2.gamma +vq_model.enc_p.encoder_text.norm_layers_1.3.beta +vq_model.enc_p.encoder_text.norm_layers_1.3.gamma +vq_model.enc_p.encoder_text.norm_layers_1.4.beta +vq_model.enc_p.encoder_text.norm_layers_1.4.gamma +vq_model.enc_p.encoder_text.norm_layers_1.5.beta +vq_model.enc_p.encoder_text.norm_layers_1.5.gamma +vq_model.enc_p.encoder_text.norm_layers_2.0.beta +vq_model.enc_p.encoder_text.norm_layers_2.0.gamma +vq_model.enc_p.encoder_text.norm_layers_2.1.beta +vq_model.enc_p.encoder_text.norm_layers_2.1.gamma +vq_model.enc_p.encoder_text.norm_layers_2.2.beta +vq_model.enc_p.encoder_text.norm_layers_2.2.gamma +vq_model.enc_p.encoder_text.norm_layers_2.3.beta +vq_model.enc_p.encoder_text.norm_layers_2.3.gamma +vq_model.enc_p.encoder_text.norm_layers_2.4.beta +vq_model.enc_p.encoder_text.norm_layers_2.4.gamma +vq_model.enc_p.encoder_text.norm_layers_2.5.beta +vq_model.enc_p.encoder_text.norm_layers_2.5.gamma +vq_model.enc_p.mrte.c_post.bias +vq_model.enc_p.mrte.c_post.weight +vq_model.enc_p.mrte.c_pre.bias +vq_model.enc_p.mrte.c_pre.weight +vq_model.enc_p.mrte.cross_attention.conv_k.bias +vq_model.enc_p.mrte.cross_attention.conv_k.weight +vq_model.enc_p.mrte.cross_attention.conv_o.bias +vq_model.enc_p.mrte.cross_attention.conv_o.weight +vq_model.enc_p.mrte.cross_attention.conv_q.bias +vq_model.enc_p.mrte.cross_attention.conv_q.weight +vq_model.enc_p.mrte.cross_attention.conv_v.bias +vq_model.enc_p.mrte.cross_attention.conv_v.weight +vq_model.enc_p.mrte.text_pre.bias +vq_model.enc_p.mrte.text_pre.weight +vq_model.enc_p.proj.bias +vq_model.enc_p.proj.weight +vq_model.enc_p.ssl_proj.bias +vq_model.enc_p.ssl_proj.weight +vq_model.enc_p.text_embedding.weight +vq_model.flow.flows.0.enc.cond_layer.bias +vq_model.flow.flows.0.enc.cond_layer.weight_g +vq_model.flow.flows.0.enc.cond_layer.weight_v +vq_model.flow.flows.0.enc.in_layers.0.bias +vq_model.flow.flows.0.enc.in_layers.0.weight_g +vq_model.flow.flows.0.enc.in_layers.0.weight_v +vq_model.flow.flows.0.enc.in_layers.1.bias +vq_model.flow.flows.0.enc.in_layers.1.weight_g +vq_model.flow.flows.0.enc.in_layers.1.weight_v +vq_model.flow.flows.0.enc.in_layers.2.bias +vq_model.flow.flows.0.enc.in_layers.2.weight_g +vq_model.flow.flows.0.enc.in_layers.2.weight_v +vq_model.flow.flows.0.enc.in_layers.3.bias +vq_model.flow.flows.0.enc.in_layers.3.weight_g +vq_model.flow.flows.0.enc.in_layers.3.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.0.bias +vq_model.flow.flows.0.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.1.bias +vq_model.flow.flows.0.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.2.bias +vq_model.flow.flows.0.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.3.bias +vq_model.flow.flows.0.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.0.post.bias +vq_model.flow.flows.0.post.weight +vq_model.flow.flows.0.pre.bias +vq_model.flow.flows.0.pre.weight +vq_model.flow.flows.2.enc.cond_layer.bias +vq_model.flow.flows.2.enc.cond_layer.weight_g +vq_model.flow.flows.2.enc.cond_layer.weight_v +vq_model.flow.flows.2.enc.in_layers.0.bias +vq_model.flow.flows.2.enc.in_layers.0.weight_g +vq_model.flow.flows.2.enc.in_layers.0.weight_v +vq_model.flow.flows.2.enc.in_layers.1.bias +vq_model.flow.flows.2.enc.in_layers.1.weight_g +vq_model.flow.flows.2.enc.in_layers.1.weight_v +vq_model.flow.flows.2.enc.in_layers.2.bias +vq_model.flow.flows.2.enc.in_layers.2.weight_g +vq_model.flow.flows.2.enc.in_layers.2.weight_v +vq_model.flow.flows.2.enc.in_layers.3.bias +vq_model.flow.flows.2.enc.in_layers.3.weight_g +vq_model.flow.flows.2.enc.in_layers.3.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.0.bias +vq_model.flow.flows.2.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.1.bias +vq_model.flow.flows.2.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.2.bias +vq_model.flow.flows.2.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.3.bias +vq_model.flow.flows.2.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.2.post.bias +vq_model.flow.flows.2.post.weight +vq_model.flow.flows.2.pre.bias +vq_model.flow.flows.2.pre.weight +vq_model.flow.flows.4.enc.cond_layer.bias +vq_model.flow.flows.4.enc.cond_layer.weight_g +vq_model.flow.flows.4.enc.cond_layer.weight_v +vq_model.flow.flows.4.enc.in_layers.0.bias +vq_model.flow.flows.4.enc.in_layers.0.weight_g +vq_model.flow.flows.4.enc.in_layers.0.weight_v +vq_model.flow.flows.4.enc.in_layers.1.bias +vq_model.flow.flows.4.enc.in_layers.1.weight_g +vq_model.flow.flows.4.enc.in_layers.1.weight_v +vq_model.flow.flows.4.enc.in_layers.2.bias +vq_model.flow.flows.4.enc.in_layers.2.weight_g +vq_model.flow.flows.4.enc.in_layers.2.weight_v +vq_model.flow.flows.4.enc.in_layers.3.bias +vq_model.flow.flows.4.enc.in_layers.3.weight_g +vq_model.flow.flows.4.enc.in_layers.3.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.0.bias +vq_model.flow.flows.4.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.1.bias +vq_model.flow.flows.4.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.2.bias +vq_model.flow.flows.4.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.3.bias +vq_model.flow.flows.4.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.4.post.bias +vq_model.flow.flows.4.post.weight +vq_model.flow.flows.4.pre.bias +vq_model.flow.flows.4.pre.weight +vq_model.flow.flows.6.enc.cond_layer.bias +vq_model.flow.flows.6.enc.cond_layer.weight_g +vq_model.flow.flows.6.enc.cond_layer.weight_v +vq_model.flow.flows.6.enc.in_layers.0.bias +vq_model.flow.flows.6.enc.in_layers.0.weight_g +vq_model.flow.flows.6.enc.in_layers.0.weight_v +vq_model.flow.flows.6.enc.in_layers.1.bias +vq_model.flow.flows.6.enc.in_layers.1.weight_g +vq_model.flow.flows.6.enc.in_layers.1.weight_v +vq_model.flow.flows.6.enc.in_layers.2.bias +vq_model.flow.flows.6.enc.in_layers.2.weight_g +vq_model.flow.flows.6.enc.in_layers.2.weight_v +vq_model.flow.flows.6.enc.in_layers.3.bias +vq_model.flow.flows.6.enc.in_layers.3.weight_g +vq_model.flow.flows.6.enc.in_layers.3.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.0.bias +vq_model.flow.flows.6.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.1.bias +vq_model.flow.flows.6.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.2.bias +vq_model.flow.flows.6.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.3.bias +vq_model.flow.flows.6.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.6.post.bias +vq_model.flow.flows.6.post.weight +vq_model.flow.flows.6.pre.bias +vq_model.flow.flows.6.pre.weight +vq_model.quantizer.vq.layers.0._codebook.embed +vq_model.ref_enc.fc.fc.bias +vq_model.ref_enc.fc.fc.weight +vq_model.ref_enc.slf_attn.fc.bias +vq_model.ref_enc.slf_attn.fc.weight +vq_model.ref_enc.slf_attn.w_ks.bias +vq_model.ref_enc.slf_attn.w_ks.weight +vq_model.ref_enc.slf_attn.w_qs.bias +vq_model.ref_enc.slf_attn.w_qs.weight +vq_model.ref_enc.slf_attn.w_vs.bias +vq_model.ref_enc.slf_attn.w_vs.weight +vq_model.ref_enc.spectral.0.fc.bias +vq_model.ref_enc.spectral.0.fc.weight +vq_model.ref_enc.spectral.3.fc.bias +vq_model.ref_enc.spectral.3.fc.weight +vq_model.ref_enc.temporal.0.conv1.conv.bias +vq_model.ref_enc.temporal.0.conv1.conv.weight +vq_model.ref_enc.temporal.1.conv1.conv.bias +vq_model.ref_enc.temporal.1.conv1.conv.weight diff --git a/genie_tts/Data/v2/Models/t2s_encoder_fp32.onnx b/genie_tts/Data/v2/Models/t2s_encoder_fp32.onnx new file mode 100644 index 0000000000000000000000000000000000000000..0bb65b8625d463ac752ad0cedd0adb64c7211957 --- /dev/null +++ b/genie_tts/Data/v2/Models/t2s_encoder_fp32.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6eb1acd47c8e6d36b777886981a49122e8e070a5eb9888d458fb188dc139f75 +size 14568 diff --git a/genie_tts/Data/v2/Models/t2s_first_stage_decoder_fp32.onnx b/genie_tts/Data/v2/Models/t2s_first_stage_decoder_fp32.onnx new file mode 100644 index 0000000000000000000000000000000000000000..a89e7198fbe241c3f1feca324809d39d04d33f38 --- /dev/null +++ b/genie_tts/Data/v2/Models/t2s_first_stage_decoder_fp32.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee7ddd22f27247f49b028c5ae8911f1f8b9ce97538c612dbb9f96057799da4c2 +size 423076 diff --git a/genie_tts/Data/v2/Models/t2s_stage_decoder_fp32.onnx b/genie_tts/Data/v2/Models/t2s_stage_decoder_fp32.onnx new file mode 100644 index 0000000000000000000000000000000000000000..fe5b9a0c90f9f7c08fdc4d1c4fd06f886965bce3 --- /dev/null +++ b/genie_tts/Data/v2/Models/t2s_stage_decoder_fp32.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de6189e17efbad95479be95ca3691f19e7c69bdaaf63b171aa27283e41660dea +size 422151 diff --git a/genie_tts/Data/v2/Models/vits_fp32.onnx b/genie_tts/Data/v2/Models/vits_fp32.onnx new file mode 100644 index 0000000000000000000000000000000000000000..28b1cc3c8d750bddbb25c1bd1b12f695a9af3bbf --- /dev/null +++ b/genie_tts/Data/v2/Models/vits_fp32.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634c21025f9fea03901c1fb5741a98ada512adeb67183e79b261c059fa4d842a +size 1654845 diff --git a/genie_tts/Data/v2ProPlus/Keys/prompt_encoder_weights.txt b/genie_tts/Data/v2ProPlus/Keys/prompt_encoder_weights.txt new file mode 100644 index 0000000000000000000000000000000000000000..8ae9345f6b061d27362302611f0fe08c8091eb29 --- /dev/null +++ b/genie_tts/Data/v2ProPlus/Keys/prompt_encoder_weights.txt @@ -0,0 +1,23 @@ +ref_enc.spectral.0.fc.weight +ref_enc.spectral.0.fc.bias +ref_enc.spectral.3.fc.weight +ref_enc.spectral.3.fc.bias +ref_enc.temporal.0.conv1.conv.weight +ref_enc.temporal.0.conv1.conv.bias +ref_enc.temporal.1.conv1.conv.weight +ref_enc.temporal.1.conv1.conv.bias +ref_enc.slf_attn.w_qs.weight +ref_enc.slf_attn.w_qs.bias +ref_enc.slf_attn.w_ks.weight +ref_enc.slf_attn.w_ks.bias +ref_enc.slf_attn.w_vs.weight +ref_enc.slf_attn.w_vs.bias +ref_enc.slf_attn.fc.weight +ref_enc.slf_attn.fc.bias +ref_enc.fc.fc.weight +ref_enc.fc.fc.bias +sv_emb.weight +sv_emb.bias +ge_to512.weight +ge_to512.bias +prelu.weight diff --git a/genie_tts/Data/v2ProPlus/Keys/vits_weights.txt b/genie_tts/Data/v2ProPlus/Keys/vits_weights.txt new file mode 100644 index 0000000000000000000000000000000000000000..4ed033cab8dfa1bd065ed90ae62e8e02b093c03b --- /dev/null +++ b/genie_tts/Data/v2ProPlus/Keys/vits_weights.txt @@ -0,0 +1,650 @@ +vq_model.enc_p.ssl_proj.weight +vq_model.enc_p.ssl_proj.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.emb_rel_k +vq_model.enc_p.encoder_ssl.attn_layers.0.emb_rel_v +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_q.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_q.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_k.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_k.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_v.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_v.bias +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_o.weight +vq_model.enc_p.encoder_ssl.attn_layers.0.conv_o.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.emb_rel_k +vq_model.enc_p.encoder_ssl.attn_layers.1.emb_rel_v +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_q.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_q.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_k.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_k.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_v.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_v.bias +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_o.weight +vq_model.enc_p.encoder_ssl.attn_layers.1.conv_o.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.emb_rel_k +vq_model.enc_p.encoder_ssl.attn_layers.2.emb_rel_v +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_q.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_q.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_k.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_k.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_v.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_v.bias +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_o.weight +vq_model.enc_p.encoder_ssl.attn_layers.2.conv_o.bias +vq_model.enc_p.encoder_ssl.norm_layers_1.0.gamma +vq_model.enc_p.encoder_ssl.norm_layers_1.0.beta +vq_model.enc_p.encoder_ssl.norm_layers_1.1.gamma +vq_model.enc_p.encoder_ssl.norm_layers_1.1.beta +vq_model.enc_p.encoder_ssl.norm_layers_1.2.gamma +vq_model.enc_p.encoder_ssl.norm_layers_1.2.beta +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_1.weight +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_1.bias +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_2.weight +vq_model.enc_p.encoder_ssl.ffn_layers.0.conv_2.bias +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_1.weight +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_1.bias +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_2.weight +vq_model.enc_p.encoder_ssl.ffn_layers.1.conv_2.bias +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_1.weight +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_1.bias +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_2.weight +vq_model.enc_p.encoder_ssl.ffn_layers.2.conv_2.bias +vq_model.enc_p.encoder_ssl.norm_layers_2.0.gamma +vq_model.enc_p.encoder_ssl.norm_layers_2.0.beta +vq_model.enc_p.encoder_ssl.norm_layers_2.1.gamma +vq_model.enc_p.encoder_ssl.norm_layers_2.1.beta +vq_model.enc_p.encoder_ssl.norm_layers_2.2.gamma +vq_model.enc_p.encoder_ssl.norm_layers_2.2.beta +vq_model.enc_p.encoder_text.attn_layers.0.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.0.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.0.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.0.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.0.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.1.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.1.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.1.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.1.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.1.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.2.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.2.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.2.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.2.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.2.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.3.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.3.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.3.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.3.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.3.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.4.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.4.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.4.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.4.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.4.conv_o.bias +vq_model.enc_p.encoder_text.attn_layers.5.emb_rel_k +vq_model.enc_p.encoder_text.attn_layers.5.emb_rel_v +vq_model.enc_p.encoder_text.attn_layers.5.conv_q.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_q.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_k.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_k.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_v.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_v.bias +vq_model.enc_p.encoder_text.attn_layers.5.conv_o.weight +vq_model.enc_p.encoder_text.attn_layers.5.conv_o.bias +vq_model.enc_p.encoder_text.norm_layers_1.0.gamma +vq_model.enc_p.encoder_text.norm_layers_1.0.beta +vq_model.enc_p.encoder_text.norm_layers_1.1.gamma +vq_model.enc_p.encoder_text.norm_layers_1.1.beta +vq_model.enc_p.encoder_text.norm_layers_1.2.gamma +vq_model.enc_p.encoder_text.norm_layers_1.2.beta +vq_model.enc_p.encoder_text.norm_layers_1.3.gamma +vq_model.enc_p.encoder_text.norm_layers_1.3.beta +vq_model.enc_p.encoder_text.norm_layers_1.4.gamma +vq_model.enc_p.encoder_text.norm_layers_1.4.beta +vq_model.enc_p.encoder_text.norm_layers_1.5.gamma +vq_model.enc_p.encoder_text.norm_layers_1.5.beta +vq_model.enc_p.encoder_text.ffn_layers.0.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.0.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.0.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.0.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.1.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.1.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.1.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.1.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.2.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.2.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.2.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.2.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.3.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.3.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.3.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.3.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.4.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.4.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.4.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.4.conv_2.bias +vq_model.enc_p.encoder_text.ffn_layers.5.conv_1.weight +vq_model.enc_p.encoder_text.ffn_layers.5.conv_1.bias +vq_model.enc_p.encoder_text.ffn_layers.5.conv_2.weight +vq_model.enc_p.encoder_text.ffn_layers.5.conv_2.bias +vq_model.enc_p.encoder_text.norm_layers_2.0.gamma +vq_model.enc_p.encoder_text.norm_layers_2.0.beta +vq_model.enc_p.encoder_text.norm_layers_2.1.gamma +vq_model.enc_p.encoder_text.norm_layers_2.1.beta +vq_model.enc_p.encoder_text.norm_layers_2.2.gamma +vq_model.enc_p.encoder_text.norm_layers_2.2.beta +vq_model.enc_p.encoder_text.norm_layers_2.3.gamma +vq_model.enc_p.encoder_text.norm_layers_2.3.beta +vq_model.enc_p.encoder_text.norm_layers_2.4.gamma +vq_model.enc_p.encoder_text.norm_layers_2.4.beta +vq_model.enc_p.encoder_text.norm_layers_2.5.gamma +vq_model.enc_p.encoder_text.norm_layers_2.5.beta +vq_model.enc_p.text_embedding.weight +vq_model.enc_p.mrte.cross_attention.conv_q.weight +vq_model.enc_p.mrte.cross_attention.conv_q.bias +vq_model.enc_p.mrte.cross_attention.conv_k.weight +vq_model.enc_p.mrte.cross_attention.conv_k.bias +vq_model.enc_p.mrte.cross_attention.conv_v.weight +vq_model.enc_p.mrte.cross_attention.conv_v.bias +vq_model.enc_p.mrte.cross_attention.conv_o.weight +vq_model.enc_p.mrte.cross_attention.conv_o.bias +vq_model.enc_p.mrte.c_pre.weight +vq_model.enc_p.mrte.c_pre.bias +vq_model.enc_p.mrte.text_pre.weight +vq_model.enc_p.mrte.text_pre.bias +vq_model.enc_p.mrte.c_post.weight +vq_model.enc_p.mrte.c_post.bias +vq_model.enc_p.encoder2.attn_layers.0.emb_rel_k +vq_model.enc_p.encoder2.attn_layers.0.emb_rel_v +vq_model.enc_p.encoder2.attn_layers.0.conv_q.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_q.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_k.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_k.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_v.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_v.bias +vq_model.enc_p.encoder2.attn_layers.0.conv_o.weight +vq_model.enc_p.encoder2.attn_layers.0.conv_o.bias +vq_model.enc_p.encoder2.attn_layers.1.emb_rel_k +vq_model.enc_p.encoder2.attn_layers.1.emb_rel_v +vq_model.enc_p.encoder2.attn_layers.1.conv_q.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_q.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_k.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_k.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_v.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_v.bias +vq_model.enc_p.encoder2.attn_layers.1.conv_o.weight +vq_model.enc_p.encoder2.attn_layers.1.conv_o.bias +vq_model.enc_p.encoder2.attn_layers.2.emb_rel_k +vq_model.enc_p.encoder2.attn_layers.2.emb_rel_v +vq_model.enc_p.encoder2.attn_layers.2.conv_q.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_q.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_k.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_k.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_v.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_v.bias +vq_model.enc_p.encoder2.attn_layers.2.conv_o.weight +vq_model.enc_p.encoder2.attn_layers.2.conv_o.bias +vq_model.enc_p.encoder2.norm_layers_1.0.gamma +vq_model.enc_p.encoder2.norm_layers_1.0.beta +vq_model.enc_p.encoder2.norm_layers_1.1.gamma +vq_model.enc_p.encoder2.norm_layers_1.1.beta +vq_model.enc_p.encoder2.norm_layers_1.2.gamma +vq_model.enc_p.encoder2.norm_layers_1.2.beta +vq_model.enc_p.encoder2.ffn_layers.0.conv_1.weight +vq_model.enc_p.encoder2.ffn_layers.0.conv_1.bias +vq_model.enc_p.encoder2.ffn_layers.0.conv_2.weight +vq_model.enc_p.encoder2.ffn_layers.0.conv_2.bias +vq_model.enc_p.encoder2.ffn_layers.1.conv_1.weight +vq_model.enc_p.encoder2.ffn_layers.1.conv_1.bias +vq_model.enc_p.encoder2.ffn_layers.1.conv_2.weight +vq_model.enc_p.encoder2.ffn_layers.1.conv_2.bias +vq_model.enc_p.encoder2.ffn_layers.2.conv_1.weight +vq_model.enc_p.encoder2.ffn_layers.2.conv_1.bias +vq_model.enc_p.encoder2.ffn_layers.2.conv_2.weight +vq_model.enc_p.encoder2.ffn_layers.2.conv_2.bias +vq_model.enc_p.encoder2.norm_layers_2.0.gamma +vq_model.enc_p.encoder2.norm_layers_2.0.beta +vq_model.enc_p.encoder2.norm_layers_2.1.gamma +vq_model.enc_p.encoder2.norm_layers_2.1.beta +vq_model.enc_p.encoder2.norm_layers_2.2.gamma +vq_model.enc_p.encoder2.norm_layers_2.2.beta +vq_model.enc_p.proj.weight +vq_model.enc_p.proj.bias +vq_model.dec.conv_pre.weight +vq_model.dec.conv_pre.bias +vq_model.dec.ups.0.bias +vq_model.dec.ups.0.weight_g +vq_model.dec.ups.0.weight_v +vq_model.dec.ups.1.bias +vq_model.dec.ups.1.weight_g +vq_model.dec.ups.1.weight_v +vq_model.dec.ups.2.bias +vq_model.dec.ups.2.weight_g +vq_model.dec.ups.2.weight_v +vq_model.dec.ups.3.bias +vq_model.dec.ups.3.weight_g +vq_model.dec.ups.3.weight_v +vq_model.dec.ups.4.bias +vq_model.dec.ups.4.weight_g +vq_model.dec.ups.4.weight_v +vq_model.dec.resblocks.0.convs1.0.bias +vq_model.dec.resblocks.0.convs1.0.weight_g +vq_model.dec.resblocks.0.convs1.0.weight_v +vq_model.dec.resblocks.0.convs1.1.bias +vq_model.dec.resblocks.0.convs1.1.weight_g +vq_model.dec.resblocks.0.convs1.1.weight_v +vq_model.dec.resblocks.0.convs1.2.bias +vq_model.dec.resblocks.0.convs1.2.weight_g +vq_model.dec.resblocks.0.convs1.2.weight_v +vq_model.dec.resblocks.0.convs2.0.bias +vq_model.dec.resblocks.0.convs2.0.weight_g +vq_model.dec.resblocks.0.convs2.0.weight_v +vq_model.dec.resblocks.0.convs2.1.bias +vq_model.dec.resblocks.0.convs2.1.weight_g +vq_model.dec.resblocks.0.convs2.1.weight_v +vq_model.dec.resblocks.0.convs2.2.bias +vq_model.dec.resblocks.0.convs2.2.weight_g +vq_model.dec.resblocks.0.convs2.2.weight_v +vq_model.dec.resblocks.1.convs1.0.bias +vq_model.dec.resblocks.1.convs1.0.weight_g +vq_model.dec.resblocks.1.convs1.0.weight_v +vq_model.dec.resblocks.1.convs1.1.bias +vq_model.dec.resblocks.1.convs1.1.weight_g +vq_model.dec.resblocks.1.convs1.1.weight_v +vq_model.dec.resblocks.1.convs1.2.bias +vq_model.dec.resblocks.1.convs1.2.weight_g +vq_model.dec.resblocks.1.convs1.2.weight_v +vq_model.dec.resblocks.1.convs2.0.bias +vq_model.dec.resblocks.1.convs2.0.weight_g +vq_model.dec.resblocks.1.convs2.0.weight_v +vq_model.dec.resblocks.1.convs2.1.bias +vq_model.dec.resblocks.1.convs2.1.weight_g +vq_model.dec.resblocks.1.convs2.1.weight_v +vq_model.dec.resblocks.1.convs2.2.bias +vq_model.dec.resblocks.1.convs2.2.weight_g +vq_model.dec.resblocks.1.convs2.2.weight_v +vq_model.dec.resblocks.2.convs1.0.bias +vq_model.dec.resblocks.2.convs1.0.weight_g +vq_model.dec.resblocks.2.convs1.0.weight_v +vq_model.dec.resblocks.2.convs1.1.bias +vq_model.dec.resblocks.2.convs1.1.weight_g +vq_model.dec.resblocks.2.convs1.1.weight_v +vq_model.dec.resblocks.2.convs1.2.bias +vq_model.dec.resblocks.2.convs1.2.weight_g +vq_model.dec.resblocks.2.convs1.2.weight_v +vq_model.dec.resblocks.2.convs2.0.bias +vq_model.dec.resblocks.2.convs2.0.weight_g +vq_model.dec.resblocks.2.convs2.0.weight_v +vq_model.dec.resblocks.2.convs2.1.bias +vq_model.dec.resblocks.2.convs2.1.weight_g +vq_model.dec.resblocks.2.convs2.1.weight_v +vq_model.dec.resblocks.2.convs2.2.bias +vq_model.dec.resblocks.2.convs2.2.weight_g +vq_model.dec.resblocks.2.convs2.2.weight_v +vq_model.dec.resblocks.3.convs1.0.bias +vq_model.dec.resblocks.3.convs1.0.weight_g +vq_model.dec.resblocks.3.convs1.0.weight_v +vq_model.dec.resblocks.3.convs1.1.bias +vq_model.dec.resblocks.3.convs1.1.weight_g +vq_model.dec.resblocks.3.convs1.1.weight_v +vq_model.dec.resblocks.3.convs1.2.bias +vq_model.dec.resblocks.3.convs1.2.weight_g +vq_model.dec.resblocks.3.convs1.2.weight_v +vq_model.dec.resblocks.3.convs2.0.bias +vq_model.dec.resblocks.3.convs2.0.weight_g +vq_model.dec.resblocks.3.convs2.0.weight_v +vq_model.dec.resblocks.3.convs2.1.bias +vq_model.dec.resblocks.3.convs2.1.weight_g +vq_model.dec.resblocks.3.convs2.1.weight_v +vq_model.dec.resblocks.3.convs2.2.bias +vq_model.dec.resblocks.3.convs2.2.weight_g +vq_model.dec.resblocks.3.convs2.2.weight_v +vq_model.dec.resblocks.4.convs1.0.bias +vq_model.dec.resblocks.4.convs1.0.weight_g +vq_model.dec.resblocks.4.convs1.0.weight_v +vq_model.dec.resblocks.4.convs1.1.bias +vq_model.dec.resblocks.4.convs1.1.weight_g +vq_model.dec.resblocks.4.convs1.1.weight_v +vq_model.dec.resblocks.4.convs1.2.bias +vq_model.dec.resblocks.4.convs1.2.weight_g +vq_model.dec.resblocks.4.convs1.2.weight_v +vq_model.dec.resblocks.4.convs2.0.bias +vq_model.dec.resblocks.4.convs2.0.weight_g +vq_model.dec.resblocks.4.convs2.0.weight_v +vq_model.dec.resblocks.4.convs2.1.bias +vq_model.dec.resblocks.4.convs2.1.weight_g +vq_model.dec.resblocks.4.convs2.1.weight_v +vq_model.dec.resblocks.4.convs2.2.bias +vq_model.dec.resblocks.4.convs2.2.weight_g +vq_model.dec.resblocks.4.convs2.2.weight_v +vq_model.dec.resblocks.5.convs1.0.bias +vq_model.dec.resblocks.5.convs1.0.weight_g +vq_model.dec.resblocks.5.convs1.0.weight_v +vq_model.dec.resblocks.5.convs1.1.bias +vq_model.dec.resblocks.5.convs1.1.weight_g +vq_model.dec.resblocks.5.convs1.1.weight_v +vq_model.dec.resblocks.5.convs1.2.bias +vq_model.dec.resblocks.5.convs1.2.weight_g +vq_model.dec.resblocks.5.convs1.2.weight_v +vq_model.dec.resblocks.5.convs2.0.bias +vq_model.dec.resblocks.5.convs2.0.weight_g +vq_model.dec.resblocks.5.convs2.0.weight_v +vq_model.dec.resblocks.5.convs2.1.bias +vq_model.dec.resblocks.5.convs2.1.weight_g +vq_model.dec.resblocks.5.convs2.1.weight_v +vq_model.dec.resblocks.5.convs2.2.bias +vq_model.dec.resblocks.5.convs2.2.weight_g +vq_model.dec.resblocks.5.convs2.2.weight_v +vq_model.dec.resblocks.6.convs1.0.bias +vq_model.dec.resblocks.6.convs1.0.weight_g +vq_model.dec.resblocks.6.convs1.0.weight_v +vq_model.dec.resblocks.6.convs1.1.bias +vq_model.dec.resblocks.6.convs1.1.weight_g +vq_model.dec.resblocks.6.convs1.1.weight_v +vq_model.dec.resblocks.6.convs1.2.bias +vq_model.dec.resblocks.6.convs1.2.weight_g +vq_model.dec.resblocks.6.convs1.2.weight_v +vq_model.dec.resblocks.6.convs2.0.bias +vq_model.dec.resblocks.6.convs2.0.weight_g +vq_model.dec.resblocks.6.convs2.0.weight_v +vq_model.dec.resblocks.6.convs2.1.bias +vq_model.dec.resblocks.6.convs2.1.weight_g +vq_model.dec.resblocks.6.convs2.1.weight_v +vq_model.dec.resblocks.6.convs2.2.bias +vq_model.dec.resblocks.6.convs2.2.weight_g +vq_model.dec.resblocks.6.convs2.2.weight_v +vq_model.dec.resblocks.7.convs1.0.bias +vq_model.dec.resblocks.7.convs1.0.weight_g +vq_model.dec.resblocks.7.convs1.0.weight_v +vq_model.dec.resblocks.7.convs1.1.bias +vq_model.dec.resblocks.7.convs1.1.weight_g +vq_model.dec.resblocks.7.convs1.1.weight_v +vq_model.dec.resblocks.7.convs1.2.bias +vq_model.dec.resblocks.7.convs1.2.weight_g +vq_model.dec.resblocks.7.convs1.2.weight_v +vq_model.dec.resblocks.7.convs2.0.bias +vq_model.dec.resblocks.7.convs2.0.weight_g +vq_model.dec.resblocks.7.convs2.0.weight_v +vq_model.dec.resblocks.7.convs2.1.bias +vq_model.dec.resblocks.7.convs2.1.weight_g +vq_model.dec.resblocks.7.convs2.1.weight_v +vq_model.dec.resblocks.7.convs2.2.bias +vq_model.dec.resblocks.7.convs2.2.weight_g +vq_model.dec.resblocks.7.convs2.2.weight_v +vq_model.dec.resblocks.8.convs1.0.bias +vq_model.dec.resblocks.8.convs1.0.weight_g +vq_model.dec.resblocks.8.convs1.0.weight_v +vq_model.dec.resblocks.8.convs1.1.bias +vq_model.dec.resblocks.8.convs1.1.weight_g +vq_model.dec.resblocks.8.convs1.1.weight_v +vq_model.dec.resblocks.8.convs1.2.bias +vq_model.dec.resblocks.8.convs1.2.weight_g +vq_model.dec.resblocks.8.convs1.2.weight_v +vq_model.dec.resblocks.8.convs2.0.bias +vq_model.dec.resblocks.8.convs2.0.weight_g +vq_model.dec.resblocks.8.convs2.0.weight_v +vq_model.dec.resblocks.8.convs2.1.bias +vq_model.dec.resblocks.8.convs2.1.weight_g +vq_model.dec.resblocks.8.convs2.1.weight_v +vq_model.dec.resblocks.8.convs2.2.bias +vq_model.dec.resblocks.8.convs2.2.weight_g +vq_model.dec.resblocks.8.convs2.2.weight_v +vq_model.dec.resblocks.9.convs1.0.bias +vq_model.dec.resblocks.9.convs1.0.weight_g +vq_model.dec.resblocks.9.convs1.0.weight_v +vq_model.dec.resblocks.9.convs1.1.bias +vq_model.dec.resblocks.9.convs1.1.weight_g +vq_model.dec.resblocks.9.convs1.1.weight_v +vq_model.dec.resblocks.9.convs1.2.bias +vq_model.dec.resblocks.9.convs1.2.weight_g +vq_model.dec.resblocks.9.convs1.2.weight_v +vq_model.dec.resblocks.9.convs2.0.bias +vq_model.dec.resblocks.9.convs2.0.weight_g +vq_model.dec.resblocks.9.convs2.0.weight_v +vq_model.dec.resblocks.9.convs2.1.bias +vq_model.dec.resblocks.9.convs2.1.weight_g +vq_model.dec.resblocks.9.convs2.1.weight_v +vq_model.dec.resblocks.9.convs2.2.bias +vq_model.dec.resblocks.9.convs2.2.weight_g +vq_model.dec.resblocks.9.convs2.2.weight_v +vq_model.dec.resblocks.10.convs1.0.bias +vq_model.dec.resblocks.10.convs1.0.weight_g +vq_model.dec.resblocks.10.convs1.0.weight_v +vq_model.dec.resblocks.10.convs1.1.bias +vq_model.dec.resblocks.10.convs1.1.weight_g +vq_model.dec.resblocks.10.convs1.1.weight_v +vq_model.dec.resblocks.10.convs1.2.bias +vq_model.dec.resblocks.10.convs1.2.weight_g +vq_model.dec.resblocks.10.convs1.2.weight_v +vq_model.dec.resblocks.10.convs2.0.bias +vq_model.dec.resblocks.10.convs2.0.weight_g +vq_model.dec.resblocks.10.convs2.0.weight_v +vq_model.dec.resblocks.10.convs2.1.bias +vq_model.dec.resblocks.10.convs2.1.weight_g +vq_model.dec.resblocks.10.convs2.1.weight_v +vq_model.dec.resblocks.10.convs2.2.bias +vq_model.dec.resblocks.10.convs2.2.weight_g +vq_model.dec.resblocks.10.convs2.2.weight_v +vq_model.dec.resblocks.11.convs1.0.bias +vq_model.dec.resblocks.11.convs1.0.weight_g +vq_model.dec.resblocks.11.convs1.0.weight_v +vq_model.dec.resblocks.11.convs1.1.bias +vq_model.dec.resblocks.11.convs1.1.weight_g +vq_model.dec.resblocks.11.convs1.1.weight_v +vq_model.dec.resblocks.11.convs1.2.bias +vq_model.dec.resblocks.11.convs1.2.weight_g +vq_model.dec.resblocks.11.convs1.2.weight_v +vq_model.dec.resblocks.11.convs2.0.bias +vq_model.dec.resblocks.11.convs2.0.weight_g +vq_model.dec.resblocks.11.convs2.0.weight_v +vq_model.dec.resblocks.11.convs2.1.bias +vq_model.dec.resblocks.11.convs2.1.weight_g +vq_model.dec.resblocks.11.convs2.1.weight_v +vq_model.dec.resblocks.11.convs2.2.bias +vq_model.dec.resblocks.11.convs2.2.weight_g +vq_model.dec.resblocks.11.convs2.2.weight_v +vq_model.dec.resblocks.12.convs1.0.bias +vq_model.dec.resblocks.12.convs1.0.weight_g +vq_model.dec.resblocks.12.convs1.0.weight_v +vq_model.dec.resblocks.12.convs1.1.bias +vq_model.dec.resblocks.12.convs1.1.weight_g +vq_model.dec.resblocks.12.convs1.1.weight_v +vq_model.dec.resblocks.12.convs1.2.bias +vq_model.dec.resblocks.12.convs1.2.weight_g +vq_model.dec.resblocks.12.convs1.2.weight_v +vq_model.dec.resblocks.12.convs2.0.bias +vq_model.dec.resblocks.12.convs2.0.weight_g +vq_model.dec.resblocks.12.convs2.0.weight_v +vq_model.dec.resblocks.12.convs2.1.bias +vq_model.dec.resblocks.12.convs2.1.weight_g +vq_model.dec.resblocks.12.convs2.1.weight_v +vq_model.dec.resblocks.12.convs2.2.bias +vq_model.dec.resblocks.12.convs2.2.weight_g +vq_model.dec.resblocks.12.convs2.2.weight_v +vq_model.dec.resblocks.13.convs1.0.bias +vq_model.dec.resblocks.13.convs1.0.weight_g +vq_model.dec.resblocks.13.convs1.0.weight_v +vq_model.dec.resblocks.13.convs1.1.bias +vq_model.dec.resblocks.13.convs1.1.weight_g +vq_model.dec.resblocks.13.convs1.1.weight_v +vq_model.dec.resblocks.13.convs1.2.bias +vq_model.dec.resblocks.13.convs1.2.weight_g +vq_model.dec.resblocks.13.convs1.2.weight_v +vq_model.dec.resblocks.13.convs2.0.bias +vq_model.dec.resblocks.13.convs2.0.weight_g +vq_model.dec.resblocks.13.convs2.0.weight_v +vq_model.dec.resblocks.13.convs2.1.bias +vq_model.dec.resblocks.13.convs2.1.weight_g +vq_model.dec.resblocks.13.convs2.1.weight_v +vq_model.dec.resblocks.13.convs2.2.bias +vq_model.dec.resblocks.13.convs2.2.weight_g +vq_model.dec.resblocks.13.convs2.2.weight_v +vq_model.dec.resblocks.14.convs1.0.bias +vq_model.dec.resblocks.14.convs1.0.weight_g +vq_model.dec.resblocks.14.convs1.0.weight_v +vq_model.dec.resblocks.14.convs1.1.bias +vq_model.dec.resblocks.14.convs1.1.weight_g +vq_model.dec.resblocks.14.convs1.1.weight_v +vq_model.dec.resblocks.14.convs1.2.bias +vq_model.dec.resblocks.14.convs1.2.weight_g +vq_model.dec.resblocks.14.convs1.2.weight_v +vq_model.dec.resblocks.14.convs2.0.bias +vq_model.dec.resblocks.14.convs2.0.weight_g +vq_model.dec.resblocks.14.convs2.0.weight_v +vq_model.dec.resblocks.14.convs2.1.bias +vq_model.dec.resblocks.14.convs2.1.weight_g +vq_model.dec.resblocks.14.convs2.1.weight_v +vq_model.dec.resblocks.14.convs2.2.bias +vq_model.dec.resblocks.14.convs2.2.weight_g +vq_model.dec.resblocks.14.convs2.2.weight_v +vq_model.dec.conv_post.weight +vq_model.dec.cond.weight +vq_model.dec.cond.bias +vq_model.flow.flows.0.pre.weight +vq_model.flow.flows.0.pre.bias +vq_model.flow.flows.0.enc.in_layers.0.bias +vq_model.flow.flows.0.enc.in_layers.0.weight_g +vq_model.flow.flows.0.enc.in_layers.0.weight_v +vq_model.flow.flows.0.enc.in_layers.1.bias +vq_model.flow.flows.0.enc.in_layers.1.weight_g +vq_model.flow.flows.0.enc.in_layers.1.weight_v +vq_model.flow.flows.0.enc.in_layers.2.bias +vq_model.flow.flows.0.enc.in_layers.2.weight_g +vq_model.flow.flows.0.enc.in_layers.2.weight_v +vq_model.flow.flows.0.enc.in_layers.3.bias +vq_model.flow.flows.0.enc.in_layers.3.weight_g +vq_model.flow.flows.0.enc.in_layers.3.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.0.bias +vq_model.flow.flows.0.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.1.bias +vq_model.flow.flows.0.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.2.bias +vq_model.flow.flows.0.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.0.enc.res_skip_layers.3.bias +vq_model.flow.flows.0.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.0.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.0.enc.cond_layer.bias +vq_model.flow.flows.0.enc.cond_layer.weight_g +vq_model.flow.flows.0.enc.cond_layer.weight_v +vq_model.flow.flows.0.post.weight +vq_model.flow.flows.0.post.bias +vq_model.flow.flows.2.pre.weight +vq_model.flow.flows.2.pre.bias +vq_model.flow.flows.2.enc.in_layers.0.bias +vq_model.flow.flows.2.enc.in_layers.0.weight_g +vq_model.flow.flows.2.enc.in_layers.0.weight_v +vq_model.flow.flows.2.enc.in_layers.1.bias +vq_model.flow.flows.2.enc.in_layers.1.weight_g +vq_model.flow.flows.2.enc.in_layers.1.weight_v +vq_model.flow.flows.2.enc.in_layers.2.bias +vq_model.flow.flows.2.enc.in_layers.2.weight_g +vq_model.flow.flows.2.enc.in_layers.2.weight_v +vq_model.flow.flows.2.enc.in_layers.3.bias +vq_model.flow.flows.2.enc.in_layers.3.weight_g +vq_model.flow.flows.2.enc.in_layers.3.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.0.bias +vq_model.flow.flows.2.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.1.bias +vq_model.flow.flows.2.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.2.bias +vq_model.flow.flows.2.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.2.enc.res_skip_layers.3.bias +vq_model.flow.flows.2.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.2.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.2.enc.cond_layer.bias +vq_model.flow.flows.2.enc.cond_layer.weight_g +vq_model.flow.flows.2.enc.cond_layer.weight_v +vq_model.flow.flows.2.post.weight +vq_model.flow.flows.2.post.bias +vq_model.flow.flows.4.pre.weight +vq_model.flow.flows.4.pre.bias +vq_model.flow.flows.4.enc.in_layers.0.bias +vq_model.flow.flows.4.enc.in_layers.0.weight_g +vq_model.flow.flows.4.enc.in_layers.0.weight_v +vq_model.flow.flows.4.enc.in_layers.1.bias +vq_model.flow.flows.4.enc.in_layers.1.weight_g +vq_model.flow.flows.4.enc.in_layers.1.weight_v +vq_model.flow.flows.4.enc.in_layers.2.bias +vq_model.flow.flows.4.enc.in_layers.2.weight_g +vq_model.flow.flows.4.enc.in_layers.2.weight_v +vq_model.flow.flows.4.enc.in_layers.3.bias +vq_model.flow.flows.4.enc.in_layers.3.weight_g +vq_model.flow.flows.4.enc.in_layers.3.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.0.bias +vq_model.flow.flows.4.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.1.bias +vq_model.flow.flows.4.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.2.bias +vq_model.flow.flows.4.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.4.enc.res_skip_layers.3.bias +vq_model.flow.flows.4.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.4.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.4.enc.cond_layer.bias +vq_model.flow.flows.4.enc.cond_layer.weight_g +vq_model.flow.flows.4.enc.cond_layer.weight_v +vq_model.flow.flows.4.post.weight +vq_model.flow.flows.4.post.bias +vq_model.flow.flows.6.pre.weight +vq_model.flow.flows.6.pre.bias +vq_model.flow.flows.6.enc.in_layers.0.bias +vq_model.flow.flows.6.enc.in_layers.0.weight_g +vq_model.flow.flows.6.enc.in_layers.0.weight_v +vq_model.flow.flows.6.enc.in_layers.1.bias +vq_model.flow.flows.6.enc.in_layers.1.weight_g +vq_model.flow.flows.6.enc.in_layers.1.weight_v +vq_model.flow.flows.6.enc.in_layers.2.bias +vq_model.flow.flows.6.enc.in_layers.2.weight_g +vq_model.flow.flows.6.enc.in_layers.2.weight_v +vq_model.flow.flows.6.enc.in_layers.3.bias +vq_model.flow.flows.6.enc.in_layers.3.weight_g +vq_model.flow.flows.6.enc.in_layers.3.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.0.bias +vq_model.flow.flows.6.enc.res_skip_layers.0.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.0.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.1.bias +vq_model.flow.flows.6.enc.res_skip_layers.1.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.1.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.2.bias +vq_model.flow.flows.6.enc.res_skip_layers.2.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.2.weight_v +vq_model.flow.flows.6.enc.res_skip_layers.3.bias +vq_model.flow.flows.6.enc.res_skip_layers.3.weight_g +vq_model.flow.flows.6.enc.res_skip_layers.3.weight_v +vq_model.flow.flows.6.enc.cond_layer.bias +vq_model.flow.flows.6.enc.cond_layer.weight_g +vq_model.flow.flows.6.enc.cond_layer.weight_v +vq_model.flow.flows.6.post.weight +vq_model.flow.flows.6.post.bias +vq_model.quantizer.vq.layers.0._codebook.embed diff --git a/genie_tts/Data/v2ProPlus/Models/prompt_encoder_fp32.onnx b/genie_tts/Data/v2ProPlus/Models/prompt_encoder_fp32.onnx new file mode 100644 index 0000000000000000000000000000000000000000..d750f20662c2abfa85ce0664c316b7ad10c4a7f6 --- /dev/null +++ b/genie_tts/Data/v2ProPlus/Models/prompt_encoder_fp32.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de17d88fdfe9598f9d25710b7299bccac3c0a79851cd8085fc210492260cc0b9 +size 44533 diff --git a/genie_tts/Data/v2ProPlus/Models/vits_fp32.onnx b/genie_tts/Data/v2ProPlus/Models/vits_fp32.onnx new file mode 100644 index 0000000000000000000000000000000000000000..0d7391be80821f4a5770ace88b37aa6b6338bb7e --- /dev/null +++ b/genie_tts/Data/v2ProPlus/Models/vits_fp32.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f9cb0882bbd028e73e5b54b94b54f53370a0f65605ca7dee4e4f1ab5edac89c +size 1613193 diff --git a/genie_tts/G2P/Chinese/ChineseG2P.py b/genie_tts/G2P/Chinese/ChineseG2P.py new file mode 100644 index 0000000000000000000000000000000000000000..df19ca7d8fed3e6a544afda6a3bd1f85a13bcdac --- /dev/null +++ b/genie_tts/G2P/Chinese/ChineseG2P.py @@ -0,0 +1,186 @@ +import os +import re +from typing import List, Tuple, Dict +import logging + +from pypinyin.contrib.tone_convert import to_finals_tone3, to_initials +import jieba_fast +import jieba_fast.posseg as psg +from g2pM import G2pM + +from ...Core.Resources import Chinese_G2P_DIR +from ..SymbolsV2 import symbols_v2, symbol_to_id_v2 +from .ToneSandhi import ToneSandhi +from .Normalization.text_normlization import TextNormalizer +from .CorrectPronunciation import correct_pronunciation +from .Erhua import ErhuaProcessor + +jieba_fast.setLogLevel(logging.ERROR) + +PUNCTUATION = ["!", "?", "…", ",", ".", "-"] +PUNCTUATION_REPLACEMENTS = { + ":": ",", ";": ",", ",": ",", "。": ".", "!": "!", + "?": "?", "\n": ".", "·": ",", "、": ",", "$": ".", + "/": ",", "—": "-", "~": "…", "~": "…", +} +SPECIAL_REPLACEMENTS = {"...": "…"} # 特殊的多字符替换 + + +class ChineseG2P: + def __init__(self): + # --- 资源加载 --- + self.g2pm: G2pM = G2pM() + self.tone_modifier: ToneSandhi = ToneSandhi() + self.erhua_processor: ErhuaProcessor = ErhuaProcessor() + self.text_normalizer: TextNormalizer = TextNormalizer() + self.pinyin_to_symbol_map: Dict[str, str] = {} + + # 预编译正则 + # 1. 匹配替换表中的字符 + self.pattern_punct_map = re.compile("|".join(re.escape(p) for p in PUNCTUATION_REPLACEMENTS.keys())) + # 2. 过滤非中文字符和允许的标点 + allowed_chars = "".join(re.escape(p) for p in PUNCTUATION) + self.pattern_filter = re.compile(r"[^\u4e00-\u9fa5" + allowed_chars + r"]+") + # 3. 句内分割 (Lookbehind) + self.pattern_split = re.compile(r"(?<=[{0}])\s*".format(allowed_chars)) + # 4. 连续标点去重 + self.pattern_consecutive = re.compile(f"([{allowed_chars}])\\1+") + # 5. 英文单词移除 + self.pattern_eng = re.compile(r"[a-zA-Z]+") + + # --- 拼音映射查找表 (用于 _pinyin_to_opencpop_phones) --- + self.v_rep_map = {"uei": "ui", "iou": "iu", "uen": "un"} + self.pinyin_rep_map = {"ing": "ying", "i": "yi", "in": "yin", "u": "wu"} + self.single_rep_map = {"v": "yu", "e": "e", "i": "y", "u": "w"} + + self.load_opencpop_dict() + + def load_opencpop_dict(self): + # 加载 Opencpop 映射表 + map_path = os.path.join(Chinese_G2P_DIR, "opencpop-strict.txt") + with open(map_path, 'r', encoding='utf-8') as f: + for line in f: + parts = line.strip().split("\t") + if len(parts) >= 2: + self.pinyin_to_symbol_map[parts[0]] = parts[1] + + def _replace_punctuation(self, text: str) -> str: + """处理特定字符替换和非标准标点清洗""" + # text = text.replace("嗯", "恩").replace("呣", "母") + for k, v in SPECIAL_REPLACEMENTS.items(): + text = text.replace(k, v) + text = self.pattern_punct_map.sub(lambda x: PUNCTUATION_REPLACEMENTS[x.group()], text) + text = self.pattern_filter.sub("", text) + return text + + def normalize_text(self, text: str) -> str: + """执行完整的文本归一化流程""" + # 1. TextNormalizer 转换 (如数字转汉字) + sentences = self.text_normalizer.normalize(text) + # 2. 标点映射与清洗 + dest_parts = [self._replace_punctuation(s) for s in sentences] + dest_text = "".join(dest_parts) + # 3. 避免重复标点 + dest_text = self.pattern_consecutive.sub(r"\1", dest_text) + return dest_text + + def _pinyin_to_opencpop_phones(self, c: str, v: str) -> List[str]: + """将声母韵母转换为 Opencpop 格式的音素""" + # 提取声调 + v_without_tone = v[:-1] + tone = v[-1] + if c: + # 多音节逻辑 + final = self.v_rep_map.get(v_without_tone, v_without_tone) + pinyin_key = c + final + else: + # 零声母/单音节逻辑 + temp_key = c + v_without_tone # c is empty string here usually + if temp_key in self.pinyin_rep_map: + pinyin_key = self.pinyin_rep_map[temp_key] + else: + # 处理首字母变化 + if temp_key and temp_key[0] in self.single_rep_map: + pinyin_key = self.single_rep_map[temp_key[0]] + temp_key[1:] + else: + pinyin_key = temp_key + # 查表获取音素 + phone_str = self.pinyin_to_symbol_map[pinyin_key] + new_c, new_v = phone_str.split(" ") + new_v = new_v + tone + return [new_c, new_v] + + def g2p(self, text: str) -> Tuple[List[str], List[int]]: + """生成音素列表和 Word-to-Phone 映射""" + sentences = [i for i in self.pattern_split.split(text) if i.strip() != ""] + all_phones = [] + all_word2ph = [] + for seg in sentences: + # 移除英文 + seg = self.pattern_eng.sub("", seg) + # 分词 + seg_cut = psg.lcut(seg) + seg_cut = self.tone_modifier.pre_merge_for_modify(seg_cut) + initials = [] + finals = [] + # G2PM 整句推理 + pinyins = self.g2pm(seg, char_split=True) + pinyins = [p.replace("u:", "v") for p in pinyins] + pre_word_length = 0 + for word, pos in seg_cut: + now_word_length = pre_word_length + len(word) + if pos == "eng": + pre_word_length = now_word_length + continue + word_pinyins = pinyins[pre_word_length:now_word_length] + # 多音字修正 + word_pinyins = correct_pronunciation(word, word_pinyins) + sub_initials = [] + sub_finals = [] + for pinyin in word_pinyins: + if pinyin[0].isalpha(): + sub_initials.append(to_initials(pinyin)) + sub_finals.append(to_finals_tone3(pinyin, neutral_tone_with_five=True)) + else: + # 处理非字母(如标点) + sub_initials.append(pinyin) + sub_finals.append(pinyin) + pre_word_length = now_word_length + # 变调处理 + sub_finals = self.tone_modifier.modified_tone(word, pos, sub_finals) + # 儿化处理 + sub_initials, sub_finals = self.erhua_processor.merge_erhua(sub_initials, sub_finals, word, pos) + initials.extend(sub_initials) + finals.extend(sub_finals) + + for c, v in zip(initials, finals): + if c == v: + # 标点符号逻辑 + all_phones.append(c) + all_word2ph.append(1) + else: + # 正常拼音转换逻辑 + try: + phone_pair = self._pinyin_to_opencpop_phones(c, v) + all_phones.extend(phone_pair) + all_word2ph.append(len(phone_pair)) + except KeyError: + # 遇到未知的拼音组合,记录错误或跳过 + continue + + return all_phones, all_word2ph + + def process(self, text: str) -> Tuple[str, List[str], List[int], List[int]]: + normalized_text = self.normalize_text(text) + # print(normalized_text) + phones, word2ph = self.g2p(normalized_text) + phones = [ph for ph in phones if ph in symbols_v2] + phones_ids = [symbol_to_id_v2[ph] for ph in phones] + return normalized_text, phones, phones_ids, word2ph + + +processor: ChineseG2P = ChineseG2P() + + +def chinese_to_phones(text: str) -> Tuple[str, List[str], List[int], List[int]]: + return processor.process(text) diff --git a/genie_tts/G2P/Chinese/CorrectPronunciation.py b/genie_tts/G2P/Chinese/CorrectPronunciation.py new file mode 100644 index 0000000000000000000000000000000000000000..33d2024fd9ccea293e44afad15a2819235848f44 --- /dev/null +++ b/genie_tts/G2P/Chinese/CorrectPronunciation.py @@ -0,0 +1,50 @@ +import os +import pickle +from typing import List, Dict, Any, Union + +from ...Core.Resources import Chinese_G2P_DIR + +# 常量定义 +DEFAULT_CACHE_PATH = os.path.join(Chinese_G2P_DIR, "polyphonic.pickle") + + +class PolyphonicDictManager: + _data: Dict[str, Any] = {} + + @classmethod + def get_data(cls, path: str = DEFAULT_CACHE_PATH) -> Dict[str, Any]: + if not cls._data: + with open(path, "rb") as f: + cls._data = pickle.load(f) + return cls._data + + +def correct_pronunciation(word: str, word_pinyin: List[str]) -> Union[List[str], str]: + """ + 根据加载的字典修正发音,作为供外部程序调用的独立接口。 + 逻辑:优先查找整词修正,如果没有整词匹配,则遍历每个字符进行单字修正。 + + Input: + word (str): 原始中文字符串,例如 "银行"。 + word_pinyins (List[str]): 当前预测的拼音列表,例如 ['yin2', 'xing2']。 + + Output: + Union[List[str], str]: 修正后的拼音列表或字符串。 + + Example: + # 字典包含整词 {'银行': ['yin2', 'hang2']} + result = correct_pronunciation("银行", ["yin2", "xing2"]) + # Result: ["yin2", "hang2"] + """ + pp_dict = PolyphonicDictManager.get_data() + new_word_pinyin = list(word_pinyin) + # 1. 尝试整词匹配 + if new_pinyin := pp_dict.get(word): + return new_pinyin + # 2. 逐字修正 + for idx, w in enumerate(word): + if idx >= len(new_word_pinyin): + break + if w_pinyin := pp_dict.get(w): + new_word_pinyin[idx] = w_pinyin[0] + return new_word_pinyin diff --git a/genie_tts/G2P/Chinese/Erhua.py b/genie_tts/G2P/Chinese/Erhua.py new file mode 100644 index 0000000000000000000000000000000000000000..8ccc87b895563bc03e4943427219a2495094208b --- /dev/null +++ b/genie_tts/G2P/Chinese/Erhua.py @@ -0,0 +1,49 @@ +from typing import List, Tuple, Set + + +class ErhuaProcessor: + """ + 处理中文G2P中的儿化音逻辑。 + """ + + def __init__(self): + self.must_erhua: Set[str] = { + "小院儿", "胡同儿", "范儿", "老汉儿", "撒欢儿", "寻老礼儿", "妥妥儿", "媳妇儿" + } + self.not_erhua: Set[str] = { + "虐儿", "为儿", "护儿", "瞒儿", "救儿", "替儿", "有儿", "一儿", "我儿", "俺儿", + "妻儿", "拐儿", "聋儿", "乞儿", "患儿", "幼儿", "孤儿", "婴儿", "婴幼儿", "连体儿", + "脑瘫儿", "流浪儿", "体弱儿", "混血儿", "蜜雪儿", "舫儿", "祖儿", "美儿", "应采儿", "可儿", + "侄儿", "孙儿", "侄孙儿", "女儿", "男儿", "红孩儿", "花儿", "虫儿", "马儿", "鸟儿", + "猪儿", "猫儿", "狗儿", "少儿", + } + + def merge_erhua(self, initials: List[str], finals: List[str], word: str, pos: str) -> Tuple[List[str], List[str]]: + # 1. 修正 er1 发音为 er2 (当'儿'在词尾且发音为er1时) + for i, phn in enumerate(finals): + if i == len(finals) - 1 and word[i] == "儿" and phn == "er1": + finals[i] = "er2" + # 2. 检查是否跳过儿化处理 + if word not in self.must_erhua and (word in self.not_erhua or pos in {"a", "j", "nr"}): + return initials, finals + # 3. 长度校验 (处理如 "……" 等长度不一致的特殊符号情况) + if len(finals) != len(word): + return initials, finals + # 4. 执行儿化合并逻辑 (与前一个字发同音) + new_initials = [] + new_finals = [] + for i, phn in enumerate(finals): + # 判断是否需要合并儿化音 + # 条件: 是最后一个字 + 是"儿" + 发音是er2/er5 + 后两字不在非儿化表中 + 前面已有韵母 + if ( + i == len(finals) - 1 + and word[i] == "儿" + and phn in {"er2", "er5"} + and word[-2:] not in self.not_erhua + and new_finals + ): + # 将 'er' 加上前一个字的声调 + phn = "er" + new_finals[-1][-1] + new_initials.append(initials[i]) + new_finals.append(phn) + return new_initials, new_finals diff --git a/genie_tts/G2P/Chinese/Normalization/__init__.py b/genie_tts/G2P/Chinese/Normalization/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/G2P/Chinese/Normalization/char_convert.py b/genie_tts/G2P/Chinese/Normalization/char_convert.py new file mode 100644 index 0000000000000000000000000000000000000000..144dd63a95dbfc1847e2fd04de7fb7d88ac5a6b9 --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/char_convert.py @@ -0,0 +1,35 @@ +# coding=utf-8 +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Traditional and simplified Chinese conversion, a simplified character may correspond to multiple traditional characters.""" + +simplified_charcters = "制咖片型超声盘鉴定仔点他命书歌粉巾字帐恤手指记忆棒形转弯沟光○〇㐄㐅㐆㐌㐖毒㐜㐡㐤㐰㐺㑇㑳㒳㒸㔾㗂㗎㝵㞎㞙㞞以㢲㢴㤅㥁㥯㨗㫺㬎㮎㮚㮸㲋㲱㲾㳮涧㵪㶸㷖㷭㹢㹴犬㺢狓㺵碗㽮㿝䍃䔢䖟䖸䗈䗥䗪䝓射䥯䦉䯝鲃鱼䲔䳗鹅䵹鼄䶑一对应映射丁不识下儿子做二休世丘之貉并中台原则串为甚谓干净了百事无成八变五十些人得道鸡升天代如并来去个国政策劲幽灵在欧洲游荡接样萝卜坑侧化传价元论醇共再准刀两断切分耕耘收获钱货物向看旧就绪险刻千金动劳永逸匙零夜半卡通回复返影踪反常态口咬气句话同吐快吹周味呼诺呜品红锅哄而散起唱和问三知生熟团漆黑火糟堆场空块面塌糊涂尘染壁厢夔已足多情露水大早到晚夫妻当关万莫开失古恨套所料既往孔见提师要家主审寸阴难买斗牛小撮部阵局展身层巴掌帆风顺席地带过年计于春头载四季期被蛇怕井绳度愿式份弹顷深前律径心意念差愁孤行俱全房厅交遮打技长把抓死拿眼泪鼻涕钥锁折段抿拍即合扫排掬挥拨拥上入击洞掷揽改故辙败文值名斑方面旁族日秋餐隔雅里终父旦时晌会霎间晃暴寒曝更月望垠际朝夕本正经利杯羹东西板枝独秀根筋杆进条龙服务概模次函数又性程总付步脚印趋登毛拔呵氧氮碳决雌雄波未平派谎言流清楚白准溜烟潭有获闻是处降琴鹤甲病发可拾沙目然了直以相眨穿睹瞥瞬矢的解石鸟神教秉虔诚秘种窝蜂穷窍笑置笔苟勾销抹杀煞等奖箍节吃箭仇双雕诗筹箩筐系列纸级士官统丝毫挂维网尽线微吭响股脑胎脉承腔臂力致效资源址器举功投般说讲规贸易叶障着慎满皆输号木电池衣倾钟高低视仁觉醒览遗角银币触溃九鼎蔽抄出驷马追重语破贫洗贯走路安蹴至几蹶振跃役胆汗较辈轮辞赞退六连遍递边针血锤音错门思闪真倒项栽雾类保护川先惊乍体哄鳞爪鸣滴泡邻域党专鼓作齐炒丑烯亥克内酯冬加奴卯肝炎基尺梁街裤镐客宠庭巳汝昌烷玲磊糖肇酉醛啷青县韪良香骨鲷丂七集河市弦喜嘴张舌堵区工业姊妹星架构巧彩扭歪拼凑余热曜武州爷浮屠美乡老阶树荤素碎落能魄鳃鳗珠丄丅丆万俟丈尚摸母娘量管群亚虎必我堂令申件装伏位博侠义界表女墟台戏臭皮匠胜诸葛亮赛顶倍催请运算包立叉戟离疫苗土史志演围揭瓦晒夷姑婆帝村宝烂尖杉碱屉桌山岔岛由纪峡坝库镇废从德后拗汤治旬食明昧曹朋友框栏极权幂曲归依猫民氟硼氯磷铁江侗自旅法司洋浦梅园温暖湾焦班幸用田略番叠皇炮捶硝苯酸腺苷棱草镜穗跳远索锦纲聚氰胺联店胚膲爱色堇紫罗兰芝茶饭菱云虫藏藩乱叛苏亲债凳学座恐恋柱测肌腹衩锥系貂企乌跪叩军车农题迭都甘油屯奏键短阿姨陪姐只顾茅庐槽驾魂鲜鹿页其菜单乘任供势午齿汉组织吊调泻唇坡城报坟外夸将尉建筑岸岗公床扬新剑升杭林栗校楼标款汽社浣海商馆剧院钢华港机械广媒环球融第医科证券综财乐育游涨犹岭疏瘾睑确兵领导缴肢膛船艾瑟尔苍蔡虞效衫覆访诉课谕议轨述野钩限敌鞋颌颔颚饶首龈站例修凡划垂届属崽颏厨拜挫摆放旋削棋榻槛礼沉注滑营狱画确仪聘花葬诏员跌辖周达酒锚闸陷陆雨雪飞威丌于丹久乏予理评产亢卑亦乎舞己悲矩圆词害志但住佞佳便俗信票案幅翁倦伦假偏倚斜亏鬼敲停备伤脾胃仅此像俭匮免宜穴焉戴兼容许冻伯仲负彼昼皂轩轾实刊划颠卫战哥比省非好黄饰别拘束掩奶睬选择摇扰烦苦枚写协厌及格受欢迎约只估侵犯割状告或缺抗拒挽撤救药喻磨灭端倪少逆逾越避靠适吉誉吝玉含延咎歹听啻渊善谋均匀堪忍够太惹妙妥妨孕症孝术室完纳推冠积宣疑辩栗碴称屈挠屑干涉衡待很忙恶忿怎么怠急耻恭息悦惑惜惟想愉愧怍慌愤启懂懈怀材才紧招认扣抵拉舍也罢插揣冒搭撞南墙扩核支攻敢雷攀敬里吗需景智暇曾罪遇朽枉止况竞争辱求愈渝溶济左右袒困补爽特寂寞示弱找谢畏强疾徐痛痒冤符眠睦瞅董何厚云措活疲羞者轻玻璃祥兆禁移稂莠稳佛换答简结果盟绝缕途给谈否羁翼耐肖胫毋宁兴舒若菲莱痕迹窠臼虚衰脸兔撒鹰棺范该详讳抬泰让须眉象众赀账费灰赖奇虑训辍辨菽麦辛近送透逞徒速续逮捕遂遑违逊斧钺艰醉锈随观弃显饱脂肪使丏丐帮丒且慢末丕替桃宗王尊凉爵各图屋脊粮署录坛吾禄职胄袭君厦丗北壑桐疹损逢陵鹬丙寅戌氨腈唑纶辰酮脱氢酶醚丞丢现掉纱帽弄扯炮碗丠両丣坐存激肩臻蒂莲悖序驱丨丩丫挺杈髻鬟细介俄伊犁京尼布订普渡央委监察检查剂圈设警队斯督剩震境航舶革防托播促质版蝾螈锋研艺历残消频谱精密制造陲邮候埔坚压坜凹汇执府究邦俘摄寮彬狼岳肺肿庸英讯诊埋粒胞括控码韩暑枪枢砥澳哇牟寿甸钻探篇签缀缝继耳肯照妇埃悬璧轴柜台辣搁浅邪跑纤阮阳私囊魔丮丰姿采丱烧丳丵丶丷丸参寨朗桂瑞砂衷霞貌凤仆舰因嫌宰峰干络牌持旨祭祷簿编罚宾办丼丿乀乂乃乄仰慕盛旷留考验阔乆乇么丑麽乊湖燃乑乒乓乕乖僻忤戾离谬迕乗危肥劫除隙浪婿乙炔肠酰吡咯盐乚乛乜嘢卿玄宫尾狐龟塔嶷兄弟泉章霄钉耙乞扎哀怜恕讨乢乣乤乥乧乨乩童乪乫乭乳晕汁液瑶浆牙癌突窦罩腐胶猪酪蛋糕菌瘤乴乵乶乷乸乹乺乼乾俸冰嘉哕嚎坤妈尸垒旱枯涸俐渴潮涩煸豆燥爹瘦瘪癣瞪袋脆姜贝隆馏乿亀亁叫咕攘扔搞男砸窜蓬麻亃亄亅却亇迟典今临繁累卵奉婚聪躬巨与迁添裂副宿岁怪恶尕仑愣杆硅硫钛铀锰芑杂异钠砷胂磺琥珀舱棍簧胡茬盗浩盆贩郎腿亍洪亐互欠助勉惠操斥诿系户译亓墓碑刑铃卅渠缤纷斗米旗宪钒灯徽瘟祖拳福谷丰脏腑绑肉腌苓蕴桥铺霸颜闹判喷冈底蛙陉矿亖亘亜罕们娜桑那努哈喀弗烈曼松森杜氏杯奥琛敦戊穆圣裔汇薛孙亟亡佚虏羊牢奋释卷卸契媾感额睫缠谊趾塞挤纽阻还配驰庄亨洛祚亪享津沪畿郊慈菴枇杷膏亭阁锃丽亳亶亹诛初责翻疯偶杰丛稠妖拖寰居吸授慧蜗吞壮魅狗矛盾益渣患忧稀描猿梦暂涯畜祸缘沸搜引擎臣横纭谁混援蒸兽狮税剖亻亼亽亡什献刹邡么仂仃仄仆富怨仈仉毕昔晨壳绍仍仏仒仕宦仗欺恃腰叹叹炬梓讫施仙后琼逝仚仝仞仟悔仡佬偿填泊拓扑簇羔购顿钦佩发棻阃驭养亿儆尤借帧赈凌叙帖李柔刚沃眦睚戒讹取飨读仨仫仮著泳卧躺韶夏裁仳仵唯贤凭钓诞仿似宋佛讽伀硕盼鹅伄儅伈伉俪柯始娃迈戈坦堡帕茨萨庙玛莉莎藤霍姆伋伍奢胥廷芳豪伎俩侍汛勒希羲雏伐憩整谟闲闲伕伙伴颐伜伝伢叔恒兹恩翰伱伲侣伶俜悧鼬伸懒缩喇叭伹伺伻伽倻辐伾似佃伫布乔妮墨佉卢佌贷劣廉昂档浓矮伞洼缓耗胸谷迷挡率龋宅沫舍疗佐贰佑占优据铧尝呢须鲁晓佗佘余坪寺瓜铳僧蒙芒陀龛哼呕坊奸孽弊揖祟茧缚誓贼佝偻瞀佟你夺赶佡佢佣佤佧贾佪佫佯佰佱洁绩酿肴佴卷佶佷佸佹佺佻佼佽佾具唤窘坏娱怒慨硬习惯聋膨胀蔓骇贵痹侀侁侂侃侄侅鸿燕侇侈糜靡侉侌妾侏儒仓鼠侐侑侔仑侘侚链侜偎傍钴循柳葫芦附価侮骂蔑侯岩截蚀局贴壶嬛宴捷携桶笺酌俣狭膝狄俅俉俊俏俎俑俓俔谚俚俛黎健呈固墒增守康箱湿祐镖镳杠盒靖膜龄俞豹猎噪孚封札筒托衍鸽剪撰稿炼厂禊练缮葺俯瞰撑冲效俳俴俵俶俷俺备俾伥倂倅储卒惶敷猝逃颉蓄崇隐倌倏忽刺蜡烛噍嚼坍扁抽毙葱楣灌灶粪背薮卖赔闭霉腾倓倔幸倘倜傥倝借箸挹浇阅倡狂倢倣値倥偬倨傲倩匡嗣冲柝珍倬倭寇猩倮倶倷倹勤赞偁偃充伪吏嗓寐惺扮拱芫茜藉虢钞偈伟晶偌宕距析滤殿疼瘫注颇偓偕鸭歇滞偝偟偢忘怡旺偨偩逼偫偭偯偰偱偲侦缉蹄偷减惰漏窥窃偸偺迹傀儡傅傈僳骂篱傎奎琳迪叟芭傒傔傕伧悉荒傜傞傢傣芽逼佣婢傮睨寄檄诵谣颂伛担辜弓惨蒿悼疤傺傻屄臆巢泄箧羡盖轧颓傿㑩僄僇佥僊働僎侨僔僖僚僝伪僣僤侥僦猴偾僩僬僭僮僯僰雇僵殖签静僾僿征陇儁侬儃儇侩朴薄儊儋儌儍傧儓俦侪拟尽儜儞儤儦儩汰哉寡渥裕酷儭儱罐儳儵儹傩俨儽兀臬臲鹫允勋勋宙宵帅憝彝谐嫂阋畅沛溢盈饥赫凶悍狠猛顽愚妣斩秦遣鞭耀敏荣槃泽爆碟磁秃缆辉霁卤朵娄孜烽酱勃汀箕裘钳耶蒙蕾彻兑软遭黜兎児韵媳爸兕觥兖兙兛兜售鍪肚兝兞兟兡兢兣樽殓涅睡禀籍赘泌啡肽奸幕涵涝熵疚眷稃衬讧赴焕椒歼植跏没试误猜栖窗肋袖颊兪卦撇胡岐廓轿疸枫茴珑厕秩募勺吨寓斤历亩迫筷厘最淫螺韬兮宽匪筛襄赢轭复兲诈刃堰戎痞蚁饷它冀铸冂冃円冇冉册嫁厉砺竭醮冏牧冑冓冔冕冖冗冘冞冢窄抑诬冥冫烘菇蛰冷凝坨橇淇淋炭饼砖碛窖醋雕雹霜冱冶炉艳嘲峻滩淡漠煖飕饮冼冽凃凄怆梗凅凇净凊凋敝蒙凔凛遵汞脢凞几凢処凰凯凵凶焰凸折刷纹预丧喽奔巡榜殡芙蓉租笼辑鞘萃凼锯镬刁蛮刂娩崩批拆摊掰蘖骤歧颗秒袂赃勿嘱忌磋琢肤刈羽刎讼戮舂桨艇刓刖霹雳刜创犊刡恙墅帜筵致劫劫刨昏默攸尿欲熏润薰圭删刮痧铲刱刲刳刴刵踏磅戳柏槐绣芹苋猬舟铭鹄鹜劫剁剃辫刭锉履铅克剌姻咽哨廊掠桅沿召瞻翅赵卜渺茫郭剒剔剕沥剚愎毅讷才剜剥啄采剞剟剡剣剤䌽剐肾驶黏剰袍剀紊铲剸剺剽剿劁劂札劈啪柴扳啦刘奭姥夼昫涓熙禅禹锡翔雁鹗刽刿弩柄蜻蛉劒劓劖劘劙澜篑赏矶釜晋甜薪逐劦熔纣虐赤囚劬劭労劵效劻劼劾峭艮勅勇励勍勐腊脖庞漫饲荡粥辄勖勗勘骄馁碌泮雇捐竹骑殊阱绩朴恳谨剿勧勩勯勰劢勋勷劝惩慰诫谏勹芡践阑匁庇拯粟扎袱裹饺匆遽匈匉匊匋匍匐茎匏匕妆痰脓蛹斋苑烤蹈塘羌熊阀螳螂疆碚竿纬荷茵邙魏匚匜匝匟扶稷匣匦拢匸匹耦匽匾匿卂叮疮禧轸堤棚迢钧炼卄卆遐卉瓷盲瓶当胱腱裸卋卌卍卐怯污贱鄙龌龊陋卓溪唐梯渔陈枣泥漳浔涧梨芬谯赡辕迦郑単驴弈洽鳌卛占筮卝卞卟吩啉屎翠厄卣卨卪卬卮榫袄玺绶钮蚤惧殆笃耸卲帘帙绕恤卼卽厂厎厓厔厖厗奚厘厍厜厝谅厕厤厥厪腻孢厮厰厳厣厹厺粕垢芜菁厼厾叁悟茸薯叄吵笄悌哺讥坫垄弧芯杠潜婴刍袁诘贪谍煽馈驳収岳缔灾贿骗叚叡吻拦蘑蜜诀燧玩砚筝椎蔺铜逗骊另觅叨唠谒杵姓喊嚷嚣咚咛塑寻恼憎擦只泣渗蝠叱吒咄咤喝籀黛舵舷叵叶铎懿昭穰苴辽叻叼吁堑嫖赌瞧爬众抒吅吆夥卺橡涤抱纵摩郡唁坠扇篮膀袜颈吋忾谘酬哭妓媛暗表缰迩妃羿絮蕃浑拐葵暮隅吔吖啶嗪戚吜啬噬咽吟哦咏吠吧唧嗒咐吪隽咀征燐苞茹钙哧吮吰吱嘎吲哚吴栋娇窟孟箫忠晗淞阖闾趼宇呐睛嘘拂捧疵熄竽笛糠吼吽呀吕韦蒙呃呆笨呇贡呉罄呋喃呎呏呔呠呡痴呣呤呦呧瑛眩扒晬淑姬瑜璇鹃呪呫哔嚅嗫呬呯呰呱呲咧噌钝呴呶呷呸呺呻哱咻啸噜吁坎坷逻呿咁咂咆哮咇咈咋蟹煦珅蔼咍咑咒诅咔哒嚓咾哝哩喱咗咠咡咢咣咥咦咨嗟询咩咪咫啮啮咭咮咱咲咳呛嗽咴啕咸咹咺呙喉咿婉恸悯赋矜绿茗蓝哂抢瞒哆嗦啰噻啾滨彗哋哌哎唷哟哏哐哞哢哤哪里哫啼喘哰哲萎蚌哳咩哽哿呗唅唆唈唉唎唏哗尧棣殇璜睿肃唔睇唕吣唞唣喳唪唬唰喏唲唳唵嘛唶唸唹唻唼唾唿啁啃鹦鹉啅埠栈榷祺铺鞅飙啊啍啎啐啓啕啖啗啜哑祈啢衔啤啥啫啱啲啵啺饥啽噶昆沁喁喂喆裙喈咙喋喌喎喑喒喓喔粗喙幛庆滋鹊喟喣喤喥喦喧骚喨喩梆吃葡萄喭驼挑吓碰枞瓣纯疱藻趟铬喵営喹喺喼喿嗀嗃嗄嗅嗈嗉嗊嗍嗐嗑嗔诟嗕嗖嗙嗛嗜痂癖嗝嗡嗤嗥嗨唢嗬嗯嗰嗲嗵叽嗷嗹嗾嗿嘀嘁嘂嘅惋嘈峪禾荫啀嘌嘏嘐嘒啯啧嘚唛嘞嘟囔嘣嘥嘦嘧嘬嘭这谑严敞馋松哓嘶嗥呒虾嘹嘻啴嘿噀噂噅噇噉噎噏噔噗噘噙噚咝噞噢噤蝉皿噩噫噭嗳噱哙噳嚏涌洒欲巫霏噷噼嚃嚄嚆抖哜尝嚔苏嚚嚜嚞嚟呖嚬嚭嚮嚯亸喾饬按竣苛嚵嘤啭冁呓膪谦囍囒囓囗囘萧酚飘溅谛囝溯眸纥銮鹘囟殉囡団囤囥囧囨囱囫囵囬囮囯囲図囶囷囸囹圄圉拟囻囿圀圂圃圊粹蠹赦圌垦圏滚鲱凿枘圕圛圜圞坯埂壤骸炕祠窑豚绅魠鲮鳖圧握圩圪垯圬圮圯炸岬幔毯祇窨菩溉圳圴圻圾坂坆沾坋坌舛壈昆垫墩椅坒坓坩埚坭坰坱坳坴坵坻坼杨挣涎帘垃垈垌垍垓垔垕垗垚垛垝垣垞垟垤垧垮垵垺垾垿埀畔埄埆埇埈埌殃隍埏埒埕埗埜垭埤埦埧埭埯埰埲埳埴埵埶绋埸培怖桩础辅埼埽堀诃侄庑堃堄摧磐贞韧砌堈堉垩堋堌堍堎垴堙堞堠礁堧堨舆堭堮蜓摘堲堳堽堿塁塄塈煤茔棵塍垲埘塓绸塕鸦沽虱塙冢塝缪塡坞埙塥塩塬塱场螨塼塽塾塿墀墁墈墉墐夯増毁墝墠墦渍钵墫墬堕墰墺墙橱壅壆壊壌壎壒榨蒜壔壕壖圹垆壜壝垅壡壬壭壱売壴壹壻壸寝壿夂夅夆変夊夌漱邑夓腕泄甥御骼夗夘夙衮瑙妊娠醣枭珊莺鹭戗幻魇夤蹀秘擂鸫姚宛闺屿庾挞拇賛蛤裨菠氅漓捞湄蚊霆鲨箐篆篷荆肆舅荔鲆巷惭骰辟邱镕镰阪漂烩鲵鲽鳄鸨胪鹏妒峨谭枰晏玑癸祝秤竺牡籁恢罡蝼蝎赐绒御梭夬夭砣榆怙枕夶夹馅奄崛葩谲奈贺祀赠奌奂奓奕䜣詝奘奜奠奡奣陶奨奁魁奫奬奰娲孩贬隶酥宄狡猾她姹嫣妁毡荼皋膻蝇嫔妄妍嫉媚娆妗趣妚妞妤碍妬娅妯娌妲妳妵妺姁姅姉姗姒姘姙姜姝姞姣姤姧姫姮娥姱姸姺姽婀娀诱慑胁娉婷娑娓娟娣娭娯娵娶娸娼婊婐婕婞婤婥溪孺婧婪婬婹婺婼婽媁媄媊媕媞媟媠媢媬媮妫媲媵媸媺媻媪眯媿嫄嫈袅嫏嫕妪嫘嫚嫜嫠嫡嫦嫩嫪毐嫫嫬嫰妩嫺娴嫽嫿妫嬃嬅嬉耍婵痴艳嬔嬖嬗嫱袅嫒嬢嬷嬦嬬嬭幼嬲嬴婶嬹嬾嬿孀娘孅娈孏曰癫屏孑孓雀孖斟篓谜摺孛矻鸠崮轲祜鸾孥邈毓棠膑孬孭孰孱孳孵泛罔衔孻孪宀宁冗拙株薇掣抚琪瓿榴谧弥宊濂祁瑕宍宏碁宓邸谳実潢町宥宧宨宬徵崎骏掖阙臊煮禽蚕宸豫寀寁寥寃檐庶寎暄碜寔寖寘寙寛寠苫寤肘洱滥蒗陕核寪弘绰螽宝擅疙瘩晷対檐専尃尅赎绌缭畴衅尌峙醌襟痲碧屁昊槌淘恵瀑牝畑莓缸羚觑蔻脏躁尔尓锐尗尙尜尟尢尥尨尪尬尭尰擒尲尶尴尸尹潽蠖蛾尻扣梢蚴鳍脬蹲屇屌蚵屐屃挪屖屘屙屛屝屡屣峦嶂岩舄屧屦屩屪屃屮戍驻钾崖嵛巅旮旯楂榄榉芋茱萸靛麓屴屹屺屼岀岊岌岍阜岑彭巩岒岝岢岚岣岧岨岫岱岵岷峁峇峋峒峓峞峠嵋峨峰峱岘峹峿崀崁崆祯崋崌崃岖昆崒崔嵬巍萤颢崚崞崟崠峥巆崤崦崧殂岽崱崳崴崶崿嵂嵇嵊泗嵌嵎嵒嵓岁嵙嵞嵡嵩嵫嵯嵴嵼嵾嵝崭崭晴嶋嶌嶒嶓嵚崂嶙嶝嶞峤嶡嶢峄嶨嶭嶮嶰嶲岙嵘巂巃巇巉岿巌巓巘巛滇芎巟巠弋回巣巤炊擘蜥蟒蛊觋巰蜀彦淖杏茂甫楞巻巽帼巿帛斐鲫蕊帑帔帗帚琉汶帟帡帣帨裙帯帰帷帹暆帏幄帮幋幌幏帻幙帮幞幠幡幢幦幨幩幪帱幭幯幰遥蹉跎馀庚鉴幵幷稚邃庀庁広庄庈庉笠庋跋庖牺庠庤庥鲸庬庱庳庴庵馨衢庹庿廃厩廆廋廌廎廏廐廑廒荫廖廛厮搏锣廞弛袤廥廧廨廪廱绵踵髓廸迫瓯邺廻廼廾廿躔弁皱弇弌弍弎弐弑吊诡憾荐弝弢弣弤弨弭弮弰弪霖繇焘斌旭溥骞弶弸弼弾彀彄别累纠强彔彖彘彟彟陌彤贻彧绘虹彪炳雕蔚鸥彰瘅彲彳彴仿彷徉徨彸彽踩敛旆徂徇徊渭畲铉裼従筌徘徙徜徕膳苏萌渐徬徭醺徯徳徴潘徻徼忀瘁胖燎怦悸颤扉犀澎湃砰恍惚绞隘忉惮挨饿忐忑忒忖応忝忞耿忡忪忭忮忱忸怩忻悠懑怏遏怔怗怚怛怞怼黍讶怫怭懦怱怲恍怵惕怸怹恁恂恇恉恌恏恒恓恔恘恚恛恝恞恟恠恣恧眄恪恫恬澹恰恿悀悁悃悄悆悊悐悒晦悚悛悜悝悤您悩悪悮悰悱凄恻德悴怅惘闷悻悾惄愫钟蒐惆惇惌惎惏惓惔惙惛耄惝疟浊恿惦德恽惴蠢惸拈愀愃愆愈愊愍愐愑愒愓愔愕恪氓蠢騃昵惬赧悫愬愮愯恺愼慁恿慅慆慇霭慉慊愠慝慥怄怂慬慱悭慴慵慷戚焚憀灼郁憃惫憋憍眺捏轼愦憔憖憙憧憬憨憪憭怃憯憷憸憹憺懃懅懆邀懊懋怿懔懐懞懠懤懥恹懫懮懰懱毖懵遁梁雍忏懽戁戄戆戉戋戕戛戝戛戠戡戢戣戤戥戦戬戭戯轰戱披菊牖戸戹戺戻卯戽锹扂楔扃扆扈扊杖牵绢铐镯赉扐搂搅烊盹瞌跟趸镲靶鼾払扗玫腮扛扞扠扡扢盔押扤扦扱罾揄绥鞍郤窾扻扼扽抃抆抈抉抌抏瞎抔缳缢擞抜拗択抨摔歉蹿牾抶抻搐泵菸拃拄拊髀抛拌脯拎拏拑擢秧沓曳挛迂拚拝拠拡拫拭拮踢拴拶拷攒拽掇芥橐簪摹疔挈瓢骥捺蹻挌挍挎挐拣挓挖掘浚挙揍聩挲挶挟挿捂捃捄捅捆捉捋胳膊揎捌捍捎躯蛛捗捘捙捜捥捩扪捭据捱捻捼捽掀掂抡臀膘掊掎掏掐笙掔掗掞棉芍掤搪阐掫掮掯揉掱掲掽掾揃揅揆搓揌诨揕揗揘揜揝揞揠揥揩揪揫橥遒麈揰揲揵揶揸背揺搆搉搊搋搌搎搔搕撼橹捣搘搠搡搢搣搤搥搦搧搨搬楦裢讪赸掏搰搲搳搴揾搷搽搾搿摀摁摂摃摎掴摒摓跤摙摛掼摞摠摦喉羯摭摮挚摰摲抠摴抟摷掺摽撂撃撅稻撊撋挦锏泼撕撙撚㧑挢撢掸撦撅撩撬撱朔揿蚍蜉挝捡擀掳闯擉缶觚擐擕擖擗擡擣擤澡腚擧擨擩擫擭摈拧撷擸撸擽擿攃摅撵攉攥攐攓撄搀撺每攩攫辔澄攮攰攲攴轶攷砭讦攽碘敁敃敇敉叙敎筏敔敕敖闰诲敜煌敧敪敳敹敺敻敿斁衽斄牒绉诌斉斎斓鹑谰驳鳢斒筲斛斝斞斠斡斢斨斫斮晾沂潟颖绛邵斲斸釳於琅斾斿旀旗旃旄涡旌旎旐旒旓旖旛旝旟旡旣浴旰獭魃旴时旻旼旽昀昃昄昇昉晰躲澈熹皎皓矾昑昕昜昝昞昡昤晖笋昦昨是昱昳昴昶昺昻晁蹇隧蔬髦晄晅晒晛晜晞晟晡晢晤晥曦晩萘莹顗晿暁暋暌暍暐暔暕煅旸暝暠暡曚暦暨暪朦胧昵暲殄冯暵暸暹暻暾曀晔昙曈曌曏曐暧曘曙曛叠昽曩骆曱甴肱曷牍禺锟曽沧耽朁朅朆杪栓夸竟粘绦朊膺朏朐朓朕朘朙瞄觐溘饔飧朠朢朣栅椆淀虱朩朮朰朱炆璋钰炽鹮朳槿朵朾朿杅杇杌陧欣钊湛漼楷瀍煜玟缨翱肇舜贽适逵杓杕杗杙荀蘅杝杞脩珓筊杰榔狍閦颦缅莞杲杳眇杴杶杸杻杼枋枌枒枓衾葄翘纾逋枙狸桠枟槁枲枳枴枵枷枸橼枹枻柁柂柃柅柈柊柎某柑橘柒柘柙柚柜柞栎柟柢柣柤柩柬柮柰柲橙柶柷柸柺査柿栃栄栒栔栘栝栟柏栩栫栭栱栲栳栴檀栵栻桀骜桁镁桄桉桋桎梏椹葚桓桔桕桜桟桫椤桭杯桯桲桴桷桹湘溟梃梊梍梐潼栀枧梜梠梡梣梧梩梱梲梳梴梵梹棁棃樱棐棑棕榈簑绷蓑枨棘棜棨棩棪棫棬棯棰棱棳棸棹椁棼碗椄苕椈椊椋椌椐椑椓椗検椤椪椰椳椴椵椷椸椽椿楀匾楅篪楋楍楎楗楘楙楛楝楟楠楢楥桢楩楪楫楬楮楯楰梅楸楹楻楽榀榃榊榎槺榕榖榘榛狉莽搒笞榠榡榤榥榦榧杩榭榰榱梿霰榼榾桤槊闩槎槑槔槖様槜槢槥椠槪槭椮槱槲槻槼槾樆樊樏樑樕樗樘樛樟樠樧樨権樲樴樵猢狲桦樻罍樾樿橁橄橆桡笥龠橕橚橛辆椭橤橧竖膈跨橾橿檩檃檇柽檍檎檑檖檗桧槚檠樯檨檫檬梼槟檴檵柠棹櫆櫌栉櫜椟櫡槠栌枥榇栊櫹棂茄櫽欀欂欃欐欑栾欙棂溴欨欬欱欵欶欷歔欸欹欻欼欿歁歃歆艎歈歊莳蝶歓歕歘歙歛歜欤歠蹦诠镶蹒跚升陟歩歮歯歰歳歴璞歺瞑歾殁夭殈殍殑殗殜殙殛殒殢殣殥殪殚僵殰殳荃殷殸殹蛟殻肴谤殴毈毉喂毎毑蕈毗毘毚茛邓毧毬毳毷毹毽毾毵牦氄氆靴氉氊氇氍氐聊氕氖気氘氙氚氛氜氝氡汹焊痉氤氲氥氦铝锌氪烃氩铵痤汪浒漉痘盂碾菖蒲蕹蛭螅氵冰氹氺氽烫氾氿渚汆汊汋汍汎汏汐汔汕褟汙汚汜蓠沼秽蔑汧汨汩汭汲汳汴堤汾沄沅沆瀣沇沈葆浸沦湎溺痼疴沌沍沏沐沔沕沘浜畹砾沚沢沬沭沮沰沱灢沴沷籽沺烹濡洄泂肛泅泆涌肓泐泑泒泓泔泖泙泚泜泝泠漩馍涛粼泞藓鳅泩泫泭泯铢泱泲洇洊泾琵琶荽蓟箔洌洎洏洑潄濯洙洚洟洢洣洧洨洩痢滔洫洮洳洴洵洸洹洺洼洿淌蜚浄浉浙赣渫浠浡浤浥淼瀚浬浭翩萍浯浰蜃淀苔蛞蝓蜇螵蛸煲鲤浃浼浽溦涂涊涐涑涒涔滂莅涘涙涪涫涬涮涴涶涷涿淄淅淆淊凄黯淓淙涟淜淝淟淠淢淤渌淦淩猥藿亵淬淮淯淰淳诣涞纺淸淹炖癯绮渇済渉渋渓渕涣渟渢滓渤澥渧渨渮渰渲渶渼湅湉湋湍湑湓湔黔湜湝浈湟湢湣湩湫湮麟湱湲湴涅満沩溍溎溏溛舐漭溠溤溧驯溮溱溲溳溵溷溻溼溽溾滁滃滉滊荥滏稽滕滘汇滝滫滮羼耷卤滹浐煎漈漊漎绎漕漖漘漙沤漜漪漾漥漦漯漰溆漶漷濞潀颍潎潏潕潗潚潝潞潠潦祉疡潲潵滗潸潺潾涠澁澂澃澉澌澍澐澒澔澙渑澣澦澧澨澫澬浍澰澴澶澼熏郁濆濇濈濉濊貊濔疣濜濠濩觞浚濮盥潍濲泺瀁滢渎渖瀌浏瀒瀔濒泸瀛潇潆瀡潴泷濑瀬弥潋瀳瀵瀹瀺瀼沣滠灉灋灒漓灖灏灞灠滦灥灨滟灪蜴灮烬獴灴灸灺炁炅鱿炗炘炙炤炫疽烙钎炯炰炱炲炴炷毁炻烀烋瘴鲳烓烔焙烜烝烳饪烺焃焄耆焌焐焓焗焜焞焠焢焮焯焱焼煁煃煆煇煊熠煍熬煐炜煕暖熏硷霾煚煝煟煠茕矸煨琐炀萁煳煺煻熀熅熇熉罴荧穹炝熘熛熜稔谙烁熤熨熯熰眶蚂颎熳熸熿燀烨燂燄盏燊燋燏燔隼燖焖燠燡灿燨燮燹燻燽燿爇爊爓爚爝爟爨蟾爯爰为爻丬爿牀牁牂牄牋窗牏牓窗釉牚腩蒡虻牠虽蛎牣牤牮牯牲牳牴牷牸牼绊牿靬犂犄犆犇犉犍犎犒荦犗犛犟犠犨犩犪犮犰狳犴犵犺狁甩狃狆狎狒獾狘狙黠狨狩狫狴狷狺狻豕狈蜘猁猇猈猊猋猓猖獗猗猘狰狞犸猞猟獕猭猱猲猳猷猸猹猺玃獀獃獉獍獏獐獒毙獙獚獜獝獞獠獢獣獧鼇蹊狯猃獬豸狝獯鬻獳犷猕猡玁菟玅玆玈珉糁禛郅玍玎玓瓅玔玕玖玗玘玞玠玡玢玤玥玦珏瑰玭玳瑁玶玷玹玼珂珇珈瑚珌馐馔珔珖珙珛珞珡珣珥珧珩珪佩珶珷珺珽琀琁陨玡琇琖琚琠琤琦琨琫琬琭琮琯琰琱琲琅琴珐珲瑀瑂瑄瑉玮瑑瑔瑗瑢瑭瑱瑲瑳瑽瑾瑿璀璨璁璅璆璈琏璊璐璘璚璝璟璠璡璥瑷璩璪璫璯璲玙璸璺璿瓀璎瓖瓘瓒瓛脐瓞瓠瓤瓧瓩瓮瓰瓱瓴瓸瓻瓼甀甁甃甄甇甋甍甎甏甑甒甓甔瓮甖甗饴蔗甙诧钜粱盎锈团甡褥産甪甬甭甮宁铠甹甽甾甿畀畁畇畈畊畋畎畓畚畛畟鄂畤畦畧荻畯畳畵畷畸畽畾疃叠疋疍疎箪疐疒疕疘疝疢疥疧疳疶疿痁痄痊痌痍痏痐痒痔痗瘢痚痠痡痣痦痩痭痯痱痳痵痻痿瘀痖瘃瘈瘉瘊瘌瘏瘐痪瘕瘖瘙瘚瘛疭瘜瘝瘗瘠瘥瘨瘭瘆瘯瘰疬瘳疠瘵瘸瘺瘘瘼癃痨痫癈癎癐癔癙癜癠疖症癞蟆癪瘿痈発踔绀蔫酵皙砬砒翎翳蔹钨镴皑鹎驹暨粤褶皀皁荚皃镈皈皌皋皒朱皕皖皘皜皝皞皤皦皨皪皫皭糙绽皴皲皻皽盅盋碗盍盚盝踞盦盩秋千盬盭眦睁瞤盯盱眙裰盵盻睐眂眅眈眊県眑眕眚眛眞眢眣眭眳眴眵眹瞓眽郛睃睅睆睊睍睎困睒睖睙睟睠睢睥睪睾睯睽睾眯瞈瞋瞍逛瞏瞕瞖眍䁖瞟瞠瞢瞫瞭瞳瞵瞷瞹瞽阇瞿眬矉矍铄矔矗矙瞩矞矟矠矣矧矬矫矰矱硪碇磙罅舫阡、矼矽礓砃砅砆砉砍砑砕砝砟砠砢砦砧砩砫砮砳艏砵砹砼硇硌硍硎硏硐硒硜硖砗磲茚钡硭硻硾碃碉碏碣碓碔碞碡碪碫碬砀碯碲砜碻礴磈磉磎硙磔磕磖磛磟磠磡磤磥蹭磪磬磴磵磹磻硗礀硚礅礌礐礚礜礞礤礧礮砻礲礵礽礿祂祄祅祆禳祊祍祏祓祔祕祗祘祛祧祫祲祻祼饵脔锢禂禇禋祦禔祎隋禖禘禚禜禝禠祃禢禤禥禨禫祢禴禸秆秈秊闱飒秋秏秕笈蘵赁秠秣秪秫秬秭秷秸稊稌稍稑稗稙稛稞稬秸稲稹稼颡稿穂穄穇穈穉穋稣贮穏穜穟秾穑穣穤穧穨穭穮穵穸窿阒窀窂窅窆窈窕窊窋窌窒窗窔窞窣窬黩蹙窑窳窴窵窭窸窗竁竃竈竑竜并竦竖篦篾笆鲛竾笉笊笎笏笐靥笓笤箓笪笫笭笮笰笱笲笳笵笸笻筀筅筇筈筎筑筘筠筤筥筦笕筒筭箸筰筱筳筴宴筸箂个箊箎箑箒箘箙箛箜篌箝箠箬镞箯箴箾篁筼筜篘篙篚篛篜篝篟篠篡篢篥篧篨篭篰篲筚篴篶篹篼箦簁簃簆簉簋簌簏簜簟簠簥簦簨簬簰簸簻籊藤籒籓籔签籚篯箨籣籥籧笾簖籫籯芾麴籵籸籹籼粁秕粋粑粔粝粛粞粢粧粨粲粳稗粻粽辟粿糅糆糈糌糍糒糔萼糗蛆蹋糢糨糬粽糯糱籴粜糸糺紃蹼鲣霉纡纨绔纫闽襻紑纰纮锭鸢鹞纴紞紟扎紩紬绂绁纻紽紾绐絁絃絅経絍绗絏缡褵絓絖絘絜绚絣螯絪絫聒絰絵绝絺絻絿綀绡綅绠绨绣綌綍綎捆綖綘継続缎绻綦綪线綮綯绾罟蝽綷縩绺绫緁绲緅緆缁绯緌緎総緑绱緖缃缄缂绵缗緤褓缌纂緪緰缑缈缏缇縁縃縄萦缙缒縏缣縕缞縚缜缟缛縠縡縢縦绦縯縰骋缧縳纤缦絷缥縻衙縿繄缫繈繊繋繐缯繖繘繙繠缋繣繨缰缲繸繻缱纁纆纇缬缵纩纑纕缵纙纚纛缾罃罆坛罋罂罎罏罖罘罛罝罠罣罥罦罨罫罭锾罳罶罹罻罽罿羂羃羇芈蕉51鸵羑羖羌羜羝羢羣羟羧羭羮羰羱羵羶羸藜鲐翀翃翅翊翌翏翕翛翟翡翣翥翦跹翪翫翚翮翯翱翽翾翿板饕鸹锨耋耇耎耏专耒耜耔耞耡耤耨耩耪耧耰鬓耵聍聃聆聎聝聡聦聱聴聂聼阈聿肄肏肐肕腋肙肜肟肧胛肫肬肭肰肴肵肸肼胊胍胏胑胔胗胙胝胠铨胤胦胩胬胭胯胰胲胴胹胻胼胾脇脘脝脞脡脣脤脥脧脰脲脳腆腊腌臜腍腒腓胨腜腠脶腥腧腬腯踝蹬镣腴腶蠕诽膂腽嗉膇膋膔腘膗膙膟黐膣膦膫膰膴膵膷脍臃臄臇臈臌臐臑臓膘臖臙臛臝臞臧蓐诩臽臾臿舀舁鳑鲏舋舎舔舗馆舝舠舡舢舨舭舲舳舴舸舺艁艄艅艉艋艑艕艖艗艘艚艜艟艣舣艨艩舻艬艭荏艴艳艸艹艻艿芃芄芊萰陂藭芏芔芘芚蕙芟芣芤茉芧芨芩芪芮芰鲢芴芷芸荛豢芼芿苄苒苘苙苜蓿苠苡苣荬苤苎苪镑苶苹苺苻苾茀茁范蠡萣茆茇茈茌茍茖茞茠茢茥茦菰茭茯茳藨茷藘茼荁荄荅荇荈菅蜢鸮荍荑荘豆荵荸荠莆莒莔莕莘莙莚莛莜莝莦莨菪莩莪莭莰莿菀菆菉菎菏菐菑菓菔芲菘菝菡菢菣菥蓂菧菫毂蓥菶菷菹醢菺菻菼菾萅萆苌萋萏萐萑萜萩萱萴莴扁萻葇葍葎葑荭葖葙葠葥苇葧葭药葳葴葶葸葹葽蒄蒎莼茏薹莅蒟蒻蒢蒦蒨蒭藁蒯蒱鉾蒴蒹蒺蒽荪蓁蓆蓇蓊蓌蓍蓏蓓蓖蓧蓪蓫荜跣藕苁蓰蓱莼蓷蓺蓼蔀蔂蔃蔆蔇蔉蔊蔋蔌蔎蔕蔘蔙蒌蔟锷蒋雯茑蔯蔳麻蔵蔸蔾荨蒇蕋蕍荞蕐蕑芸莸蕖蕗蕝蕞蕠蕡蒉蕣蕤蕨蕳蓣蕸蕺蕻薀薁薃薅薆荟薉芗薏薐蔷薖薘剃谔钗薜薠薢薤薧薨薫薬薳薶薷薸薽薾薿藄藇藋荩藐藙藚藟藦藳藴苈藷藾蘀蘁蕲苹蘗蘘蘝蘤蘧蘩蘸蘼虀虆虍蟠虒虓虖虡虣虥虩虬虰蛵蛇虷鳟虺虼蚆蚈蚋蚓蚔蚖蚘蚜蚡蚣蚧蚨蚩蚪蚯蚰蜒蚱蚳蚶蚹蚺蚻蚿蛀蛁蛄蛅蝮蛌蛍蛐蟮蛑蛓蛔蛘蛚蛜蛡蛣蜊蛩蛱蜕螫蜅蚬蜈蝣蜋蜍蜎蜑蠊蜛饯蜞蜣蜨蜩蜮蜱蜷蜺蜾蜿蝀蝃蝋蝌蝍蝎蝏蝗蝘蝙蝝鲼蝡蝤蝥猿蝰虻蝲蝴蝻螃蠏蛳螉螋螒螓螗螘螙螚蟥螟螣螥螬螭䗖螾螀蟀蟅蝈蟊蟋蟑蟓蟛蟜蟟蟢虮蟨蟪蟭蛲蟳蛏蟷蟺蟿蠁蠂蠃虿蠋蛴蠓蚝蠗蠙蠚蠛蠜蠧蟏蠩蜂蠮蠰蠲蠵蠸蠼蠽衁衄衄衇衈衉衋衎衒同衖胡衞裳钩衭衲衵衹衺衿袈裟袗袚袟袢袪袮袲袴袷袺袼褙袽裀裉袅裋夹裍裎裒裛裯裱裲裴裾褀褂褉褊裈褎褐褒褓褔褕袆褚褡褢褦褧褪褫袅褯褰褱裆褛褽褾襁褒襆裥襉襋襌襏襚襛襜裣襞襡襢褴襦襫襬襭襮襕襶襼襽襾覂覃覅霸覉覊覌覗觇覚覜觍觎覧覩觊觏覰観觌觔觕觖觜觽觝觡酲觩觫觭觱觳觯觷觼觾觿言赅讣訇訏訑訒诂讬訧訬訳訹证訾詀詅诋毁詈詊讵詑诒诐詗诎察詨诜詶詸詹詻诙诖誂誃诔锄诓誋诳诶悖誙诮诰誧説読誯谇訚谄谆諆諌诤诹诼諕谂谀諝谝諟喧谥諴諵谌谖誊謆謇歌謍謏謑谡谥謡謦謪谪讴謷謼谩哗譅譆譈譊讹譒撰谮鑫譞噪譩谵譬譱譲谴譸譹谫讅讆詟䜩雠讐谗谶讙谠讟谽豁豉豇岂豊豋豌豏豔豞豖豗豜豝豣豦豨豭豱豳豵豶豷豺豻貅貆狸猊貔貘䝙貜貤餍贳餸贶贲赂賏赊赇赒賝赓赕賨赍斗賮賵賸赚赙赜赟贉赆赑贕赝赬赭赱赳迄趁趂趄趐趑趒趔趡趦趫趮趯趱趴趵趷趹趺趿跁跂跅跆踬跄跐跕跖跗跙跛跦跧跩跫跬跮跱跲跴跺跼跽踅踆踈踉踊踒踖踘踜踟躇蹰踠踡踣踤踥踦踧跷踫踮逾踱踊踶踹踺踼踽躞蹁蹂躏蹎蹐蹓蹔跸蹚蹜蹝迹蹠蹡蹢跶蹧蹩蹪蹯鞠蹽躃躄躅踌跻躐踯跞躘躙躗躝躠蹑躜躧躩躭躰躬躶軃軆辊軏轫軘軜軝腭転軥軨軭軱轱辘軷轵轺軽軿輀輂辇辂辁輈挽輗辄辎辋輠輤輬輭輮辏輴輵輶輹輼辗辒轇轏轑轒辚轕轖轗轘轙轝轞轹轳罪辣辞辵辶辺込辿迅迋迍麿迓迣迤逦迥迨迮迸迺迻迿逄逅逌逍逑逓迳逖逡逭逯逴逶逹遄遅侦遘遛遝遢遨遫遯遰遴绕遹遻邂邅邉邋邎邕邗邘邛邠邢邧邨邯郸邰邲邳邴邶邷邽邾邿郃郄郇郈郔郕郗郙郚郜郝郞郏郠郢郪郫郯郰郲郳郴郷郹郾郿鄀鄄郓鄇鄈鄋鄍鄎鄏鄐鄑邹邬鄕郧鄗鄘鄚鄜鄞鄠鄢鄣鄤鄦鄩鄫鄬鄮鄯鄱郐鄷鄹邝鄻鄾鄿酃酅酆酇郦酊酋酎酏酐酣酔酕醄酖酗酞酡酢酤酩酴酹酺醁醅醆醊醍醐醑醓醖醝酝醡醤醨醪醭醯醰酦醲醴醵醸醹醼醽醾釂酾酽釆釈鲈镏阊钆钇钌钯钋鼢鼹钐钏釪釬釭釱钍釸钕钫鈃钭鈆鈇钚鈊鈌钤钣鈒鈤钬钪鈬铌铈钶铛钹铍钸钿鉄鉆铊铇鉌铋鉏铂钷铆钵鉥钲鉨钼钽鉱鉲鉶铰铒鉼铪銍銎铣銕镂铫铦铑铷銤铱铟銧铥铕铯銭銰焊銶锑锉汞鋂锒鋆鋈鋊铤鋍铗鋐鋑鋕鋘鋙锊锓锔锇铓鋭铖锆锂铽鋳鋹鋺鉴镚钎錀锞锖锫锩錍铔锕錔锱铮锛錞锬锜錤錩錬録铼錼锝钔锴鍉镀鍏鍐铡鍚锻锽锸锲锘鍫鍭鍱鍴锶鍹锗针锺锿镅鎉鎋鎌鎍鎏鎒鎓鎗镉鎚鎞镃鎤铩锼鎭鎯镒镍鎴镓鎸鎹镎镟鏊镆镠镝鏖铿锵鏚镗镘镛鏠鏦錾镤鏸镪鏻鏽鏾铙鐄鐇鐏铹镦镡鐗馗镫镢镨鐡锎镄鐩镌鐬鐱镭鐶鐻鐽镱鑀鑅镔鑐鑕鑚鑛鑢鑤镥鑪镧鑯鑱鑴鑵镊镢钃镻闫闬闶闳閒闵閗閟阂関合閤哄阆閲阉閺阎阏阍阌暗闉阕阗闑闒闿闘闚阚闟闠闤闼阞阢阤阨阬阯阹阼阽陁陑陔陛陜陡陥陬骘陴険陼陾阴隃隈隒隗隞隠隣隤隩隮隰颧隳隷隹雂雈雉雊雎雑雒雗雘雚雝雟雩雰雱驿霂霅霈霊沾霒霓霙霝霢霣霤霨霩霪霫霮靁叇叆靑靓靣腼靪靮靰靳靷靸靺靼靿鞀鞃鞄鞍鞗鞙鞚鞝鞞鞡鞣鞨鞫鞬鞮鞶鞹鞾鞑韅鞯驮韍韎韔韖韘韝韫韡韣韭韭韱韹韺頀刮頄顸顼頍颀颃颁頖頞頠頫頬颅頯頲颕頼悴顋顑颙颛颜顕顚顜颟顣颥颞飐飑台飓颸飏飖颽颾颿飀飂飚飌翻飡飣饲飥饨饫飮飧飶餀餂饸饹餇餈饽哺馂餖餗餚馄馃餟餠餤餧餩餪餫糊餮糇餲饧馎糕饩馈馊馌馒饇馑馓膳饎饐饘饟馕馘馥馝馡馣骝骡馵馹駃駄駅駆駉駋驽駓驵駗骀驸駜骂骈駪駬骃駴骎駹駽駾騂騄骓騆騉騋骒骐麟騑騒験騕骛騠騢騣騤騧骧騵驺骟騺蓦骖骠骢驆驈骅驌骁驎骣驒驔驖驙驦驩驫骺鲠骫骭肮骱骴骶骷髅骾髁髂髄髆膀髇髑髌髋髙髝髞髟髡髣髧髪髫髭髯髲髳髹髺髽髾鬁鬃鬅鬈鬋鬎鬏鬐鬑鬒鬖鬗鬘鬙鬠鬣斗鬫鬬阄鬯鬰鬲鬵鬷魆魈魊魋魍魉魑魖鳔魛魟魣魦魨魬鲂魵魸鮀鲅鮆鲧鲇鲍鲋鮓鲒鲕鮟鱇鮠鮦鮨鲔鲑鮶鮸鮿鲧鯄鯆鲩鯈鲻鯕鲭鲞鯙鯠鲲鯥鲰鲶鳀鯸鳊鲗䲠鹣鳇鰋鳄鳆鰕鰛鰜鲥鰤鳏鰦鳎鳐鳁鳓鰶鲦鲡鰼鰽鱀鱄鳙鱆鳕鱎鱐鳝鳝鳜鲟鲎鱠鳣鱨鲚鱮鱲鱵鱻鲅鳦凫鳯鳲鳷鳻鴂鴃鴄鸩鴈鴎鸰鴔鴗鸳鸯鸲鹆鸱鴠鴢鸪鴥鸸鹋鴳鸻鴷鴽鵀鵁鸺鹁鵖鵙鹈鹕鹅鵟鵩鹌鵫鵵鵷鵻鹍鶂鶊鶏鶒鹙鶗鶡鶤鶦鶬鶱鹟鶵鶸鶹鹡鶿鹚鷁鷃鷄鷇䴘䴘鷊鷏鹧鷕鹥鸷鷞鷟鸶鹪鹩鷩鷫鷭鹇鹇鸴鷾䴙鸂鸇䴙鸏鸑鸒鸓鸬鹳鸜鹂鹸咸鹾麀麂麃麄麇麋麌麐麑麒麚麛麝麤麸面麫麮麯麰麺麾黁黈黉黢黒黓黕黙黝黟黥黦黧黮黰黱黪黶黹黻黼黾鼋鼂鼃鼅鼈鼍鼏鼐鼒冬鼖鼙鼚鼛鼡鼩鼱鼪鼫鼯鼷鼽齁齆齇齈齉齌赍齑龀齕齗龅齚龇齞龃龉龆齢出齧齩齮齯齰齱齵齾厐龑龒龚龖龘龝龡龢龤" + +traditional_characters = "制咖片型超聲盤鑒定仔點他命書歌粉巾字帳恤手指記憶棒形轉彎溝光○〇㐄㐅㐆㐌㐖毒㐜㐡㐤㐰㐺㑇㑳㒳㒸㔾㗂㗎㝵㞎㞙㞞㠯㢲㢴㤅㥁㥯㨗㫺㬎㮎㮚㮸㲋㲱㲾㳮㵎㵪㶸㷖㷭㹢㹴犬㺢狓㺵㼝㽮㿝䍃䔢䖟䖸䗈䗥䗪䝓䠶䥯䦉䯝䰾魚䲔䳗䳘䵹鼄䶑一對應映射丁不識下兒子做二休世丘之貉並中台原則串為甚謂乾淨了百事無成八變五十些人得道雞升天代如併來去個國政策勁幽靈在歐洲遊蕩接樣蘿蔔坑側化傳價元論醇共再准刀兩斷切分耕耘收穫錢貨物向看舊就緒險刻千金動勞永逸匙零夜半卡通回復返影蹤反常態口咬氣句話同吐快吹周味呼諾嗚品紅鍋哄而散起唱和問三知生熟團漆黑火糟堆場空塊麵塌糊塗塵染壁廂夔已足多情露水大早到晚夫妻當關萬莫開失古恨套所料既往孔見提師要家主審寸陰難買鬥牛小撮部陣局展身層巴掌帆風順席地帶過年計於春頭載四季期被蛇怕井繩度願式份彈頃深前律徑心意念差愁孤行俱全房廳交遮打技長把抓死拿眼淚鼻涕鑰鎖折段抿拍即合掃排掬揮撥擁上入擊洞擲攬改故轍敗文值名斑方面旁族日秋餐隔雅里終父旦時晌會霎間晃暴寒曝更月望垠際朝夕本正經利杯羹東西板枝獨秀根筋桿進條龍服務概模次函數又性程總付步腳印趨登毛拔呵氧氮碳決雌雄波未平派謊言流清楚白準溜煙潭有獲聞是處降琴鶴甲病發可拾沙目然瞭直以相眨穿睹瞥瞬矢的解石鳥神教秉虔誠秘種窩蜂窮竅笑置筆苟勾銷抹殺煞等獎箍節吃箭仇雙鵰詩籌籮筐系列紙級士官統絲毫掛維網盡線微吭響股腦胎脈承腔臂力致效資源址器舉功投般說講規貿易葉障著慎滿皆輸號木電池衣傾鐘高低視仁覺醒覽遺角銀幣觸潰九鼎蔽抄出駟馬追重語破貧洗貫走路安蹴至幾蹶振躍役膽汗較輩輪辭贊退六連遍遞邊針血錘音錯門思閃真倒項栽霧類保護川先驚乍體鬨鱗爪鳴滴泡鄰域黨專鼓作齊炒丑烯亥克內酯冬加奴卯肝炎基尺梁街褲鎬客寵庭巳汝昌烷玲磊糖肇酉醛啷青縣韙良香骨鯛丂七集河市弦喜嘴張舌堵區工業姊妹星架構巧彩扭歪拼湊餘熱曜武州爺浮屠美鄉老階樹葷素碎落能魄鰓鰻珠丄丅丆万俟丈尚摸母娘量管群亞虎必我堂令申件裝伏位博俠義界表女墟臺戲臭皮匠勝諸葛亮賽頂倍催請運算包立叉戟離疫苗土史志演圍揭瓦曬夷姑婆帝村寶爛尖杉鹼屜桌山岔島由紀峽壩庫鎮廢從德後拗湯治旬食明昧曹朋友框欄極權冪曲歸依貓民氟硼氯磷鐵江侗自旅法司洋浦梅園溫暖灣焦班幸用田略番疊皇炮捶硝苯酸腺苷稜草鏡穗跳遠索錦綱聚氰胺聯店胚膲愛色堇紫羅蘭芝茶飯菱雲蟲藏藩亂叛蘇親債凳學座恐戀柱測肌腹衩錐係貂企烏跪叩軍車農題迭都甘油屯奏鍵短阿姨陪姐隻顧茅廬槽駕魂鮮鹿頁其菜單乘任供勢午齒漢組織吊調瀉唇坡城報墳外夸將尉建築岸崗公床揚新劍昇杭林栗校樓標款汽社浣海商館劇院鋼華港機械廣媒環球融第醫科證券綜財樂育游漲猶嶺疏癮瞼確兵領導繳肢膛船艾瑟爾蒼蔡虞傚衫覆訪訴課諭議軌述野鉤限敵鞋頜頷顎饒首齦站例修凡劃垂屆屬崽頦廚拜挫擺放旋削棋榻檻禮沉注滑營獄畫确儀聘花葬詔員跌轄週達酒錨閘陷陸雨雪飛威丌于丹久乏予理評產亢卑亦乎舞己悲矩圓詞害誌但住佞佳便俗信票案幅翁倦倫假偏倚斜虧鬼敲停備傷脾胃僅此像儉匱免宜穴焉戴兼容許凍伯仲負彼晝皂軒輊實刊划顛衛戰哥比省非好黃飾別拘束掩奶睬選擇搖擾煩苦枚寫協厭及格受歡迎約只估侵犯割狀告或缺抗拒挽撤救藥喻磨滅端倪少逆逾越避靠適吉譽吝玉含延咎歹聽啻淵善謀均勻堪忍夠太惹妙妥妨孕症孝術室完納推冠積宣疑辯慄碴稱屈撓屑干涉衡待很忙惡忿怎麼怠急恥恭息悅惑惜惟想愉愧怍慌憤啟懂懈懷材才緊招認扣抵拉捨也罷插揣冒搭撞南牆擴核支攻敢雷攀敬裡嗎需景智暇曾罪遇朽枉止況競爭辱求癒渝溶濟左右袒困補爽特寂寞示弱找謝畏強疾徐痛癢冤符眠睦瞅董何厚云措活疲羞者輕玻璃祥兆禁移稂莠穩佛換答簡結果盟絕縷途給談否羈翼耐肖脛毋寧興舒若菲萊痕跡窠臼虛衰臉兔撒鷹棺範該詳諱抬泰讓鬚眉象眾貲賬費灰賴奇慮訓輟辨菽麥辛近送透逞徒速續逮捕遂遑違遜斧鉞艱醉鏽隨觀棄顯飽脂肪使丏丐幫丒且慢末丕替桃宗王尊涼爵各圖屋脊糧署錄壇吾祿職胄襲君廈丗北壑桐疹損逢陵鷸丙寅戌氨腈唑綸辰酮脫氫酶醚丞丟現掉紗帽弄扯砲碗丠両丣坐存激肩臻蒂蓮悖序驅丨丩丫挺杈髻鬟細介俄伊犁京尼布訂普渡央委監察檢查劑圈設警隊斯督剩震境航舶革防托播促質版蠑螈鋒研藝歷殘消頻譜精密製造陲郵候埔堅壓壢凹匯執府究邦俘攝寮彬狼嶽肺腫庸英訊診埋粒胞括控碼韓暑槍樞砥澳哇牟壽甸鑽探篇簽綴縫繼耳肯照婦埃懸璧軸櫃檯辣擱淺邪跑纖阮陽私囊魔丮丰姿采丱燒丳丵丶丷丸參寨朗桂瑞砂衷霞貌鳳僕艦因嫌宰峰幹絡牌持旨祭禱簿編罰賓辦丼丿乀乂乃乄仰慕盛曠留考驗闊乆乇么醜麼乊湖燃乑乒乓乕乖僻忤戾离謬迕乗危肥劫除隙浪婿乙炔腸酰吡咯鹽乚乛乜嘢卿玄宮尾狐龜塔嶷兄弟泉章霄釘耙乞扎哀憐恕討乢乣乤乥乧乨乩童乪乫乭乳暈汁液瑤漿牙癌突竇罩腐膠豬酪蛋糕菌瘤乴乵乶乷乸乹乺乼乾俸冰嘉噦嚎坤媽屍壘旱枯涸俐渴潮澀煸豆燥爹瘦癟癬瞪袋脆薑貝隆餾乿亀亁叫咕攘扔搞男砸竄蓬麻亃亄亅卻亇遲典今臨繁累卵奉婚聰躬巨與遷添裂副宿歲怪噁尕崙愣杆硅硫鈦鈾錳芑雜異鈉砷胂磺琥珀艙棍簧胡茬盜浩盆販郎腿亍洪亐互欠助勉惠操斥諉繫戶譯亓墓碑刑鈴卅渠繽紛斗米旗憲釩燈徽瘟祖拳福穀豐臟腑綁肉醃苓蘊橋鋪霸顏鬧判噴岡底蛙陘礦亖亙亜罕們娜桑那努哈喀弗烈曼松森杜氏盃奧琛敦戊穆聖裔彙薛孫亟亡佚虜羊牢奮釋卷卸契媾感額睫纏誼趾塞擠紐阻還配馳莊亨洛祚亪享津滬畿郊慈菴枇杷膏亭閣鋥麗亳亶亹誅初責翻瘋偶傑叢稠妖拖寰居吸授慧蝸吞壯魅狗矛盾益渣患憂稀描猿夢暫涯畜禍緣沸搜引擎臣橫紜誰混援蒸獸獅稅剖亻亼亽亾什獻剎邡麽仂仃仄仆富怨仈仉畢昔晨殼紹仍仏仒仕宦仗欺恃腰嘆歎炬梓訖施仙后瓊逝仚仝仞仟悔仡佬償填泊拓撲簇羔購頓欽佩髮棻閫馭養億儆尤藉幀賑凌敘帖李柔剛沃眥睚戒訛取饗讀仨仫仮著泳臥躺韶夏裁仳仵唯賢憑釣誕仿似宋彿諷伀碩盼鵝伄儅伈伉儷柯始娃邁戈坦堡帕茨薩廟瑪莉莎藤霍姆伋伍奢胥廷芳豪伎倆侍汛勒希羲雛伐憩整謨閑閒伕伙伴頤伜伝伢叔恆茲恩翰伱伲侶伶俜悧鼬伸懶縮喇叭伹伺伻伽倻輻伾佀佃佇佈喬妮墨佉盧佌貸劣廉昂檔濃矮傘窪緩耗胸谷迷擋率齲宅沫舍療佐貳佑佔優據鏵嘗呢須魯曉佗佘余坪寺瓜銃僧蒙芒陀龕哼嘔坊姦孽弊揖祟繭縛誓賊佝僂瞀佟你奪趕佡佢佣佤佧賈佪佫佯佰佱潔績釀餚佴捲佶佷佸佹佺佻佼佽佾具喚窘壞娛怒慨硬習慣聾膨脹蔓駭貴痺侀侁侂侃侄侅鴻燕侇侈糜靡侉侌妾侏儒倉鼠侐侑侔侖侘侚鏈侜偎傍鈷循柳葫蘆附価侮罵蔑侯岩截蝕侷貼壺嬛宴捷攜桶箋酌俁狹膝狄俅俉俊俏俎俑俓俔諺俚俛黎健呈固墒增守康箱濕祐鏢鑣槓盒靖膜齡俞豹獵噪孚封札筒託衍鴿剪撰稿煉廠禊練繕葺俯瞰撐衝俲俳俴俵俶俷俺俻俾倀倂倅儲卒惶敷猝逃頡蓄崇隱倌倏忽刺蠟燭噍嚼坍扁抽斃蔥楣灌灶糞背藪賣賠閉霉騰倓倔倖倘倜儻倝借箸挹澆閱倡狂倢倣値倥傯倨傲倩匡嗣沖柝珍倬倭寇猩倮倶倷倹勤讚偁偃充偽吏嗓寐惺扮拱芫茜藉虢鈔偈偉晶偌宕距析濾殿疼癱註頗偓偕鴨歇滯偝偟偢忘怡旺偨偩偪偫偭偯偰偱偲偵緝蹄偷減惰漏窺竊偸偺迹傀儡傅傈僳傌籬傎奎琳迪叟芭傒傔傕傖悉荒傜傞傢傣芽逼傭婢傮睨寄檄誦謠頌傴擔辜弓慘蒿悼疤傺傻屄臆巢洩篋羨蓋軋頹傿儸僄僇僉僊働僎僑僔僖僚僝僞僣僤僥僦猴僨僩僬僭僮僯僰僱僵殖籤靜僾僿征隴儁儂儃儇儈朴薄儊儋儌儍儐儓儔儕儗儘儜儞儤儦儩汰哉寡渥裕酷儭儱罐儳儵儹儺儼儽兀臬臲鷲允勛勳宙宵帥憝彞諧嫂鬩暢沛溢盈飢赫兇悍狠猛頑愚妣斬秦遣鞭耀敏榮槃澤爆碟磁禿纜輝霽鹵朵婁孜烽醬勃汀箕裘鉗耶懞蕾徹兌軟遭黜兎児韻媳爸兕觥兗兙兛兜售鍪肚兝兞兟兡兢兣樽殮涅睡稟籍贅泌啡肽奸幕涵澇熵疚眷稃襯訌赴煥椒殲植跏沒試誤猜棲窗肋袖頰兪卦撇鬍岐廓轎疸楓茴瓏廁秩募勺噸寓斤曆畝迫筷釐最淫螺韜兮寬匪篩襄贏軛複兲詐刃堰戎痞蟻餉它冀鑄冂冃円冇冉冊嫁厲礪竭醮冏牧冑冓冔冕冖冗冘冞冢窄抑誣冥冫烘菇蟄冷凝坨橇淇淋炭餅磚磧窖醋雕雹霜冱冶爐艷嘲峻灘淡漠煖颼飲冼冽凃凄愴梗凅凇凈凊凋敝濛凔凜遵汞脢凞几凢処凰凱凵凶焰凸摺刷紋預喪嘍奔巡榜殯芙蓉租籠輯鞘萃凼鋸鑊刁蠻刂娩崩批拆攤掰櫱驟歧顆秒袂贓勿囑忌磋琢膚刈羽刎訟戮舂槳艇刓刖霹靂刜創犢刡恙墅幟筵緻刦刧刨昏默攸尿慾薰潤薰圭刪刮痧鏟刱刲刳刴刵踏磅戳柏槐繡芹莧蝟舟銘鵠鶩刼剁剃辮剄剉履鉛剋剌姻咽哨廊掠桅沿召瞻翅趙卜渺茫郭剒剔剕瀝剚愎毅訥纔剜剝啄採剞剟剡剣剤綵剮腎駛黏剰袍剴紊剷剸剺剽剿劁劂劄劈啪柴扳啦劉奭姥夼昫涓熙禪禹錫翔雁鶚劊劌弩柄蜻蛉劒劓劖劘劙瀾簣賞磯釜晉甜薪逐劦熔紂虐赤囚劬劭労劵効劻劼劾峭艮勅勇勵勍勐臘脖龐漫飼盪粥輒勖勗勘驕餒碌泮雇捐竹騎殊阱勣樸懇謹勦勧勩勯勰勱勲勷勸懲慰誡諫勹芡踐闌匁庇拯粟紮袱裹餃匆遽匈匉匊匋匍匐莖匏匕妝痰膿蛹齋苑烤蹈塘羌熊閥螳螂疆碚竿緯荷茵邙魏匚匜匝匟扶稷匣匭攏匸匹耦匽匾匿卂叮瘡禧軫堤棚迢鈞鍊卄卆遐卉瓷盲瓶噹胱腱裸卋卌卍卐怯污賤鄙齷齪陋卓溪唐梯漁陳棗泥漳潯澗梨芬譙贍轅迦鄭単驢弈洽鰲卛占筮卝卞卟吩啉屎翠厄卣卨卪卬卮榫襖璽綬鈕蚤懼殆篤聳卲帘帙繞卹卼卽厂厎厓厔厖厗奚厘厙厜厝諒厠厤厥厪膩孢厮厰厳厴厹厺粕垢蕪菁厼厾叁悟茸薯叄吵笄悌哺譏坫壟弧芯杠潛嬰芻袁詰貪諜煽饋駁収岳締災賄騙叚叡吻攔蘑蜜訣燧玩硯箏椎藺銅逗驪另覓叨嘮謁杵姓喊嚷囂咚嚀塑尋惱憎擦祇泣滲蝠叱吒咄咤喝籀黛舵舷叵叶鐸懿昭穰苴遼叻叼吁塹嫖賭瞧爬衆抒吅吆夥巹橡滌抱縱摩郡唁墜扇籃膀襪頸吋愾諮酬哭妓媛暗錶韁邇妃羿絮蕃渾拐葵暮隅吔吖啶嗪戚吜嗇噬嚥吟哦詠吠吧唧嗒咐吪雋咀徵燐苞茹鈣哧吮吰吱嘎吲哚吳棟嬌窟孟簫忠晗淞闔閭趼宇吶睛噓拂捧疵熄竽笛糠吼吽呀呂韋矇呃呆笨呇貢呉罄呋喃呎呏呔呠呡癡呣呤呦呧瑛眩扒晬淑姬瑜璇鵑呪呫嗶嚅囁呬呯呰呱呲咧噌鈍呴呶呷呸呺呻哱咻嘯嚕籲坎坷邏呿咁咂咆哮咇咈咋蟹煦珅藹咍咑咒詛咔噠嚓咾噥哩喱咗咠咡咢咣咥咦咨嗟詢咩咪咫嚙齧咭咮咱咲咳嗆嗽咴咷咸咹咺咼喉咿婉慟憫賦矜綠茗藍哂搶瞞哆嗦囉噻啾濱彗哋哌哎唷喲哏哐哞哢哤哪裏哫啼喘哰哲萎蚌哳哶哽哿唄唅唆唈唉唎唏嘩堯棣殤璜睿肅唔睇唕唚唞唣喳唪唬唰喏唲唳唵嘛唶唸唹唻唼唾唿啁啃鸚鵡啅埠棧榷祺舖鞅飆啊啍啎啐啓啕啖啗啜啞祈啢啣啤啥啫啱啲啵啺饑啽噶崑沁喁喂喆裙喈嚨喋喌喎喑喒喓喔粗喙幛慶滋鵲喟喣喤喥喦喧騷喨喩梆喫葡萄喭駝挑嚇碰樅瓣純皰藻趟鉻喵営喹喺喼喿嗀嗃嗄嗅嗈嗉嗊嗍嗐嗑嗔詬嗕嗖嗙嗛嗜痂癖嗝嗡嗤嗥嗨嗩嗬嗯嗰嗲嗵嘰嗷嗹嗾嗿嘀嘁嘂嘅惋嘈峪禾蔭嘊嘌嘏嘐嘒嘓嘖嘚嘜嘞嘟囔嘣嘥嘦嘧嘬嘭這謔嚴敞饞鬆嘵嘶嘷嘸蝦嘹嘻嘽嘿噀噂噅噇噉噎噏噔噗噘噙噚噝噞噢噤蟬皿噩噫噭噯噱噲噳嚏涌灑欲巫霏噷噼嚃嚄嚆抖嚌嚐嚔囌嚚嚜嚞嚟嚦嚬嚭嚮嚯嚲嚳飭按竣苛嚵嚶囀囅囈膪謙囍囒囓囗囘蕭酚飄濺諦囝溯眸紇鑾鶻囟殉囡団囤囥囧囨囪囫圇囬囮囯囲図囶囷囸囹圄圉擬囻囿圀圂圃圊粹蠹赦圌墾圏滾鯡鑿枘圕圛圜圞坯埂壤骸炕祠窯豚紳魠鯪鱉圧握圩圪垯圬圮圯炸岬幔毯祇窨菩溉圳圴圻圾坂坆沾坋坌舛壈昆墊墩椅坒坓坩堝坭坰坱坳坴坵坻坼楊掙涎簾垃垈垌垍垓垔垕垗垚垛垝垣垞垟垤垧垮垵垺垾垿埀畔埄埆埇埈埌殃隍埏埒埕埗埜埡埤埦埧埭埯埰埲埳埴埵埶紼埸培怖樁礎輔埼埽堀訶姪廡堃堄摧磐貞韌砌堈堉堊堋堌堍堎堖堙堞堠礁堧堨輿堭堮蜓摘堲堳堽堿塁塄塈煤塋棵塍塏塒塓綢塕鴉沽虱塙塚塝繆塡塢塤塥塩塬塱塲蟎塼塽塾塿墀墁墈墉墐夯増毀墝墠墦漬缽墫墬墮墰墺墻櫥壅壆壊壌壎壒榨蒜壔壕壖壙壚壜壝壠壡壬壭壱売壴壹壻壼寢壿夂夅夆変夊夌漱邑夓腕泄甥禦骼夗夘夙袞瑙妊娠醣梟珊鶯鷺戧幻魘夤蹀祕擂鶇姚宛閨嶼庾撻拇賛蛤裨菠氅漓撈湄蚊霆鯊箐篆篷荊肆舅荔鮃巷慚骰辟邱鎔鐮阪漂燴鯢鰈鱷鴇臚鵬妒峨譚枰晏璣癸祝秤竺牡籟恢罡螻蠍賜絨御梭夬夭砣榆怙枕夶夾餡奄崛葩譎奈賀祀贈奌奐奓奕訢詝奘奜奠奡奣陶奨奩魁奫奬奰媧孩貶隸酥宄狡猾她奼嫣妁氈荼皋膻蠅嬪妄妍嫉媚嬈妗趣妚妞妤礙妬婭妯娌妲妳妵妺姁姅姉姍姒姘姙姜姝姞姣姤姧姫姮娥姱姸姺姽婀娀誘懾脅娉婷娑娓娟娣娭娯娵娶娸娼婊婐婕婞婤婥谿孺婧婪婬婹婺婼婽媁媄媊媕媞媟媠媢媬媮媯媲媵媸媺媻媼眯媿嫄嫈嫋嫏嫕嫗嫘嫚嫜嫠嫡嫦嫩嫪毐嫫嫬嫰嫵嫺嫻嫽嫿嬀嬃嬅嬉耍嬋痴豔嬔嬖嬗嬙嬝嬡嬢嬤嬦嬬嬭幼嬲嬴嬸嬹嬾嬿孀孃孅孌孏曰癲屏孑孓雀孖斟簍謎摺孛矻鳩崮軻祜鸞孥邈毓棠臏孬孭孰孱孳孵泛罔銜孻孿宀宁宂拙株薇掣撫琪瓿榴謐彌宊濂祁瑕宍宏碁宓邸讞実潢町宥宧宨宬徵崎駿掖闕臊煮禽蠶宸豫寀寁寥寃簷庶寎暄磣寔寖寘寙寛寠苫寤肘洱濫蒗陝覈寪弘綽螽寳擅疙瘩晷対檐専尃尅贖絀繚疇釁尌峙醌襟痲碧屁昊槌淘恵瀑牝畑莓缸羚覷蔻髒躁尒尓銳尗尙尜尟尢尥尨尪尬尭尰擒尲尶尷尸尹潽蠖蛾尻釦梢蚴鰭脬蹲屇屌蚵屐屓挪屖屘屙屛屝屢屣巒嶂巖舄屧屨屩屪屭屮戍駐鉀崖嵛巔旮旯楂欖櫸芋茱萸靛麓屴屹屺屼岀岊岌岍阜岑彭鞏岒岝岢嵐岣岧岨岫岱岵岷峁峇峋峒峓峞峠嵋峩峯峱峴峹峿崀崁崆禎崋崌崍嶇崐崒崔嵬巍螢顥崚崞崟崠崢巆崤崦崧殂崬崱崳崴崶崿嵂嵇嵊泗嵌嵎嵒嵓嵗嵙嵞嵡嵩嵫嵯嵴嵼嵾嶁嶃嶄晴嶋嶌嶒嶓嶔嶗嶙嶝嶞嶠嶡嶢嶧嶨嶭嶮嶰嶲嶴嶸巂巃巇巉巋巌巓巘巛滇芎巟巠弋迴巣巤炊擘蜥蟒蠱覡巰蜀彥淖杏茂甫楞巻巽幗巿帛斐鯽蕊帑帔帗帚琉汶帟帡帣帨帬帯帰帷帹暆幃幄幇幋幌幏幘幙幚幞幠幡幢幦幨幩幪幬幭幯幰遙蹉跎餘庚鑑幵幷稚邃庀庁広庄庈庉笠庋跋庖犧庠庤庥鯨庬庱庳庴庵馨衢庹庿廃廄廆廋廌廎廏廐廑廒廕廖廛廝搏鑼廞弛袤廥廧廨廩廱綿踵髓廸廹甌鄴廻廼廾廿躔弁皺弇弌弍弎弐弒弔詭憾薦弝弢弣弤弨弭弮弰弳霖繇燾斌旭溥騫弶弸弼弾彀彄彆纍糾彊彔彖彘彟彠陌彤貽彧繪虹彪炳彫蔚鷗彰癉彲彳彴彷彷徉徨彸彽踩斂旆徂徇徊渭畬鉉裼従筌徘徙徜徠膳甦萌漸徬徭醺徯徳徴潘徻徼忀瘁胖燎怦悸顫扉犀澎湃砰恍惚絞隘忉憚挨餓忐忑忒忖応忝忞耿忡忪忭忮忱忸怩忻悠懣怏遏怔怗怚怛怞懟黍訝怫怭懦怱怲怳怵惕怸怹恁恂恇恉恌恏恒恓恔恘恚恛恝恞恟恠恣恧眄恪恫恬澹恰恿悀悁悃悄悆悊悐悒晦悚悛悜悝悤您悩悪悮悰悱悽惻悳悴悵惘悶悻悾惄愫鍾蒐惆惇惌惎惏惓惔惙惛耄惝瘧濁惥惦惪惲惴惷惸拈愀愃愆愈愊愍愐愑愒愓愔愕愙氓蠢騃昵愜赧愨愬愮愯愷愼慁慂慅慆慇靄慉慊慍慝慥慪慫慬慱慳慴慵慷慼焚憀灼鬱憃憊憋憍眺捏軾憒憔憖憙憧憬憨憪憭憮憯憷憸憹憺懃懅懆邀懊懋懌懍懐懞懠懤懥懨懫懮懰懱毖懵遁樑雍懺懽戁戄戇戉戔戕戛戝戞戠戡戢戣戤戥戦戩戭戯轟戱披菊牖戸戹戺戻戼戽鍬扂楔扃扆扈扊杖牽絹銬鐲賚扐摟攪烊盹瞌跟躉鑔靶鼾払扗玫腮扛扞扠扡扢盔押扤扦扱罾揄綏鞍郤窾扻扼扽抃抆抈抉抌抏瞎抔繯縊擻抜抝択抨摔歉躥牾抶抻搐泵菸拃拄拊髀拋拌脯拎拏拑擢秧沓曳攣迂拚拝拠拡拫拭拮踢拴拶拷攢拽掇芥橐簪摹疔挈瓢驥捺蹻挌挍挎挐揀挓挖掘浚挙揍聵挲挶挾挿捂捃捄捅捆捉捋胳膊揎捌捍捎軀蛛捗捘捙捜捥捩捫捭据捱捻捼捽掀掂掄臀膘掊掎掏掐笙掔掗掞棉芍掤搪闡掫掮掯揉掱掲掽掾揃揅揆搓揌諢揕揗揘揜揝揞揠揥揩揪揫櫫遒麈揰揲揵揶揸揹揺搆搉搊搋搌搎搔搕撼櫓搗搘搠搡搢搣搤搥搦搧搨搬楦褳訕赸搯搰搲搳搴搵搷搽搾搿摀摁摂摃摎摑摒摓跤摙摛摜摞摠摦睺羯摭摮摯摰摲摳摴摶摷摻摽撂撃撅稻撊撋撏鐧潑撕撙撚撝撟撢撣撦撧撩撬撱朔撳蚍蜉撾撿擀擄闖擉缶觚擐擕擖擗擡擣擤澡腚擧擨擩擫擭擯擰擷擸擼擽擿攃攄攆攉攥攐攓攖攙攛每攩攫轡澄攮攰攲攴軼攷砭訐攽碘敁敃敇敉敍敎筏敔敕敖閏誨敜煌敧敪敱敹敺敻敿斁衽斄牒縐謅斉斎斕鶉讕駮鱧斒筲斛斝斞斠斡斢斨斫斮晾沂潟穎絳邵斲斸釳於琅斾斿旀旂旃旄渦旌旎旐旒旓旖旛旝旟旡旣浴旰獺魃旴旹旻旼旽昀昃昄昇昉晰躲澈熹皎皓礬昑昕昜昝昞昡昤暉筍昦昨昰昱昳昴昶昺昻晁蹇隧蔬髦晄晅晒晛晜晞晟晡晢晤晥曦晩萘瑩顗晿暁暋暌暍暐暔暕煅暘暝暠暡曚暦暨暪朦朧暱暲殄馮暵暸暹暻暾曀曄曇曈曌曏曐曖曘曙曛曡曨曩駱曱甴肱曷牘禺錕曽滄耽朁朅朆杪栓誇竟粘絛朊膺朏朐朓朕朘朙瞄覲溘饔飧朠朢朣柵椆澱蝨朩朮朰朱炆璋鈺熾鹮朳槿朶朾朿杅杇杌隉欣釗湛漼楷瀍煜玟纓翱肈舜贄适逵杓杕杗杙荀蘅杝杞脩珓筊杰榔狍閦顰緬莞杲杳眇杴杶杸杻杼枋枌枒枓衾葄翹紓逋枙狸椏枟槁枲枳枴枵枷枸櫞枹枻柁柂柃柅柈柊柎某柑橘柒柘柙柚柜柞櫟柟柢柣柤柩柬柮柰柲橙柶柷柸柺査柿栃栄栒栔栘栝栟栢栩栫栭栱栲栳栴檀栵栻桀驁桁鎂桄桉桋桎梏椹葚桓桔桕桜桟桫欏桭桮桯桲桴桷桹湘溟梃梊梍梐潼梔梘梜梠梡梣梧梩梱梲梳梴梵梹棁棃櫻棐棑棕櫚簑繃蓑棖棘棜棨棩棪棫棬棯棰棱棳棸棹槨棼椀椄苕椈椊椋椌椐椑椓椗検椤椪椰椳椴椵椷椸椽椿楀楄楅篪楋楍楎楗楘楙楛楝楟楠楢楥楨楩楪楫楬楮楯楰楳楸楹楻楽榀榃榊榎槺榕榖榘榛狉莽榜笞榠榡榤榥榦榧榪榭榰榱槤霰榼榾榿槊閂槎槑槔槖様槜槢槥槧槪槭槮槱槲槻槼槾樆樊樏樑樕樗樘樛樟樠樧樨権樲樴樵猢猻樺樻罍樾樿橁橄橆橈笥龠橕橚橛輛橢橤橧豎膈跨橾橿檁檃檇檉檍檎檑檖檗檜檟檠檣檨檫檬檮檳檴檵檸櫂櫆櫌櫛櫜櫝櫡櫧櫨櫪櫬櫳櫹櫺茄櫽欀欂欃欐欑欒欙欞溴欨欬欱欵欶欷歔欸欹欻欼欿歁歃歆艎歈歊蒔蝶歓歕歘歙歛歜歟歠蹦詮鑲蹣跚陞陟歩歮歯歰歳歴璞歺瞑歾歿殀殈殍殑殗殜殙殛殞殢殣殥殪殫殭殰殳荃殷殸殹蛟殻殽謗毆毈毉餵毎毑蕈毗毘毚茛鄧毧毬毳毷毹毽毾毿氂氄氆靴氉氊氌氍氐聊氕氖気氘氙氚氛氜氝氡洶焊痙氤氳氥氦鋁鋅氪烴氬銨痤汪滸漉痘盂碾菖蒲蕹蛭螅氵氷氹氺氽燙氾氿渚汆汊汋汍汎汏汐汔汕褟汙汚汜蘺沼穢衊汧汨汩汭汲汳汴隄汾沄沅沆瀣沇沈葆浸淪湎溺痼痾沌沍沏沐沔沕沘浜畹礫沚沢沬沭沮沰沱灢沴沷籽沺烹濡洄泂肛泅泆湧肓泐泑泒泓泔泖泙泚泜泝泠漩饃濤粼濘蘚鰍泩泫泭泯銖泱泲洇洊涇琵琶荽薊箔洌洎洏洑潄濯洙洚洟洢洣洧洨洩痢滔洫洮洳洴洵洸洹洺洼洿淌蜚浄浉浙贛渫浠浡浤浥淼瀚浬浭翩萍浯浰蜃淀苔蛞蝓蜇螵蛸煲鯉浹浼浽溦涂涊涐涑涒涔滂涖涘涙涪涫涬涮涴涶涷涿淄淅淆淊淒黯淓淙漣淜淝淟淠淢淤淥淦淩猥藿褻淬淮淯淰淳詣淶紡淸淹燉癯綺渇済渉渋渓渕渙渟渢滓渤澥渧渨渮渰渲渶渼湅湉湋湍湑湓湔黔湜湝湞湟湢湣湩湫湮麟湱湲湴湼満溈溍溎溏溛舐漭溠溤溧馴溮溱溲溳溵溷溻溼溽溾滁滃滉滊滎滏稽滕滘滙滝滫滮羼耷滷滹滻煎漈漊漎繹漕漖漘漙漚漜漪漾漥漦漯漰漵漶漷濞潀潁潎潏潕潗潚潝潞潠潦祉瘍潲潵潷潸潺潾潿澁澂澃澉澌澍澐澒澔澙澠澣澦澧澨澫澬澮澰澴澶澼熏郁濆濇濈濉濊貊濔疣濜濠濩觴濬濮盥濰濲濼瀁瀅瀆瀋瀌瀏瀒瀔瀕瀘瀛瀟瀠瀡瀦瀧瀨瀬瀰瀲瀳瀵瀹瀺瀼灃灄灉灋灒灕灖灝灞灠灤灥灨灩灪蜴灮燼獴灴灸灺炁炅魷炗炘炙炤炫疽烙釺炯炰炱炲炴炷燬炻烀烋瘴鯧烓烔焙烜烝烳飪烺焃焄耆焌焐焓焗焜焞焠焢焮焯焱焼煁煃煆煇煊熠煍熬煐煒煕煗燻礆霾煚煝煟煠煢矸煨瑣煬萁煳煺煻熀熅熇熉羆熒穹熗熘熛熜稔諳爍熤熨熯熰眶螞熲熳熸熿燀燁燂燄盞燊燋燏燔隼燖燜燠燡燦燨燮燹燻燽燿爇爊爓爚爝爟爨蟾爯爰爲爻爿爿牀牁牂牄牋牎牏牓牕釉牚腩蒡虻牠雖蠣牣牤牮牯牲牳牴牷牸牼絆牿靬犂犄犆犇犉犍犎犒犖犗犛犟犠犨犩犪犮犰狳犴犵犺狁甩狃狆狎狒獾狘狙黠狨狩狫狴狷狺狻豕狽蜘猁猇猈猊猋猓猖獗猗猘猙獰獁猞猟獕猭猱猲猳猷猸猹猺玃獀獃獉獍獏獐獒獘獙獚獜獝獞獠獢獣獧鼇蹊獪獫獬豸獮獯鬻獳獷獼玀玁菟玅玆玈珉糝禛郅玍玎玓瓅玔玕玖玗玘玞玠玡玢玤玥玦玨瑰玭玳瑁玶玷玹玼珂珇珈瑚珌饈饌珔珖珙珛珞珡珣珥珧珩珪珮珶珷珺珽琀琁隕琊琇琖琚琠琤琦琨琫琬琭琮琯琰琱琲瑯琹琺琿瑀瑂瑄瑉瑋瑑瑔瑗瑢瑭瑱瑲瑳瑽瑾瑿璀璨璁璅璆璈璉璊璐璘璚璝璟璠璡璥璦璩璪璫璯璲璵璸璺璿瓀瓔瓖瓘瓚瓛臍瓞瓠瓤瓧瓩瓮瓰瓱瓴瓸瓻瓼甀甁甃甄甇甋甍甎甏甑甒甓甔甕甖甗飴蔗甙詫鉅粱盎銹糰甡褥産甪甬甭甮甯鎧甹甽甾甿畀畁畇畈畊畋畎畓畚畛畟鄂畤畦畧荻畯畳畵畷畸畽畾疃疉疋疍疎簞疐疒疕疘疝疢疥疧疳疶疿痁痄痊痌痍痏痐痒痔痗瘢痚痠痡痣痦痩痭痯痱痳痵痻痿瘀瘂瘃瘈瘉瘊瘌瘏瘐瘓瘕瘖瘙瘚瘛瘲瘜瘝瘞瘠瘥瘨瘭瘮瘯瘰癧瘳癘瘵瘸瘺瘻瘼癃癆癇癈癎癐癔癙癜癠癤癥癩蟆癪癭癰発踔紺蔫酵皙砬砒翎翳蘞鎢鑞皚鵯駒鱀粵褶皀皁莢皃鎛皈皌皐皒硃皕皖皘皜皝皞皤皦皨皪皫皭糙綻皴皸皻皽盅盋盌盍盚盝踞盦盩鞦韆盬盭眦睜瞤盯盱眙裰盵盻睞眂眅眈眊県眑眕眚眛眞眢眣眭眳眴眵眹瞓眽郛睃睅睆睊睍睎睏睒睖睙睟睠睢睥睪睪睯睽睾瞇瞈瞋瞍逛瞏瞕瞖瞘瞜瞟瞠瞢瞫瞭瞳瞵瞷瞹瞽闍瞿矓矉矍鑠矔矗矙矚矞矟矠矣矧矬矯矰矱硪碇磙罅舫阡、矼矽礓砃砅砆砉砍砑砕砝砟砠砢砦砧砩砫砮砳艏砵砹砼硇硌硍硎硏硐硒硜硤硨磲茚鋇硭硻硾碃碉碏碣碓碔碞碡碪碫碬碭碯碲碸碻礡磈磉磎磑磔磕磖磛磟磠磡磤磥蹭磪磬磴磵磹磻磽礀礄礅礌礐礚礜礞礤礧礮礱礲礵礽礿祂祄祅祆禳祊祍祏祓祔祕祗祘祛祧祫祲祻祼餌臠錮禂禇禋禑禔禕隋禖禘禚禜禝禠禡禢禤禥禨禫禰禴禸稈秈秊闈颯秌秏秕笈蘵賃秠秣秪秫秬秭秷秸稊稌稍稑稗稙稛稞稬稭稲稹稼顙稾穂穄穇穈穉穋穌貯穏穜穟穠穡穣穤穧穨穭穮穵穸窿闃窀窂窅窆窈窕窊窋窌窒窓窔窞窣窬黷蹙窰窳窴窵窶窸窻竁竃竈竑竜竝竦竪篦篾笆鮫竾笉笊笎笏笐靨笓笤籙笪笫笭笮笰笱笲笳笵笸笻筀筅筇筈筎筑筘筠筤筥筦筧筩筭筯筰筱筳筴讌筸箂箇箊箎箑箒箘箙箛箜篌箝箠箬鏃箯箴箾篁篔簹篘篙篚篛篜篝篟篠篡篢篥篧篨篭篰篲篳篴篶篹篼簀簁簃簆簉簋簌簏簜簟簠簥簦簨簬簰簸簻籊籐籒籓籔籖籚籛籜籣籥籧籩籪籫籯芾麴籵籸籹籼粁粃粋粑粔糲粛粞粢粧粨粲粳粺粻粽闢粿糅糆糈糌糍糒糔萼糗蛆蹋糢糨糬糭糯糱糴糶糸糺紃蹼鰹黴紆紈絝紉閩襻紑紕紘錠鳶鷂紝紞紟紥紩紬紱紲紵紽紾紿絁絃絅経絍絎絏縭褵絓絖絘絜絢絣螯絪絫聒絰絵絶絺絻絿綀綃綅綆綈綉綌綍綎綑綖綘継続緞綣綦綪綫綮綯綰罟蝽綷縩綹綾緁緄緅緆緇緋緌緎総緑緔緖緗緘緙緜緡緤緥緦纂緪緰緱緲緶緹縁縃縄縈縉縋縏縑縕縗縚縝縞縟縠縡縢縦縧縯縰騁縲縳縴縵縶縹縻衙縿繄繅繈繊繋繐繒繖繘繙繠繢繣繨繮繰繸繻繾纁纆纇纈纉纊纑纕纘纙纚纛缾罃罆罈罋罌罎罏罖罘罛罝罠罣罥罦罨罫罭鍰罳罶罹罻罽罿羂羃羇羋蕉51鴕羑羖羗羜羝羢羣羥羧羭羮羰羱羵羶羸藜鮐翀翃翄翊翌翏翕翛翟翡翣翥翦躚翪翫翬翮翯翺翽翾翿闆饕鴰鍁耋耇耎耏耑耒耜耔耞耡耤耨耩耪耬耰鬢耵聹聃聆聎聝聡聦聱聴聶聼閾聿肄肏肐肕腋肙肜肟肧胛肫肬肭肰肴肵肸肼胊胍胏胑胔胗胙胝胠銓胤胦胩胬胭胯胰胲胴胹胻胼胾脇脘脝脞脡脣脤脥脧脰脲脳腆腊腌臢腍腒腓腖腜腠腡腥腧腬腯踝蹬鐐腴腶蠕誹膂膃膆膇膋膔膕膗膙膟黐膣膦膫膰膴膵膷膾臃臄臇臈臌臐臑臓臕臖臙臛臝臞臧蓐詡臽臾臿舀舁鰟鮍舋舎舔舗舘舝舠舡舢舨舭舲舳舴舸舺艁艄艅艉艋艑艕艖艗艘艚艜艟艣艤艨艩艫艬艭荏艴艶艸艹艻艿芃芄芊萰陂藭芏芔芘芚蕙芟芣芤茉芧芨芩芪芮芰鰱芴芷芸蕘豢芼芿苄苒苘苙苜蓿苠苡苣蕒苤苧苪鎊苶苹苺苻苾茀茁范蠡萣茆茇茈茌茍茖茞茠茢茥茦菰茭茯茳藨茷藘茼荁荄荅荇荈菅蜢鴞荍荑荘荳荵荸薺莆莒莔莕莘莙莚莛莜莝莦莨菪莩莪莭莰莿菀菆菉菎菏菐菑菓菔菕菘菝菡菢菣菥蓂菧菫轂鎣菶菷菹醢菺菻菼菾萅萆萇萋萏萐萑萜萩萱萴萵萹萻葇葍葎葑葒葖葙葠葥葦葧葭葯葳葴葶葸葹葽蒄蒎蒓蘢薹蒞蒟蒻蒢蒦蒨蒭藁蒯蒱鉾蒴蒹蒺蒽蓀蓁蓆蓇蓊蓌蓍蓏蓓蓖蓧蓪蓫蓽跣藕蓯蓰蓱蓴蓷蓺蓼蔀蔂蔃蔆蔇蔉蔊蔋蔌蔎蔕蔘蔙蔞蔟鍔蔣雯蔦蔯蔳蔴蔵蔸蔾蕁蕆蕋蕍蕎蕐蕑蕓蕕蕖蕗蕝蕞蕠蕡蕢蕣蕤蕨蕳蕷蕸蕺蕻薀薁薃薅薆薈薉薌薏薐薔薖薘薙諤釵薜薠薢薤薧薨薫薬薳薶薷薸薽薾薿藄藇藋藎藐藙藚藟藦藳藴藶藷藾蘀蘁蘄蘋蘗蘘蘝蘤蘧蘩蘸蘼虀虆虍蟠虒虓虖虡虣虥虩虯虰蛵虵虷鱒虺虼蚆蚈蚋蚓蚔蚖蚘蚜蚡蚣蚧蚨蚩蚪蚯蚰蜒蚱蚳蚶蚹蚺蚻蚿蛀蛁蛄蛅蝮蛌蛍蛐蟮蛑蛓蛔蛘蛚蛜蛡蛣蜊蛩蛺蛻螫蜅蜆蜈蝣蜋蜍蜎蜑蠊蜛餞蜞蜣蜨蜩蜮蜱蜷蜺蜾蜿蝀蝃蝋蝌蝍蝎蝏蝗蝘蝙蝝鱝蝡蝤蝥蝯蝰蝱蝲蝴蝻螃蠏螄螉螋螒螓螗螘螙螚蟥螟螣螥螬螭螮螾螿蟀蟅蟈蟊蟋蟑蟓蟛蟜蟟蟢蟣蟨蟪蟭蟯蟳蟶蟷蟺蟿蠁蠂蠃蠆蠋蠐蠓蠔蠗蠙蠚蠛蠜蠧蠨蠩蠭蠮蠰蠲蠵蠸蠼蠽衁衂衄衇衈衉衋衎衒衕衖衚衞裳鈎衭衲衵衹衺衿袈裟袗袚袟袢袪袮袲袴袷袺袼褙袽裀裉裊裋裌裍裎裒裛裯裱裲裴裾褀褂褉褊褌褎褐褒褓褔褕褘褚褡褢褦褧褪褫褭褯褰褱襠褸褽褾襁襃襆襇襉襋襌襏襚襛襜襝襞襡襢襤襦襫襬襭襮襴襶襼襽襾覂覃覅覇覉覊覌覗覘覚覜覥覦覧覩覬覯覰観覿觔觕觖觜觽觝觡酲觩觫觭觱觳觶觷觼觾觿言賅訃訇訏訑訒詁託訧訬訳訹証訾詀詅詆譭詈詊詎詑詒詖詗詘詧詨詵詶詸詹詻詼詿誂誃誄鋤誆誋誑誒誖誙誚誥誧説読誯誶誾諂諄諆諌諍諏諑諕諗諛諝諞諟諠諡諴諵諶諼謄謆謇謌謍謏謑謖謚謡謦謪謫謳謷謼謾譁譅譆譈譊譌譒譔譖鑫譞譟譩譫譬譱譲譴譸譹譾讅讆讋讌讎讐讒讖讙讜讟谽豁豉豇豈豊豋豌豏豔豞豖豗豜豝豣豦豨豭豱豳豵豶豷豺豻貅貆貍貎貔貘貙貜貤饜貰餸貺賁賂賏賒賕賙賝賡賧賨賫鬭賮賵賸賺賻賾贇贉贐贔贕贗赬赭赱赳迄趁趂趄趐趑趒趔趡趦趫趮趯趲趴趵趷趹趺趿跁跂跅跆躓蹌跐跕跖跗跙跛跦跧跩跫跬跮跱跲跴跺跼跽踅踆踈踉踊踒踖踘踜踟躇躕踠踡踣踤踥踦踧蹺踫踮踰踱踴踶踹踺踼踽躞蹁蹂躪蹎蹐蹓蹔蹕蹚蹜蹝蹟蹠蹡蹢躂蹧蹩蹪蹯鞠蹽躃躄躅躊躋躐躑躒躘躙躛躝躠躡躦躧躩躭躰躳躶軃軆輥軏軔軘軜軝齶転軥軨軭軱軲轆軷軹軺軽軿輀輂輦輅輇輈輓輗輙輜輞輠輤輬輭輮輳輴輵輶輹輼輾轀轇轏轑轒轔轕轖轗轘轙轝轞轢轤辠辢辤辵辶辺込辿迅迋迍麿迓迣迤邐迥迨迮迸迺迻迿逄逅逌逍逑逓逕逖逡逭逯逴逶逹遄遅遉遘遛遝遢遨遫遯遰遴遶遹遻邂邅邉邋邎邕邗邘邛邠邢邧邨邯鄲邰邲邳邴邶邷邽邾邿郃郄郇郈郔郕郗郙郚郜郝郞郟郠郢郪郫郯郰郲郳郴郷郹郾郿鄀鄄鄆鄇鄈鄋鄍鄎鄏鄐鄑鄒鄔鄕鄖鄗鄘鄚鄜鄞鄠鄢鄣鄤鄦鄩鄫鄬鄮鄯鄱鄶鄷鄹鄺鄻鄾鄿酃酅酆酇酈酊酋酎酏酐酣酔酕醄酖酗酞酡酢酤酩酴酹酺醁醅醆醊醍醐醑醓醖醝醞醡醤醨醪醭醯醰醱醲醴醵醸醹醼醽醾釂釃釅釆釈鱸鎦閶釓釔釕鈀釙鼢鼴釤釧釪釬釭釱釷釸釹鈁鈃鈄鈆鈇鈈鈊鈌鈐鈑鈒鈤鈥鈧鈬鈮鈰鈳鐺鈸鈹鈽鈿鉄鉆鉈鉋鉌鉍鉏鉑鉕鉚鉢鉥鉦鉨鉬鉭鉱鉲鉶鉸鉺鉼鉿銍銎銑銕鏤銚銛銠銣銤銥銦銧銩銪銫銭銰銲銶銻銼銾鋂鋃鋆鋈鋊鋌鋍鋏鋐鋑鋕鋘鋙鋝鋟鋦鋨鋩鋭鋮鋯鋰鋱鋳鋹鋺鋻鏰鐱錀錁錆錇錈錍錏錒錔錙錚錛錞錟錡錤錩錬録錸錼鍀鍆鍇鍉鍍鍏鍐鍘鍚鍛鍠鍤鍥鍩鍫鍭鍱鍴鍶鍹鍺鍼鍾鎄鎇鎉鎋鎌鎍鎏鎒鎓鎗鎘鎚鎞鎡鎤鎩鎪鎭鎯鎰鎳鎴鎵鎸鎹鎿鏇鏊鏌鏐鏑鏖鏗鏘鏚鏜鏝鏞鏠鏦鏨鏷鏸鏹鏻鏽鏾鐃鐄鐇鐏鐒鐓鐔鐗馗鐙鐝鐠鐡鐦鐨鐩鐫鐬鐱鐳鐶鐻鐽鐿鑀鑅鑌鑐鑕鑚鑛鑢鑤鑥鑪鑭鑯鑱鑴鑵鑷钁钃镻閆閈閌閎閒閔閗閟閡関閤閤閧閬閲閹閺閻閼閽閿闇闉闋闐闑闒闓闘闚闞闟闠闤闥阞阢阤阨阬阯阹阼阽陁陑陔陛陜陡陥陬騭陴険陼陾隂隃隈隒隗隞隠隣隤隩隮隰顴隳隷隹雂雈雉雊雎雑雒雗雘雚雝雟雩雰雱驛霂霅霈霊霑霒霓霙霝霢霣霤霨霩霪霫霮靁靆靉靑靚靣靦靪靮靰靳靷靸靺靼靿鞀鞃鞄鞌鞗鞙鞚鞝鞞鞡鞣鞨鞫鞬鞮鞶鞹鞾韃韅韉馱韍韎韔韖韘韝韞韡韣韭韮韱韹韺頀颳頄頇頊頍頎頏頒頖頞頠頫頬顱頯頲頴頼顇顋顑顒顓顔顕顚顜顢顣顬顳颭颮颱颶颸颺颻颽颾颿飀飂飈飌飜飡飣飤飥飩飫飮飱飶餀餂餄餎餇餈餑餔餕餖餗餚餛餜餟餠餤餧餩餪餫餬餮餱餲餳餺餻餼餽餿饁饅饇饉饊饍饎饐饘饟饢馘馥馝馡馣騮騾馵馹駃駄駅駆駉駋駑駓駔駗駘駙駜駡駢駪駬駰駴駸駹駽駾騂騄騅騆騉騋騍騏驎騑騒験騕騖騠騢騣騤騧驤騵騶騸騺驀驂驃驄驆驈驊驌驍驎驏驒驔驖驙驦驩驫骺鯁骫骭骯骱骴骶骷髏骾髁髂髄髆髈髐髑髕髖髙髝髞髟髡髣髧髪髫髭髯髲髳髹髺髽髾鬁鬃鬅鬈鬋鬎鬏鬐鬑鬒鬖鬗鬘鬙鬠鬣鬪鬫鬬鬮鬯鬰鬲鬵鬷魆魈魊魋魍魎魑魖鰾魛魟魣魦魨魬魴魵魸鮀鮁鮆鮌鮎鮑鮒鮓鮚鮞鮟鱇鮠鮦鮨鮪鮭鮶鮸鮿鯀鯄鯆鯇鯈鯔鯕鯖鯗鯙鯠鯤鯥鯫鯰鯷鯸鯿鰂鰆鶼鰉鰋鰐鰒鰕鰛鰜鰣鰤鰥鰦鰨鰩鰮鰳鰶鰷鱺鰼鰽鱀鱄鱅鱆鱈鱎鱐鱓鱔鱖鱘鱟鱠鱣鱨鱭鱮鱲鱵鱻鲅鳦鳧鳯鳲鳷鳻鴂鴃鴄鴆鴈鴎鴒鴔鴗鴛鴦鴝鵒鴟鴠鴢鴣鴥鴯鶓鴳鴴鴷鴽鵀鵁鵂鵓鵖鵙鵜鶘鵞鵟鵩鵪鵫鵵鵷鵻鵾鶂鶊鶏鶒鶖鶗鶡鶤鶦鶬鶱鶲鶵鶸鶹鶺鶿鷀鷁鷃鷄鷇鷈鷉鷊鷏鷓鷕鷖鷙鷞鷟鷥鷦鷯鷩鷫鷭鷳鷴鷽鷾鷿鸂鸇鸊鸏鸑鸒鸓鸕鸛鸜鸝鹸鹹鹺麀麂麃麄麇麋麌麐麑麒麚麛麝麤麩麪麫麮麯麰麺麾黁黈黌黢黒黓黕黙黝黟黥黦黧黮黰黱黲黶黹黻黼黽黿鼂鼃鼅鼈鼉鼏鼐鼒鼕鼖鼙鼚鼛鼡鼩鼱鼪鼫鼯鼷鼽齁齆齇齈齉齌齎齏齔齕齗齙齚齜齞齟齬齠齢齣齧齩齮齯齰齱齵齾龎龑龒龔龖龘龝龡龢龤" + +assert len(simplified_charcters) == len(simplified_charcters) + +s2t_dict = {} +t2s_dict = {} +for i, item in enumerate(simplified_charcters): + s2t_dict[item] = traditional_characters[i] + t2s_dict[traditional_characters[i]] = item + + +def tranditional_to_simplified(text: str) -> str: + return "".join([t2s_dict[item] if item in t2s_dict else item for item in text]) + + +def simplified_to_traditional(text: str) -> str: + return "".join([s2t_dict[item] if item in s2t_dict else item for item in text]) diff --git a/genie_tts/G2P/Chinese/Normalization/chronology.py b/genie_tts/G2P/Chinese/Normalization/chronology.py new file mode 100644 index 0000000000000000000000000000000000000000..fbf6495aebec4fa59e6e436c26b1a2914812643c --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/chronology.py @@ -0,0 +1,144 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from .num import ( + DIGITS, + num2str, + verbalize_cardinal, + verbalize_digit, +) + + +def _time_num2str(num_string: str) -> str: + """A special case for verbalizing number in time.""" + result = num2str(num_string.lstrip("0")) + if num_string.startswith("0"): + result = DIGITS["0"] + result + return result + + +# 时刻表达式 +RE_TIME = re.compile( + r"([0-1]?[0-9]|2[0-3])" + r":([0-5][0-9])" + r"(:([0-5][0-9]))?" +) + +# 时间范围,如8:30-12:30 +RE_TIME_RANGE = re.compile( + r"([0-1]?[0-9]|2[0-3])" + r":([0-5][0-9])" + r"(:([0-5][0-9]))?" + r"(~|-)" + r"([0-1]?[0-9]|2[0-3])" + r":([0-5][0-9])" + r"(:([0-5][0-9]))?" +) + + +def replace_time(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + + is_range = len(match.groups()) > 5 + + hour = match.group(1) + minute = match.group(2) + second = match.group(4) + hour_2 = "" + minute_2 = "" + second_2 = "" + + if is_range: + hour_2 = match.group(6) + minute_2 = match.group(7) + second_2 = match.group(9) + + result = f"{num2str(hour)}点" + if minute.lstrip("0"): + if int(minute) == 30: + result += "半" + else: + result += f"{_time_num2str(minute)}分" + if second and second.lstrip("0"): + result += f"{_time_num2str(second)}秒" + + if is_range: + result += "至" + result += f"{num2str(hour_2)}点" + if minute_2.lstrip("0"): + if int(minute) == 30: + result += "半" + else: + result += f"{_time_num2str(minute_2)}分" + if second_2 and second_2.lstrip("0"): + result += f"{_time_num2str(second_2)}秒" + + return result + + +RE_DATE = re.compile( + r"(\d{4}|\d{2})年" + r"((0?[1-9]|1[0-2])月)?" + r"(((0?[1-9])|((1|2)[0-9])|30|31)([日号]))?" +) + + +def replace_date(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + year = match.group(1) + month = match.group(3) + day = match.group(5) + result = "" + if year: + result += f"{verbalize_digit(year)}年" + if month: + result += f"{verbalize_cardinal(month)}月" + if day: + result += f"{verbalize_cardinal(day)}{match.group(9)}" + return result + + +# 用 / 或者 - 分隔的 YY/MM/DD 或者 YY-MM-DD 日期 +RE_DATE2 = re.compile(r"(\d{4})([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])") + + +def replace_date2(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + year = match.group(1) + month = match.group(3) + day = match.group(4) + result = "" + if year: + result += f"{verbalize_digit(year)}年" + if month: + result += f"{verbalize_cardinal(month)}月" + if day: + result += f"{verbalize_cardinal(day)}日" + return result diff --git a/genie_tts/G2P/Chinese/Normalization/constants.py b/genie_tts/G2P/Chinese/Normalization/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..b31adf329f9c2b2db30a0f2d6589c070b596d52f --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/constants.py @@ -0,0 +1,61 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +import string +from pypinyin.compat import SUPPORT_UCS4 + +# 全角半角转换 +# 英文字符全角 -> 半角映射表 (num: 52) +F2H_ASCII_LETTERS = {ord(char) + 65248: ord(char) for char in string.ascii_letters} + +# 英文字符半角 -> 全角映射表 +H2F_ASCII_LETTERS = {value: key for key, value in F2H_ASCII_LETTERS.items()} + +# 数字字符全角 -> 半角映射表 (num: 10) +F2H_DIGITS = {ord(char) + 65248: ord(char) for char in string.digits} +# 数字字符半角 -> 全角映射表 +H2F_DIGITS = {value: key for key, value in F2H_DIGITS.items()} + +# 标点符号全角 -> 半角映射表 (num: 32) +F2H_PUNCTUATIONS = {ord(char) + 65248: ord(char) for char in string.punctuation} +# 标点符号半角 -> 全角映射表 +H2F_PUNCTUATIONS = {value: key for key, value in F2H_PUNCTUATIONS.items()} + +# 空格 (num: 1) +F2H_SPACE = {"\u3000": " "} +H2F_SPACE = {" ": "\u3000"} + +# 非"有拼音的汉字"的字符串,可用于NSW提取 +if SUPPORT_UCS4: + RE_NSW = re.compile( + r"(?:[^" + r"\u3007" # 〇 + r"\u3400-\u4dbf" # CJK扩展A:[3400-4DBF] + r"\u4e00-\u9fff" # CJK基本:[4E00-9FFF] + r"\uf900-\ufaff" # CJK兼容:[F900-FAFF] + r"\U00020000-\U0002A6DF" # CJK扩展B:[20000-2A6DF] + r"\U0002A703-\U0002B73F" # CJK扩展C:[2A700-2B73F] + r"\U0002B740-\U0002B81D" # CJK扩展D:[2B740-2B81D] + r"\U0002F80A-\U0002FA1F" # CJK兼容扩展:[2F800-2FA1F] + r"])+" + ) +else: + RE_NSW = re.compile( # pragma: no cover + r"(?:[^" + r"\u3007" # 〇 + r"\u3400-\u4dbf" # CJK扩展A:[3400-4DBF] + r"\u4e00-\u9fff" # CJK基本:[4E00-9FFF] + r"\uf900-\ufaff" # CJK兼容:[F900-FAFF] + r"])+" + ) diff --git a/genie_tts/G2P/Chinese/Normalization/num.py b/genie_tts/G2P/Chinese/Normalization/num.py new file mode 100644 index 0000000000000000000000000000000000000000..be23c1d29247b1d6f5fefc8e4781677d6ed9cf72 --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/num.py @@ -0,0 +1,340 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Rules to verbalize numbers into Chinese characters. +https://zh.wikipedia.org/wiki/中文数字#現代中文 +""" + +import re +from collections import OrderedDict +from typing import List + +DIGITS = {str(i): tran for i, tran in enumerate("零一二三四五六七八九")} +UNITS = OrderedDict( + { + 1: "十", + 2: "百", + 3: "千", + 4: "万", + 8: "亿", + } +) + +COM_QUANTIFIERS = "(处|台|架|枚|趟|幅|平|方|堵|间|床|株|批|项|例|列|篇|栋|注|亩|封|艘|把|目|套|段|人|所|朵|匹|张|座|回|场|尾|条|个|首|阙|阵|网|炮|顶|丘|棵|只|支|袭|辆|挑|担|颗|壳|窠|曲|墙|群|腔|砣|座|客|贯|扎|捆|刀|令|打|手|罗|坡|山|岭|江|溪|钟|队|单|双|对|出|口|头|脚|板|跳|枝|件|贴|针|线|管|名|位|身|堂|课|本|页|家|户|层|丝|毫|厘|分|钱|两|斤|担|铢|石|钧|锱|忽|(千|毫|微)克|毫|厘|(公)分|分|寸|尺|丈|里|寻|常|铺|程|(千|分|厘|毫|微)米|米|撮|勺|合|升|斗|石|盘|碗|碟|叠|桶|笼|盆|盒|杯|钟|斛|锅|簋|篮|盘|桶|罐|瓶|壶|卮|盏|箩|箱|煲|啖|袋|钵|年|月|日|季|刻|时|周|天|秒|分|小时|旬|纪|岁|世|更|夜|春|夏|秋|冬|代|伏|辈|丸|泡|粒|颗|幢|堆|条|根|支|道|面|片|张|颗|块|元|(亿|千万|百万|万|千|百)|(亿|千万|百万|万|千|百|美|)元|(亿|千万|百万|万|千|百|十|)吨|(亿|千万|百万|万|千|百|)块|角|毛|分)" + +# 分数表达式 +RE_FRAC = re.compile(r"(-?)(\d+)/(\d+)") + + +def replace_frac(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + nominator = match.group(2) + denominator = match.group(3) + sign: str = "负" if sign else "" + nominator: str = num2str(nominator) + denominator: str = num2str(denominator) + result = f"{sign}{denominator}分之{nominator}" + return result + + +# 百分数表达式 +RE_PERCENTAGE = re.compile(r"(-?)(\d+(\.\d+)?)%") + + +def replace_percentage(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + percent = match.group(2) + sign: str = "负" if sign else "" + percent: str = num2str(percent) + result = f"{sign}百分之{percent}" + return result + + +# 整数表达式 +# 带负号的整数 -10 +RE_INTEGER = re.compile(r"(-)" r"(\d+)") + + +def replace_negative_num(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + number = match.group(2) + sign: str = "负" if sign else "" + number: str = num2str(number) + result = f"{sign}{number}" + return result + + +# 编号-无符号整形 +# 00078 +RE_DEFAULT_NUM = re.compile(r"\d{3}\d*") + + +def replace_default_num(match): + """ + Args: + match (re.Match) + Returns: + str + """ + number = match.group(0) + return verbalize_digit(number, alt_one=True) + + +# 加减乘除 +# RE_ASMD = re.compile( +# r'((-?)((\d+)(\.\d+)?)|(\.(\d+)))([\+\-\×÷=])((-?)((\d+)(\.\d+)?)|(\.(\d+)))') +RE_ASMD = re.compile( + r"((-?)((\d+)(\.\d+)?[⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]*)|(\.\d+[⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]*)|([A-Za-z][⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]*))([+\-×÷=])((-?)((\d+)(\.\d+)?[⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]*)|(\.\d+[⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]*)|([A-Za-z][⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]*))" +) + +asmd_map = {"+": "加", "-": "减", "×": "乘", "÷": "除", "=": "等于"} + + +def replace_asmd(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + result = match.group(1) + asmd_map[match.group(8)] + match.group(9) + return result + + +# 次方专项 +RE_POWER = re.compile(r"[⁰¹²³⁴⁵⁶⁷⁸⁹ˣʸⁿ]+") + +power_map = { + "⁰": "0", + "¹": "1", + "²": "2", + "³": "3", + "⁴": "4", + "⁵": "5", + "⁶": "6", + "⁷": "7", + "⁸": "8", + "⁹": "9", + "ˣ": "x", + "ʸ": "y", + "ⁿ": "n", +} + + +def replace_power(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + power_num = "" + for m in match.group(0): + power_num += power_map[m] + result = "的" + power_num + "次方" + return result + + +# 数字表达式 +# 纯小数 +RE_DECIMAL_NUM = re.compile(r"(-?)((\d+)(\.\d+))" r"|(\.(\d+))") +# 正整数 + 量词 +RE_POSITIVE_QUANTIFIERS = re.compile(r"(\d+)([多余几+])?" + COM_QUANTIFIERS) +RE_NUMBER = re.compile(r"(-?)((\d+)(\.\d+)?)" r"|(\.(\d+))") + + +def replace_positive_quantifier(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + number = match.group(1) + match_2 = match.group(2) + if match_2 == "+": + match_2 = "多" + match_2: str = match_2 if match_2 else "" + quantifiers: str = match.group(3) + number: str = num2str(number) + number = "两" if number == "二" else number + result = f"{number}{match_2}{quantifiers}" + return result + + +def replace_number(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + number = match.group(2) + pure_decimal = match.group(5) + if pure_decimal: + result = num2str(pure_decimal) + else: + sign: str = "负" if sign else "" + number: str = num2str(number) + result = f"{sign}{number}" + return result + + +# 范围表达式 +# match.group(1) and match.group(8) are copy from RE_NUMBER + +RE_RANGE = re.compile( + r""" + (? str: + """ + Args: + match (re.Match) + Returns: + str + """ + first, second = match.group(1), match.group(6) + first = RE_NUMBER.sub(replace_number, first) + second = RE_NUMBER.sub(replace_number, second) + result = f"{first}到{second}" + return result + + +# ~至表达式 +RE_TO_RANGE = re.compile( + r"((-?)((\d+)(\.\d+)?)|(\.(\d+)))(%|°C|℃|度|摄氏度|cm2|cm²|cm3|cm³|cm|db|ds|kg|km|m2|m²|m³|m3|ml|m|mm|s)[~]((-?)((\d+)(\.\d+)?)|(\.(\d+)))(%|°C|℃|度|摄氏度|cm2|cm²|cm3|cm³|cm|db|ds|kg|km|m2|m²|m³|m3|ml|m|mm|s)" +) + + +def replace_to_range(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + result = match.group(0).replace("~", "至") + return result + + +RE_VERSION_NUM = re.compile(r"((\d+)(\.\d+)(\.\d+)?(\.\d+)+)") + + +def replace_vrsion_num(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + result = "" + for c in match.group(1): + if c == ".": + result += "点" + else: + result += num2str(c) + return result + + +def _get_value(value_string: str, use_zero: bool = True) -> List[str]: + stripped = value_string.lstrip("0") + if len(stripped) == 0: + return [] + elif len(stripped) == 1: + if use_zero and len(stripped) < len(value_string): + return [DIGITS["0"], DIGITS[stripped]] + else: + return [DIGITS[stripped]] + else: + largest_unit = next(power for power in reversed(UNITS.keys()) if power < len(stripped)) + first_part = value_string[:-largest_unit] + second_part = value_string[-largest_unit:] + return _get_value(first_part) + [UNITS[largest_unit]] + _get_value(second_part) + + +def verbalize_cardinal(value_string: str) -> str: + if not value_string: + return "" + + # 000 -> '零' , 0 -> '零' + value_string = value_string.lstrip("0") + if len(value_string) == 0: + return DIGITS["0"] + + result_symbols = _get_value(value_string) + # verbalized number starting with '一十*' is abbreviated as `十*` + if len(result_symbols) >= 2 and result_symbols[0] == DIGITS["1"] and result_symbols[1] == UNITS[1]: + result_symbols = result_symbols[1:] + return "".join(result_symbols) + + +def verbalize_digit(value_string: str, alt_one=False) -> str: + result_symbols = [DIGITS[digit] for digit in value_string] + result = "".join(result_symbols) + if alt_one: + result = result.replace("一", "幺") + return result + + +def num2str(value_string: str) -> str: + integer_decimal = value_string.split(".") + if len(integer_decimal) == 1: + integer = integer_decimal[0] + decimal = "" + elif len(integer_decimal) == 2: + integer, decimal = integer_decimal + else: + raise ValueError(f"The value string: '${value_string}' has more than one point in it.") + + result = verbalize_cardinal(integer) + + if decimal.endswith("0"): + decimal = decimal.rstrip("0") + "0" + else: + decimal = decimal.rstrip("0") + + if decimal: + # '.22' is verbalized as '零点二二' + # '3.20' is verbalized as '三点二' + result = result if result else "零" + result += "点" + verbalize_digit(decimal) + return result diff --git a/genie_tts/G2P/Chinese/Normalization/phonecode.py b/genie_tts/G2P/Chinese/Normalization/phonecode.py new file mode 100644 index 0000000000000000000000000000000000000000..3560ac2ed265580e5f150da191c21acea7390087 --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/phonecode.py @@ -0,0 +1,59 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from .num import verbalize_digit + +# 规范化固话/手机号码 +# 手机 +# http://www.jihaoba.com/news/show/13680 +# 移动:139、138、137、136、135、134、159、158、157、150、151、152、188、187、182、183、184、178、198 +# 联通:130、131、132、156、155、186、185、176 +# 电信:133、153、189、180、181、177 +RE_MOBILE_PHONE = re.compile(r"(? str: + if mobile: + sp_parts = phone_string.strip("+").split() + result = ",".join([verbalize_digit(part, alt_one=True) for part in sp_parts]) + return result + else: + sil_parts = phone_string.split("-") + result = ",".join([verbalize_digit(part, alt_one=True) for part in sil_parts]) + return result + + +def replace_phone(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + return phone2str(match.group(0), mobile=False) + + +def replace_mobile(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + return phone2str(match.group(0)) diff --git a/genie_tts/G2P/Chinese/Normalization/quantifier.py b/genie_tts/G2P/Chinese/Normalization/quantifier.py new file mode 100644 index 0000000000000000000000000000000000000000..b8fb1309dc7838c874d33b969bbc59b282273fc5 --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/quantifier.py @@ -0,0 +1,62 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +from .num import num2str + +# 温度表达式,温度会影响负号的读法 +# -3°C 零下三度 +RE_TEMPERATURE = re.compile(r"(-?)(\d+(\.\d+)?)(°C|℃|度|摄氏度)") +measure_dict = { + "cm2": "平方厘米", + "cm²": "平方厘米", + "cm3": "立方厘米", + "cm³": "立方厘米", + "cm": "厘米", + "db": "分贝", + "ds": "毫秒", + "kg": "千克", + "km": "千米", + "m2": "平方米", + "m²": "平方米", + "m³": "立方米", + "m3": "立方米", + "ml": "毫升", + "m": "米", + "mm": "毫米", + "s": "秒", +} + + +def replace_temperature(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + temperature = match.group(2) + unit = match.group(3) + sign: str = "零下" if sign else "" + temperature: str = num2str(temperature) + unit: str = "摄氏度" if unit == "摄氏度" else "度" + result = f"{sign}{temperature}{unit}" + return result + + +def replace_measure(sentence) -> str: + for q_notation in measure_dict: + if q_notation in sentence: + sentence = sentence.replace(q_notation, measure_dict[q_notation]) + return sentence diff --git a/genie_tts/G2P/Chinese/Normalization/text_normlization.py b/genie_tts/G2P/Chinese/Normalization/text_normlization.py new file mode 100644 index 0000000000000000000000000000000000000000..be29aa03265691e12ad029c5146ca5da2a3ea94b --- /dev/null +++ b/genie_tts/G2P/Chinese/Normalization/text_normlization.py @@ -0,0 +1,169 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +from typing import List + +from .char_convert import tranditional_to_simplified +from .chronology import ( + RE_DATE, + RE_DATE2, + RE_TIME, + RE_TIME_RANGE, + replace_date, + replace_date2, + replace_time, +) +from .constants import ( + F2H_ASCII_LETTERS, + F2H_DIGITS, + F2H_SPACE, +) +from .num import ( + RE_VERSION_NUM, RE_DECIMAL_NUM, RE_DEFAULT_NUM, RE_FRAC, + RE_INTEGER, RE_NUMBER, RE_PERCENTAGE, RE_POSITIVE_QUANTIFIERS, + RE_RANGE, RE_TO_RANGE, RE_ASMD, RE_POWER, + replace_vrsion_num, replace_default_num, replace_frac, + replace_negative_num, replace_number, replace_percentage, + replace_positive_quantifier, replace_range, replace_to_range, + replace_asmd, replace_power +) +from .phonecode import ( + RE_MOBILE_PHONE, + RE_NATIONAL_UNIFORM_NUMBER, + RE_TELEPHONE, + replace_mobile, + replace_phone, +) +from .quantifier import ( + RE_TEMPERATURE, + replace_measure, + replace_temperature, +) + + +class TextNormalizer: + def __init__(self): + self.SENTENCE_SPLITOR = re.compile(r"([:、,;。?!,;?!][”’]?)") + + def _split(self, text: str, lang="zh") -> List[str]: + """Split long text into sentences with sentence-splitting punctuations. + Args: + text (str): The input text. + Returns: + List[str]: Sentences. + """ + # Only for pure Chinese here + if lang == "zh": + text = text.replace(" ", "") + # 过滤掉特殊字符 + text = re.sub(r"[——《》【】<>{}()()#&@“”^_|\\]", "", text) + text = self.SENTENCE_SPLITOR.sub(r"\1\n", text) + text = text.strip() + sentences = [sentence.strip() for sentence in re.split(r"\n+", text)] + return sentences + + def _post_replace(self, sentence: str) -> str: + sentence = sentence.replace("/", "每") + # sentence = sentence.replace('~', '至') + # sentence = sentence.replace('~', '至') + sentence = sentence.replace("①", "一") + sentence = sentence.replace("②", "二") + sentence = sentence.replace("③", "三") + sentence = sentence.replace("④", "四") + sentence = sentence.replace("⑤", "五") + sentence = sentence.replace("⑥", "六") + sentence = sentence.replace("⑦", "七") + sentence = sentence.replace("⑧", "八") + sentence = sentence.replace("⑨", "九") + sentence = sentence.replace("⑩", "十") + sentence = sentence.replace("α", "阿尔法") + sentence = sentence.replace("β", "贝塔") + sentence = sentence.replace("γ", "伽玛").replace("Γ", "伽玛") + sentence = sentence.replace("δ", "德尔塔").replace("Δ", "德尔塔") + sentence = sentence.replace("ε", "艾普西龙") + sentence = sentence.replace("ζ", "捷塔") + sentence = sentence.replace("η", "依塔") + sentence = sentence.replace("θ", "西塔").replace("Θ", "西塔") + sentence = sentence.replace("ι", "艾欧塔") + sentence = sentence.replace("κ", "喀帕") + sentence = sentence.replace("λ", "拉姆达").replace("Λ", "拉姆达") + sentence = sentence.replace("μ", "缪") + sentence = sentence.replace("ν", "拗") + sentence = sentence.replace("ξ", "克西").replace("Ξ", "克西") + sentence = sentence.replace("ο", "欧米克伦") + sentence = sentence.replace("π", "派").replace("Π", "派") + sentence = sentence.replace("ρ", "肉") + sentence = sentence.replace("ς", "西格玛").replace("Σ", "西格玛").replace("σ", "西格玛") + sentence = sentence.replace("τ", "套") + sentence = sentence.replace("υ", "宇普西龙") + sentence = sentence.replace("φ", "服艾").replace("Φ", "服艾") + sentence = sentence.replace("χ", "器") + sentence = sentence.replace("ψ", "普赛").replace("Ψ", "普赛") + sentence = sentence.replace("ω", "欧米伽").replace("Ω", "欧米伽") + # 兜底数学运算,顺便兼容懒人用语 + sentence = sentence.replace("+", "加") + sentence = sentence.replace("-", "减") + sentence = sentence.replace("×", "乘") + sentence = sentence.replace("÷", "除") + sentence = sentence.replace("=", "等") + # re filter special characters, have one more character "-" than line 68 + sentence = re.sub(r"[-——《》【】<=>{}()()#&@“”^_|\\]", "", sentence) + return sentence + + def normalize_sentence(self, sentence: str) -> str: + # basic character conversions + sentence = tranditional_to_simplified(sentence) + sentence = sentence.translate(F2H_ASCII_LETTERS).translate(F2H_DIGITS).translate(F2H_SPACE) + + # number related NSW verbalization + sentence = RE_DATE.sub(replace_date, sentence) + sentence = RE_DATE2.sub(replace_date2, sentence) + + # range first + sentence = RE_TIME_RANGE.sub(replace_time, sentence) + sentence = RE_TIME.sub(replace_time, sentence) + + # 处理~波浪号作为至的替换 + sentence = RE_TO_RANGE.sub(replace_to_range, sentence) + sentence = RE_TEMPERATURE.sub(replace_temperature, sentence) + sentence = replace_measure(sentence) + + # 处理数学运算 + while RE_ASMD.search(sentence): + sentence = RE_ASMD.sub(replace_asmd, sentence) + sentence = RE_POWER.sub(replace_power, sentence) + + sentence = RE_FRAC.sub(replace_frac, sentence) + sentence = RE_PERCENTAGE.sub(replace_percentage, sentence) + sentence = RE_MOBILE_PHONE.sub(replace_mobile, sentence) + + sentence = RE_TELEPHONE.sub(replace_phone, sentence) + sentence = RE_NATIONAL_UNIFORM_NUMBER.sub(replace_phone, sentence) + + sentence = RE_RANGE.sub(replace_range, sentence) + + sentence = RE_INTEGER.sub(replace_negative_num, sentence) + sentence = RE_VERSION_NUM.sub(replace_vrsion_num, sentence) + sentence = RE_DECIMAL_NUM.sub(replace_number, sentence) + sentence = RE_POSITIVE_QUANTIFIERS.sub(replace_positive_quantifier, sentence) + sentence = RE_DEFAULT_NUM.sub(replace_default_num, sentence) + sentence = RE_NUMBER.sub(replace_number, sentence) + sentence = self._post_replace(sentence) + + return sentence + + def normalize(self, text: str) -> List[str]: + sentences = self._split(text) + sentences = [self.normalize_sentence(sent) for sent in sentences] + return sentences diff --git a/genie_tts/G2P/Chinese/ToneSandhi.py b/genie_tts/G2P/Chinese/ToneSandhi.py new file mode 100644 index 0000000000000000000000000000000000000000..e57622b21f3abbd13a3ada62c67c96e9aa67e57e --- /dev/null +++ b/genie_tts/G2P/Chinese/ToneSandhi.py @@ -0,0 +1,354 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +中文拼音变调(Tone Sandhi)自动处理器 +""" + +from typing import List +from typing import Tuple +import jieba_fast as jieba +from pypinyin import lazy_pinyin +from pypinyin import Style + + +class ToneSandhi: + def __init__(self): + self.must_neural_tone_words = { + "麻烦", "麻利", "鸳鸯", "高粱", "骨头", "骆驼", "马虎", "首饰", "馒头", "馄饨", + "风筝", "难为", "队伍", "阔气", "闺女", "门道", "锄头", "铺盖", "铃铛", "铁匠", + "钥匙", "里脊", "里头", "部分", "那么", "道士", "造化", "迷糊", "连累", "这么", + "这个", "运气", "过去", "软和", "转悠", "踏实", "跳蚤", "跟头", "趔趄", "财主", + "豆腐", "讲究", "记性", "记号", "认识", "规矩", "见识", "裁缝", "补丁", "衣裳", + "衣服", "衙门", "街坊", "行李", "行当", "蛤蟆", "蘑菇", "薄荷", "葫芦", "葡萄", + "萝卜", "荸荠", "苗条", "苗头", "苍蝇", "芝麻", "舒服", "舒坦", "舌头", "自在", + "膏药", "脾气", "脑袋", "脊梁", "能耐", "胳膊", "胭脂", "胡萝", "胡琴", "胡同", + "聪明", "耽误", "耽搁", "耷拉", "耳朵", "老爷", "老实", "老婆", "老头", "老太", + "翻腾", "罗嗦", "罐头", "编辑", "结实", "红火", "累赘", "糨糊", "糊涂", "精神", + "粮食", "簸箕", "篱笆", "算计", "算盘", "答应", "笤帚", "笑语", "笑话", "窟窿", + "窝囊", "窗户", "稳当", "稀罕", "称呼", "秧歌", "秀气", "秀才", "福气", "祖宗", + "砚台", "码头", "石榴", "石头", "石匠", "知识", "眼睛", "眯缝", "眨巴", "眉毛", + "相声", "盘算", "白净", "痢疾", "痛快", "疟疾", "疙瘩", "疏忽", "畜生", "生意", + "甘蔗", "琵琶", "琢磨", "琉璃", "玻璃", "玫瑰", "玄乎", "狐狸", "状元", "特务", + "牲口", "牙碜", "牌楼", "爽快", "爱人", "热闹", "烧饼", "烟筒", "烂糊", "点心", + "炊帚", "灯笼", "火候", "漂亮", "滑溜", "溜达", "温和", "清楚", "消息", "浪头", + "活泼", "比方", "正经", "欺负", "模糊", "槟榔", "棺材", "棒槌", "棉花", "核桃", + "栅栏", "柴火", "架势", "枕头", "枇杷", "机灵", "本事", "木头", "木匠", "朋友", + "月饼", "月亮", "暖和", "明白", "时候", "新鲜", "故事", "收拾", "收成", "提防", + "挖苦", "挑剔", "指甲", "指头", "拾掇", "拳头", "拨弄", "招牌", "招呼", "抬举", + "护士", "折腾", "扫帚", "打量", "打算", "打点", "打扮", "打听", "打发", "扎实", + "扁担", "戒指", "懒得", "意识", "意思", "情形", "悟性", "怪物", "思量", "怎么", + "念头", "念叨", "快活", "忙活", "志气", "心思", "得罪", "张罗", "弟兄", "开通", + "应酬", "庄稼", "干事", "帮手", "帐篷", "希罕", "师父", "师傅", "巴结", "巴掌", + "差事", "工夫", "岁数", "屁股", "尾巴", "少爷", "小气", "小伙", "将就", "对头", + "对付", "寡妇", "家伙", "客气", "实在", "官司", "学问", "学生", "字号", "嫁妆", + "媳妇", "媒人", "婆家", "娘家", "委屈", "姑娘", "姐夫", "妯娌", "妥当", "妖精", + "奴才", "女婿", "头发", "太阳", "大爷", "大方", "大意", "大夫", "多少", "多么", + "外甥", "壮实", "地道", "地方", "在乎", "困难", "嘴巴", "嘱咐", "嘟囔", "嘀咕", + "喜欢", "喇嘛", "喇叭", "商量", "唾沫", "哑巴", "哈欠", "哆嗦", "咳嗽", "和尚", + "告诉", "告示", "含糊", "吓唬", "后头", "名字", "名堂", "合同", "吆喝", "叫唤", + "口袋", "厚道", "厉害", "千斤", "包袱", "包涵", "匀称", "勤快", "动静", "动弹", + "功夫", "力气", "前头", "刺猬", "刺激", "别扭", "利落", "利索", "利害", "分析", + "出息", "凑合", "凉快", "冷战", "冤枉", "冒失", "养活", "关系", "先生", "兄弟", + "便宜", "使唤", "佩服", "作坊", "体面", "位置", "似的", "伙计", "休息", "什么", + "人家", "亲戚", "亲家", "交情", "云彩", "事情", "买卖", "主意", "丫头", "丧气", + "两口", "东西", "东家", "世故", "不由", "不在", "下水", "下巴", "上头", "上司", + "丈夫", "丈人", "一辈", "那个", "菩萨", "父亲", "母亲", "咕噜", "邋遢", "费用", + "冤家", "甜头", "介绍", "荒唐", "大人", "泥鳅", "幸福", "熟悉", "计划", "扑腾", + "蜡烛", "姥爷", "照顾", "喉咙", "吉他", "弄堂", "蚂蚱", "凤凰", "拖沓", "寒碜", + "糟蹋", "倒腾", "报复", "逻辑", "盘缠", "喽啰", "牢骚", "咖喱", "扫把", "惦记", + } + self.must_not_neural_tone_words = { + "男子", "女子", "分子", "原子", "量子", "莲子", "石子", "瓜子", "电子", "人人", + "虎虎", "幺幺", "干嘛", "学子", "哈哈", "数数", "袅袅", "局地", "以下", "娃哈哈", + "花花草草", "留得", "耕地", "想想", "熙熙", "攘攘", "卵子", "死死", "冉冉", "恳恳", + "佼佼", "吵吵", "打打", "考考", "整整", "莘莘", "落地", "算子", "家家户户", "青青", + } + self.punc = ":,;。?!“”‘’':,;.?!" + + # the meaning of jieba pos tag: https://blog.csdn.net/weixin_44174352/article/details/113731041 + # e.g. + # word: "家里" + # pos: "s" + # finals: ['ia1', 'i3'] + def _neural_sandhi(self, word: str, pos: str, finals: List[str]) -> List[str]: + # reduplication words for n. and v. e.g. 奶奶, 试试, 旺旺 + for j, item in enumerate(word): + if ( + j - 1 >= 0 + and item == word[j - 1] + and pos[0] in {"n", "v", "a"} + and word not in self.must_not_neural_tone_words + ): + finals[j] = finals[j][:-1] + "5" + ge_idx = word.find("个") + if len(word) >= 1 and word[-1] in "吧呢哈啊呐噻嘛吖嗨呐哦哒额滴哩哟喽啰耶喔诶": + finals[-1] = finals[-1][:-1] + "5" + elif len(word) >= 1 and word[-1] in "的地得": + finals[-1] = finals[-1][:-1] + "5" + # e.g. 走了, 看着, 去过 + elif len(word) == 1 and word in "了着过" and pos in {"ul", "uz", "ug"}: + finals[-1] = finals[-1][:-1] + "5" + elif len(word) > 1 and word[-1] in "们子" and pos in {"r", "n"} and word not in self.must_not_neural_tone_words: + finals[-1] = finals[-1][:-1] + "5" + # e.g. 桌上, 地下, 家里 + elif len(word) > 1 and word[-1] in "上下里" and pos in {"s", "l", "f"}: + finals[-1] = finals[-1][:-1] + "5" + # e.g. 上来, 下去 + elif len(word) > 1 and word[-1] in "来去" and word[-2] in "上下进出回过起开": + finals[-1] = finals[-1][:-1] + "5" + # 个做量词 + elif ( + ge_idx >= 1 and (word[ge_idx - 1].isnumeric() or word[ge_idx - 1] in "几有两半多各整每做是") + ) or word == "个": + finals[ge_idx] = finals[ge_idx][:-1] + "5" + else: + if word in self.must_neural_tone_words or word[-2:] in self.must_neural_tone_words: + finals[-1] = finals[-1][:-1] + "5" + + word_list = self._split_word(word) + finals_list = [finals[: len(word_list[0])], finals[len(word_list[0]):]] + for i, word in enumerate(word_list): + # conventional neural in Chinese + if word in self.must_neural_tone_words or word[-2:] in self.must_neural_tone_words: + finals_list[i][-1] = finals_list[i][-1][:-1] + "5" + finals = sum(finals_list, []) + return finals + + @staticmethod + def _bu_sandhi(word: str, finals: List[str]) -> List[str]: + # e.g. 看不懂 + if len(word) == 3 and word[1] == "不": + finals[1] = finals[1][:-1] + "5" + else: + for i, char in enumerate(word): + # "不" before tone4 should be bu2, e.g. 不怕 + if char == "不" and i + 1 < len(word) and finals[i + 1][-1] == "4": + finals[i] = finals[i][:-1] + "2" + return finals + + def _yi_sandhi(self, word: str, finals: List[str]) -> List[str]: + # "一" in number sequences, e.g. 一零零, 二一零 + if word.find("一") != -1 and all([item.isnumeric() for item in word if item != "一"]): + return finals + # "一" between reduplication words should be yi5, e.g. 看一看 + elif len(word) == 3 and word[1] == "一" and word[0] == word[-1]: + finals[1] = finals[1][:-1] + "5" + # when "一" is ordinal word, it should be yi1 + elif word.startswith("第一"): + finals[1] = finals[1][:-1] + "1" + else: + for i, char in enumerate(word): + if char == "一" and i + 1 < len(word): + # "一" before tone4 should be yi2, e.g. 一段 + if finals[i + 1][-1] == "4": + finals[i] = finals[i][:-1] + "2" + # "一" before non-tone4 should be yi4, e.g. 一天 + else: + # "一" 后面如果是标点,还读一声 + if word[i + 1] not in self.punc: + finals[i] = finals[i][:-1] + "4" + return finals + + @staticmethod + def _split_word(word: str) -> List[str]: + word_list = jieba.cut_for_search(word) + word_list = sorted(word_list, key=lambda i: len(i), reverse=False) + first_subword = word_list[0] + first_begin_idx = word.find(first_subword) + if first_begin_idx == 0: + second_subword = word[len(first_subword):] + new_word_list = [first_subword, second_subword] + else: + second_subword = word[: -len(first_subword)] + new_word_list = [second_subword, first_subword] + return new_word_list + + def _three_sandhi(self, word: str, finals: List[str]) -> List[str]: + if len(word) == 2 and self._all_tone_three(finals): + finals[0] = finals[0][:-1] + "2" + elif len(word) == 3: + word_list = self._split_word(word) + if self._all_tone_three(finals): + # disyllabic + monosyllabic, e.g. 蒙古/包 + if len(word_list[0]) == 2: + finals[0] = finals[0][:-1] + "2" + finals[1] = finals[1][:-1] + "2" + # monosyllabic + disyllabic, e.g. 纸/老虎 + elif len(word_list[0]) == 1: + finals[1] = finals[1][:-1] + "2" + else: + finals_list = [finals[: len(word_list[0])], finals[len(word_list[0]):]] + if len(finals_list) == 2: + for i, sub in enumerate(finals_list): + # e.g. 所有/人 + if self._all_tone_three(sub) and len(sub) == 2: + finals_list[i][0] = finals_list[i][0][:-1] + "2" + # e.g. 好/喜欢 + elif ( + i == 1 + and not self._all_tone_three(sub) + and finals_list[i][0][-1] == "3" + and finals_list[0][-1][-1] == "3" + ): + finals_list[0][-1] = finals_list[0][-1][:-1] + "2" + finals = sum(finals_list, []) + # split idiom into two words whose length is 2 + elif len(word) == 4: + finals_list = [finals[:2], finals[2:]] + finals = [] + for sub in finals_list: + if self._all_tone_three(sub): + sub[0] = sub[0][:-1] + "2" + finals += sub + + return finals + + @staticmethod + def _all_tone_three(finals: List[str]) -> bool: + # 增加 len(x) > 0 的判断,防止空字符串导致崩溃 + return all(len(x) > 0 and x[-1] == "3" for x in finals) + + @staticmethod + def _merge_bu(seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + last_word = "" + for word, pos in seg: + if last_word == "不": + word = last_word + word + if word != "不": + new_seg.append((word, pos)) + last_word = word[:] + if last_word == "不": + new_seg.append((last_word, "d")) + return new_seg + + @staticmethod + def _merge_yi(seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + i = 0 + # function 1 + while i < len(seg): + word, pos = seg[i] + merged = False + if i - 1 >= 0 and word == "一" and i + 1 < len(seg): + last = new_seg[-1] if new_seg else seg[i - 1] + if last[0] == seg[i + 1][0] and last[1] == "v" and seg[i + 1][1] == "v": + combined = last[0] + "一" + seg[i + 1][0] + new_seg[-1] = [combined, last[1]] + i += 2 + merged = True + if not merged: + new_seg.append([word, pos]) + i += 1 + seg = new_seg + new_seg = [] + # function 2 + for word, pos in seg: + if new_seg and new_seg[-1][0] == "一": + new_seg[-1][0] = new_seg[-1][0] + word + else: + new_seg.append([word, pos]) + return new_seg + + # the first and the second words are all_tone_three + def _merge_continuous_three_tones(self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + sub_finals_list = [ + lazy_pinyin(word, neutral_tone_with_five=True, style=Style.FINALS_TONE3) for (word, pos) in seg + ] + assert len(sub_finals_list) == len(seg) + merge_last = [False] * len(seg) + for i, (word, pos) in enumerate(seg): + if ( + i - 1 >= 0 + and self._all_tone_three(sub_finals_list[i - 1]) + and self._all_tone_three(sub_finals_list[i]) + and not merge_last[i - 1] + ): + # if the last word is reduplication, not merge, because reduplication need to be _neural_sandhi + if not self._is_reduplication(seg[i - 1][0]) and len(seg[i - 1][0]) + len(seg[i][0]) <= 3: + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + merge_last[i] = True + else: + new_seg.append([word, pos]) + else: + new_seg.append([word, pos]) + + return new_seg + + @staticmethod + def _is_reduplication(word: str) -> bool: + return len(word) == 2 and word[0] == word[1] + + # the last char of first word and the first char of second word is tone_three + def _merge_continuous_three_tones_2(self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + sub_finals_list = [ + lazy_pinyin(word, neutral_tone_with_five=True, style=Style.FINALS_TONE3) for (word, pos) in seg + ] + assert len(sub_finals_list) == len(seg) + merge_last = [False] * len(seg) + for i, (word, pos) in enumerate(seg): + if ( + i - 1 >= 0 + and sub_finals_list[i - 1][-1][-1] == "3" + and sub_finals_list[i][0][-1] == "3" + and not merge_last[i - 1] + ): + # if the last word is reduplication, not merge, because reduplication need to be _neural_sandhi + if not self._is_reduplication(seg[i - 1][0]) and len(seg[i - 1][0]) + len(seg[i][0]) <= 3: + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + merge_last[i] = True + else: + new_seg.append([word, pos]) + else: + new_seg.append([word, pos]) + return new_seg + + @staticmethod + def _merge_er(seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + for i, (word, pos) in enumerate(seg): + if i - 1 >= 0 and word == "儿" and seg[i - 1][0] != "#": + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + else: + new_seg.append([word, pos]) + return new_seg + + @staticmethod + def _merge_reduplication(seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + for i, (word, pos) in enumerate(seg): + if new_seg and word == new_seg[-1][0]: + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + else: + new_seg.append([word, pos]) + return new_seg + + def pre_merge_for_modify(self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + seg = self._merge_bu(seg) + seg = self._merge_yi(seg) + seg = self._merge_reduplication(seg) + seg = self._merge_continuous_three_tones(seg) + seg = self._merge_continuous_three_tones_2(seg) + seg = self._merge_er(seg) + return seg + + def modified_tone(self, word: str, pos: str, finals: List[str]) -> List[str]: + finals = self._bu_sandhi(word, finals) + finals = self._yi_sandhi(word, finals) + finals = self._neural_sandhi(word, pos, finals) + finals = self._three_sandhi(word, finals) + return finals diff --git a/genie_tts/G2P/Chinese/__init__.py b/genie_tts/G2P/Chinese/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/G2P/English/EnglishG2P.py b/genie_tts/G2P/English/EnglishG2P.py new file mode 100644 index 0000000000000000000000000000000000000000..5325385ce889018d128f9fec298de7836a0c5aeb --- /dev/null +++ b/genie_tts/G2P/English/EnglishG2P.py @@ -0,0 +1,296 @@ +import pickle +import os +import re +from typing import List, Dict, Tuple + +import numpy as np +import nltk +from nltk.tokenize import TweetTokenizer +from nltk import pos_tag + +from .Normalization import normalize +from .WordSegment import segment_text +from ..SymbolsV2 import symbols_v2, symbol_to_id_v2 +from ..SymbolsV2 import PUNCTUATION +from ...Core.Resources import English_G2P_DIR + +# nltk 路径和分词器初始化 +nltk.data.path.append(English_G2P_DIR) +word_tokenize = TweetTokenizer().tokenize + +# 路径定义 +CMU_DICT_PATH = os.path.join(English_G2P_DIR, "cmudict.rep") +CMU_DICT_FAST_PATH = os.path.join(English_G2P_DIR, "cmudict-fast.rep") +CMU_DICT_HOT_PATH = os.path.join(English_G2P_DIR, "engdict-hot.rep") +CACHE_PATH = os.path.join(English_G2P_DIR, "engdict_cache.pickle") +NAMECACHE_PATH = os.path.join(English_G2P_DIR, "namedict_cache.pickle") +MODEL_PATH = os.path.join(English_G2P_DIR, "checkpoint20.npz") + +# 正则表达式和映射 +REP_MAP = { + "[;::,;]": ",", + '["’]': "'", + "。": ".", + "!": "!", + "?": "?", +} +REP_MAP_PATTERN = re.compile("|".join(re.escape(p) for p in REP_MAP.keys())) +PUNCTUATIONS_FOR_REGEX = "".join(re.escape(p) for p in PUNCTUATION) +CONSECUTIVE_PUNCTUATION_PATTERN = re.compile(rf"([{PUNCTUATIONS_FOR_REGEX}\s])([{PUNCTUATIONS_FOR_REGEX}])+") + + +# 辅助函数 +def _read_cmu_dict(file_path: str) -> Dict[str, List[str]]: + g2p_dict = {} + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith(';;;'): continue + parts = re.split(r'\s+', line, maxsplit=1) + if len(parts) < 2: continue + word, pron_str = parts[0].lower(), parts[1] + pron = pron_str.split(" ") + word = re.sub(r'\(\d+\)$', '', word) + if word not in g2p_dict: g2p_dict[word] = [pron] + return g2p_dict + + +def _load_and_cache_dict() -> Dict[str, List[List[str]]]: + with open(CACHE_PATH, "rb") as f: + g2p_dict = pickle.load(f) + hot_dict = _read_cmu_dict(CMU_DICT_HOT_PATH) + if hot_dict: g2p_dict.update(hot_dict) + return g2p_dict + + +def replace_phs(phs: List[str]) -> List[str]: + rep_map = {"'": "-"} + phs_new = [] + for ph in phs: + if ph in symbols_v2: + phs_new.append(ph) + elif ph in rep_map: + phs_new.append(rep_map[ph]) + return phs_new + + +def replace_consecutive_punctuation(text: str) -> str: + return CONSECUTIVE_PUNCTUATION_PATTERN.sub(r"\1", text) + + +def text_normalize(text: str) -> str: + text = REP_MAP_PATTERN.sub(lambda x: REP_MAP[x.group()], text) + text = normalize(text) + text = replace_consecutive_punctuation(text) + return text + + +class CleanG2p: + """ + 一个集成了神经网络预测功能的、独立的英文G2P转换器。 + - 不再依赖 g2p_en 库,将模型推理逻辑直接内置。 + - 依赖 numpy 库进行计算。 + """ + + def __init__(self): + # 1. 初始化标准组件 + self.cmu = _load_and_cache_dict() + self.namedict = self._load_name_dict() + for word in ["AE", "AI", "AR", "IOS", "HUD", "OS"]: + self.cmu.pop(word.lower(), None) + self._setup_homographs() + + # 2. 初始化神经网络模型组件 + self._setup_nn_components() + self._load_nn_model() + + def _setup_nn_components(self): + """设置 G2P 神经网络所需的字母和音素表。""" + self.graphemes = ["", "", ""] + list("abcdefghijklmnopqrstuvwxyz") + self.phonemes = ["", "", "", ""] + ['AA0', 'AA1', 'AA2', 'AE0', 'AE1', 'AE2', 'AH0', 'AH1', + 'AH2', 'AO0', + 'AO1', 'AO2', 'AW0', 'AW1', 'AW2', 'AY0', 'AY1', 'AY2', + 'B', 'CH', 'D', 'DH', + 'EH0', 'EH1', 'EH2', 'ER0', 'ER1', 'ER2', 'EY0', 'EY1', + 'EY2', 'F', 'G', 'HH', + 'IH0', 'IH1', 'IH2', 'IY0', 'IY1', 'IY2', 'JH', 'K', 'L', + 'M', 'N', 'NG', 'OW0', 'OW1', + 'OW2', 'OY0', 'OY1', 'OY2', 'P', 'R', 'S', 'SH', 'T', 'TH', + 'UH0', 'UH1', 'UH2', 'UW', + 'UW0', 'UW1', 'UW2', 'V', 'W', 'Y', 'Z', 'ZH'] + self.g2idx = {g: idx for idx, g in enumerate(self.graphemes)} + self.idx2g = {idx: g for idx, g in enumerate(self.graphemes)} + self.p2idx = {p: idx for idx, p in enumerate(self.phonemes)} + self.idx2p = {idx: p for idx, p in enumerate(self.phonemes)} + + def _load_nn_model(self): + """从 .npz 文件加载预训练的神经网络权重。""" + if not os.path.exists(MODEL_PATH): + raise FileNotFoundError(f"G2P model file not found at: {MODEL_PATH}. " + f"Please ensure 'checkpoint20.npz' is in the correct directory.") + + variables = np.load(MODEL_PATH) + self.enc_emb = variables["enc_emb"] + self.enc_w_ih = variables["enc_w_ih"] + self.enc_w_hh = variables["enc_w_hh"] + self.enc_b_ih = variables["enc_b_ih"] + self.enc_b_hh = variables["enc_b_hh"] + self.dec_emb = variables["dec_emb"] + self.dec_w_ih = variables["dec_w_ih"] + self.dec_w_hh = variables["dec_w_hh"] + self.dec_b_ih = variables["dec_b_ih"] + self.dec_b_hh = variables["dec_b_hh"] + self.fc_w = variables["fc_w"] + self.fc_b = variables["fc_b"] + # logger.info("G2P neural network model loaded successfully.") + + @staticmethod + def _sigmoid(x): + return 1 / (1 + np.exp(-x)) + + def _grucell(self, x, h, w_ih, w_hh, b_ih, b_hh): + rzn_ih = np.matmul(x, w_ih.T) + b_ih + rzn_hh = np.matmul(h, w_hh.T) + b_hh + rz_ih, n_ih = rzn_ih[:, :rzn_ih.shape[-1] * 2 // 3], rzn_ih[:, rzn_ih.shape[-1] * 2 // 3:] + rz_hh, n_hh = rzn_hh[:, :rzn_hh.shape[-1] * 2 // 3], rzn_hh[:, rzn_hh.shape[-1] * 2 // 3:] + rz = self._sigmoid(rz_ih + rz_hh) + r, z = np.split(rz, 2, -1) + n = np.tanh(n_ih + r * n_hh) + h = (1 - z) * n + z * h + return h + + def _gru(self, x, steps, w_ih, w_hh, b_ih, b_hh, h0=None): + if h0 is None: + h0 = np.zeros((x.shape[0], w_hh.shape[1]), np.float32) + h = h0 + outputs = np.zeros((x.shape[0], steps, w_hh.shape[1]), np.float32) + for t in range(steps): + h = self._grucell(x[:, t, :], h, w_ih, w_hh, b_ih, b_hh) + outputs[:, t, ::] = h + return outputs + + def _encode(self, word: str) -> np.ndarray: + chars = list(word.lower()) + [""] + x = [self.g2idx.get(char, self.g2idx[""]) for char in chars] + x = np.take(self.enc_emb, np.expand_dims(x, 0), axis=0) + return x + + def predict(self, word: str) -> List[str]: + """使用内置的神经网络模型预测单词的发音。""" + # Encoder + enc = self._encode(word) + enc = self._gru(enc, len(word) + 1, self.enc_w_ih, self.enc_w_hh, + self.enc_b_ih, self.enc_b_hh, h0=np.zeros((1, self.enc_w_hh.shape[-1]), np.float32)) + last_hidden = enc[:, -1, :] + + # Decoder + dec = np.take(self.dec_emb, [self.p2idx[""]], axis=0) # Start with + h = last_hidden + preds = [] + for _ in range(20): # Max steps + h = self._grucell(dec, h, self.dec_w_ih, self.dec_w_hh, self.dec_b_ih, self.dec_b_hh) + logits = np.matmul(h, self.fc_w.T) + self.fc_b + pred_idx = logits.argmax() + if pred_idx == self.p2idx[""]: break + preds.append(pred_idx) + dec = np.take(self.dec_emb, [pred_idx], axis=0) + + return [self.idx2p.get(idx, "") for idx in preds] + + # --- 标准 G2P 逻辑 --- + + @staticmethod + def _load_name_dict() -> Dict[str, List[List[str]]]: + if os.path.exists(NAMECACHE_PATH): + with open(NAMECACHE_PATH, "rb") as f: return pickle.load(f) + return {} + + def _setup_homographs(self): + self.homograph2features: Dict[str, Tuple[List[str], List[str], str]] = { + "read": (["R", "EH1", "D"], ["R", "IY1", "D"], "VBD"), + "complex": (["K", "AH0", "M", "P", "L", "EH1", "K", "S"], ["K", "AA1", "M", "P", "L", "EH0", "K", "S"], + "JJ"), + "lead": (["L", "IY1", "D"], ["L", "EH1", "D"], "NN"), + "presents": (["P", "R", "IY0", "Z", "EH1", "N", "T", "S"], ["P", "R", "EH1", "Z", "AH0", "N", "T", "S"], + "VBZ"), + } + + def __call__(self, text: str) -> List[str]: + original_words = word_tokenize(text) + normalized_text = text_normalize(text) + normalized_words = word_tokenize(normalized_text) + + corrected_words = [] + original_idx, normalized_idx = 0, 0 + while original_idx < len(original_words) and normalized_idx < len(normalized_words): + if original_words[original_idx] == "I" and \ + " ".join(normalized_words[normalized_idx:normalized_idx + 2]) == "the first": + corrected_words.append("I") + original_idx += 1 + normalized_idx += 2 + else: + corrected_words.append(normalized_words[normalized_idx]) + original_idx += 1 + normalized_idx += 1 + if normalized_idx < len(normalized_words): + corrected_words.extend(normalized_words[normalized_idx:]) + + if not corrected_words: return [] + + tokens = pos_tag(corrected_words) + prons = [] + for o_word, pos in tokens: + word = o_word.lower() + if re.search("[a-z]", word) is None: + pron = [word] + elif word in self.homograph2features: + pron1, pron2, pos1 = self.homograph2features[word] + pron = pron1 if pos.startswith(pos1) else pron2 + else: + pron = self._query_word(o_word) + prons.extend(pron) + prons.extend([" "]) + return prons[:-1] if prons else [] + + def _query_word(self, o_word: str) -> List[str]: + word = o_word.lower() + if word in self.cmu: + if o_word == "A": return ["AH0"] + return self.cmu[word][0] + if o_word.istitle() and word in self.namedict: + return self.namedict[word][0] + if word.endswith("'s") and len(word) > 2: + base_pron = self._query_word(word[:-2]) + if base_pron: + last_ph = base_pron[-1] + if last_ph in {"S", "Z", "SH", "ZH", "CH", "JH"}: return base_pron + ["AH0", "Z"] + if last_ph in {"P", "T", "K", "F", "TH"}: return base_pron + ["S"] + return base_pron + ["Z"] + if "-" in word and len(word) > 1: + parts = [p for p in word.split("-") if p] + if len(parts) > 1: + result = [ph for part in parts for ph in self._query_word(part)] + if result: return result + segments = segment_text(word) + if len(segments) > 1 and "".join(segments) == word: + result = [ph for segment in segments for ph in self._query_word(segment)] + if result: return result + + return self.predict(o_word) + + +_g2p_instance: CleanG2p = CleanG2p() + + +def g2p(text: str) -> List[str]: + if _g2p_instance is None: raise RuntimeError("G2P model is not available.") + raw_phonemes = _g2p_instance(text) + undesired = {" ", "", "UW", "", ""} + phones = ["UNK" if ph == "" else ph for ph in raw_phonemes if ph not in undesired] + return replace_phs(phones) + + +def english_to_phones(text: str) -> List[int]: + phones = g2p(text) + phones = [symbol_to_id_v2[ph] for ph in phones] + return phones diff --git a/genie_tts/G2P/English/Normalization.py b/genie_tts/G2P/English/Normalization.py new file mode 100644 index 0000000000000000000000000000000000000000..ce5046d73caeaf8750e67a6088012d13d777737a --- /dev/null +++ b/genie_tts/G2P/English/Normalization.py @@ -0,0 +1,286 @@ +import re +import unicodedata +from calendar import month_name + + +# ------------------- 核心:自实现数字转单词 (替代 inflect) ------------------- + +def _number_to_words_custom(num_str): + """一个不依赖inflect的、简化的数字到单词转换器。""" + num_str = str(num_str).strip() + if not num_str.isdigit(): return num_str + + num = int(num_str) + if num == 0: return 'zero' + + units = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] + teens = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", + "nineteen"] + tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"] + thousands = ["", "thousand", "million", "billion", "trillion"] + + def convert_less_than_thousand(n): + if n == 0: return "" + if n < 10: return units[n] + if n < 20: return teens[n - 10] + if n < 100: return tens[n // 10] + (" " + units[n % 10] if n % 10 != 0 else "") + return units[n // 100] + " hundred" + (" " + convert_less_than_thousand(n % 100) if n % 100 != 0 else "") + + words = [] + i = 0 + if num == 0: return "zero" + while num > 0: + if num % 1000 != 0: + words.insert(0, convert_less_than_thousand(num % 1000) + " " + thousands[i]) + num //= 1000 + i += 1 + return " ".join(words).strip() + + +def _ordinal_custom(num_str): + """一个不依赖inflect的、简化的序数词转换器。""" + num = int(num_str) + if 10 <= num % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(num % 10, 'th') + return _number_to_words_custom(str(num)) + suffix + + +# ------------------- 初始化和常量定义 (无 inflect) ------------------- + +_measurement_map = { + "km/h": ["kilometer per hour", "kilometers per hour"], "mph": ["mile per hour", "miles per hour"], + "°C": ["degree celsius", "degrees celsius"], "°F": ["degree fahrenheit", "degrees fahrenheit"], + "tbsp": ["tablespoon", "tablespoons"], "tsp": ["teaspoon", "teaspoons"], + "km": ["kilometer", "kilometers"], "kg": ["kilogram", "kilograms"], "min": ["minute", "minutes"], + "ft": ["foot", "feet"], "cm": ["centimeter", "centimeters"], "m": ["meter", "meters"], + "L": ["liter", "liters"], "h": ["hour", "hours"], "s": ["second", "seconds"], +} + +_abbreviations = [ + (re.compile(r"\bMr\.(?=[\s,.]|\Z)", re.IGNORECASE), "Mister"), + (re.compile(r"\bMrs\.(?=[\s,.]|\Z)", re.IGNORECASE), "Missus"), + (re.compile(r"\bDr\.(?=[\s,.]|\Z)", re.IGNORECASE), "Doctor"), + (re.compile(r"\bProf\.(?=[\s,.]|\Z)", re.IGNORECASE), "Professor"), + (re.compile(r"\bSt\.(?=[\s,.]|\Z)", re.IGNORECASE), "Street"), + (re.compile(r"\bCo\.(?=[\s,.]|\Z)", re.IGNORECASE), "Company"), + (re.compile(r"\bLtd\.(?=[\s,.]|\Z)", re.IGNORECASE), "Limited"), + (re.compile(r"\be\.g\.(?=[\s,.]|\Z)", re.IGNORECASE), "for example"), + (re.compile(r"\bi\.e\.(?=[\s,.]|\Z)", re.IGNORECASE), "that is"), +] + +# ------------------- 正则表达式定义 (与原来保持一致) ------------------- +_currency_suffix_re = re.compile(r"([£$€])([\d,.]*\d)\s*(million|billion|thousand)\b", re.IGNORECASE) +_phone_re = re.compile(r"(\+?\d{1,3}-)?\b(\d{3})-(?:(\d{3})-)?(\d{4})\b") +_roman_re = re.compile(r"\b(XIX|XVIII|XVII|XVI|XV|XIV|XIII|XII|XI|X|IX|VIII|VII|VI|V|IV|III|II)\b", re.IGNORECASE) +_decade_re = re.compile(r"\b((?:1[89]|20)\d0)s\b") +_score_re = re.compile(r"\b(\d{1,2})-(\d{1,2})\b") +_dimension_re = re.compile(r"\b(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)(?:\s*x\s*(\d+(?:\.\d+)?))?\b") +_alphanumeric_re = re.compile(r"\b([a-zA-Z]+[0-9]+|[0-9]+[a-zA-Z]+)\b") +_date_re = re.compile(r"\b(0?[1-9]|1[0-2])/([0-2]?\d|3[01])/(\d{2,4})\b") +_ordinal_number_re = re.compile(r"\b(\d+)\. ") +_comma_number_re = re.compile(r"(\d[\d,]+\d)") +_currency_re = re.compile(r"([£$€])(\d*\.?\d+)|(\d*\.?\d+)\s*([£$€])") +_time_re = re.compile(r"\b([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?(\s*(?:a\.?m\.?|p\.?m\.?))?\b", re.IGNORECASE) +units = "|".join(re.escape(key) for key in sorted(_measurement_map.keys(), key=len, reverse=True)) +_measurement_re = re.compile(rf"(? 0: m_word = f" oh {_number_to_words_custom(m)}" if m < 10 else f" {_number_to_words_custom(m)}" + result = f"{h_word}{m_word}".lstrip() + if s_str: result += f" and {_number_to_words_custom(int(s_str))} seconds" + if am_pm: result += ' pm' if 'p' in am_pm.lower() else ' am' + return result + + +def _expand_measurement(m): + num_str, unit = m.groups() + is_neg = num_str.startswith('-') + if is_neg: num_str = num_str[1:] + if '/' in num_str: + num_word = _expand_fraction(re.match(_fraction_re, num_str)) + is_plural = True + else: + num_word = _number_to_words_custom(num_str) + is_plural = float(num_str) != 1 + unit_word = _measurement_map[unit][1] if is_plural else _measurement_map[unit][0] + result = f"{num_word} {unit_word}" + return f"minus {result}" if is_neg else result + + +def _expand_currency(m): + symbol, amount_str = (m.group(1), m.group(2)) if m.group(1) else (m.group(4), m.group(3)) + amount_str = (amount_str or "").replace(",", "") + if amount_str.startswith('.'): amount_str = '0' + amount_str + major_map = {"$": ("dollar", "dollars"), "£": ("pound", "pounds"), "€": ("euro", "euros")} + minor_map = {"$": ("cent", "cents"), "£": ("penny", "pence"), "€": ("cent", "cents")} + major_singular, major_plural = major_map.get(symbol, ("", "")) + parts = amount_str.split('.') + major_val = int(parts[0]) if parts[0] else 0 + minor_val = int(parts[1].ljust(2, '0')) if len(parts) > 1 and parts[1] else 0 + result = [] + if major_val > 0: + result.append(f"{_number_to_words_custom(major_val)} {major_singular if major_val == 1 else major_plural}") + if minor_val > 0: + minor_singular, minor_plural = minor_map.get(symbol, ("", "")) + result.append(f"{_number_to_words_custom(minor_val)} {minor_singular if minor_val == 1 else minor_plural}") + return " and ".join(result) or f"zero {major_plural}" + + +def _expand_decimal_number(m): + num_str = m.group(1) + parts = num_str.split('.') + integer_part = _number_to_words_custom(parts[0]) + fractional_part = ' '.join(_number_to_words_custom(digit) for digit in parts[1]) + return f"{integer_part} point {fractional_part}" + + +def _expand_date(m): + month, day, year = m.groups() + month_word = month_name[int(month)] + day_word = _ordinal_custom(day) + year_num = int(year) + if len(year) == 2: year_num += 2000 if year_num < 50 else 1900 + return f"{month_word} {day_word}, {_expand_number_positive(str(year_num))}" + + +def _expand_fraction(m): + n, d = int(m.group(1)), int(m.group(2)) + if d == 0: return m.group(0) + common_fractions = {(1, 2): "one half", (1, 4): "one quarter", (3, 4): "three quarters"} + if (n, d) in common_fractions: return common_fractions[(n, d)] + return f"{_number_to_words_custom(n)} over {_number_to_words_custom(d)}" + + +def _expand_ordinal_word(m): + return _ordinal_custom(m.group(0)[:-2]) + + +def _expand_number(m): + num_str = m.group(0) + if num_str.startswith('-'): return f"minus {_expand_number_positive(num_str[1:])}" + return _expand_number_positive(num_str) + + +def _expand_number_positive(num_str): + num = int(num_str) + if 2000 <= num < 2010: return f"two thousand and {_number_to_words_custom(num % 100)}" + if 1100 <= num < 2100 and num % 100 != 0: + return f"{_number_to_words_custom(num // 100)} {_number_to_words_custom(num % 100)}" + return _number_to_words_custom(num_str) + + +def _expand_acronym(m): return " ".join(m.group(0)) + + +def normalize(text): + text = "".join(char for char in unicodedata.normalize("NFD", text) if unicodedata.category(char) != "Mn") + text = re.sub(r"@", " at ", text) + for regex, replacement in _abbreviations: text = regex.sub(replacement, text) + text = re.sub(_currency_suffix_re, _expand_currency_suffix, text) + text = re.sub(_phone_re, _expand_phone_number, text) + text = re.sub(_dimension_re, _expand_dimension, text) + text = re.sub(_roman_re, _expand_roman, text) + text = re.sub(_decade_re, _expand_decade, text) + text = re.sub(_score_re, _expand_score, text) + text = re.sub(_date_re, _expand_date, text) + text = re.sub(_time_re, _expand_time, text) + text = re.sub(_ordinal_number_re, _convert_ordinal, text) + text = re.sub(_comma_number_re, _remove_commas, text) + text = re.sub(_currency_re, _expand_currency, text) + text = re.sub(_measurement_re, _expand_measurement, text) + text = re.sub(_fraction_re, _expand_fraction, text) + text = re.sub(_decimal_number_re, _expand_decimal_number, text) + text = re.sub(_ordinal_re, _expand_ordinal_word, text) + text = re.sub(_alphanumeric_re, _expand_alphanumeric, text) + text = re.sub(_acronym_re, _expand_acronym, text) + text = re.sub(_number_re, _expand_number, text) + text = text.lower() + text = re.sub(r"%", " percent", text) + domain_re = re.compile(r'\b([a-z0-9-]+)\.([a-z]{2,})\b') + while domain_re.search(text): text = domain_re.sub(r'\1 dot \2', text) + text = re.sub(r"[^a-z0-9'.,?!:;-]", " ", text) + text = re.sub(r"\s+", " ", text) + return text.strip() diff --git a/genie_tts/G2P/English/WordSegment.py b/genie_tts/G2P/English/WordSegment.py new file mode 100644 index 0000000000000000000000000000000000000000..0359d9ca60ad76fbecc4ad2ba92d635942c6b959 --- /dev/null +++ b/genie_tts/G2P/English/WordSegment.py @@ -0,0 +1,143 @@ +import io +import math +import os +from typing import List, Iterator, Tuple, Dict + +from ...Core.Resources import English_G2P_DIR + + +class WordSegmenter: + """ + Contains the core logic for word segmentation, adapted from the original library. + """ + ALPHABET = set('abcdefghijklmnopqrstuvwxyz0123456789') + TOTAL = 1024908267229.0 + LIMIT = 24 + + def __init__(self): + self.unigrams: Dict[str, float] = {} + self.bigrams: Dict[str, float] = {} + self.words: List[str] = [] + self.total: float = 0.0 + + def load(self, data_directory: str): + """ + Load unigram, bigram, and word counts from the specified data directory. + This is the main modification from the original library. + """ + unigrams_path = os.path.join(data_directory, 'unigrams.txt') + bigrams_path = os.path.join(data_directory, 'bigrams.txt') + words_path = os.path.join(data_directory, 'words.txt') + + for file_path in [unigrams_path, bigrams_path, words_path]: + if not os.path.exists(file_path): + raise FileNotFoundError( + f"Word segmentation data file not found: {file_path}. " + "Please ensure the data directory is correct." + ) + + self.unigrams.update(self._parse(unigrams_path)) + self.bigrams.update(self._parse(bigrams_path)) + with io.open(words_path, encoding='utf-8') as reader: + self.words.extend(reader.read().splitlines()) + + self.total = self.TOTAL + + @staticmethod + def _parse(filename: str) -> Dict[str, float]: + """Read `filename` and parse tab-separated file of word and count pairs.""" + with io.open(filename, encoding='utf-8') as reader: + # CORRECTED LINE: The generator now defines `line` before the comprehension uses it. + lines = (line.split('\t') for line in reader) + return {word: float(number) for word, number in lines if len(word) > 0 and len(number) > 0} + + def score(self, word: str, previous: str = None) -> float: + """Score `word` in the context of `previous` word.""" + if previous is None: + if word in self.unigrams: + return self.unigrams[word] / self.total + return 10.0 / (self.total * 10 ** len(word)) + + bigram = f'{previous} {word}' + if bigram in self.bigrams and previous in self.unigrams: + return self.bigrams[bigram] / self.total / self.score(previous) + + return self.score(word) + + def isegment(self, text: str) -> Iterator[str]: + """Return iterator of words that is the best segmenation of `text`.""" + memo = {} + + def search(text: str, previous: str = '') -> Tuple[float, List[str]]: + if text == '': + return 0.0, [] + + def candidates() -> Iterator[Tuple[float, List[str]]]: + for prefix, suffix in self._divide(text): + prefix_score = math.log10(self.score(prefix, previous)) + + pair = (suffix, prefix) + if pair not in memo: + memo[pair] = search(suffix, prefix) + suffix_score, suffix_words = memo[pair] + + yield prefix_score + suffix_score, [prefix] + suffix_words + + return max(candidates()) + + clean_text = self._clean(text) + + # Original logic to avoid recursion limits by chunking + size = 250 + prefix = '' + if len(clean_text) > size: + for offset in range(0, len(clean_text), size): + chunk = clean_text[offset:(offset + size)] + _, chunk_words = search(prefix + chunk) + + if len(chunk_words) > 5: + prefix = ''.join(chunk_words[-5:]) + del chunk_words[-5:] + else: # handle case where chunk is small + prefix = ''.join(chunk_words) + chunk_words = [] + + for word in chunk_words: + yield word + + _, prefix_words = search(prefix) + for word in prefix_words: + yield word + else: + _, words = search(clean_text) + for word in words: + yield word + + def segment(self, text: str) -> List[str]: + """Return list of words that is the best segmenation of `text`.""" + return list(self.isegment(text)) + + def _divide(self, text: str) -> Iterator[Tuple[str, str]]: + """Yield `(prefix, suffix)` pairs from `text`.""" + for pos in range(1, min(len(text), self.LIMIT) + 1): + yield text[:pos], text[pos:] + + @classmethod + def _clean(cls, text: str) -> str: + """Return `text` lower-cased with non-alphanumeric characters removed.""" + text_lower = text.lower() + return ''.join(letter for letter in text_lower if letter in cls.ALPHABET) + + +# --- Public Interface --- +# Create a single instance to be used by the importing module. + +_segmenter = WordSegmenter() +_segmenter.load(os.path.join(English_G2P_DIR, 'wordsegment')) + + +def segment_text(text: str) -> List[str]: + """ + Public function to segment a text string into a list of words. + """ + return _segmenter.segment(text) diff --git a/genie_tts/G2P/English/__init__.py b/genie_tts/G2P/English/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/G2P/Japanese/JapaneseG2P.py b/genie_tts/G2P/Japanese/JapaneseG2P.py new file mode 100644 index 0000000000000000000000000000000000000000..94362fc4b62601530de7b08699e43e91eaa4e591 --- /dev/null +++ b/genie_tts/G2P/Japanese/JapaneseG2P.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" +用于纯日语的 G2P。 +""" +import re +import pyopenjtalk +from typing import List +from ..SymbolsV2 import symbols_v2, symbol_to_id_v2 + +# 匹配连续的标点符号 +_CONSECUTIVE_PUNCTUATION_RE = re.compile(r"([,./?!~…・])\1+") + +# 匹配需要转换为日语读法的特殊符号 +_SYMBOLS_TO_JAPANESE = [ + (re.compile("%"), "パーセント"), + (re.compile("%"), "パーセント"), +] + +# 匹配日语字符(汉字、假名、全角字母数字等) +_JAPANESE_CHARACTERS_RE = re.compile( + r"[A-Za-z\d\u3005\u3040-\u30ff\u4e00-\u9fff\uff11-\uff19\uff21-\uff3a\uff41-\uff5a\uff66-\uff9d]" +) + +# 匹配非日语字符(标点、空格等) +_JAPANESE_MARKS_RE = re.compile( + r"[^A-Za-z\d\u3005\u3040-\u30ff\u4e00-\u9fff\uff11-\uff19\uff21-\uff3a\uff41-\uff5a\uff66-\uff9d]" +) + + +class JapaneseG2P: + """ + 一个简化的、封装好的日语Grapheme-to-Phoneme(字素到音素)转换器。 + + 本版本假设 pyopenjtalk 库已安装,并且不使用任何用户自定义词典。 + 它专注于提供一个纯粹、高效的文本到音素转换接口。 + """ + + @staticmethod + def _text_normalize(text: str) -> str: + """对输入文本进行基础的规范化处理。""" + for regex, replacement in _SYMBOLS_TO_JAPANESE: + text = re.sub(regex, replacement, text) + text = _CONSECUTIVE_PUNCTUATION_RE.sub(r"\1", text) + text = text.lower() + return text + + @staticmethod + def _post_replace_phoneme(ph: str) -> str: + """对单个音素或标点进行后处理替换。""" + rep_map = { + ":": ",", ";": ",", ",": ",", "。": ".", + "!": "!", "?": "?", "\n": ".", "·": ",", + "、": ",", "...": "…", + } + return rep_map.get(ph, ph) + + @staticmethod + def _numeric_feature_by_regex(regex: str, s: str) -> int: + """从OpenJTalk标签中提取数值特征。""" + match = re.search(regex, s) + return int(match.group(1)) if match else -50 + + @staticmethod + def _pyopenjtalk_g2p_prosody(text: str) -> List[str]: + """使用pyopenjtalk提取音素及韵律符号。""" + labels = pyopenjtalk.make_label(pyopenjtalk.run_frontend(text)) + phones = [] + for n, lab_curr in enumerate(labels): + p3 = re.search(r"-(.*?)\+", lab_curr).group(1) + if p3 in "AEIOU": + p3 = p3.lower() + + if p3 == "sil": + if n == 0: + phones.append("^") + elif n == len(labels) - 1: + e3 = JapaneseG2P._numeric_feature_by_regex(r"!(\d+)_", lab_curr) + phones.append("?" if e3 == 1 else "$") + continue + elif p3 == "pau": + phones.append("_") + continue + else: + phones.append(p3) + + a1 = JapaneseG2P._numeric_feature_by_regex(r"/A:([0-9\-]+)\+", lab_curr) + a2 = JapaneseG2P._numeric_feature_by_regex(r"\+(\d+)\+", lab_curr) + a3 = JapaneseG2P._numeric_feature_by_regex(r"\+(\d+)/", lab_curr) + f1 = JapaneseG2P._numeric_feature_by_regex(r"/F:(\d+)_", lab_curr) + lab_next = labels[n + 1] if n + 1 < len(labels) else "" + a2_next = JapaneseG2P._numeric_feature_by_regex(r"\+(\d+)\+", lab_next) + + if a3 == 1 and a2_next == 1 and p3 in "aeiouAEIOUNcl": + phones.append("#") + elif a1 == 0 and a2_next == a2 + 1 and a2 != f1: + phones.append("]") + elif a2 == 1 and a2_next == 2: + phones.append("[") + + return phones + + @staticmethod + def g2p(text: str, with_prosody: bool = True) -> List[str]: + """ + 将日语文本转换为音素序列。 + + Args: + text (str): 待转换的日语文本。 + with_prosody (bool): 是否在输出中包含韵律符号。默认为 True。 + + Returns: + List[str]: 音素和符号的列表。 + """ + if not text.strip(): + return [] + + # 1. 文本规范化 + norm_text = JapaneseG2P._text_normalize(text) + + # 2. 使用标点符号分割字符串,得到日语文本片段 + japanese_segments = _JAPANESE_MARKS_RE.split(norm_text) + punctuation_marks = _JAPANESE_MARKS_RE.findall(norm_text) + + phonemes = [] + for i, segment in enumerate(japanese_segments): + if segment: + if with_prosody: # 移除分析结果中句首(^)/句尾($)的符号,因为我们按片段处理 + phones = JapaneseG2P._pyopenjtalk_g2p_prosody(segment)[1:-1] + else: + phones = pyopenjtalk.g2p(segment).split(" ") + phonemes.extend(phones) + + # 将对应的标点符号添加回来 + if i < len(punctuation_marks): + mark = punctuation_marks[i].strip() + if mark: + phonemes.append(mark) + + # 3. 对最终列表中的每个元素进行后处理(主要转换全角标点) + processed_phonemes = [JapaneseG2P._post_replace_phoneme(p) for p in phonemes] + + return processed_phonemes + + +def japanese_to_phones(text: str) -> List[int]: + phones = JapaneseG2P.g2p(text) + phones = [ph for ph in phones if ph in symbols_v2] + # print(phones) + phones = [symbol_to_id_v2[ph] for ph in phones] + return phones diff --git a/genie_tts/G2P/Japanese/__init__.py b/genie_tts/G2P/Japanese/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/G2P/SymbolsV2.py b/genie_tts/G2P/SymbolsV2.py new file mode 100644 index 0000000000000000000000000000000000000000..37b24b76aa2fdbaa198ba6f15e2cc9af7503631e --- /dev/null +++ b/genie_tts/G2P/SymbolsV2.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +from typing import List, Dict + +# ------------------------- +# 基础符号集定义 +# ------------------------- + +# 标点和特殊分隔符 +PUNCTUATION = ["!", "?", "…", ",", "."] +PUNCTUATION_SYMBOLS = ["!", "?", "…", ",", ".", "-", "SP", "SP2", "SP3", "UNK"] + +# 中文普通话(Pinyin)符号 +# 声母 +PINYIN_INITIALS = [ + "AA", "EE", "OO", "b", "c", "ch", "d", "f", "g", "h", "j", "k", "l", + "m", "n", "p", "q", "r", "s", "sh", "t", "w", "x", "y", "z", "zh", +] +# 基础韵母 (不带声调) +PINYIN_FINALS_BASE = [ + "E", "En", "a", "ai", "an", "ang", "ao", "e", "ei", "en", "eng", "er", + "i", "i0", "ia", "ian", "iang", "iao", "ie", "in", "ing", "iong", + "ir", "iu", "o", "ong", "ou", "u", "ua", "uai", "uan", "uang", "ui", + "un", "uo", "v", "van", "ve", "vn", +] + +# 日语 (Romaji) 符号 +JAPANESE_SYMBOLS = [ + "I", "N", "U", "a", "b", "by", "ch", "cl", "d", "dy", "e", "f", "g", + "gy", "h", "hy", "i", "j", "k", "ky", "m", "my", "n", "ny", "o", "p", + "py", "r", "ry", "s", "sh", "t", "ts", "u", "v", "w", "y", "z", +] + +# 英语 (ARPAbet) 符号 +ARPABET_SYMBOLS = { + "AH0", "S", "AH1", "EY2", "AE2", "EH0", "OW2", "UH0", "NG", "B", "G", + "AY0", "M", "AA0", "F", "AO0", "ER2", "UH1", "IY1", "AH2", "DH", "IY0", + "EY1", "IH0", "K", "N", "W", "IY2", "T", "AA1", "ER1", "EH2", "OY0", + "UH2", "UW1", "Z", "AW2", "AW1", "V", "UW2", "AA2", "ER", "AW0", + "UW0", "R", "OW1", "EH1", "ZH", "AE0", "IH2", "IH", "Y", "JH", "P", + "AY1", "EY0", "OY2", "TH", "HH", "D", "ER0", "CH", "AO1", "AE1", + "AO2", "OY1", "AY2", "IH1", "OW0", "L", "SH", +} + +# 韩语 (Hangul) 符号 +KOREAN_SYMBOLS = "ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㄲㄸㅃㅆㅉㅏㅓㅗㅜㅡㅣㅐㅔ空停" + +# 粤语 (Jyutping/Yale) 符号 +CANTONESE_SYMBOLS = { + "Yeot3", "Yip1", "Yyu3", "Yeng4", "Yut5", "Yaan5", "Ym5", "Yaan6", "Yang1", "Yun4", + "Yon2", "Yui5", "Yun2", "Yat3", "Ye", "Yeot1", "Yoeng5", "Yoek2", "Yam2", "Yeon6", + "Yu6", "Yiu3", "Yaang6", "Yp5", "Yai4", "Yoek4", "Yit6", "Yam5", "Yoeng6", "Yg1", + "Yk3", "Yoe4", "Yam3", "Yc", "Yyu4", "Yyut1", "Yiu4", "Ying3", "Yip3", "Yaap3", + "Yau3", "Yan4", "Yau1", "Yap4", "Yk6", "Yok3", "Yai1", "Yeot6", "Yan2", "Yoek6", + "Yt1", "Yoi1", "Yit5", "Yn4", "Yaau3", "Yau4", "Yuk6", "Ys", "Yuk", "Yin6", + "Yung6", "Ya", "You", "Yaai5", "Yau5", "Yoi3", "Yaak3", "Yaat3", "Ying2", "Yok5", + "Yeng2", "Yyut3", "Yam1", "Yip5", "You1", "Yam6", "Yaa5", "Yi6", "Yek4", "Yyu2", + "Yuk5", "Yaam1", "Yang2", "Yai", "Yiu6", "Yin4", "Yok4", "Yot3", "Yui2", "Yeoi5", + "Yyun6", "Yyu5", "Yoi5", "Yeot2", "Yim4", "Yeoi2", "Yaan1", "Yang6", "Yong1", "Yaang4", + "Yung5", "Yeon1", "Yin2", "Ya3", "Yaang3", "Yg", "Yk2", "Yaau5", "Yut1", "Yt5", + "Yip4", "Yung4", "Yj", "Yong3", "Ya1", "Yg6", "Yaau6", "Yit3", "Yun3", "Ying1", + "Yn2", "Yg4", "Yl", "Yp3", "Yn3", "Yak1", "Yang5", "Yoe6", "You2", "Yap2", + "Yak2", "Yt3", "Yot5", "Yim2", "Yi1", "Yn6", "Yaat5", "Yaam3", "Yoek5", "Ye3", + "Yeon4", "Yaa2", "Yu3", "Yim6", "Ym", "Yoe3", "Yaai2", "Ym2", "Ya6", "Yeng6", + "Yik4", "Yot4", "Yaai4", "Yyun3", "Yu1", "Yoeng1", "Yaap2", "Yuk3", "Yoek3", "Yeng5", + "Yeoi1", "Yiu2", "Yok1", "Yo1", "Yoek1", "Yoeng2", "Yeon5", "Yiu1", "Yoeng4", "Yuk2", + "Yat4", "Yg5", "Yut4", "Yan6", "Yin3", "Yaa6", "Yap1", "Yg2", "Yoe5", "Yt4", + "Ya5", "Yo4", "Yyu1", "Yak3", "Yeon2", "Yong4", "Ym1", "Ye2", "Yaang5", "Yoi2", + "Yeng3", "Yn", "Yyut4", "Yau", "Yaak2", "Yaan4", "Yek2", "Yin1", "Yi5", "Yoe2", + "Yei5", "Yaat6", "Yak5", "Yp6", "Yok6", "Yei2", "Yaap1", "Yyut5", "Yi4", "Yim1", + "Yk5", "Ye4", "Yok2", "Yaam6", "Yat2", "Yon6", "Yei3", "Yyu6", "Yeot5", "Yk4", + "Yai6", "Yd", "Yg3", "Yei6", "Yau2", "Yok", "Yau6", "Yung3", "Yim5", "Yut6", + "Yit1", "Yon3", "Yat1", "Yaam2", "Yyut2", "Yui6", "Yt2", "Yek6", "Yt", "Ye6", + "Yang3", "Ying6", "Yaau1", "Yeon3", "Yng", "Yh", "Yang4", "Ying5", "Yaap6", "Yoeng3", + "Yyun4", "You3", "Yan5", "Yat5", "Yot1", "Yun1", "Yi3", "Yaa1", "Yaap4", "You6", + "Yaang2", "Yaap5", "Yaa3", "Yaak6", "Yeng1", "Yaak1", "Yo5", "Yoi4", "Yam4", "Yik1", + "Ye1", "Yai5", "Yung1", "Yp2", "Yui4", "Yaak4", "Yung2", "Yak4", "Yaat4", "Yeoi4", + "Yut2", "Yin5", "Yaau4", "Yap6", "Yb", "Yaam4", "Yw", "Yut3", "Yong2", "Yt6", + "Yaai6", "Yap5", "Yik5", "Yun6", "Yaam5", "Yun5", "Yik3", "Ya2", "Yyut6", "Yon4", + "Yk1", "Yit4", "Yak6", "Yaan2", "Yuk1", "Yai2", "Yik2", "Yaat2", "Yo3", "Ykw", + "Yn5", "Yaa", "Ye5", "Yu4", "Yei1", "Yai3", "Yyun5", "Yip2", "Yaau2", "Yiu5", + "Ym4", "Yeoi6", "Yk", "Ym6", "Yoe1", "Yeoi3", "Yon", "Yuk4", "Yaai3", "Yaa4", + "Yot6", "Yaang1", "Yei4", "Yek1", "Yo", "Yp", "Yo6", "Yp4", "Yan3", "Yoi", + "Yap3", "Yek3", "Yim3", "Yz", "Yot2", "Yoi6", "Yit2", "Yu5", "Yaan3", "Yan1", + "Yon5", "Yp1", "Yong5", "Ygw", "Yak", "Yat6", "Ying4", "Yu2", "Yf", "Ya4", + "Yon1", "You4", "Yik6", "Yui1", "Yaat1", "Yeot4", "Yi2", "Yaai1", "Yek5", "Ym3", + "Yong6", "You5", "Yyun1", "Yn1", "Yo2", "Yip6", "Yui3", "Yaak5", "Yyun2" +} + + +def _generate_pinyin_finals_with_tones(base_finals, num_tones=5): + """根据基础韵母和声调数量,自动生成带声调的韵母列表。""" + finals_with_tones = [] + for tone in range(1, num_tones + 1): + for final in base_finals: + finals_with_tones.append(f"{final}{tone}") + return finals_with_tones + + +def create_master_symbol_list(): + pinyin_finals = _generate_pinyin_finals_with_tones(PINYIN_FINALS_BASE) + + main_symbols = set() + main_symbols.add("_") # 添加下划线符号 + main_symbols.update(PINYIN_INITIALS) + main_symbols.update(pinyin_finals) + main_symbols.update(JAPANESE_SYMBOLS) + main_symbols.update(PUNCTUATION_SYMBOLS) + main_symbols.update(ARPABET_SYMBOLS) + + master_list = sorted(list(main_symbols)) + master_list.extend(["[", "]"]) + master_list.extend(sorted(list(KOREAN_SYMBOLS))) + master_list.extend(sorted(list(CANTONESE_SYMBOLS))) + return master_list + + +symbols_v2: List[str] = create_master_symbol_list() +symbol_to_id_v2: Dict[str, int] = {s: i for i, s in enumerate(symbols_v2)} diff --git a/genie_tts/G2P/__init__.py b/genie_tts/G2P/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/GUI/AudioPlayer.py b/genie_tts/GUI/AudioPlayer.py new file mode 100644 index 0000000000000000000000000000000000000000..76e615ce9985896bd2efca8a199fd2d54a649dc7 --- /dev/null +++ b/genie_tts/GUI/AudioPlayer.py @@ -0,0 +1,94 @@ +import sounddevice as sd +import threading +import queue +from typing import Union, Optional, Callable +import numpy as np +import os +import soundfile as sf + + +def run_in_sub_thread(func) -> Callable[..., threading.Thread]: + def wrapper(*args, **kwargs) -> threading.Thread: + thread = threading.Thread(target=func, args=args, kwargs=kwargs) + thread.daemon = True + thread.start() + return thread + + return wrapper + + +class AudioPlayer: + CHUNK_SIZE: int = 1024 + + def __init__(self): + self._task_queue: queue.Queue[bytes | str] = queue.Queue() + self._worker_thread: Optional[threading.Thread] = None + self._stop_event: threading.Event = threading.Event() + self._start_worker() + + def _start_worker(self): + """启动工作线程(如果它尚未运行或已关闭)。""" + if self._worker_thread and self._worker_thread.is_alive(): + return + self._stop_event.clear() + self._worker_thread = self._playback_worker() + + @run_in_sub_thread + def _playback_worker(self) -> None: + while not self._stop_event.is_set(): + try: + task: str = self._task_queue.get(timeout=0.1) + except queue.Empty: + continue + + stream = None + try: + if isinstance(task, str) and os.path.isfile(task): + with sf.SoundFile(task, 'r') as f: + if sd is not None: + stream = sd.OutputStream( + samplerate=f.samplerate, + channels=f.channels, + dtype='float32', + ) + stream.start() + while not self._stop_event.is_set(): + chunk = f.read(self.CHUNK_SIZE, dtype='float32') + if not chunk.any(): + break + stream.write(chunk) + except Exception as e: + if isinstance(e, sf.SoundFileError): + print(f"无法读取或解析音频文件: {task}, 错误: {e}") + else: + print(f"播放时发生错误: {e}") + finally: + if stream: + stream.stop() + stream.close() + self._task_queue.task_done() + + def play(self, source: Union[str, np.ndarray]): + """将音频源加入播放队列。""" + self._start_worker() + self._task_queue.put(source) + + def stop(self): + """停止播放并清空播放队列。""" + self._stop_event.set() + if self._worker_thread and self._worker_thread.is_alive(): + self._worker_thread.join() + self._stop_event.clear() + + with self._task_queue.mutex: + self._task_queue.queue.clear() + while self._task_queue.unfinished_tasks > 0: + self._task_queue.task_done() + + def wait(self): + """阻塞,直到队列中所有任务都播放完成。""" + self._task_queue.join() + + def close(self): + """永久关闭播放器并释放资源。""" + self.stop() diff --git a/genie_tts/GUI/ConverterWidget.py b/genie_tts/GUI/ConverterWidget.py new file mode 100644 index 0000000000000000000000000000000000000000..a1d6bbf8596e1c6aebe67dfd328992dada328614 --- /dev/null +++ b/genie_tts/GUI/ConverterWidget.py @@ -0,0 +1,204 @@ +import sys +import os +import datetime + +from PySide6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QPushButton, QTextEdit, QFileDialog, + QListView, QTreeView, QAbstractItemView +) +from PySide6.QtCore import Signal, QObject, QSettings, QThread + +from ..Converter.Converter import convert +from ..Converter.v2.Converter import find_ckpt_and_pth + + +def get_timestamp_msg(message: str, level: str = "INFO") -> str: + """辅助函数:生成类似 Logging 格式的带时间戳字符串""" + now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return f"{now} - {level} - {message}" + + +class Worker(QObject): + finished = Signal() + log_signal = Signal(str) + + def __init__(self, folders): + super().__init__() + self.folders = folders + + def log(self, message: str, level: str = "INFO"): + """内部辅助方法,用于格式化并发送日志""" + formatted_msg = get_timestamp_msg(message, level) + self.log_signal.emit(formatted_msg) + + def run(self): + """执行转换任务""" + try: + root_output_dir = os.path.abspath("./Output") + for folder in self.folders: + character_name: str = os.path.basename(folder) + output_dir: str = os.path.join(root_output_dir, character_name) + if os.path.exists(output_dir): + self.log(f'输出文件夹 {output_dir} 已存在,将覆盖内容。', "WARNING") + torch_ckpt_path, torch_pth_path = find_ckpt_and_pth(folder) + if not torch_ckpt_path or not torch_pth_path: + self.log(f'无法处理文件夹 {folder} 。请保证文件夹内有 GPT—SOVITS V2 导出的 .pth 和 .ckpt 模型。', + "ERROR") + continue + self.log(f'正在处理 {folder} 。') + # 调用转换逻辑 + convert(torch_ckpt_path, torch_pth_path, output_dir) + self.log(f'{folder} 处理完成。') # 可选:提示完成 + os.startfile(root_output_dir) + except Exception as e: + self.log(f"任务执行过程中发生未捕获异常: {str(e)}", "ERROR") + finally: + self.finished.emit() + + +class ConverterWidget(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle('GENIE Converter (PySide6 Version)') + self.resize(1280, 720) + + self.settings = QSettings("MyCompany", "GENIE Converter") + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(15) + + self.folder_button = QPushButton('📂 选择一个或多个文件夹') + self.folder_button.setFixedHeight(40) + self.folder_button.clicked.connect(self.open_folder_dialog) + + self.log_display = QTextEdit() + self.log_display.setReadOnly(True) + + main_layout.addWidget(self.folder_button) + main_layout.addWidget(self.log_display) + + self.apply_stylesheet() + + self.thread = None + self.worker = None + + self.append_formatted_log("欢迎使用 GENIE Converter!") + self.append_formatted_log("支持将 GPT—SOVITS V2/V2ProPlus 模型导出为 GENIE 引擎所需的格式。") + self.append_formatted_log("请选择一个或多个文件夹,每个文件夹中包含一对 .pth 和 .ckpt 文件。") + self.append_formatted_log("您可以使用 Ctrl 或 Shift 键来进行多选。\n") + + def apply_stylesheet(self): + self.setStyleSheet(""" + QWidget { + background-color: #2b2b2b; + color: #f0f0f0; + font-family: 'Segoe UI', 'Microsoft YaHei', 'Arial'; + font-size: 14px; + } + QPushButton { + background-color: #007bff; + color: white; + border: none; + padding: 10px; + border-radius: 5px; + font-weight: bold; + } + QPushButton:hover { background-color: #0056b3; } + QPushButton:pressed { background-color: #004494; } + QPushButton:disabled { + background-color: #555; + color: #aaa; + } + QTextEdit { + background-color: #1e1e1e; + border: 1px solid #444; + border-radius: 5px; + padding: 8px; + font-family: 'Consolas', 'Courier New', monospace; + } + QScrollBar:vertical { + border: none; background: #2b2b2b; width: 12px; margin: 0; + } + QScrollBar::handle:vertical { + background: #555; min-height: 20px; border-radius: 6px; + } + QScrollBar::handle:vertical:hover { background: #007bff; } + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } + """) + + def append_log(self, text: str): + """直接给主线程调用的日志打印方法""" + self.log_display.append(text) + self.scroll_to_bottom() + + def append_formatted_log(self, text: str, level="INFO"): + """给主线程调用的带格式日志方法""" + msg = get_timestamp_msg(text, level) + self.append_log(msg) + + def scroll_to_bottom(self): + self.log_display.verticalScrollBar().setValue( + self.log_display.verticalScrollBar().maximum() + ) + + def open_folder_dialog(self): + last_dir = self.settings.value("last_dir", "") + + dialog = QFileDialog(self, '请选择文件夹', str(last_dir)) + dialog.setFileMode(QFileDialog.FileMode.Directory) + dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True) + + list_view = dialog.findChild(QListView, 'listView') + if list_view: + list_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + + tree_view = dialog.findChild(QTreeView) + if tree_view: + tree_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + + if dialog.exec(): + selected_folders = dialog.selectedFiles() + if selected_folders: + self.run_conversion_task(selected_folders) + self.settings.setValue("last_dir", os.path.dirname(selected_folders[0])) + + def run_conversion_task(self, folders): + self.folder_button.setEnabled(False) + self.folder_button.setText("🔄 转换中,请稍候...") + + self.thread = QThread() + self.worker = Worker(folders) + self.worker.moveToThread(self.thread) + + # 连接线程控制信号 + self.thread.started.connect(self.worker.run) + self.worker.finished.connect(self.on_conversion_finished) + self.worker.log_signal.connect(self.append_log) + + self.thread.start() + + def on_conversion_finished(self): + self.thread.quit() + self.thread.wait() + self.worker.deleteLater() + self.thread.deleteLater() + self.thread = None + self.worker = None + + self.folder_button.setEnabled(True) + self.folder_button.setText("📂 选择一个或多个文件夹") + self.append_formatted_log("所有任务已完成。", "INFO") + + def closeEvent(self, event): + if self.thread is not None and self.thread.isRunning(): + self.thread.quit() + self.thread.wait() + super().closeEvent(event) + + +def start_gui() -> None: + app = QApplication(sys.argv) + window = ConverterWidget() + window.show() + sys.exit(app.exec()) diff --git a/genie_tts/GUI/GUI.py b/genie_tts/GUI/GUI.py new file mode 100644 index 0000000000000000000000000000000000000000..c2b4e4408dc8d8567ce30e4bb3ada1f494d62185 --- /dev/null +++ b/genie_tts/GUI/GUI.py @@ -0,0 +1,649 @@ +import sys +import os +import shutil +from typing import List, Optional, TextIO, Any +import uuid + +import soundfile as sf +import numpy as np + +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QTextEdit, + QFileDialog, QGroupBox, QScrollArea, QMessageBox, QTabWidget, QFormLayout, QFrame, +) +from PySide6.QtCore import ( + Qt, Signal, Slot, QObject, +) +from PySide6.QtGui import QTextCursor, QCloseEvent + +from ..Utils.TextSplitter import TextSplitter +from .Utils import ( + generate_output_filenames, FileSelectorWidget, FileSelectionMode, MyComboBox, sanitize_filename, + MyTextEdit +) +from .AudioPlayer import AudioPlayer +from .PresetManager import PresetManager +from .ServerManager import InferenceWorker +from .ConverterWidget import ConverterWidget + +""" +抄自 Genie CUDA Runtime +""" + +CACHE_DIR = './UserData/Cache/GenieGUI' +os.makedirs(CACHE_DIR, exist_ok=True) + + +# ==================== 后台工作线程 ==================== + +class LogRedirector(QObject): + """重定向 stdout 到 Signal""" + textWritten = Signal(str) + + def __init__(self): + super().__init__() + self._old_stdout: TextIO = sys.stdout + + def write(self, text: Any): + text = str(text) + self.textWritten.emit(text) + if self._old_stdout is not None: + self._old_stdout.write(text) + + def flush(self): + pass + + +# ==================== UI 组件实现 ==================== + +class PreviewItemWidget(QFrame): + """单条音频预览组件""" + + def __init__( + self, + index: int, + text: str, + file_path: str, + player: AudioPlayer, + parent: QWidget = None + ): + super().__init__(parent) + self.text: str = text + self.file_path: str = file_path + self.player: AudioPlayer = player + self.setFrameShape(QFrame.Shape.StyledPanel) + self.setFrameShadow(QFrame.Shadow.Raised) + + layout = QHBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + + # 编号 + lbl_id = QLabel(f"#{index}") + lbl_id.setFixedWidth(40) + lbl_id.setStyleSheet("font-weight: bold; color: #555;") + + # 文本 + lbl_text = QLabel(text) + lbl_text.setFixedWidth(240) + lbl_text.setToolTip(text) + + # 按钮 - 播放 + btn_play = QPushButton("▶ 播放") + btn_play.setFixedWidth(80) + btn_play.clicked.connect(self._play_audio) + + # 按钮 - 保存 + btn_save = QPushButton("⬇ 保存") + btn_save.setFixedWidth(80) + btn_save.clicked.connect(self._save_file) + + # 按钮 - 删除 (新增) + btn_del = QPushButton("🗑 删除") + btn_del.setFixedWidth(80) + btn_del.setStyleSheet("color: #ff4d4d;") # 以此区分删除按钮 + btn_del.clicked.connect(self._delete_item) + + layout.addWidget(lbl_id) + layout.addWidget(lbl_text, 1) # Stretch + layout.addWidget(btn_play) + layout.addWidget(btn_save) + layout.addWidget(btn_del) # 添加到布局 + + def _play_audio(self): + # 播放前先停止其他播放 + self.player.stop() + self.player.play(self.file_path) + + def _save_file(self): + filename = sanitize_filename(self.text) + save_path, _ = QFileDialog.getSaveFileName( + self, "保存音频", f"{filename}.wav", "WAV Audio (*.wav)" + ) + if save_path: + try: + shutil.copy(self.file_path, save_path) + QMessageBox.information(self, "成功", "文件保存成功!") + except Exception as e: + QMessageBox.critical(self, "错误", f"保存失败: {e}") + + def _delete_item(self): + """删除当前条目及对应的文件""" + # 1. 停止播放 + self.player.stop() + + # 2. 尝试删除物理文件 (避免垃圾堆积) + try: + if os.path.exists(self.file_path): + os.remove(self.file_path) + print(f"[INFO] 已删除文件: {self.file_path}") + except Exception as e: + print(f"[WARN] 删除文件失败: {e}") + + # 3. 从界面移除自身 + self.deleteLater() + + +class LogWidget(QWidget): + """日志显示Tab""" + + def __init__(self, parent: QWidget = None): + super().__init__(parent) + layout = QVBoxLayout(self) + self.text_edit: QTextEdit = QTextEdit() + self.text_edit.setReadOnly(True) + self.text_edit.setStyleSheet( + "background-color: #1e1e1e;" + "color: #ecf0f1;" + "font-family: Consolas;" + "font-size: 12pt;" + ) + layout.addWidget(self.text_edit) + + @Slot(str) + def append_log(self, text: str): + self.text_edit.moveCursor(QTextCursor.MoveOperation.End) + self.text_edit.insertPlainText(text) + self.text_edit.moveCursor(QTextCursor.MoveOperation.End) + + +class TTSWidget(QWidget): + """TTS 主交互界面""" + + def __init__(self, player: AudioPlayer, parent: QWidget = None): + super().__init__(parent) + self.player: AudioPlayer = player + self.splitter: TextSplitter = TextSplitter() + self.current_gen_id: int = 0 + self.current_worker: Optional[InferenceWorker] = None + + main_layout = QVBoxLayout(self) + + # ---------------- 顶部:预设管理器 ---------------- + self.preset_manager = PresetManager( + presets_file='./UserData/GenieGuiConfig.json', + state_getter=self.get_ui_state, + ) + self.preset_manager.sig_load_state.connect(self.apply_ui_state) + main_layout.addWidget(self.preset_manager) + + # ---------------- 中间:滚动设置区 ---------------- + scroll = QScrollArea() + scroll.setWidgetResizable(True) + content_widget = QWidget() + + content_layout = QHBoxLayout(content_widget) + left_column_layout = QVBoxLayout() + right_column_layout = QVBoxLayout() + + # ==================== 左侧列内容 ==================== + + # 模型设置组 + group_model = QGroupBox("模型设置") + self.layout_model = QFormLayout() + self.combo_model_type = MyComboBox() + self.combo_model_type.addItems(["Genie-TTS"]) + self.combo_model_type.currentTextChanged.connect(self._update_model_ui_visibility) + self.combo_model_type.setEnabled(False) + self.file_gpt = FileSelectorWidget("gpt_path", FileSelectionMode.FILE, "Checkpoints (*.ckpt)") + self.file_vits = FileSelectorWidget("vits_path", FileSelectionMode.FILE, "Models (*.pth)") + self.file_genie = FileSelectorWidget("genie_dir", FileSelectionMode.DIRECTORY) + self.file_gpt.pathChanged.connect(self._on_gpt_path_changed) + self.file_vits.pathChanged.connect(self._on_vits_path_changed) + self.layout_model.addRow("模型类型:", self.combo_model_type) + self.layout_model.addRow("GPT模型 (.ckpt):", self.file_gpt) + self.layout_model.addRow("VITS模型 (.pth):", self.file_vits) + self.layout_model.addRow("Genie模型目录:", self.file_genie) + group_model.setLayout(self.layout_model) + # 参考音频组 + group_ref = QGroupBox("参考音频") + layout_ref = QFormLayout() + self.file_ref_audio = FileSelectorWidget("ref_audio", FileSelectionMode.FILE, "Audio (*.wav *.mp3)") + self.input_ref_text = QLineEdit() + self.input_ref_text.setPlaceholderText("请输入参考音频对应的文本...") + btn_play_ref = QPushButton("▶️") + btn_play_ref.setFixedWidth(30) + btn_play_ref.clicked.connect(self._play_ref_audio) + hbox_ref_text = QHBoxLayout() + hbox_ref_text.addWidget(self.input_ref_text) + hbox_ref_text.addWidget(btn_play_ref) + layout_ref.addRow("音频文件:", self.file_ref_audio) + layout_ref.addRow("音频文本:", hbox_ref_text) + group_ref.setLayout(layout_ref) + + left_column_layout.addWidget(group_model) + left_column_layout.addWidget(group_ref) + left_column_layout.addStretch() + + # ==================== 右侧列内容 ==================== + + # === 推理设置组 === + group_infer = QGroupBox("推理参数") + layout_infer = QFormLayout() + self.combo_device = MyComboBox() + self.combo_device.addItems(["CPU"]) + self.combo_device.setEnabled(False) + self.combo_quality = MyComboBox() + self.combo_quality.addItems(["质量优先"]) + self.combo_quality.setEnabled(False) + self.combo_split = MyComboBox() + self.combo_split.addItems(["不切分", "智能切分", "按行切分"]) + self.combo_mode = MyComboBox() + self.combo_mode.addItems(["串行推理"]) + self.combo_mode.setEnabled(False) + self.combo_lang = MyComboBox() + self.combo_lang.addItems(["Chinese", "English", "Japanese"]) + layout_infer.addRow("推理设备:\n(重启生效)", self.combo_device) + layout_infer.addRow("推理需求:", self.combo_quality) + layout_infer.addRow("分句方式:", self.combo_split) + layout_infer.addRow("推理模式:", self.combo_mode) + layout_infer.addRow("目标语言:", self.combo_lang) + group_infer.setLayout(layout_infer) + + # === 自动保存组 === + group_save = QGroupBox("自动保存设置") + self.layout_save = QFormLayout() + self.combo_save_mode = MyComboBox() + self.combo_save_mode.addItems(["禁用自动保存", "保存为单个文件", "保存为多个文件"]) + self.combo_save_mode.currentIndexChanged.connect(self._update_save_ui_state) + default_out_path = os.path.join(os.path.expanduser("~"), "Desktop", "Genie 输出语音") + self.file_out_dir = FileSelectorWidget("out_dir", FileSelectionMode.DIRECTORY) + self.file_out_dir.set_path(default_out_path) + self.layout_save.addRow("保存方式:", self.combo_save_mode) + self.layout_save.addRow("输出文件夹:", self.file_out_dir) + group_save.setLayout(self.layout_save) + + right_column_layout.addWidget(group_infer) + right_column_layout.addWidget(group_save) + right_column_layout.addStretch() + + content_layout.addLayout(left_column_layout, 1) + content_layout.addLayout(right_column_layout, 1) + scroll.setWidget(content_widget) + main_layout.addWidget(scroll, 5) + + # ==================== 底部:输入控制 + 输出预览 ==================== + + # 创建底部容器 widget + bottom_widget = QWidget() + bottom_layout = QHBoxLayout(bottom_widget) + bottom_layout.setContentsMargins(0, 0, 0, 0) # 去除边距让它贴合 + + # --- 输入控制组 --- + group_input = QGroupBox("目标文本") + layout_input = QVBoxLayout() + self.text_input = MyTextEdit() + self.text_input.setPlaceholderText("请输入要合成的目标文本...") + self.text_input.setFixedHeight(300) + self.btn_start = QPushButton("开始推理") + self.btn_start.setFixedHeight(40) + self.btn_start.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + border-radius: 5px; + } + QPushButton:hover { background-color: #45a049; } + QPushButton:disabled { background-color: #cccccc; } + """) + self.btn_start.clicked.connect(self._start_inference) + layout_input.addWidget(self.text_input) + layout_input.addWidget(self.btn_start) + group_input.setLayout(layout_input) + + # --- 输出预览组 --- + group_preview = QGroupBox("输出音频预览") + preview_layout = QVBoxLayout() + self.preview_scroll = QScrollArea() + self.preview_scroll.setWidgetResizable(True) + self.preview_container = QWidget() + self.preview_list_layout = QVBoxLayout(self.preview_container) + self.preview_list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.preview_scroll.setWidget(self.preview_container) + preview_layout.addWidget(self.preview_scroll) + group_preview.setLayout(preview_layout) + + bottom_layout.addWidget(group_input, 1) + bottom_layout.addWidget(group_preview, 1) + main_layout.addWidget(bottom_widget, 3) + + self.apply_ui_state(self.preset_manager.current_preset_data) + + # ==================== 状态管理接口 (供 PresetManager 调用) ==================== + + @property + def current_preset_name(self) -> str: + return self.preset_manager.current_preset_name + + @property + def current_preset_data(self) -> dict: + return self.preset_manager.current_preset_data + + def get_ui_state(self) -> dict: + """收集当前UI状态为字典""" + return { + "model_type": self.combo_model_type.currentText(), + "gpt_path": self.file_gpt.get_path(), + "vits_path": self.file_vits.get_path(), + "genie_dir": self.file_genie.get_path(), + "ref_audio": self.file_ref_audio.get_path(), + "ref_text": self.input_ref_text.text(), + "device": self.combo_device.currentText().lower(), + "quality": self.combo_quality.currentText(), + "split": self.combo_split.currentText(), + "mode": self.combo_mode.currentText(), + "lang": self.combo_lang.currentText(), + "save_mode": self.combo_save_mode.currentText(), + "out_dir": self.file_out_dir.get_path() + } + + @Slot(dict) + def apply_ui_state(self, data: dict) -> None: + """将字典数据应用到UI""" + + def set_combo_text(combo: MyComboBox, text: str) -> None: + index = combo.findText(text) + if index >= 0: + combo.setCurrentIndex(index) + + set_combo_text(self.combo_model_type, data.get("model_type", "")) + self.file_gpt.set_path(data.get("gpt_path", ""), block_signals=True) + self.file_vits.set_path(data.get("vits_path", ""), block_signals=True) + self.file_genie.set_path(data.get("genie_dir", "")) + self.file_ref_audio.set_path(data.get("ref_audio", "")) + self.input_ref_text.setText(data.get("ref_text", "")) + + set_combo_text(self.combo_device, data.get("device", "")) + set_combo_text(self.combo_quality, data.get("quality", "")) + set_combo_text(self.combo_split, data.get("split", "")) + set_combo_text(self.combo_mode, data.get("mode", "")) + set_combo_text(self.combo_lang, data.get("lang", "")) + set_combo_text(self.combo_save_mode, data.get("save_mode", "")) + + self.file_out_dir.set_path(data.get("out_dir", "")) + + # 确保UI显隐状态正确 + self._update_model_ui_visibility() + self._update_save_ui_state() + + # ==================== UI 逻辑处理 ==================== + + def _update_model_ui_visibility(self, *args) -> None: + """根据模型类型控制文件选择器的显隐""" + is_gpt = self.combo_model_type.currentText() == "GPT-SoVITS" + self.layout_model.setRowVisible(self.file_gpt, is_gpt) + self.layout_model.setRowVisible(self.file_vits, is_gpt) + self.layout_model.setRowVisible(self.file_genie, not is_gpt) + + @Slot(str) + def _on_gpt_path_changed(self, path: str): + if path and os.path.exists(path) and not self.file_vits.get_path(): + self._try_auto_fill_sibling(path, ".pth", self.file_vits) + + @Slot(str) + def _on_vits_path_changed(self, path: str): + if path and os.path.exists(path) and not self.file_gpt.get_path(): + self._try_auto_fill_sibling(path, ".ckpt", self.file_gpt) + + @staticmethod + def _try_auto_fill_sibling(current_path: str, target_ext: str, target_widget: FileSelectorWidget): + try: + directory = os.path.dirname(current_path) + if not os.path.exists(directory): + return + for f in os.listdir(directory): + if f.lower().endswith(target_ext.lower()): + full_path = os.path.join(directory, f) + target_widget.set_path(full_path) + print(f"[INFO] 自动关联模型文件: {full_path}") + break + except Exception as e: + print(f"[WARN] 自动关联文件失败: {e}") + + def _update_save_ui_state(self) -> None: + enabled = self.combo_save_mode.currentText() != "禁用自动保存" + self.layout_save.setRowVisible(self.file_out_dir, enabled) + + def _play_ref_audio(self) -> None: + path = self.file_ref_audio.get_path() + if os.path.exists(path): + self.player.stop() + self.player.play(path) + else: + QMessageBox.warning(self, "错误", "参考音频文件不存在") + + def _get_split_texts(self, text: str) -> List[str]: + method = self.combo_split.currentText() + if method == "不切分": + return [text] + elif method == "按行切分": + return [line.strip() for line in text.split('\n') if line.strip()] + elif method == "智能切分": + return self.splitter.split(text) + return [text] + + def _start_inference(self) -> None: + text = self.text_input.toPlainText().strip() + if not text: + QMessageBox.warning(self, "提示", "请输入目标文本") + return + + ref_path = self.file_ref_audio.get_path() + ref_text = self.input_ref_text.text().strip() + if not ref_path or not ref_text: + QMessageBox.warning(self, "提示", "请设置参考音频") + return + + if not self.file_genie.get_path(): + QMessageBox.warning(self, "提示", "请选择Genie模型目录") + return + + out_dir = self.file_out_dir.get_path() + save_mode = self.combo_save_mode.currentText() + if not out_dir and save_mode != "禁用自动保存": + desktop = os.path.join(os.path.expanduser("~"), "Desktop", "Genie Output") + self.file_out_dir.set_path(desktop) + print(f"[INFO] 未设置输出文件夹, 将在桌面创建!") + + self.btn_start.setEnabled(False) + self.btn_start.setText("推理中...") + self._chain_import_model() + + # ==================== 推理链式调用 ==================== + + def _chain_import_model(self) -> None: + req = { + "character_name": self.current_preset_name, + "onnx_model_dir": self.file_genie.get_path(), + "language": self.combo_lang.currentText(), + } + worker = InferenceWorker(req, mode="load_character") + worker.finished.connect(lambda s, m, d: self._on_import_finished(s, m)) + worker.start() + self.current_worker = worker + + @Slot(bool, str) + def _on_import_finished(self, success: bool, msg: str) -> None: + if not success: + self._reset_ui_state() + QMessageBox.critical(self, "模型加载失败", msg) + return + print(f"[INFO] {msg}") + self._chain_set_ref() + + def _chain_set_ref(self) -> None: + req = { + "character_name": self.current_preset_name, + "audio_path": self.file_ref_audio.get_path(), + "audio_text": self.input_ref_text.text().strip(), + "language": self.combo_lang.currentText(), + } + worker = InferenceWorker(req, mode="set_reference_audio") + worker.finished.connect(lambda s, m, d: self._on_set_ref_finished(s, m)) + worker.start() + self.current_worker = worker + + @Slot(bool, str) + def _on_set_ref_finished(self, success: bool, msg: str) -> None: + if not success: + self._reset_ui_state() + QMessageBox.critical(self, "设置参考音频失败", msg) + return + print(f"[INFO] {msg}") + self._chain_tts() + + def _chain_tts(self) -> None: + text_full = self.text_input.toPlainText().strip() + text_list = self._get_split_texts(text_full) + + print(f"[INFO] 开始串行推理, 分句结果: {text_list}") + self._process_serial_step(0, text_list, [], 32000) + + def _process_serial_step( + self, + index: int, + text_list: List[str], + audio_accumulator: List[np.ndarray], + sample_rate: int + ) -> None: + # 1. 终止条件:所有句子处理完毕 + if index >= len(text_list): + save_mode = self.combo_save_mode.currentText() + out_dir = self.file_out_dir.get_path() + + if out_dir: + os.makedirs(out_dir, exist_ok=True) + + if audio_accumulator and save_mode != "保存为多个文件": + full_text = ''.join(text_list) + full_audio = np.concatenate(audio_accumulator, axis=0) + if save_mode == "保存为单个文件": + target_names = generate_output_filenames(folder=out_dir, original_texts=[full_text]) + save_path = os.path.join(out_dir, target_names[0]) + else: # "禁用自动保存" + save_path = os.path.join(CACHE_DIR, f"{uuid.uuid4().hex}.wav") + sf.write(save_path, data=full_audio, samplerate=sample_rate, subtype='PCM_16') + self._add_to_preview(full_text, save_path) + + print(f"\n[INFO] 串行推理全部完成,共 {len(text_list)} 句。") + self._reset_ui_state() + return + + # 2. 递归进行:发起当前句子的请求 + req = { + "character_name": self.current_preset_name, + "text": text_list[index], + } + worker = InferenceWorker(req, mode="tts") + worker.finished.connect( + lambda s, m, d: self._on_serial_step_finished(s, m, d, index, text_list, audio_accumulator) + ) + worker.start() + self.current_worker = worker + + @Slot(bool, str, object, int, object, object, object) + def _on_serial_step_finished( + self, + success: bool, + msg: str, + return_data: dict, + index: int, + text_list: List[str], + audio_accumulator: List[np.ndarray] + ) -> None: + if not success: + self._reset_ui_state() + QMessageBox.critical(self, "推理失败", f"第 {index + 1} 句出错: {msg}") + return + + sr = return_data.get("sample_rate", 32000) + audio_list = return_data.get("audio_list", []) + save_mode = self.combo_save_mode.currentText() + out_dir = self.file_out_dir.get_path() + if out_dir: + os.makedirs(out_dir, exist_ok=True) + + if audio_list: + audio_accumulator.append(audio_list[0]) + if save_mode == "保存为多个文件": + target_names = generate_output_filenames(folder=out_dir, original_texts=[text_list[index]]) + save_path = os.path.join(out_dir, target_names[0]) + sf.write(save_path, data=audio_list[0], samplerate=sr, subtype='FLOAT') + self._add_to_preview(text_list[index], save_path) + else: + print(f"[WARN] 第 {index + 1} 句返回空音频") + + # 继续处理下一句 + self._process_serial_step(index + 1, text_list, audio_accumulator, sr) + + def _add_to_preview(self, text: str, path: str) -> None: + item = PreviewItemWidget(self.current_gen_id, text, path, self.player) + self.preview_list_layout.insertWidget(0, item) + self.current_gen_id += 1 + + def _reset_ui_state(self) -> None: + self.btn_start.setEnabled(True) + self.btn_start.setText("开始推理") + + def closeEvent(self, event: QCloseEvent) -> None: + # 委托 PresetManager 处理保存逻辑 + self.preset_manager.shutdown() + super().closeEvent(event) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Genie TTS Inference GUI") + self.resize(1300, 900) + + # 初始化音频播放器 + self.player: AudioPlayer = AudioPlayer() + + # 初始化日志重定向 + self.log_widget: LogWidget = LogWidget() + sys.stdout = LogRedirector() + sys.stdout.textWritten.connect(self.log_widget.append_log) + + # 初始化主界面 + self.tabs: QTabWidget = QTabWidget() + self.tts_widget = TTSWidget(self.player) + self.conv_widget = ConverterWidget() + + self.tabs.addTab(self.log_widget, "GUI Log") + self.tabs.addTab(self.tts_widget, "TTS Inference") + self.tabs.addTab(self.conv_widget, "Converter") + self.tabs.setCurrentIndex(1) # 默认显示TTS页 + + self.setCentralWidget(self.tabs) + + def closeEvent(self, event: QCloseEvent) -> None: + if os.path.exists(CACHE_DIR): + shutil.rmtree(CACHE_DIR) + if hasattr(self, 'player'): + self.player.stop() + # 线程安全退出后,再恢复 stdout + sys.stdout = sys.__stdout__ + if hasattr(self, 'tts_widget'): + self.tts_widget.closeEvent(event) + event.accept() diff --git a/genie_tts/GUI/PresetManager.py b/genie_tts/GUI/PresetManager.py new file mode 100644 index 0000000000000000000000000000000000000000..15f25131bfcfb19f10cea7267651241c8f2be497 --- /dev/null +++ b/genie_tts/GUI/PresetManager.py @@ -0,0 +1,188 @@ +import os +import json +from typing import Callable, Optional, Dict + +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QComboBox, QGroupBox, QMessageBox, QInputDialog +) +from PySide6.QtCore import Signal, QSettings, Slot + + +class PresetManager(QWidget): + # 信号:通知主界面加载数据 + sig_load_state = Signal(dict) + + def __init__(self, + presets_file: str, + state_getter: Callable[[], dict], + parent: QWidget = None): + super().__init__(parent) + self.presets_file: str = presets_file + self.state_getter: Callable[[], dict] = state_getter + self.presets: Dict[str, dict] = {} + self.current_preset_name: Optional[str] = None + self._init_ui() + self._load_from_disk() + # 初始化完成后,应用一次初始状态 + self._apply_initial_preset() + + def _init_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + group = QGroupBox("预设管理 (自动保存)") + h_layout = QHBoxLayout() + + self.lbl_info = QLabel("当前:") + + self.combo_presets = QComboBox() + self.combo_presets.textActivated.connect(self._on_preset_switch_triggered) + + btn_new = QPushButton("新建") + btn_new.clicked.connect(self.create_preset) + + # === 新增:重命名按钮 === + btn_rename = QPushButton("重命名") + btn_rename.clicked.connect(self.rename_preset) + # ===================== + + btn_del = QPushButton("删除") + btn_del.clicked.connect(self.delete_preset) + + h_layout.addWidget(self.lbl_info) + h_layout.addWidget(self.combo_presets, 1) + h_layout.addWidget(btn_new) + h_layout.addWidget(btn_rename) # 添加到布局 + h_layout.addWidget(btn_del) + + group.setLayout(h_layout) + layout.addWidget(group) + + @property + def current_preset_data(self) -> dict: + return self.presets.get(self.current_preset_name, {}) + + def _load_from_disk(self): + """从磁盘加载 JSON""" + if os.path.exists(self.presets_file): + try: + with open(self.presets_file, 'r', encoding='utf-8') as f: + self.presets = json.load(f) + except Exception as e: + print(f"[ERROR] 预设文件损坏: {e}") + self.presets = {} + + if not self.presets: + self.presets = {"Default": {}} + + self.combo_presets.clear() + self.combo_presets.addItems(list(self.presets.keys())) + + def _save_to_disk(self): + """写入磁盘""" + try: + os.makedirs(os.path.dirname(self.presets_file), exist_ok=True) + with open(self.presets_file, 'w', encoding='utf-8') as f: + json.dump(self.presets, f, indent=4, ensure_ascii=False) + except Exception as e: + print(f"[ERROR] 保存预设失败: {e}") + + def _apply_initial_preset(self): + """初始化时恢复上次的选择""" + last_used = QSettings("MyTTS", "GUI").value("last_preset", "Default") + if last_used not in self.presets and self.combo_presets.count() > 0: + last_used = self.combo_presets.itemText(0) + self.current_preset_name = last_used + self.combo_presets.setCurrentText(last_used) + self._load_preset_data(last_used) + + @Slot(str) + def _on_preset_switch_triggered(self, new_preset_name: str): + if new_preset_name == self.current_preset_name: + return + if self.current_preset_name: + self._save_current_state_to_memory(self.current_preset_name) + self._load_preset_data(new_preset_name) + self.current_preset_name = new_preset_name + QSettings("MyTTS", "GUI").setValue("last_preset", new_preset_name) + self._save_to_disk() + + def _save_current_state_to_memory(self, preset_name: str): + """调用回调获取主界面状态,并更新到内存字典""" + if self.state_getter and preset_name in self.presets: + current_data = self.state_getter() + self.presets[preset_name] = current_data + + def _load_preset_data(self, preset_name: str): + """发送信号给主界面加载数据""" + data = self.presets.get(preset_name, {}) + self.sig_load_state.emit(data) + print(f"[INFO] 已加载预设: {preset_name}") + + # ================= 公共接口 ================= + + def create_preset(self): + """新建预设""" + if self.current_preset_name: + self._save_current_state_to_memory(self.current_preset_name) + + name, ok = QInputDialog.getText(self, "新建预设", "名称:") + if ok and name: + if name in self.presets: + QMessageBox.warning(self, "警告", "预设名已存在") + return + self.presets[name] = {} + self.combo_presets.addItem(name) + self.combo_presets.setCurrentText(name) + self.current_preset_name = name + self._save_to_disk() + self._load_preset_data(name) + print(f"[INFO] 已创建预设: {name}") + + def rename_preset(self): + """重命名当前预设""" + current_name = self.current_preset_name + if not current_name: + return + + # 先保存当前状态到内存,确保重命名时带走的是最新数据 + self._save_current_state_to_memory(current_name) + + new_name, ok = QInputDialog.getText(self, "重命名预设", "新名称:", text=current_name) + if ok and new_name and new_name != current_name: + if new_name in self.presets: + QMessageBox.warning(self, "警告", "预设名已存在") + return + # 迁移数据 + self.presets[new_name] = self.presets.pop(current_name) + self.current_preset_name = new_name + # 更新下拉框显示的文本(更新当前选中的这一项) + current_index = self.combo_presets.currentIndex() + self.combo_presets.setItemText(current_index, new_name) + # 更新配置记录 + QSettings("MyTTS", "GUI").setValue("last_preset", new_name) + self._save_to_disk() + print(f"[INFO] 已重命名预设: {current_name} -> {new_name}") + + def delete_preset(self): + """删除当前预设""" + target = self.current_preset_name + if len(self.presets) <= 1: + QMessageBox.warning(self, "禁止", "至少保留一个预设") + return + + if QMessageBox.StandardButton.Yes == QMessageBox.question(self, "确认", f"删除 '{target}'?"): + del self.presets[target] + self.combo_presets.removeItem(self.combo_presets.currentIndex()) + new_name = self.combo_presets.currentText() + self.current_preset_name = new_name + self._load_preset_data(new_name) + self._save_to_disk() + print(f"[INFO] 已删除预设: {target}") + + def shutdown(self): + """关闭时触发""" + if self.current_preset_name: + self._save_current_state_to_memory(self.current_preset_name) + self._save_to_disk() diff --git a/genie_tts/GUI/ServerManager.py b/genie_tts/GUI/ServerManager.py new file mode 100644 index 0000000000000000000000000000000000000000..7928b80aba69632032eff0a25143faebe7c719c1 --- /dev/null +++ b/genie_tts/GUI/ServerManager.py @@ -0,0 +1,61 @@ +from PySide6.QtCore import Signal, QThread + +from ..Utils.Shared import context +from ..Internal import load_character, set_reference_audio +from ..Core.Inference import tts_client +from ..ModelManager import model_manager + + +class InferenceWorker(QThread): + """执行推理任务的 Worker""" + finished = Signal(bool, str, object) # success, message, data + + def __init__(self, request_data: dict, mode: str): + super().__init__() + self.req: dict = request_data + self.mode: str = mode + + def run(self) -> None: + try: + if self.mode == 'load_character': + load_character( + character_name=self.req['character_name'], + onnx_model_dir=self.req['onnx_model_dir'], + language=self.req['language'], + ) + self.finished.emit(True, "导入角色完成", None) + + elif self.mode == 'set_reference_audio': + set_reference_audio( + character_name=self.req['character_name'], + audio_path=self.req['audio_path'], + audio_text=self.req['audio_text'], + language=self.req['language'], + ) + self.finished.emit(True, "设置参考音频完成", None) + + elif self.mode == 'tts': + gsv_model = model_manager.get(self.req['character_name']) + tts_client.stop_event.clear() + audio_chunk = tts_client.tts( + text=self.req['text'], + prompt_audio=context.current_prompt_audio, + encoder=gsv_model.T2S_ENCODER, + first_stage_decoder=gsv_model.T2S_FIRST_STAGE_DECODER, + stage_decoder=gsv_model.T2S_STAGE_DECODER, + vocoder=gsv_model.VITS, + prompt_encoder=gsv_model.PROMPT_ENCODER, + language=gsv_model.LANGUAGE, + ) + audio_chunk = audio_chunk.squeeze() + try: + return_data = { + "sample_rate": 32000, + "audio_list": [audio_chunk], + } + self.finished.emit(True, "推理完成", return_data) + except Exception as e: + self.finished.emit(False, f"数据解析失败: {e}", None) + + except Exception as e: + self.finished.emit(False, f"请求异常: {str(e)}", None) diff --git a/genie_tts/GUI/Utils.py b/genie_tts/GUI/Utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c801e2b4635bcba08c40aa64926b9ad408252727 --- /dev/null +++ b/genie_tts/GUI/Utils.py @@ -0,0 +1,257 @@ +import os +import re +from enum import Enum +from datetime import datetime +import socket +from typing import List + +from PySide6.QtWidgets import ( + QWidget, QHBoxLayout, QPushButton, QLineEdit, QFileDialog, QMessageBox, QComboBox, QTextEdit +) +from PySide6.QtCore import (Qt, Signal, QSettings, Property, QObject, QEvent, QMimeData) +from PySide6.QtGui import QFont + + +def sanitize_filename(filename: str, replacement: str = '') -> str: + """ + 将文本清理为合法的 Windows 文件名。 + + Args: + filename (str): 原始文件名。 + replacement (str): 非法字符的替换字符,默认为空,建议使用 "_"。 + + Returns: + str: 清理后的文件名。 + """ + # 1. 去除非法字符 + # \/:*?"<>| 是标准非法字符 + # \x00-\x1f 是控制字符 (如换行符、制表符等),Windows 也不允许 + cleaned = re.sub(r'[\\/:*?"<>|\x00-\x1f]', replacement, filename) + + # 2. 去除首尾的空格和点 + # Windows 文件名不能以空格或点结尾,也不能以空格开头(虽然允许但通常不推荐) + cleaned = cleaned.strip().rstrip('.') + + # 3. 处理 Windows 保留文件名 (CON, PRN, AUX, NUL, COM1-9, LPT1-9) + # 这些名字不论加什么扩展名都是非法的 (例如 con.txt 也是非法的) + reserved_names = { + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" + } + + # 如果文件名(全大写)是保留字,或者文件名是保留字+扩展名(如 con.txt),则加下划线前缀 + filename_upper = cleaned.upper() + file_stem = filename_upper.split('.')[0] # 获取不带后缀的主文件名 + + if filename_upper in reserved_names or file_stem in reserved_names: + cleaned = "_" + cleaned + + # 4. 处理空文件名 (如果输入全是乱码被删光了) + if not cleaned: + cleaned = "unnamed_file" + + # 5. 限制长度 (Windows API 通常限制 255 字符,但在某些路径下更短) + cleaned = truncate_text(cleaned) + + return cleaned + + +def is_port_free(port: int) -> bool: + """返回端口是否可用""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(('', port)) + except OSError: + return False + return True + + +def find_free_port(preferred: int = 8000) -> int: + # 先尝试 preferred 端口 + if is_port_free(preferred): + return preferred + + # 否则用系统自动分配 + s = socket.socket() + s.bind(('', 0)) + port = s.getsockname()[1] + s.close() + return port + + +def truncate_text(text: str, max_len: int = 30) -> str: + """ + 按指定宽度截断文本 + """ + result = "" + cur_len = 0 + for ch in text: + char_len = 2 if ('\u4e00' <= ch <= '\u9fff') else 1 + if cur_len + char_len > max_len: + break + result += ch + cur_len += char_len + return result + + +def generate_output_filenames(folder: str, original_texts: List[str]) -> List[str]: + """ + 批量生成文件名: + 输入 original_texts 列表 + 输出等长 filenames 列表 + """ + today = datetime.now().strftime("%Y%m%d") + pattern = re.compile(rf'^\[{today}]\[(\d{{3}})]') + + # ① 查找当天现存最大编号 + max_n = 0 + if os.path.isdir(folder): + for name in os.listdir(folder): + m = pattern.match(name) + if m: + n = int(m.group(1)) + max_n = max(max_n, n) + + filenames = [] + cur_n = max_n + + # ② 依次生成新文件名 + for text in original_texts: + cur_n += 1 + n_str = f"{cur_n:03d}" + + cleaned = sanitize_filename(text) + + filename = f"[{today}][{n_str}]{cleaned}.wav" + filenames.append(filename) + + return filenames + + +# ==================== 通用组件 ==================== + +class FileSelectionMode(Enum): + FILE = 0 + DIRECTORY = 1 + + +class FileSelectorWidget(QWidget): + """一个包含行编辑和浏览按钮的复合控件,支持文件和文件夹选择。(原样引用并稍作适配)""" + pathChanged = Signal(str) + + def __init__( + self, + setting_key: str, + selection_mode: FileSelectionMode = FileSelectionMode.DIRECTORY, + file_filter: str = "All Files (*)", + parent: QWidget = None, + ): + super().__init__(parent) + self.setting_key: str = setting_key + self.selection_mode: FileSelectionMode = selection_mode + self.file_filter: str = file_filter + + # 使用 QSettings 模拟 STUDIO_SETTINGS + self.settings = QSettings("MyTTS", "GUI") + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + self.path_edit: QLineEdit = QLineEdit() + self.path_edit.setReadOnly(True) + self.path_edit.setPlaceholderText("未选择路径") + + self.browse_button: QPushButton = QPushButton("📁") + self.browse_button.setCursor(Qt.CursorShape.PointingHandCursor) + default_font = QFont() + default_font.setPointSize(10) + self.browse_button.setFont(default_font) + self.browse_button.setFixedSize(30, 30) + + self.clear_button: QPushButton = QPushButton("❌") + self.clear_button.setCursor(Qt.CursorShape.PointingHandCursor) + self.clear_button.setFont(default_font) + self.clear_button.setFixedSize(30, 30) + + layout.addWidget(self.path_edit) + layout.addWidget(self.browse_button) + layout.addWidget(self.clear_button) + + self.browse_button.clicked.connect(self._open_dialog) + self.clear_button.clicked.connect(self._clear_path) + self.path_edit.textChanged.connect(self.pathChanged) + + def _open_dialog(self): + path = self.path_edit.text() + if path and os.path.exists(path): + start_path = path + else: + # 【修改点】默认路径改为 Desktop + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + start_path = str(self.settings.value( + f"last_path_{self.setting_key}", defaultValue=desktop_path + )) + + if self.selection_mode == FileSelectionMode.DIRECTORY: + selected_path = QFileDialog.getExistingDirectory( + self, "选择文件夹", start_path + ) + else: + selected_path, _ = QFileDialog.getOpenFileName( + self, "选择文件", start_path, self.file_filter + ) + + if selected_path: + self.set_path(selected_path) + parent_path = os.path.dirname(selected_path) + if parent_path: + self.settings.setValue( + f"last_path_{self.setting_key}", parent_path) + + def _clear_path(self): + if not self.path_edit.text(): + return + reply = QMessageBox.question(self, '确认', '您确定要清空路径吗?', + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No) + if reply == QMessageBox.StandardButton.Yes: + self.set_path("") + + def get_path(self) -> str: + text = self.path_edit.text() + # 即使路径不存在(可能是输入时),也返回文本供逻辑判断,或者严格校验 + return text + + def set_path(self, path: str, block_signals: bool = False): + if block_signals: + self.path_edit.blockSignals(True) + self.path_edit.setText(path) + if block_signals: + self.path_edit.blockSignals(False) + + path = Property(str, fget=get_path, fset=set_path, notify=pathChanged) # type: ignore + + +class WheelEventFilter(QObject): + def eventFilter(self, obj, event): + if event.type() == QEvent.Type.Wheel and isinstance(obj, QComboBox): + return True # 阻止默认滚轮行为 + return super().eventFilter(obj, event) + + +class MyComboBox(QComboBox): + def __init__(self, parent: QWidget = None): + super().__init__(parent) + self._wheelFilter = WheelEventFilter() + self.installEventFilter(self._wheelFilter) + + +class MyTextEdit(QTextEdit): + def insertFromMimeData(self, source: QMimeData) -> None: + # 仅取纯文本 + if source.hasText(): + self.insertPlainText(source.text()) + else: + super().insertFromMimeData(source) diff --git a/genie_tts/GetPhonesAndBert.py b/genie_tts/GetPhonesAndBert.py new file mode 100644 index 0000000000000000000000000000000000000000..c181a948a162341643864c289fbdc81b6a281850 --- /dev/null +++ b/genie_tts/GetPhonesAndBert.py @@ -0,0 +1,34 @@ +import numpy as np +from typing import Tuple +from .Utils.Constants import BERT_FEATURE_DIM +from .ModelManager import model_manager + + +def get_phones_and_bert(prompt_text: str, language: str = 'japanese') -> Tuple[np.ndarray, np.ndarray]: + if language.lower() == 'english': + from .G2P.English.EnglishG2P import english_to_phones + phones = english_to_phones(prompt_text) + text_bert = np.zeros((len(phones), BERT_FEATURE_DIM), dtype=np.float32) + elif language.lower() == 'chinese': + from .G2P.Chinese.ChineseG2P import chinese_to_phones + text_clean, _, phones, word2ph = chinese_to_phones(prompt_text) + if model_manager.load_roberta_model(): + encoded = model_manager.roberta_tokenizer.encode(text_clean) + input_ids = np.array([encoded.ids], dtype=np.int64) + attention_mask = np.array([encoded.attention_mask], dtype=np.int64) + ort_inputs = { + 'input_ids': input_ids, + 'attention_mask': attention_mask, + 'repeats': np.array(word2ph, dtype=np.int64), + } + outputs = model_manager.roberta_model.run(None, ort_inputs) + text_bert = outputs[0].astype(np.float32) + else: + text_bert = np.zeros((len(phones), BERT_FEATURE_DIM), dtype=np.float32) + else: + from .G2P.Japanese.JapaneseG2P import japanese_to_phones + phones = japanese_to_phones(prompt_text) + text_bert = np.zeros((len(phones), BERT_FEATURE_DIM), dtype=np.float32) + + phones_seq = np.array([phones], dtype=np.int64) + return phones_seq, text_bert diff --git a/genie_tts/Internal.py b/genie_tts/Internal.py new file mode 100644 index 0000000000000000000000000000000000000000..77d87595b1aee44698d906c20c1d9d81589a1cbe --- /dev/null +++ b/genie_tts/Internal.py @@ -0,0 +1,395 @@ +# 请严格遵循导入顺序。 +# 1、环境变量。 +import os +from os import PathLike + +os.environ["HF_HUB_ENABLE_PROGRESS_BAR"] = "1" + +# 2、Logging & Warnings。 +import logging +import warnings + +warnings.filterwarnings("ignore", category=UserWarning, module="jieba_fast._compat") +logging.basicConfig(level=logging.INFO, format="%(message)s", datefmt="[%X]") +logger = logging.getLogger(__name__) + +# 3、ONNX。 +import onnxruntime + +onnxruntime.set_default_logger_severity(3) + +# 导入剩余库。 + +from pathlib import Path +import json +import asyncio +from typing import AsyncIterator, Optional, Union, Dict + +from .Audio.ReferenceAudio import ReferenceAudio +from .Core.Resources import ensure_exists, Chinese_G2P_DIR, English_G2P_DIR +from .Core.TTSPlayer import tts_player +from .ModelManager import model_manager +from .Utils.Shared import context +from .Utils.Language import normalize_language +from .PredefinedCharacter import download_chara, CHARA_LANG, CHARA_ALIAS_MAP + +# A module-level private dictionary to store reference audio configurations. +_reference_audios: Dict[str, dict] = {} +SUPPORTED_AUDIO_EXTS = {'.wav', '.flac', '.ogg', '.aiff', '.aif'} + + +def check_onnx_model_dir(onnx_model_dir: Union[str, os.PathLike]) -> None: + """ + Checks if the directory contains the necessary ONNX model files for Genie TTS (v2 or v2ProPlus). + Raises a FileNotFoundError with detailed instructions if validation fails. + """ + model_path = Path(onnx_model_dir) + + # 1. Check if directory exists + if not model_path.exists() or not model_path.is_dir(): + raise FileNotFoundError(f"The model directory '{onnx_model_dir}' does not exist or is not a directory.") + + # 2. Define required files + # Base files required by both v2 and v2ProPlus + required_base_files = { + "t2s_encoder_fp32.bin", + "t2s_encoder_fp32.onnx", + "t2s_first_stage_decoder_fp32.onnx", + "t2s_shared_fp16.bin", + "t2s_stage_decoder_fp32.onnx", + "vits_fp16.bin", + "vits_fp32.onnx" + } + + # 3. Get current files in directory + existing_files = set(f.name for f in model_path.iterdir() if f.is_file()) + + # 4. Validate + # We check if the base files exist. If base files are missing, the model is definitely unusable. + if not required_base_files.issubset(existing_files): + missing = required_base_files - existing_files + + # Construct detailed error message + error_msg = ( + f"\n\n[Genie Error] Invalid ONNX model directory: '{model_path}'\n" + "===============================================================\n" + f"Missing base files: {', '.join(missing)}\n" + "A valid model folder must contain at least the following files.\n" + "1. [v2 Base] (Required for all models):\n" + " - t2s_encoder_fp32.bin\n" + " - t2s_encoder_fp32.onnx\n" + " - t2s_first_stage_decoder_fp32.onnx\n" + " - t2s_shared_fp16.bin\n" + " - t2s_stage_decoder_fp32.onnx\n" + " - vits_fp16.bin\n" + " - vits_fp32.onnx\n" + "2. [v2ProPlus Additions] (Required for v2pp features):\n" + " - prompt_encoder_fp16.bin\n" + " - prompt_encoder_fp32.onnx\n" + "===============================================================\n" + ) + raise FileNotFoundError(error_msg) + + +def load_character( + character_name: str, + onnx_model_dir: Union[str, PathLike], + language: str, +) -> None: + """ + Loads a character model from an ONNX model directory. + + Args: + character_name (str): The name to assign to the loaded character. + onnx_model_dir (str | PathLike): The directory path containing the ONNX model files. + language (str): The language of the character model. + """ + check_onnx_model_dir(onnx_model_dir) + + language = normalize_language(language) + if language not in ['Japanese', 'English', 'Chinese']: + raise ValueError('Unknown language') + + if language == 'Chinese': + ensure_exists(Chinese_G2P_DIR, "Chinese_G2P_DIR") + elif language == 'English': + ensure_exists(English_G2P_DIR, "English_G2P_DIR") + + model_path: str = os.fspath(onnx_model_dir) + model_manager.load_character( + character_name=character_name, + model_dir=model_path, + language=language, + ) + + +def unload_character( + character_name: str, +) -> None: + """ + Unloads a previously loaded character model to free up resources. + + Args: + character_name (str): The name of the character to unload. + """ + model_manager.remove_character( + character_name=character_name, + ) + + +def set_reference_audio( + character_name: str, + audio_path: Union[str, PathLike], + audio_text: str, + language: str = None, +) -> None: + """ + Sets the reference audio for a character to be used for voice cloning. + + This must be called for a character before using 'tts' or 'tts_async'. + + Args: + character_name (str): The name of the character. + audio_path (str | PathLike): The file path to the reference audio (e.g., a WAV file). + audio_text (str): The transcript of the reference audio. + language (str): The language of the reference audio. + """ + audio_path: str = os.fspath(audio_path) + + # 检查文件后缀是否支持 + ext = os.path.splitext(audio_path)[1].lower() + if ext not in SUPPORTED_AUDIO_EXTS: + logger.error( + f"Audio format '{ext}' is not supported. Only the following formats are supported: {SUPPORTED_AUDIO_EXTS}" + ) + return + + if language is None: + gsv_model = model_manager.get(character_name) + if gsv_model: + language = gsv_model.LANGUAGE + else: + raise ValueError('No language specified') + language = normalize_language(language) + if language not in ['Japanese', 'English', 'Chinese']: + raise ValueError('Unknown language') + + _reference_audios[character_name] = { + 'audio_path': audio_path, + 'audio_text': audio_text, + 'language': language, + } + # print(_reference_audios[character_name]) + context.current_prompt_audio = ReferenceAudio( + prompt_wav=audio_path, + prompt_text=audio_text, + language=language, + ) + + +async def tts_async( + character_name: str, + text: str, + play: bool = False, + split_sentence: bool = False, + save_path: Union[str, PathLike, None] = None, +) -> AsyncIterator[bytes]: + """ + Asynchronously generates speech from text and yields audio chunks. + + This function returns an async iterator that provides the audio data in + real-time as it's being generated. + + Args: + character_name (str): The name of the character to use for synthesis. + text (str): The text to be synthesized into speech. + play (bool, optional): If True, plays the audio as it's generated. Defaults to False. + split_sentence (bool, optional): If True, splits the text into sentences for synthesis. Defaults to False. + save_path (str | PathLike | None, optional): If provided, saves the generated audio to this file path. Defaults to None. + + Yields: + bytes: A chunk of the generated audio data. + + Raises: + ValueError: If 'set_reference_audio' has not been called for the character. + """ + if character_name not in _reference_audios: + raise ValueError("Please call 'set_reference_audio' first to set the reference audio.") + + if save_path: + save_path = os.fspath(save_path) + parent_dir = os.path.dirname(save_path) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) + + # 1. 创建 asyncio 队列和获取当前事件循环 + stream_queue: asyncio.Queue[Union[bytes, None]] = asyncio.Queue() + loop = asyncio.get_running_loop() + + # 2. 定义回调函数,用于在线程和 asyncio 之间安全地传递数据 + def tts_chunk_callback(c: Optional[bytes]): + """This callback is called from the TTS worker thread.""" + loop.call_soon_threadsafe(stream_queue.put_nowait, c) + + # 设置 TTS 上下文 + context.current_speaker = character_name + context.current_prompt_audio = ReferenceAudio( + prompt_wav=_reference_audios[character_name]['audio_path'], + prompt_text=_reference_audios[character_name]['audio_text'], + language=_reference_audios[character_name]['language'], + ) + + # 3. 使用新的回调接口启动 TTS 会话 + tts_player.start_session( + play=play, + split=split_sentence, + save_path=save_path, + chunk_callback=tts_chunk_callback, + ) + + # 馈送文本并通知会话结束 + tts_player.feed(text) + tts_player.end_session() + + # 4. 从队列中异步读取数据并产生 + while True: + chunk = await stream_queue.get() + if chunk is None: + break + yield chunk + + +def tts( + character_name: str, + text: str, + play: bool = False, + split_sentence: bool = True, + save_path: Union[str, PathLike, None] = None, +) -> None: + """ + Synchronously generates speech from text. + + This is a blocking function that will not return until the entire TTS + process is complete. + + Args: + character_name (str): The name of the character to use for synthesis. + text (str): The text to be synthesized into speech. + play (bool, optional): If True, plays the audio. + split_sentence (bool, optional): If True, splits the text into sentences for synthesis. + save_path (str | PathLike | None, optional): If provided, saves the generated audio to this file path. Defaults to None. + """ + if character_name not in _reference_audios: + logger.error("Please call 'set_reference_audio' first to set the reference audio.") + return + + if save_path: + save_path = os.fspath(save_path) + parent_dir = os.path.dirname(save_path) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) + + context.current_speaker = character_name + context.current_prompt_audio = ReferenceAudio( + prompt_wav=_reference_audios[character_name]['audio_path'], + prompt_text=_reference_audios[character_name]['audio_text'], + language=_reference_audios[character_name]['language'], + ) + + tts_player.start_session( + play=play, + split=split_sentence, + save_path=save_path, + ) + tts_player.feed(text) + tts_player.end_session() + tts_player.wait_for_tts_completion() + + +def wait_for_playback_done() -> None: + """ + Wait until all TTS tasks have finished processing and playback has fully completed. + """ + tts_player.wait_for_playback_done() + + +def stop() -> None: + """ + Stops the currently playing text-to-speech audio. + """ + tts_player.stop() + + +def convert_to_onnx( + torch_ckpt_path: Union[str, PathLike], + torch_pth_path: Union[str, PathLike], + output_dir: Union[str, PathLike], +) -> None: + """ + Converts PyTorch model checkpoints to the ONNX format. + + This function requires PyTorch to be installed. + + Args: + torch_ckpt_path (str | PathLike): The path to the T2S model (.ckpt) file. + torch_pth_path (str | PathLike): The path to the VITS model (.pth) file. + output_dir (str | PathLike): The directory where the ONNX models will be saved. + """ + try: + import torch + except ImportError: + logger.error("❌ PyTorch is not installed. Please run `pip install torch` first.") + return + + from .Converter.Converter import convert + + torch_ckpt_path = os.fspath(torch_ckpt_path) + torch_pth_path = os.fspath(torch_pth_path) + output_dir = os.fspath(output_dir) + + convert( + torch_pth_path=torch_pth_path, + torch_ckpt_path=torch_ckpt_path, + output_dir=output_dir, + ) + + +def clear_reference_audio_cache() -> None: + """ + Clears the cache of reference audio data. + """ + ReferenceAudio.clear_cache() + + +def load_predefined_character(character_name: str) -> None: + """ + Download and load a predefined character model for TTS inference. + """ + character_name = character_name.lower().strip() + if character_name not in CHARA_ALIAS_MAP: + logger.error(f"No predefined character model found for {character_name}") + return + character_name = CHARA_ALIAS_MAP[character_name] + + save_path = download_chara(character_name) + model_manager.load_character( + character_name=character_name, + model_dir=os.path.join(save_path, 'tts_models'), + language=CHARA_LANG[character_name], + ) + + with open(os.path.join(save_path, "prompt_wav.json"), "r", encoding="utf-8") as f: + prompt_wav_dict: Dict[str, Dict[str, str]] = json.load(f) + + audio_text = prompt_wav_dict["Normal"]["text"] + audio_path = os.path.join(save_path, "prompt_wav", prompt_wav_dict["Normal"]["wav"]) + _reference_audios[character_name] = { + 'audio_path': audio_path, + 'audio_text': audio_text, + 'language': CHARA_LANG[character_name], + } + context.current_prompt_audio = ReferenceAudio( + prompt_wav=audio_path, + prompt_text=audio_text, + language=CHARA_LANG[character_name], + ) diff --git a/genie_tts/ModelManager.py b/genie_tts/ModelManager.py new file mode 100644 index 0000000000000000000000000000000000000000..f3e84bf65f6f378922fe88191e5ba1561a848e48 --- /dev/null +++ b/genie_tts/ModelManager.py @@ -0,0 +1,324 @@ +""" +不再新建 .bin 文件。 +修改后内存: 6448 MB +修改前内存: 5952 MB +""" + +import gc +import logging +import os +from dataclasses import dataclass +from typing import Optional, List, Dict + +import numpy as np +import onnx +import onnxruntime +from onnxruntime import InferenceSession +from tokenizers import Tokenizer + +from .Core.Resources import (HUBERT_MODEL_DIR, SV_MODEL, ROBERTA_MODEL_DIR) +from .Utils.Utils import LRUCacheDict + +onnxruntime.set_default_logger_severity(3) +logger = logging.getLogger(__name__) + + +class GSVModelFile: + T2S_ENCODER_FP32: str = 't2s_encoder_fp32.onnx' + + T2S_FIRST_STAGE_DECODER_FP32: str = 't2s_first_stage_decoder_fp32.onnx' + T2S_FIRST_STAGE_DECODER_FP16: str = 't2s_first_stage_decoder_fp16.onnx' + T2S_STAGE_DECODER_FP32: str = 't2s_stage_decoder_fp32.onnx' + T2S_STAGE_DECODER_FP16: str = 't2s_stage_decoder_fp16.onnx' + T2S_DECODER_WEIGHT_FP16: str = 't2s_shared_fp16.bin' + + VITS_FP32: str = 'vits_fp32.onnx' + VITS_WEIGHT_FP16: str = 'vits_fp16.bin' + + PROMPT_ENCODER: str = 'prompt_encoder_fp32.onnx' + PROMPT_ENCODER_WEIGHT_FP16: str = 'prompt_encoder_fp16.bin' + + HUBERT_MODEL = os.path.join(HUBERT_MODEL_DIR, "chinese-hubert-base.onnx") + HUBERT_MODEL_WEIGHT_FP16 = os.path.join(HUBERT_MODEL_DIR, "chinese-hubert-base_weights_fp16.bin") + + ROBERTA_MODEL = os.path.join(ROBERTA_MODEL_DIR, 'RoBERTa.onnx') + ROBERTA_TOKENIZER = os.path.join(ROBERTA_MODEL_DIR, 'roberta_tokenizer') + + +@dataclass +class GSVModel: + LANGUAGE: str + T2S_ENCODER: InferenceSession + T2S_FIRST_STAGE_DECODER: InferenceSession + T2S_STAGE_DECODER: InferenceSession + VITS: InferenceSession + PROMPT_ENCODER: Optional[InferenceSession] = None + PROMPT_ENCODER_PATH: Optional[str] = None + + +def load_session_with_fp16_conversion( + onnx_path: str, + fp16_bin_path: str, + providers: List[str], + sess_options: Optional[onnxruntime.SessionOptions] = None +) -> InferenceSession: + """ + 通用函数:读取 ONNX 和 FP16 权重文件,在内存中将权重转换为 FP32, + 注入到 ONNX 模型中并加载 InferenceSession,不产生临时文件。 + """ + if not os.path.exists(onnx_path): + raise FileNotFoundError(f"ONNX Model not found: {onnx_path}") + if not os.path.exists(fp16_bin_path): + raise FileNotFoundError(f"FP16 Weight file not found: {fp16_bin_path}") + + model_proto = onnx.load(onnx_path, load_external_data=False) + fp16_data = np.fromfile(fp16_bin_path, dtype=np.float16) + fp32_data = fp16_data.astype(np.float32) + fp32_bytes = fp32_data.tobytes() + + # 遍历并修补模型中的 External Data Initializers + for tensor in model_proto.graph.initializer: + # 检查该 Tensor 是否使用外部数据 + if tensor.data_location == onnx.TensorProto.EXTERNAL: + offset = 0 + length = 0 + # 解析外部数据信息 + for entry in tensor.external_data: + if entry.key == 'offset': + offset = int(entry.value) + elif entry.key == 'length': + length = int(entry.value) + + if offset + length > len(fp32_bytes): + logger.warning( + f"Tensor {tensor.name} requested a data range that exceeds the size of the provided bin file. " + f"Offset: {offset}, Length: {length}, Buffer: {len(fp32_bytes)}" + ) + continue + + tensor_data = fp32_bytes[offset: offset + length] + tensor.raw_data = tensor_data + + del tensor.external_data[:] + tensor.data_location = onnx.TensorProto.DEFAULT + + try: + session = InferenceSession( + model_proto.SerializeToString(), + providers=providers, + sess_options=sess_options + ) + return session + except Exception as e: + logger.error(f"Failed to load in-memory model {os.path.basename(onnx_path)}: {e}") + raise e + + +class ModelManager: + def __init__(self): + capacity_str = os.getenv('Max_Cached_Character_Models', '3') + self.character_to_model: Dict[str, Dict[str, Optional[InferenceSession]]] = LRUCacheDict( + capacity=int(capacity_str) + ) + self.character_to_language: Dict[str, str] = {} + self.character_model_paths: Dict[str, str] = {} + self.providers = ["CPUExecutionProvider"] + + self.cn_hubert: Optional[InferenceSession] = None + self.speaker_verification_model: Optional[InferenceSession] = None + self.roberta_model: Optional[InferenceSession] = None + self.roberta_tokenizer: Optional[Tokenizer] = None + + def load_roberta_model(self, model_path: str = GSVModelFile.ROBERTA_MODEL) -> bool: + if self.roberta_model is not None: + return True + if not os.path.exists(model_path): + # logger.warning(f'RoBERTa model does not exist: {model_path}. BERT features will not be used.') + return False + try: + self.roberta_model = onnxruntime.InferenceSession( + model_path, + providers=self.providers, + ) + self.roberta_tokenizer = Tokenizer.from_file( + os.path.join(GSVModelFile.ROBERTA_TOKENIZER, 'tokenizer.json') + ) + logger.info(f"Successfully loaded RoBERTa model.") + return True + except Exception as e: + logger.error( + f"Error: Failed to load ONNX model '{GSVModelFile.ROBERTA_MODEL}'.\n" + f"Details: {e}" + ) + return False + + def load_sv_model(self, model_path: str = SV_MODEL) -> bool: + if self.speaker_verification_model is not None: + return True + try: + self.speaker_verification_model = onnxruntime.InferenceSession( + model_path, + providers=self.providers, + ) + logger.info(f"Successfully loaded Speaker Verification model.") + return True + except Exception as e: + logger.error( + f"Error: Failed to load ONNX model '{SV_MODEL}'.\n" + f"Details: {e}" + ) + return False + + def load_cn_hubert(self, model_path: str = GSVModelFile.HUBERT_MODEL) -> bool: + if self.cn_hubert is not None: + return True + try: + # Hubert 也应用内存转换逻辑 + if model_path == GSVModelFile.HUBERT_MODEL and os.path.exists(GSVModelFile.HUBERT_MODEL_WEIGHT_FP16): + self.cn_hubert = load_session_with_fp16_conversion( + model_path, + GSVModelFile.HUBERT_MODEL_WEIGHT_FP16, + self.providers + ) + else: + self.cn_hubert = onnxruntime.InferenceSession( + model_path, + providers=self.providers, + ) + logger.info("Successfully loaded CN_HuBERT model.") + return True + except Exception as e: + logger.error( + f"Error: Failed to load ONNX model '{GSVModelFile.HUBERT_MODEL}'.\n" + f"Details: {e}" + ) + return False + + def get(self, character_name: str) -> Optional[GSVModel]: + character_name = character_name.lower() + language = self.character_to_language.get(character_name, 'Japanese') + if character_name in self.character_to_model: + model_map: dict = self.character_to_model[character_name] + # 简化获取逻辑 + t2s_first_stage_decoder = model_map.get(GSVModelFile.T2S_FIRST_STAGE_DECODER_FP32) or \ + model_map.get(GSVModelFile.T2S_FIRST_STAGE_DECODER_FP16) + t2s_stage_decoder = model_map.get(GSVModelFile.T2S_STAGE_DECODER_FP32) or \ + model_map.get(GSVModelFile.T2S_STAGE_DECODER_FP16) + prompt_encoder_path = os.path.join(self.character_model_paths[character_name], GSVModelFile.PROMPT_ENCODER) + + return GSVModel( + LANGUAGE=language, + T2S_ENCODER=model_map[GSVModelFile.T2S_ENCODER_FP32], + T2S_FIRST_STAGE_DECODER=t2s_first_stage_decoder, + T2S_STAGE_DECODER=t2s_stage_decoder, + VITS=model_map[GSVModelFile.VITS_FP32], + PROMPT_ENCODER=model_map[GSVModelFile.PROMPT_ENCODER], + PROMPT_ENCODER_PATH=prompt_encoder_path, + ) + if character_name in self.character_model_paths: + model_dir = self.character_model_paths[character_name] + if self.load_character(character_name, model_dir, language=language): + return self.get(character_name) + else: + del self.character_model_paths[character_name] + return None + return None + + def has_character(self, character_name: str) -> bool: + character_name = character_name.lower() + return character_name in self.character_model_paths + + def load_character( + self, + character_name: str, + model_dir: str, + language: str, + ) -> bool: + """ + 加载角色模型,如果需要,在内存中动态转换 FP16 权重。 + """ + character_name = character_name.lower() + if character_name in self.character_to_model: + _ = self.character_to_model[character_name] + return True + + model_dict: Dict[str, Optional[InferenceSession]] = {} + + # 定义 ONNX 文件到 FP16 Bin 文件的映射关系 + onnx_to_fp16_map = { + GSVModelFile.T2S_FIRST_STAGE_DECODER_FP32: GSVModelFile.T2S_DECODER_WEIGHT_FP16, + GSVModelFile.T2S_STAGE_DECODER_FP32: GSVModelFile.T2S_DECODER_WEIGHT_FP16, + GSVModelFile.VITS_FP32: GSVModelFile.VITS_WEIGHT_FP16, + GSVModelFile.PROMPT_ENCODER: GSVModelFile.PROMPT_ENCODER_WEIGHT_FP16 + } + + # 确定需要加载的模型列表 + model_files_to_load = [ + GSVModelFile.T2S_ENCODER_FP32, + GSVModelFile.VITS_FP32, + GSVModelFile.PROMPT_ENCODER, + ] + + fp32_decoders = [GSVModelFile.T2S_FIRST_STAGE_DECODER_FP32, GSVModelFile.T2S_STAGE_DECODER_FP32] + model_files_to_load.extend(fp32_decoders) + + try: + for model_file in model_files_to_load: + model_path = os.path.normpath(os.path.join(model_dir, model_file)) + + # 设置 Session Options + sess_options = onnxruntime.SessionOptions() + sess_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL + + if os.path.exists(model_path): + fp16_bin_name = onnx_to_fp16_map.get(model_file) + fp16_bin_path = os.path.join(model_dir, fp16_bin_name) if fp16_bin_name else None + + if fp16_bin_path and os.path.exists(fp16_bin_path): + model_dict[model_file] = load_session_with_fp16_conversion( + model_path, fp16_bin_path, self.providers, sess_options + ) + else: + model_dict[model_file] = onnxruntime.InferenceSession( + model_path, + providers=self.providers, + sess_options=sess_options, + ) + elif model_file == GSVModelFile.PROMPT_ENCODER: + model_dict[model_file] = None + else: + raise FileNotFoundError(f'文件 {model_path} 不存在!') + + # 日志信息 + is_v2pp = model_dict[GSVModelFile.PROMPT_ENCODER] is not None + logger.info( + f"Character {character_name.capitalize()} loaded successfully.\n" + f"- Model Path: {model_dir}\n" + f"- Model Type: {'V2ProPlus' if is_v2pp else 'V2'}" + ) + + self.character_to_model[character_name] = model_dict + self.character_to_language[character_name] = language + self.character_model_paths[character_name] = model_dir + return True + + except Exception as e: + logger.error( + f"Error: Failed to load ONNX model '{model_dir}'.\n" + f"Details: {e}" + ) + return False + + def remove_all_character(self) -> None: + self.character_to_model.clear() + gc.collect() + + def remove_character(self, character_name: str) -> None: + character_name = character_name.lower() + if character_name in self.character_to_model: + del self.character_to_model[character_name] + gc.collect() + logger.info(f"Character {character_name.capitalize()} removed successfully.") + + +model_manager: ModelManager = ModelManager() diff --git a/genie_tts/PredefinedCharacter.py b/genie_tts/PredefinedCharacter.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ba07dd6b4a5eb4107e8a5a61f1b6f1d4ec82c8 --- /dev/null +++ b/genie_tts/PredefinedCharacter.py @@ -0,0 +1,39 @@ +from huggingface_hub import snapshot_download +import os +from typing import Dict + +CHARA_LANG: Dict[str, str] = { + 'mika': 'Japanese', + 'feibi': 'Chinese', + 'thirtyseven': 'English', +} +CHARA_ALIAS_MAP: Dict[str, str] = { + "mika": "mika", + "misono mika": "mika", + "圣园未花": "mika", + "未花": "mika", + "みその みか": "mika", + "feibi": "feibi", + "菲比": "feibi", + "37": "thirtyseven", + "thirtyseven": "thirtyseven", +} + + +def download_chara(chara: str, version: str = "v2ProPlus") -> str: + local_dir = os.path.join("CharacterModels", version, chara) + if os.path.exists(local_dir): + print(f"✔ Model for '{chara}' already exists locally. Skipping download.") + return local_dir + + print(f"🚀 Starting download of model for character '{chara}'. This may take a few moments... ⏳") + remote_path = f"CharacterModels/{version}/{chara}/*" + snapshot_download( + repo_id="High-Logic/Genie", + repo_type="model", + allow_patterns=remote_path, + local_dir=".", + local_dir_use_symlinks=True, # 软链接 + ) + print(f"🎉 All model files for '{chara}' have been downloaded to '{os.path.abspath(local_dir)}' 📂") + return local_dir diff --git a/genie_tts/Server.py b/genie_tts/Server.py new file mode 100644 index 0000000000000000000000000000000000000000..458bccd4c2cce79f46ed85f99bebbb969821a587 --- /dev/null +++ b/genie_tts/Server.py @@ -0,0 +1,169 @@ +import asyncio +import os +from typing import AsyncIterator, Optional, Callable, Union, Dict +import logging + +import uvicorn +from fastapi import FastAPI, HTTPException +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +from .Audio.ReferenceAudio import ReferenceAudio +from .Core.TTSPlayer import tts_player +from .ModelManager import model_manager +from .Utils.Shared import context +from .Utils.Language import normalize_language + +logger = logging.getLogger(__name__) + +_reference_audios: Dict[str, dict] = {} +SUPPORTED_AUDIO_EXTS = {'.wav', '.flac', '.ogg', '.aiff', '.aif'} + +app = FastAPI() + + +class CharacterPayload(BaseModel): + character_name: str + onnx_model_dir: str + language: str + + +class UnloadCharacterPayload(BaseModel): + character_name: str + + +class ReferenceAudioPayload(BaseModel): + character_name: str + audio_path: str + audio_text: str + language: str + + +class TTSPayload(BaseModel): + character_name: str + text: str + split_sentence: bool = False + save_path: Optional[str] = None + + +@app.post("/load_character") +def load_character_endpoint(payload: CharacterPayload): + try: + model_manager.load_character( + character_name=payload.character_name, + model_dir=payload.onnx_model_dir, + language=normalize_language(payload.language), + ) + return {"status": "success", "message": f"Character '{payload.character_name}' loaded."} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/unload_character") +def unload_character_endpoint(payload: UnloadCharacterPayload): + try: + model_manager.remove_character(character_name=payload.character_name) + return {"status": "success", "message": f"Character '{payload.character_name}' unloaded."} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/set_reference_audio") +def set_reference_audio_endpoint(payload: ReferenceAudioPayload): + ext = os.path.splitext(payload.audio_path)[1].lower() + if ext not in SUPPORTED_AUDIO_EXTS: + raise HTTPException( + status_code=400, + detail=f"Audio format '{ext}' is not supported. Supported formats: {SUPPORTED_AUDIO_EXTS}", + ) + _reference_audios[payload.character_name] = { + 'audio_path': payload.audio_path, + 'audio_text': payload.audio_text, + 'language': normalize_language(payload.language), + } + return {"status": "success", "message": f"Reference audio for '{payload.character_name}' set."} + + +def run_tts_in_background( + character_name: str, + text: str, + split_sentence: bool, + save_path: Optional[str], + chunk_callback: Callable[[Optional[bytes]], None] +): + try: + context.current_speaker = character_name + context.current_prompt_audio = ReferenceAudio( + prompt_wav=_reference_audios[character_name]['audio_path'], + prompt_text=_reference_audios[character_name]['audio_text'], + language=_reference_audios[character_name]['language'], + ) + tts_player.start_session( + play=False, + split=split_sentence, + save_path=save_path, + chunk_callback=chunk_callback, + ) + tts_player.feed(text) + tts_player.end_session() + tts_player.wait_for_tts_completion() + except Exception as e: + logger.error(f"Error in TTS background task: {e}", exc_info=True) + + +async def audio_stream_generator(queue: asyncio.Queue) -> AsyncIterator[bytes]: + while True: + chunk = await queue.get() + if chunk is None: + break + yield chunk + + +@app.post("/tts") +async def tts_endpoint(payload: TTSPayload): + if payload.character_name not in _reference_audios: + raise HTTPException(status_code=404, detail="Character not found or reference audio not set.") + + loop = asyncio.get_running_loop() + stream_queue: asyncio.Queue[Union[bytes, None]] = asyncio.Queue() + + def tts_chunk_callback(chunk: Optional[bytes]): + loop.call_soon_threadsafe(stream_queue.put_nowait, chunk) + + loop.run_in_executor( + None, + run_tts_in_background, + payload.character_name, + payload.text, + payload.split_sentence, + payload.save_path, + tts_chunk_callback + ) + + return StreamingResponse(audio_stream_generator(stream_queue), media_type="audio/wav") + + +@app.post("/stop") +def stop_endpoint(): + try: + tts_player.stop() + return {"status": "success", "message": "TTS stopped."} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/clear_reference_audio_cache") +def clear_reference_audio_cache_endpoint(): + try: + ReferenceAudio.clear_cache() + return {"status": "success", "message": "Reference audio cache cleared."} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +def start_server(host: str = "127.0.0.1", port: int = 8000, workers: int = 1): + uvicorn.run(app, host=host, port=port, workers=workers) + + +if __name__ == "__main__": + start_server(host="0.0.0.0", port=8000, workers=1) diff --git a/genie_tts/Utils/Constants.py b/genie_tts/Utils/Constants.py new file mode 100644 index 0000000000000000000000000000000000000000..6b134e10b7612f06b6cc548938b27cf745575d1a --- /dev/null +++ b/genie_tts/Utils/Constants.py @@ -0,0 +1,2 @@ +BERT_FEATURE_DIM = 1024 +PACKAGE_NAME = "genie_tts" diff --git a/genie_tts/Utils/Language.py b/genie_tts/Utils/Language.py new file mode 100644 index 0000000000000000000000000000000000000000..7d6f23aa8bdedcddd125c7400d51381ee3b5f1cc --- /dev/null +++ b/genie_tts/Utils/Language.py @@ -0,0 +1,26 @@ +language_map = { + # Chinese + "chinese": "Chinese", + "zh": "Chinese", + "zh-cn": "Chinese", + "zh-tw": "Chinese", + "zh-hans": "Chinese", + "zh-hant": "Chinese", + + # English + "english": "English", + "en": "English", + "en-us": "English", + "en-gb": "English", + "eng": "English", + + # Japanese + "japanese": "Japanese", + "jp": "Japanese", + "ja": "Japanese", + "nihongo": "Japanese", +} + + +def normalize_language(lang: str) -> str: + return language_map.get(lang.lower(), lang) diff --git a/genie_tts/Utils/Shared.py b/genie_tts/Utils/Shared.py new file mode 100644 index 0000000000000000000000000000000000000000..8f3166f5599d718478e9ee13d4177b75afbc1aa2 --- /dev/null +++ b/genie_tts/Utils/Shared.py @@ -0,0 +1,13 @@ +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..Audio.ReferenceAudio import ReferenceAudio + + +class Context: + def __init__(self): + self.current_speaker: str = '' + self.current_prompt_audio: Optional['ReferenceAudio'] = None + + +context: Context = Context() diff --git a/genie_tts/Utils/TextSplitter.py b/genie_tts/Utils/TextSplitter.py new file mode 100644 index 0000000000000000000000000000000000000000..b9dca1045dbad88a5ed8a9315ac4d0c4307f76b8 --- /dev/null +++ b/genie_tts/Utils/TextSplitter.py @@ -0,0 +1,123 @@ +import re +from typing import List, Set, Pattern + + +class TextSplitter: + def __init__(self, max_len: int = 40, min_len: int = 5): + """ + 初始化文本切分器。 + + :param max_len: 软限制最大长度 (Effective Length)。超过此长度遇到分隔符时会切分。 + :param min_len: 硬限制最小长度 (Effective Length)。小于此长度遇到终止符也不会切分。 + """ + self.max_len: int = max_len + self.min_len: int = min_len + + # 1. 定义基础字符集合 + # 只要标点块中包含这些字符,就视为 Ending (终止符) + self.end_chars: Set[str] = { + '。', '!', '?', '…', + '!', '?', '.' + } + + # 2. 定义标点符号全集 (用于正则匹配和长度计算过滤) + self.all_puncts_chars: Set[str] = self.end_chars | { + ',', '、', ';', ':', '——', + ',', ';', ':', + '“', '”', '‘', '’', '"', "'", + } + + # 3. 编译正则表达式 + # 使用非捕获组 (?:) 配合 + 号,实现贪婪匹配连续标点 + # sort + escape 确保正则安全且优先匹配长标点 + sorted_puncts: List[str] = sorted(list(self.all_puncts_chars), key=len, reverse=True) + escaped_puncts: List[str] = [re.escape(p) for p in sorted_puncts] + self.pattern: Pattern = re.compile(f"((?:{'|'.join(escaped_puncts)})+)") + + @staticmethod + def get_char_width(char: str) -> int: + """计算单字符宽度:ASCII算1,其他(中日韩)算2""" + return 1 if ord(char) < 128 else 2 + + def get_effective_len(self, text: str) -> int: + """ + 计算字符串的有效长度。 + 逻辑:跳过标点符号,仅计算内容字符。 + 例如:"你好......" -> 有效长度为 4 (你好),而不是 10。 + """ + length = 0 + for char in text: + # 如果是标点符号集合里的字符,不计入长度 + if char in self.all_puncts_chars: + continue + length += self.get_char_width(char) + return length + + def is_terminator_block(self, block: str) -> bool: + """ + 判断一个标点块是否起到结束句子的作用。 + 只要块中包含任意一个结束字符(如句号),则视为结束块。 + """ + for char in block: + if char in self.end_chars: + return True + return False + + def split(self, text: str) -> List[str]: + """核心切分逻辑""" + if not text: + return [] + + text = text.replace('\n', '') + + # 正则切分,segments 格式如: ['你好', '......', '我是谁', '?!', ''] + segments: List[str] = self.pattern.split(text) + + sentences: List[str] = [] + current_buffer: str = "" + + for segment in segments: + if not segment: + continue + + # 判断当前片段是否是标点块(通过首字符判断即可,正则保证了一致性) + is_punct_block = segment[0] in self.all_puncts_chars + + if is_punct_block: + current_buffer += segment + + # 计算缓冲区内容的【有效长度】 + eff_len = self.get_effective_len(current_buffer) + + # 判断逻辑 + if self.is_terminator_block(segment): + # Case B: 结束符号 -> 检查 min_len + if eff_len >= self.min_len: + sentences.append(current_buffer.strip()) + current_buffer = "" + # else: 有效长度太短,合并到下一句 + else: + # Case A-B: 分隔符号 -> 检查 max_len + if eff_len >= self.max_len: + sentences.append(current_buffer.strip()) + current_buffer = "" + # else: 没到最大长度,继续累积 + else: + # 纯文本 + current_buffer += segment + + # 处理残留缓冲区 + if current_buffer: + self._flush_buffer(sentences, current_buffer) + + return sentences + + def _flush_buffer(self, sentences: List[str], buffer: str): + candidate = buffer.strip() + if not candidate: + return + eff_len = self.get_effective_len(candidate) + if eff_len > 0: + sentences.append(candidate) + elif sentences: + sentences[-1] += candidate diff --git a/genie_tts/Utils/UserData.py b/genie_tts/Utils/UserData.py new file mode 100644 index 0000000000000000000000000000000000000000..1d1bd517e02d35f683e92c3536fbc6027ab2b438 --- /dev/null +++ b/genie_tts/Utils/UserData.py @@ -0,0 +1,44 @@ +import json +from pathlib import Path +from typing import Dict, Any +import logging +import importlib.resources + +from .Constants import PACKAGE_NAME + +logger = logging.getLogger(__name__) + + +class UserDataManager: + def __init__(self, file_path: str = "./UserData.json"): + self.file_path = Path(file_path) + self._data: Dict[str, Any] = self._load() + + def _load(self) -> Dict[str, Any]: + if self.file_path.exists(): + try: + with self.file_path.open('r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + logger.warning(f"Failed to read user data file {self.file_path}. Using empty configuration. Error: {e}") + + return {} + return {} + + def _save(self): + try: + with self.file_path.open('w', encoding='utf-8') as f: + json.dump(self._data, f, indent=4, ensure_ascii=False) # type: ignore + except IOError as e: + logger.warning(f"Failed to write user data file {self.file_path}. Error: {e}") + + def get(self, key: str, default: Any = None) -> Any: + return self._data.get(key, default) + + def set(self, key: str, value: Any): + self._data[key] = value + self._save() + + +userdata_file: str = str(importlib.resources.files(PACKAGE_NAME) / 'UserData.json') +userdata_manager = UserDataManager(file_path=userdata_file) diff --git a/genie_tts/Utils/Utils.py b/genie_tts/Utils/Utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1453655a72e22716f4e966ef418480497f0a314c --- /dev/null +++ b/genie_tts/Utils/Utils.py @@ -0,0 +1,28 @@ +from collections import OrderedDict +import queue + + +class LRUCacheDict(OrderedDict): + def __init__(self, capacity): + super().__init__() + self.capacity = capacity + + def __getitem__(self, key): + value = super().__getitem__(key) + self.move_to_end(key) # 访问后移到末尾 + return value + + def __setitem__(self, key, value): + if key in self: + self.move_to_end(key) + super().__setitem__(key, value) + if len(self) > self.capacity: + self.popitem(last=False) # 删除最旧的(第一个) + + +def clear_queue(q: queue.Queue) -> None: + while not q.empty(): + try: + q.get_nowait() + except queue.Empty: + break diff --git a/genie_tts/Utils/__init__.py b/genie_tts/Utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/genie_tts/__init__.py b/genie_tts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d910acdb31af65801d20206bbebfb55544af3570 --- /dev/null +++ b/genie_tts/__init__.py @@ -0,0 +1,29 @@ +from .Internal import ( + load_character, + unload_character, + set_reference_audio, + tts_async, + tts, + stop, + convert_to_onnx, + clear_reference_audio_cache, + load_predefined_character, + wait_for_playback_done, +) +from .Server import start_server +from .Core.Resources import download_genie_data + +__all__ = [ + "load_character", + "unload_character", + "set_reference_audio", + "tts_async", + "tts", + "stop", + "convert_to_onnx", + "clear_reference_audio_cache", + "start_server", + "load_predefined_character", + "wait_for_playback_done", + 'download_genie_data', +]