Revert "refactor(chat): 优化错别字生成器逻辑与文档"

This reverts commit ceee3c1fbf.
This commit is contained in:
minecraft1024a
2025-09-06 20:16:49 +08:00
parent ceee3c1fbf
commit e848f89c59
2 changed files with 103 additions and 122 deletions

View File

@@ -2,14 +2,12 @@
错别字生成器 - 基于拼音和字频的中文错别字生成工具 错别字生成器 - 基于拼音和字频的中文错别字生成工具
""" """
import itertools import orjson
import math import math
import os import os
import random import random
import time import time
import jieba import jieba
import orjson
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@@ -32,16 +30,11 @@ class ChineseTypoGenerator:
初始化错别字生成器。 初始化错别字生成器。
Args: Args:
error_rate (float): 控制单个汉字被替换为错别字的基础概率。 error_rate (float): 单个汉字被替换为同音字的概率。
这个概率会根据词语长度进行调整,词语越长,单个字出错的概率越低 min_freq (int): 候选替换字的最小词频阈值,低于此阈值的字将被忽略
min_freq (int): 候选替换字的最小词频阈值。一个汉字的频率低于此值时, tone_error_rate (float): 在选择同音字时,使用错误声调的概率。
它不会被选为替换候选字,以避免生成过于生僻的错别字 word_replace_rate (float): 整个词语被替换为同音词的概率
tone_error_rate (float): 在寻找同音字时,有多大的概率会故意使用一个错误的声调来寻找候选字 max_freq_diff (int): 允许的原始字与替换字之间的最大频率差异
这可以模拟常见的声调错误,如 "shí" -> "shì"
word_replace_rate (float): 控制一个多字词语被整体替换为同音词的概率。
例如,“天气” -> “天气”。
max_freq_diff (int): 允许的原始字与替换字之间的最大归一化频率差异。
用于确保替换字与原字的常用程度相似,避免用非常常见的字替换罕见字,反之亦然。
""" """
self.error_rate = error_rate self.error_rate = error_rate
self.min_freq = min_freq self.min_freq = min_freq
@@ -58,12 +51,11 @@ class ChineseTypoGenerator:
def _load_or_create_char_frequency(self): def _load_or_create_char_frequency(self):
""" """
加载或创建汉字频率字典。 加载或创建汉字频率字典。
如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载以提高启动速度 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载。
否则,通过解析 `jieba` 的内置词典文件 `dict.txt` 来创建。 否则,通过解析 `jieba` 的词典文件来创建,并保存为缓存
创建过程中,会统计每个汉字的累计频率,并进行归一化处理,然后保存为缓存文件。
Returns: Returns:
dict: 一个将汉字映射到其归一化频率0-1000范围的字典。 dict: 一个将汉字映射到其归一化频率的字典。
""" """
cache_file = Path("depends-data/char_frequency.json") cache_file = Path("depends-data/char_frequency.json")
@@ -100,13 +92,10 @@ class ChineseTypoGenerator:
def _create_pinyin_dict(): def _create_pinyin_dict():
""" """
创建从拼音到汉字的映射字典。 创建从拼音到汉字的映射字典。
该方法会遍历 Unicode 中定义的常用汉字范围 (U+4E00 至 U+9FFF) 遍历常用汉字范围,为每个汉字生成带声调的拼音,并构建映射。
为每个汉字生成带数字声调的拼音(例如 'hao3'),并构建一个从拼音到包含该拼音的所有汉字列表的映射。
这个字典是生成同音字和同音词的基础。
Returns: Returns:
defaultdict: 一个将拼音字符串映射到汉字字符列表的字典。 defaultdict: 一个将拼音映射到汉字列表的字典。
例如: {'hao3': ['', '', ...]}
""" """
# 定义常用汉字的Unicode范围 # 定义常用汉字的Unicode范围
chars = [chr(i) for i in range(0x4E00, 0x9FFF)] chars = [chr(i) for i in range(0x4E00, 0x9FFF)]
@@ -115,10 +104,11 @@ class ChineseTypoGenerator:
# 为范围内的每个汉字建立拼音到汉字的映射 # 为范围内的每个汉字建立拼音到汉字的映射
for char in chars: for char in chars:
try: try:
# 获取带数字声调的拼音 (e.g., 'hao3')
py = pinyin(char, style=Style.TONE3) py = pinyin(char, style=Style.TONE3)
if py:
pinyin_dict[py].append(char) pinyin_dict[py].append(char)
except (IndexError, TypeError): except Exception:
# 忽略无法转换拼音的字符
continue continue
return pinyin_dict return pinyin_dict
@@ -154,28 +144,23 @@ class ChineseTypoGenerator:
characters = list(sentence) characters = list(sentence)
result = [] result = []
for char in characters: for char in characters:
# 忽略所有非中文字符
if self._is_chinese_char(char): if self._is_chinese_char(char):
try: # 获取带数字声调的拼音
py = pinyin(char, style=Style.TONE3) py = pinyin(char, style=Style.TONE3)
if py:
result.append((char, py)) result.append((char, py))
except (IndexError, TypeError):
continue
return result return result
@staticmethod @staticmethod
def _get_similar_tone_pinyin(py): def _get_similar_tone_pinyin(py):
""" """
为一个给定的拼音生成一个声调错误的相似拼音。 为一个给定的拼音生成一个声调错误的相似拼音。
例如,输入 'hao3',可能返回 'hao1''hao2''hao4'
此函数用于模拟中文输入时常见的声调错误。
对于轻声(拼音末尾无数字声调),会随机分配一个声调。
Args: Args:
py (str): 带数字声调的原始拼音 (e.g., 'hao3')。 py (str): 带数字声调的原始拼音 (e.g., 'hao3')。
Returns: Returns:
str: 一个声调被随机改变的拼音。 str: 一个声调被随机改变的拼音。
""" """
# 检查拼音是否有效 # 检查拼音是否有效
if not py or len(py) < 1: if not py or len(py) < 1:
@@ -201,15 +186,11 @@ class ChineseTypoGenerator:
def _calculate_replacement_probability(self, orig_freq, target_freq): def _calculate_replacement_probability(self, orig_freq, target_freq):
""" """
根据原始字和目标替换字的频率差异,计算替换概率。 根据原始字和目标替换字的频率差异,计算替换概率。
这个概率模型遵循以下原则: 频率相近的字有更高的替换概率。
1. 如果目标字比原始字更常用,替换概率为 1.0(倾向于换成更常见的字)。
2. 如果频率差异超过 `max_freq_diff` 阈值,替换概率为 0.0。
3. 否则,使用指数衰减函数计算概率,频率差异越大,替换概率越低。
这使得替换更倾向于选择频率相近的字。
Args: Args:
orig_freq (float): 原始字的归一化频率。 orig_freq (float): 原始字的频率。
target_freq (float): 目标替换字的归一化频率。 target_freq (float): 目标替换字的频率。
Returns: Returns:
float: 替换概率,介于 0.0 和 1.0 之间。 float: 替换概率,介于 0.0 和 1.0 之间。
@@ -229,19 +210,14 @@ class ChineseTypoGenerator:
def _get_similar_frequency_chars(self, char, py, num_candidates=5): def _get_similar_frequency_chars(self, char, py, num_candidates=5):
""" """
获取与给定汉字发音相似且频率相近的候选替换字。 获取与给定汉字发音相似且频率相近的候选替换字。
此方法首先根据 `tone_error_rate` 决定是否寻找声调错误的同音字,
然后合并声调正确的同音字。接着,根据字频进行过滤和排序:
1. 移除原始字本身和频率低于 `min_freq` 的字。
2. 计算每个候选字的替换概率。
3. 按替换概率降序排序,并返回前 `num_candidates` 个候选字。
Args: Args:
char (str): 原始汉字。 char (str): 原始汉字。
py (str): 原始汉字的拼音。 py (str): 原始汉字的拼音。
num_candidates (int): 返回的候选字数量上限 num_candidates (int): 返回的候选字数量。
Returns: Returns:
list or None: 一个包含候选替换字的列表,如果没有找到合适的候选字则返回 None。 list or None: 一个包含候选替换字的列表,如果没有找到则返回 None。
""" """
homophones = [] homophones = []
@@ -278,6 +254,7 @@ class ChineseTypoGenerator:
if not candidates_with_prob: if not candidates_with_prob:
return None return None
# 根据替换概率从高到低排序
candidates_with_prob.sort(key=lambda x: x, reverse=True) candidates_with_prob.sort(key=lambda x: x, reverse=True)
# 返回概率最高的几个候选字 # 返回概率最高的几个候选字
@@ -292,9 +269,9 @@ class ChineseTypoGenerator:
word (str): 输入的词语。 word (str): 输入的词语。
Returns: Returns:
List[str]: 包含每个汉字拼音的列表。 list: 包含每个汉字拼音的列表。
""" """
return ["".join(p) for p in pinyin(word, style=Style.TONE3)] return [py for py in pinyin(word, style=Style.TONE3)]
@staticmethod @staticmethod
def _segment_sentence(sentence): def _segment_sentence(sentence):
@@ -311,17 +288,14 @@ class ChineseTypoGenerator:
def _get_word_homophones(self, word): def _get_word_homophones(self, word):
""" """
获取一个词语的所有同音词。 获取一个词语的同音词。
该方法首先获取词语中每个字的拼音,然后为每个字找到所有同音字 只返回在jieba词典中存在且频率较高的有意义词语
接着,使用 `itertools.product` 生成所有可能的同音字组合。
最后,过滤掉原始词本身,并只保留在 `jieba` 词典中存在的、有意义的词语。
这可以有效避免生成无意义的同音词组合。
Args: Args:
word (str): 原始词语。 word (str): 原始词语。
Returns: Returns:
List[str]: 一个包含所有有效同音词的列表。 list: 一个包含同音词的列表。
""" """
if len(word) <= 1: if len(word) <= 1:
return [] return []
@@ -336,28 +310,45 @@ class ChineseTypoGenerator:
return [] # 如果某个字没有同音字,则无法构成同音词 return [] # 如果某个字没有同音字,则无法构成同音词
candidates.append(chars) candidates.append(chars)
all_combinations = itertools.product(*candidates) # 生成所有可能的同音字组合
homophones = [ import itertools
"".join(combo)
for combo in all_combinations
if ("".join(combo) != word and "".join(combo) in jieba.dt.FREQ)
]
return homophones all_combinations = itertools.product(*candidates)
# 加载jieba词典以验证组合出的词是否为有效词语
dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt")
valid_words = {}
with open(dict_path, "r", encoding="utf-8") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2:
valid_words[parts] = float(parts[0][1])
original_word_freq = valid_words.get(word, 0)
# 设置一个最小词频阈值,过滤掉非常生僻的词
min_word_freq = original_word_freq * 0.1
homophones = []
for combo in all_combinations:
new_word = "".join(combo)
# 检查新词是否为有效词语且与原词不同
if new_word != word and new_word in valid_words:
new_word_freq = valid_words[new_word]
if new_word_freq >= min_word_freq:
# 计算综合评分,结合词频和平均字频
char_avg_freq = sum(self.char_frequency.get(c, 0) for c in new_word) / len(new_word)
combined_score = new_word_freq * 0.7 + char_avg_freq * 0.3
if combined_score >= self.min_freq:
homophones.append((new_word, combined_score))
# 按综合分数排序并返回前5个结果
sorted_homophones = sorted(homophones, key=lambda x: x, reverse=True)
return [w for w, _ in sorted_homophones[:5]]
def create_typo_sentence(self, sentence): def create_typo_sentence(self, sentence):
""" """
为输入句子生成一个包含错别字的版本。 为输入句子生成一个包含错别字的版本。
这是核心的错别字生成方法,其流程如下: 该方法会先对句子进行分词,然后根据概率进行整词替换或单字替换。
1. 使用 jieba 对输入句子进行分词。
2. 遍历每个词语:
a. 如果词语长度大于1根据 `word_replace_rate` 概率尝试进行整词替换。
如果找到了合适的同音词,则替换并跳过后续步骤。
b. 如果不进行整词替换,则遍历词语中的每个汉字。
c. 对每个汉字,调用 `_char_replace` 方法,根据 `error_rate` 和词语长度调整后的概率,
决定是否进行单字替换。
3. 将处理后的词语拼接成最终的错别字句子。
4. 从所有发生的替换中,随机选择一个作为修正建议返回。
Args: Args:
sentence (str): 原始中文句子。 sentence (str): 原始中文句子。
@@ -366,7 +357,7 @@ class ChineseTypoGenerator:
tuple: 包含三个元素的元组: tuple: 包含三个元素的元组:
- original_sentence (str): 原始句子。 - original_sentence (str): 原始句子。
- typo_sentence (str): 包含错别字的句子。 - typo_sentence (str): 包含错别字的句子。
- correction_suggestion (Optional[tuple(str, str)]): 一个随机的修正建议,格式为 (错字/词, 正确字/词),或 None。 - correction_suggestion (str or None): 一个随机的修正建议(可能是正确的字或词),或 None。
""" """
result = [] result = []
typo_info = [] # 用于调试,记录详细的替换信息 typo_info = [] # 用于调试,记录详细的替换信息
@@ -406,55 +397,43 @@ class ChineseTypoGenerator:
word_typos.append((typo_word, word)) word_typos.append((typo_word, word))
continue continue
new_word = "".join( # 步骤2: 如果不进行整词替换,则对词中的每个字进行单字替换
self._char_replace(char, py, len(word), typo_info, char_typos) for char, py in zip(word, word_pinyin) new_word = []
) for char, py in zip(word, word_pinyin, strict=False):
result.append(new_word) # 词语越长,其中单个字被替换的概率越低
char_error_rate = self.error_rate * (0.7 ** (len(word) - 1))
all_typos = word_typos + char_typos if random.random() < char_error_rate:
correction_suggestion = random.choice(all_typos) if all_typos and random.random() < 0.5 else None
return sentence, "".join(result), correction_suggestion
def _char_replace(self, char, py, word_len, typo_info, char_typos):
"""
根据概率替换单个汉字。
这个内部方法处理单个汉字的替换逻辑。
Args:
char (str): 要处理的原始汉字。
py (str): 原始汉字的拼音。
word_len (int): 原始汉字所在的词语的长度。
typo_info (list): 用于记录详细调试信息的列表。
char_typos (list): 用于记录 (错字, 正确字) 的列表。
Returns:
str: 替换后的汉字(可能是原汉字,也可能是错别字)。
"""
# 根据词语长度调整错误率:词语越长,单个字出错的概率越低。
# 这是一个启发式规则,模拟人们在输入长词时更不容易打错单个字。
char_error_rate = self.error_rate * (0.7 ** (word_len - 1))
if random.random() >= char_error_rate:
return char
# 获取发音和频率都相似的候选错别字
similar_chars = self._get_similar_frequency_chars(char, py) similar_chars = self._get_similar_frequency_chars(char, py)
if not similar_chars: if similar_chars:
return char
# 从候选列表中随机选择一个
typo_char = random.choice(similar_chars) typo_char = random.choice(similar_chars)
orig_freq = self.char_frequency.get(char, 0) orig_freq = self.char_frequency.get(char, 0)
typo_freq = self.char_frequency.get(typo_char, 0) typo_freq = self.char_frequency.get(typo_char, 0)
# 根据频率计算最终是否替换
# 根据频率差异再次进行概率判断,决定是否执行替换 if random.random() < self._calculate_replacement_probability(orig_freq, typo_freq):
if random.random() >= self._calculate_replacement_probability(orig_freq, typo_freq): new_word.append(typo_char)
return char
# 执行替换,并记录相关信息
typo_py = pinyin(typo_char, style=Style.TONE3) typo_py = pinyin(typo_char, style=Style.TONE3)
typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq))
char_typos.append((typo_char, char)) char_typos.append((typo_char, char))
return typo_char continue
# 如果不替换,则保留原字
new_word.append(char)
result.append("".join(new_word))
# 步骤3: 生成修正建议
correction_suggestion = None
# 有50%的概率提供一个修正建议
if random.random() < 0.5:
# 优先从整词错误中选择
if word_typos:
_, correct_word = random.choice(word_typos)
correction_suggestion = correct_word
# 其次从单字错误中选择
elif char_typos:
_, correct_char = random.choice(char_typos)
correction_suggestion = correct_char
return sentence, "".join(result), correction_suggestion
@staticmethod @staticmethod
def format_typo_info(typo_info): def format_typo_info(typo_info):

View File

@@ -239,6 +239,8 @@ def process_human_text(
enable_splitter: 是否启用消息分割器 enable_splitter: 是否启用消息分割器
enable_chinese_typo: 是否启用错字生成器 enable_chinese_typo: 是否启用错字生成器
""" """
if isinstance(content, list):
content = "".join(map(str, content))
if not isinstance(content, str): if not isinstance(content, str):
raise ValueError("content 必须是字符串类型") raise ValueError("content 必须是字符串类型")
try: try: