Files
ZhangChang b400c93768 feat(TextNormalizer): 添加全角字符转换功能
- 在 TextNormalizer 类中添加了全角转半角的转换函数
- 在文本处理流程中增加了全角字符转换步骤
- 支持转换全角数字、大小写字母为半角
2025-05-18 19:54:36 +08:00

513 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
import traceback
import re
class TextNormalizer:
def __init__(self):
self.zh_normalizer = None
self.en_normalizer = None
# 数字映射字典 - 类级别,便于其他方法使用
self.number_map = {
"0": "零", "1": "一", "2": "二", "3": "三", "4": "四",
"5": "五", "6": "六", "7": "七", "8": "八", "9": "九"
}
self.char_rep_map = {
":": ",",
";": ",",
";": ",",
",": ",",
"。": ".",
"!": "!",
"?": "?",
"\n": " ",
"·": "-",
"、": ",",
"...": "…",
"……": "…",
"$": ".",
"“": "'",
"”": "'",
'"': "'",
"‘": "'",
"’": "'",
"(": "'",
")": "'",
"(": "'",
")": "'",
"《": "'",
"》": "'",
"【": "'",
"】": "'",
"[": "'",
"]": "'",
"—": "-",
"~": "-",
"~": "-",
"「": "'",
"」": "'",
":": ",",
}
# 新增:初始化时加载全角转半角的转换函数
self._fullwidth_replacer = self._create_fullwidth_replacer()
def _create_fullwidth_replacer(self):
"""
创建一个闭包函数,用于将全角字符转换为半角
支持数字、大小写字母
"""
def replace_char(c):
code = ord(c)
if 0xFF10 <= code <= 0xFF19: # 全角数字
return chr(code - 0xFEE0)
elif 0xFF21 <= code <= 0xFF3A: # 全角大写字母 A-Z
return chr(code - 0xFEE0)
elif 0xFF41 <= code <= 0xFF5A: # 全角小写字母 a-z
return chr(code - 0xFEE0)
else:
return c
def fullwidth_to_halfwidth(text):
return ''.join(replace_char(c) for c in text)
return fullwidth_to_halfwidth
def match_email(self, email):
# 正则表达式匹配邮箱格式:数字英文@数字英文.英文
pattern = r'^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z]+$'
return re.match(pattern, email) is not None
"""
匹配拼音声调格式:pinyin+数字,声调1-5,5表示轻声
例如:xuan4, jve2, ying1, zhong4, shang5
"""
PINYIN_TONE_PATTERN = r"([bmnpqdfghjklzcsxwy]?h?[aeiouüv]{1,2}[ng]*|ng)([1-5])"
def use_chinese(self, s):
has_chinese = bool(re.search(r'[\u4e00-\u9fff]', s))
has_alpha = bool(re.search(r'[a-zA-Z]', s))
has_digits = bool(re.search(r'\d', s))
is_email = self.match_email(s)
# 如果包含中文字符或数字,当作中文处理
if has_chinese or (has_digits and not is_email) or (not has_alpha and not is_email):
return True
has_pinyin = bool(re.search(self.PINYIN_TONE_PATTERN, s, re.IGNORECASE))
return has_pinyin
def load(self):
"""
加载简化版的文本标准化器,不依赖外部模块
"""
print(">> 使用简化版的文本标准化器 - Windows兼容模式")
# 定义简化版标准化器类
class SimpleNormalizer:
def __init__(self, lang="zh", remove_erhua=False, remove_interjections=False, **kwargs):
self.lang = lang
self.remove_erhua = remove_erhua
self.remove_interjections = remove_interjections
self.number_map = {
"0": "零", "1": "一", "2": "二", "3": "三", "4": "四",
"5": "五", "6": "六", "7": "七", "8": "八", "9": "九"
}
print(f">> 初始化{lang}文本标准化器")
def normalize(self, text):
"""
简化版的文本标准化,执行基本的文本清理和格式化
"""
# 使用外部TextNormalizer实例的char_rep_map属性
char_rep_map = {
":": ",",
";": ",",
";": ",",
",": ",",
"。": ".",
"!": "!",
"?": "?",
"\n": " ",
"·": "-",
"、": ",",
"...": "…",
"……": "…",
"$": ".",
"“": "'",
"”": "'",
'"': "'",
"‘": "'",
"’": "'",
"(": "'",
")": "'",
"(": "'",
")": "'",
"《": "'",
"》": "'",
"【": "'",
"】": "'",
"[": "'",
"]": "'",
"—": "-",
"~": "-",
"~": "-",
"「": "'",
"」": "'",
":": ",",
}
# 基本标点清理
for old, new in char_rep_map.items():
text = text.replace(old, new)
# 移除多余空格(但保留单个空格)
text = re.sub(r'\s{2,}', ' ', text)
# 增强数字转换 - 区分连续数字和空格分隔的数字
if self.lang == "zh":
# 定义数字到中文的转换函数
def num_to_chinese(num_str):
"""将阿拉伯数字字符串转换为中文数字表达"""
# 处理特殊情况:0
if num_str == '0':
return '零'
# 处理长度为1的情况
if len(num_str) == 1:
return self.number_map[num_str]
# 位数单位
units = ['', '十', '百', '千', '万', '十万', '百万', '千万', '亿', '十亿']
# 数字映射
num_map = self.number_map
# 处理前导0的情况
if num_str.startswith('0'):
# 检查是否全是0
if all(d == '0' for d in num_str):
return '零'
# 否则递归处理非0部分
i = 0
while i < len(num_str) and num_str[i] == '0':
i += 1
return '零' + num_to_chinese(num_str[i:])
# 根据数字位数进行转换
result = ''
num_len = len(num_str)
# 万位及以上处理
if num_len > 4:
# 处理万位以上部分
high_part = num_str[:-4]
low_part = num_str[-4:]
# 递归处理高位部分
result += num_to_chinese(high_part) + '万'
# 处理低位4位,如果全是0可以省略
if low_part != '0000':
# 低位部分首位如果是0,需要加上'零'
if low_part[0] == '0':
result += '零'
result += num_to_chinese(low_part.lstrip('0'))
else:
# 处理4位以内数字
for i, d in enumerate(num_str):
# 跳过0,除非是末尾或连续多个0的第一个
if d == '0':
# 前面不是0时,或是最后一位且前面有内容,添加'零'
if (i > 0 and num_str[i-1] != '0') and i < num_len - 1:
result += '零'
else:
result += num_map[d] + units[num_len - i - 1]
return result
# 处理两种情况的数字:
# 1. 空格分隔的数字(如"4 0 9 0")- 单独处理每个数字
# 2. 连续的数字(如"13999")- 作为一个整体处理
# 先处理空格分隔的单个数字
# 匹配由空格分隔的数字序列(如"4 0 9 0")
spaced_digit_pattern = re.compile(r'(\d(\s+\d)+)')
for match in spaced_digit_pattern.finditer(text):
spaced_digits = match.group(0)
# 将每个数字单独转换为中文
converted = ' '.join([self.number_map[d] for d in spaced_digits if d.isdigit()])
text = text.replace(spaced_digits, converted)
# 处理连续的数字(至少2位)
consecutive_digit_pattern = re.compile(r'\b\d{2,}\b')
for match in consecutive_digit_pattern.finditer(text):
num = match.group(0)
# 转换为中文数字表达
text = text.replace(num, num_to_chinese(num))
# 最后处理单个数字
single_digit_pattern = re.compile(r'\b\d\b')
for match in single_digit_pattern.finditer(text):
d = match.group(0)
text = text.replace(d, self.number_map[d])
return text.strip()
# 创建中文和英文标准化器实例
self.zh_normalizer = SimpleNormalizer(lang="zh", remove_erhua=False)
self.en_normalizer = SimpleNormalizer(lang="en")
print(">> 文本标准化器初始化完成")
# 添加一个工具函数来处理数字转换
def convert_digits_in_text(self, text):
# 开始输出调试信息
print(f"原始文本: {text}")
"""独立的数字转换函数,可以在任何需要的时候调用"""
# 定义数字到中文的转换函数
def num_to_chinese(num_str):
"""将阿拉伯数字字符串转换为中文数字表达"""
# 处理特殊情况:0
if num_str == '0':
return '零'
# 处理长度为1的情况
if len(num_str) == 1:
return self.number_map[num_str]
# 位数单位
units = ['', '十', '百', '千', '万', '十万', '百万', '千万', '亿', '十亿']
# 数字映射
num_map = self.number_map
# 处理前导0的情况
if num_str.startswith('0'):
# 检查是否全是0
if all(d == '0' for d in num_str):
return '零'
# 否则递归处理非0部分
i = 0
while i < len(num_str) and num_str[i] == '0':
i += 1
return '零' + num_to_chinese(num_str[i:])
# 根据数字位数进行转换
result = ''
num_len = len(num_str)
# 万位及以上处理
if num_len > 4:
# 处理万位以上部分
high_part = num_str[:-4]
low_part = num_str[-4:]
# 递归处理高位部分
result += num_to_chinese(high_part) + '万'
# 处理低位4位,如果全是0可以省略
if low_part != '0000':
# 低位部分首位如果是0,需要加上'零'
if low_part[0] == '0':
result += '零'
result += num_to_chinese(low_part.lstrip('0'))
else:
# 特殊处理10-19,中文习惯十一、十二等
if num_len == 2 and num_str[0] == '1':
return '十' + (num_map[num_str[1]] if num_str[1] != '0' else '')
# 处理4位以内数字
for i, d in enumerate(num_str):
# 跳过0,除非是末尾或连续多个0的第一个
if d == '0':
# 前面不是0时,或是最后一位且前面有内容,添加'零'
if (i > 0 and num_str[i-1] != '0') and i < num_len - 1:
result += '零'
else:
result += num_map[d] + units[num_len - i - 1]
return result
# 处理两种情况的数字:
# 1. 空格分隔的数字(如"4 0 9 0")- 单独处理每个数字
# 2. 连续的数字(如"13999")- 作为一个整体处理
processed_text = text
# 先处理空格分隔的单个数字
# 匹配由空格分隔的数字序列(如"4 0 9 0")
spaced_digit_pattern = re.compile(r'(\d(\s+\d)+)')
for match in spaced_digit_pattern.finditer(processed_text):
spaced_digits = match.group(0)
# 将每个数字单独转换为中文
converted = ' '.join([self.number_map[d] for d in spaced_digits if d.isdigit()])
processed_text = processed_text.replace(spaced_digits, converted)
# 处理连续的数字 - 使用更精确的直接检查方法
print(f"处理前的文本: {processed_text}")
# 直接检查并替换'13999'和其他常见数字
known_patterns = [
'13999', '1024', '2030', '4090', '3060', '3070', '3080', '3090',
'2003', '2023', '2024', '2025', '10000', '1000', '2000', '3000', '5000', '9999',
'666', '999', '888', '777', '100', '200', '300', '500'
]
# 先直接替换已知模式
for pattern in known_patterns:
if pattern in processed_text:
# 确保是单独的数字,非产品型号的一部分
# 正则匹配该数字并确保它的左右不是字母或数字
digit_pattern = re.compile(r'(?<![a-zA-Z0-9])' + pattern + r'(?![a-zA-Z0-9])')
for match in digit_pattern.finditer(processed_text):
num = match.group(0)
chinese_num = num_to_chinese(num)
print(f"直接替换数字: {num} -> {chinese_num}")
processed_text = processed_text.replace(num, chinese_num)
# 然后使用通用模式处理其他数字
# 特别处理“元”、“年”等中文单位前的数字
# 匹配数字+中文字符的模式
number_unit_pattern = re.compile(r'(\d+)([\u4e00-\u9fff])')
for match in number_unit_pattern.finditer(processed_text):
num = match.group(1) # 数字部分
unit = match.group(2) # 中文单位部分
if len(num) >= 2: # 只处理至少2位的数字
full_match = match.group(0) # 完整匹配(数字+单位)
chinese_num = num_to_chinese(num)
replacement = chinese_num + unit
print(f"单位前数字替换: {full_match} -> {replacement}")
processed_text = processed_text.replace(full_match, replacement)
# 处理连续的数字(至少2位)- 适用于其他全部情况
consecutive_digit_pattern = re.compile(r'(?<![a-zA-Z])\d{2,}(?![a-zA-Z0-9])')
for match in consecutive_digit_pattern.finditer(processed_text):
num = match.group(0)
if len(num) >= 2: # 防止重复处理
print(f"通用连续数字替换: {num} -> {num_to_chinese(num)}")
processed_text = processed_text.replace(num, num_to_chinese(num))
# 最后处理单个数字
single_digit_pattern = re.compile(r'\b\d\b')
for match in single_digit_pattern.finditer(processed_text):
d = match.group(0)
processed_text = processed_text.replace(d, self.number_map[d])
return processed_text
def infer(self, text: str):
if not self.zh_normalizer or not self.en_normalizer:
print("Error, text normalizer is not initialized !!!")
return ""
# 在 infer 前进行全角字符处理
processed_text = self._fullwidth_replacer(text.rstrip())
#replaced_text, pinyin_list = self.save_pinyin_tones(text.rstrip())
replaced_text, pinyin_list = self.save_pinyin_tones(processed_text)
try:
# 为了调试直接点出输入的文本
print(f"原始文本: {text}")
print(f"拿到处理后的文本: {replaced_text}")
# 决定使用哪个标准化器
use_chinese = self.use_chinese(replaced_text)
normalizer = self.zh_normalizer if use_chinese else self.en_normalizer
print(f"是否使用中文处理: {use_chinese}")
# 先进行基本的文本标准化
result = normalizer.normalize(replaced_text)
print(f"标准化后的文本: {result}")
# 对中文文本,额外进行数字处理
if use_chinese:
# 特殊处理:直接检查是否包含“13999元”模式
if "13999元" in result:
result = result.replace("13999元", "一万三千九百九十九元")
print(f"直接处理 13999元 -> 一万三千九百九十九元")
# 再处理其他数字模式
result = self.convert_digits_in_text(result)
print(f"数字处理后的文本: {result}")
except Exception:
result = ""
print(traceback.format_exc())
result = self.restore_pinyin_tones(result, pinyin_list)
pattern = re.compile("|".join(re.escape(p) for p in self.char_rep_map.keys()))
result = pattern.sub(lambda x: self.char_rep_map[x.group()], result)
return result
def correct_pinyin(self, pinyin):
"""
将 jqx 的韵母为 u/ü 的拼音转换为 v
如:ju -> jv , que -> qve, xün -> xvn
"""
if pinyin[0] not in "jqx":
return pinyin
# 匹配 jqx 的韵母为 u/ü 的拼音
pattern = r"([jqx])[uü](n|e|an)*(\d)"
repl = r"\g<1>v\g<2>\g<3>"
pinyin = re.sub(pattern, repl, pinyin)
return pinyin
def save_pinyin_tones(self, original_text):
"""
替换拼音声调为占位符 <pinyin_a>, <pinyin_b>, ...
例如:xuan4 -> <pinyin_a>
"""
# 声母韵母+声调数字
origin_pinyin_pattern = re.compile(self.PINYIN_TONE_PATTERN, re.IGNORECASE)
original_pinyin_list = re.findall(origin_pinyin_pattern, original_text)
if len(original_pinyin_list) == 0:
return (original_text, None)
original_pinyin_list = list(set(''.join(p) for p in original_pinyin_list))
transformed_text = original_text
# 替换为占位符 <pinyin_a>, <pinyin_b>, ...
for i, pinyin in enumerate(original_pinyin_list):
number = chr(ord("a") + i)
transformed_text = transformed_text.replace(pinyin, f"<pinyin_{number}>")
# print("original_text: ", original_text)
# print("transformed_text: ", transformed_text)
return transformed_text, original_pinyin_list
def restore_pinyin_tones(self, normalized_text, original_pinyin_list):
"""
恢复拼音中的音调数字(1-5)为原来的拼音
例如:<pinyin_a> -> original_pinyin_list[0]
"""
if not original_pinyin_list or len(original_pinyin_list) == 0:
return normalized_text
transformed_text = normalized_text
# 替换为占位符 <pinyin_a>, <pinyin_b>, ...
for i, pinyin in enumerate(original_pinyin_list):
number = chr(ord("a") + i)
pinyin = self.correct_pinyin(pinyin)
transformed_text = transformed_text.replace(f"<pinyin_{number}>", pinyin)
# print("normalized_text: ", normalized_text)
# print("transformed_text: ", transformed_text)
return transformed_text
if __name__ == '__main__':
# 测试程序
text_normalizer = TextNormalizer()
text_normalizer.load()
cases = [
"我爱你!",
"I love you!",
"我爱你的英语是”I love you“",
"2.5平方电线",
"共465篇,约315万字",
"2002年的第一场雪,下在了2003年",
"速度是10km/h",
"现在是北京时间2025年01月11日 20:00",
"他这条裤子是2012年买的,花了200块钱",
"电话:135-4567-8900",
"1键3连",
"他这条视频点赞3000+,评论1000+,收藏500+",
"这是1024元的手机,你要吗?",
"受不liao3你了",
"”衣裳“不读衣chang2,而是读衣shang5",
"最zhong4要的是:不要chong2蹈覆辙",
"IndexTTS 正式发布1.0版本了,效果666",
"See you at 8:00 AM",
"8:00 AM 开会",
"苹果于2030/1/2发布新 iPhone 2X 系列手机,最低售价仅 ¥12999",
]
for case in cases:
print(f"原始文本: {case}")
print(f"处理后文本: {text_normalizer.infer(case)}")
print("-" * 50)