语音合成(Text-to-Speech, TTS)技术,旨在将文本信息转化为自然流畅的人类语音,已成为人机交互、内容播报和辅助技术等领域的核心组成部分 。近年来,大型语言模型(LLM)的崛起为TTS领域带来了革命性的进步,使得合成语音在自然度、表现力和个性化方面达到了前所未有的高度 。然而,高质量的语音合成始于对输入文本的精确理解和规范化处理。
中文文本因其语言特性,如缺乏显式词边界、多音字、以及数字、日期、特殊符号等非标准词(Non-Standard Words, NSW)的复杂表述方式,对TTS系统的前端处理提出了严峻挑战 。文本归一化(Text Normalization, TN),作为TTS系统前端的关键环节,其任务是将书面文本转换为标准化的、可供后续声学模型直接处理的口语化文本形式 。
中文文本归一化是将原始文本中包含的非标准词语(NSW)转换为TTS系统能够直接处理和正确发音的标准书面语或口语化表达的过程 。这一过程对于提升合成语音的质量至关重要。
A. 中文文本归一化的核心作用与重要性
B. 中文文本归一化的主要挑战
中文文本归一化面临的挑战主要源于中文语言的特性和NSW的多样性:
C. 中文文本归一化流程与关键处理对象
典型的中文文本归一化流程通常包含预处理、非标准词识别与转换、后处理三个主要阶段 。
中文文本归一化的复杂性在于,它不仅仅是简单的字符串替换,而是需要结合语言学知识、上下文信息,有时甚至需要机器学习模型来解决歧义问题 。一个设计良好的TN系统能够显著提升TTS的最终效果。
基于LLM的TTS模型如SparkTTS和CosyVoice,凭借其强大的文本理解和生成能力,在语音合成领域取得了显著进展。然而,这些模型在处理原始中文文本时,对文本归一化的依赖程度和处理方式各有侧重。
SparkTTS是一个基于Qwen2.5 LLM的高效文本转语音模型 。它通过BiCodec将语音分解为语义标记和全局说话人属性标记,并利用Qwen2.5进行建模 。
尽管SparkTTS基于Qwen2.5,但其公开的资料(包括论文摘要和GitHub)并未详细说明其内置的中文文本归一化具体规则和覆盖范围 。虽然LLM可能隐式处理一些简单的NSW,但对于复杂的中文特有现象(如“幺”的读法、特定的日期和金额格式转换),其鲁棒性从测试结果来看并不理想,而归一化之后的文本则具有较好的处理效果。
因此对于SparkTTS,尽管其依赖的Qwen2.5 LLM具有强大的文本处理能力,但为了确保在各种复杂中文输入下的语音合成准确性和自然度,进行显式的中文文本归一化预处理仍然是推荐的做法。依赖LLM自身隐式处理所有NSW可能存在风险,特别是在对发音准确性要求极高的场景。一个外部的、规则明确的TN模块可以作为重要的保障。
CosyVoice是另一个基于LLM(如Qwen)的先进TTS系统,支持多语言和零样本语音克隆,并特别强调了流式合成能力 。 SparkTTS仅开源了低采样率的模型和其推理代码,而CosyVoice的开源较为详细,涵盖了各种模型、数据集一集训练的代码,是推荐入手的模型。
“原始文本输入”与BPE分词器:CosyVoice 2的一个特点是直接使用“原始文本”作为输入,并通过一个基于BPE(Byte Pair Encoding)的分词器进行处理 。这种方式旨在简化数据预处理流程,使模型能够以端到端的方式学习词汇在不同上下文中的发音。
集成的文本归一化选项:CosyVoice在其GitHub仓库中明确提到了文本归一化处理。它提供了两种选项:
WeTextProcessing:默认使用的文本归一化工具,如果ttsfrd
未安装 。WeTextProcessing
是一个功能较全面的中文TN工具包 。
ttsfrd:一个可选的文本归一化包,据称可能提供更好的TN性能 。其规则基于Zhiyang Zhou的工作,涵盖了多种中文NSW类型 。
frontend.py
中的处理逻辑:CosyVoice
的frontend.py
代码(如中部分展示)清晰地显示了在文本送入模型前,会根据配置选择ttsfrd
或WeTextProcessing(通过self.zh_tn_model.normalize(text)
)对中文文本进行归一化处理。
结论:对于CosyVoice,中文文本归一化是其系统设计中明确考虑并集成的一环。用户可以选择使用其默认的WeTextProcessing
或配置性能可能更优的ttsfrd。这意味着CosyVoice认识到,即使采用了先进的LLM和端到端学习策略,一个专门的中文TN模块对于处理复杂中文输入、保证合成质量仍然是必要的。
为了在Python环境中实现中文文本归一化,开发者可以利用一系列开源库。这些库功能各异,有的专注于基础的字符处理,有的提供全面的NSW规则,有的则针对特定类型的转换。
unicodedata
(Python标准库)Python
import unicodedata
text_full_width = "IPHONE手机,价格:1234元,ABC"
# 使用NFKC进行归一化,全角数字和字母会转为半角
normalized_text_nfkc = unicodedata.normalize('NFKC', text_full_width)
print(f"原始文本: {text_full_width}")
print(f"NFKC 归一化后: {normalized_text_nfkc}")
# 预期输出: IPHONE手机,价格:1234元,ABC
相关集成: 阿里云LLM文本标准化组件使用ftfy.fix_text(text, normalization=‘NFKC’),其核心也依赖unicodedata的原理 。
OpenCC-Python
(或 opencc-python-reimplemented
, OpenCC)
import unicodedata
text_full_width = "IPHONE手机,价格:1234元,ABC"
# 使用NFKC进行归一化,全角数字和字母会转为半角
normalized_text_nfkc = unicodedata.normalize('NFKC', text_full_width)
print(f"原始文本: {text_full_width}")
print(f"NFKC 归一化后: {normalized_text_nfkc}")
# 预期输出: IPHONE手机,价格:1234元,ABC
pip install WeTextProcessing
。通常需要通过conda安装其依赖pynini:conda install -c conda-forge pynini
。from tn.chinese.normalizer import Normalizer as ZhNormalizer
# 假设WeTextProcessing已正确安装,并且规则文件可访问
# 初始化中文归一化器,例如,选择去除儿化音
# 在实际使用中,cache_dir可能需要指向WeTextProcessing规则编译后的缓存路径
# overwrite_cache=True 会强制重新编译规则,首次运行时或规则更新后使用
try:
zh_tn_model = ZhNormalizer(remove_erhua=True, overwrite_cache=False)
except Exception as e:
print(f"初始化ZhNormalizer失败,请确保pynini和相关规则已正确配置: {e}")
print("尝试不使用缓存 (可能较慢,且需要规则源文件):")
# zh_tn_model = ZhNormalizer(remove_erhua=True, overwrite_cache=True, cache_dir=None) # 示例
raise e
text_to_normalize = "总量的1/5以上,价格是¥13.5,今天是2022/01/28,我儿子喜欢这地儿。电话是13800138000。"
normalized_text = zh_tn_model.normalize(text_to_normalize)
print(f"原始文本: {text_to_normalize}")
print(f"WeTextProcessing 归一化后: {normalized_text}")
# 预期输出 [8]:
# 总量的五分之一以上,价格是十三点五元,今天是二零零二年一月二十八日,我儿子喜欢这地。电话是幺三八零零幺三八零零零。
功能: 基于加权有限状态转换器(WFST)的文本归一化和反归一化工具,支持包括中文(zh)在内的多种语言 。
安装: pip install nemo_text_processing
或从源码安装。依赖pynini,推荐通过conda安装:conda install -c conda-forge pynini==2.1.5
(版本号可能需根据NeMo版本调整) 。
中文TN主要特性: 支持中文的TN和ITN 。WFST基础使其在模式匹配方面非常强大和高效。可以利用缓存的编译语法文件(.far)来加速处理 。
代码示例 (Python调用方式): NeMo的TN功能主要通过normalize.py脚本或其内部的Normalizer类来使用。以下是一个概念性的Python调用示例,具体API参数和用法请参考NeMo官方文档。
相关资料: NeMo提供了详细的WFST教程,解释了如何构建和使用语法 。中文的特定语法规则文件(.tsv或Python定义的规则)位于NeMo-text-processing/nemo_text_processing/text_normalization/zh/目录下,例如cardinal.py用于处理基数词 。
num2chinese / ChineseNumberUtils
pip install num2chinese
或 pip install ChineseNumberUtils
。from cnc import convert
# 阿拉伯数字转中文
number1 = 578.5
chinese_numeral_s = convert.number2chinese(number1, language="S") # 简体
chinese_numeral_t_big = convert.number2chinese(number1, language="T", bigNumber=True) # 繁体大写
print(f"数字: {number1}")
print(f"简体中文数字: {chinese_numeral_s}") # 预期: 五百七十八点五
print(f"繁体大写中文数字: {chinese_numeral_t_big}") # 预期: 伍佰柒拾捌點伍
# 中文转阿拉伯数字
chinese_text1 = "两千零一十二"
arabic_number1 = convert.chinese2number(chinese_text1)
chinese_text2 = "贰佰零贰" # 大写简体
arabic_number2 = convert.chinese2number(chinese_text2)
print(f"中文文本: {chinese_text1} => 阿拉伯数字: {arabic_number1}") # 预期: 2012
print(f"中文文本: {chinese_text2} => 阿拉伯数字: {arabic_number2}") # 预期: 202
ttsfrd
# 概念性代码,ttsfrd的直接API调用需查阅其具体实现或文档
# class TTSFRD_Handler:
# def __init__(self):
# # 此处进行ttsfrd相关初始化
# # self.frd = ttsfrd.Frontend() # 假设的初始化方式
# pass
# def normalize_text(self, chinese_text: str) -> str:
# # normalized_data = self.frd.do_voicegen_frd(chinese_text) # 调用核心处理
# # sentences_data = json.loads(normalized_data)["sentences"]
# # return "".join([item["text"] for item in sentences_data])
# return "示例:ttsfrd处理后的文本" # 占位符
# handler = TTSFRD_Handler()
# text = "价格12块5,日期86年8月18日"
# normalized = handler.normalize_text(text)
# print(f"ttsfrd 归一化后 (概念): {normalized}")
from whisper_normalizer.basic import BasicTextNormalizer
# BasicTextNormalizer主要进行一些通用清理,如小写化、移除特定标点等
# 对中文的NSW处理能力有限
normalizer = BasicTextNormalizer()
text = "这是 一段CHINESE文本,包含123。"
normalized_text = normalizer(text)
print(f"原始文本: {text}")
print(f"Whisper BasicTextNormalizer 后: {normalized_text}")
# 预期输出 (可能只是小写化和一些符号处理): 这是 一段chinese文本,包含123。
在实际应用中,通常需要组合使用这些库来实现一个鲁棒的中文文本归一化流程。以下是一个概念性的Python函数,展示了如何将多个工具串联起来:
import unicodedata
import opencc # 假设使用官方 opencc 包
from tn.chinese.normalizer import Normalizer as WeTextProcessingNormalizer # 假设 WeTextProcessing 已安装且路径配置正确
from cnc import convert as num_converter # 假设 ChineseNumberUtils 已安装
# 初始化转换器 (一次性)
oc_converter = None
try:
oc_converter = opencc.OpenCC('t2s.json') # 示例:繁体到简体
except Exception as e:
print(f"OpenCC 初始化失败: {e}")
wetext_normalizer = None
try:
# 实际使用时,请确保 pynini 及 WeTextProcessing 的规则文件路径正确
# cache_dir 指向编译好的.far 文件目录,若为 None 或路径无效,可能尝试从源规则编译
wetext_normalizer = WeTextProcessingNormalizer(remove_erhua=True, overwrite_cache=False)
except Exception as e:
print(f"WeTextProcessing Normalizer 初始化失败: {e}")
def comprehensive_chinese_tn_pipeline(text: str) -> str:
print(f"原始输入: {text}")
# 步骤 1: Unicode 归一化 (例如 NFKC)
try:
text = unicodedata.normalize('NFKC', text)
print(f"Unicode NFKC 归一化后: {text}")
except Exception as e:
print(f"Unicode 归一化失败: {e}")
# 根据策略决定是否继续或抛出异常
# 步骤 2: 繁简转换 (按需选择,此处示例为繁转简)
if oc_converter:
try:
text = oc_converter.convert(text)
print(f"OpenCC 繁转简后: {text}")
except Exception as e:
print(f"OpenCC 转换失败: {e}")
else:
print("OpenCC 未初始化,跳过繁简转换。")
# 步骤 3: 使用综合性工具包进行NSW归一化 (如 WeTextProcessing)
if wetext_normalizer:
try:
text = wetext_normalizer.normalize(text)
print(f"WeTextProcessing 归一化后: {text}")
except Exception as e:
print(f"WeTextProcessing 处理失败: {e}")
else:
print("WeTextProcessing Normalizer 未初始化,跳过NSW归一化。")
# 步骤 4: (可选) 针对特定需求,使用专用库进行补充处理
# 例如,如果WeTextProcessing对某种数字格式处理不符合预期,可以在此用 ChineseNumberUtils 等进行修正
# 此处仅为示例,实际中需要判断是否必要
# if "特定未处理模式" in text:
# text = custom_specific_normalization(text, num_converter)
# print(f"特定规则补充处理后: {text}")
# 步骤 5: 最终清理 (例如,去除可能由多步处理引入的多余空格)
text = " ".join(text.split())
print(f"最终清理后: {text}")
return text
# 示例调用
raw_text_example = "臺北的氣溫是25°C,價格是NT$100元,佔總數的1/2。電話是02-27376666。"
if __name__ == '__main__':
# 确保在主模块执行时进行初始化和调用,避免多进程问题
# (上面初始化部分已移至全局,实际应用中需考虑初始化时机)
if not oc_converter or not wetext_normalizer:
print("错误:一个或多个核心归一化组件未能成功初始化。请检查依赖和配置。")
else:
normalized_for_tts = comprehensive_chinese_tn_pipeline(raw_text_example)
print(f"\n最终送往TTS的文本: {normalized_for_tts}")
# 预期输出 (概念性,实际输出依赖各库的具体实现和规则):
# 台北的气温是25摄氏度,价格是新台币100元,占总数的二分之一。电话是零二二七三七六六六六。
这个流程展示了多层次处理的思想:从基础的字符层面标准化,到简繁统一,再到复杂的NSW处理,最后进行清理。实际应用中,每一步的选择和顺序可能需要根据具体需求和所用库的特性进行调整和优化。重要的是,没有一个单一的库能够完美解决所有中文TN问题,组合使用往往是必要的。
表1: 主要Python中文文本归一化库对比
库名称 | 主要功能 | 中文TN关键特性 | 安装方式 (pip) | 主要依赖 | 理想使用场景 |
---|---|---|---|---|---|
unicodedata | Unicode字符数据库访问与标准化 | 全角转半角 (NFKC),字符属性查询 | Python内置 | 无 | 文本预处理,字符层面一致性处理 |
opencc-python / OpenCC | 中文简繁体转换 | 支持简繁、地区词汇(大陆、台湾、香港)转换 | opencc 或 opencc-python-reimplemented | OpenCC核心库 (通常随包提供) | 统一输入文本的简繁体 |
WeTextProcessing | 中文TN与ITN | 数字、日期、时间、货币、度量衡、序列号归一化;儿化音处理;字符宽度转换;标点规则;白名单 | WeTextProcessing | pynini (通常需conda安装) | 作为TTS系统主要的中文TN引擎,处理各类NSW |
NVIDIA NeMo-text-processing | 多语言TN与ITN (基于WFST) | 支持中文TN/ITN;WFST规则驱动;可缓存语法 | nemo_text_processing | pynini (通常需conda安装), PyTorch(可选) | 需要高性能、基于WFST的TN方案,特别是在NVIDIA生态中 |
ChineseNumberUtils | 中文数字与阿拉伯数字互转 | 支持大数、小数、简繁体、大写数字、“二/两”区分 | ChineseNumberUtils | 无 | 专门处理数字与中文数字字符的转换,或作为大型TN系统的补充 |
ttsfrd | 中文TN (CosyVoice选用) | 基于规则的NSW处理(数字、日期、金额等) | 通过CosyVoice提供的.whl文件安装 | pynini (通过ttsfrd_dependency.whl) | 主要用于CosyVoice系统,或希望使用其特定规则集的场景 |
whisper-normalizer | OpenAI Whisper的文本归一化算法实现 | 基础文本清理,英文归一化。对中文NSW处理能力有限,不推荐直接用于复杂中文TN | whisper-normalizer | 无 | ASR评估时的文本标准化,不适合作为中文TTS的主要TN工具 |
这个表格为开发者选择合适的Python库提供了清晰的对比和参考。选择时应综合考虑项目需求、输入文本的复杂程度、对特定NSW处理的精细度要求以及系统环境的依赖。通常,一个稳健的中文TN方案会结合unicodedata进行预处理,OpenCC进行简繁转换,再选用WeTextProcessing或NeMo-text-processing作为主要的NSW处理引擎,并可能辅以ChineseNumberUtils等专用库处理特定细节。
为LLM驱动的TTS系统实现高效且准确的中文文本归一化,需要周全的策略和对可用工具的深入理解。同时,该领域仍在不断发展,展现出一些值得关注的趋势。中文文本归一化在LLM驱动的TTS中的最佳实践
中文文本归一化技术,尤其是在LLM的背景下,正朝着更智能、更融合的方向发展: