From e848f89c59c9b285661cffd5fb1b775861691409 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:16:49 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"refactor(chat):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=94=99=E5=88=AB=E5=AD=97=E7=94=9F=E6=88=90=E5=99=A8=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=8E=E6=96=87=E6=A1=A3"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ceee3c1fbff075e093f1dd53c7b25df38dea0f78. --- src/chat/utils/typo_generator.py | 223 +++++++++++------------- src/plugin_system/apis/generator_api.py | 2 + 2 files changed, 103 insertions(+), 122 deletions(-) diff --git a/src/chat/utils/typo_generator.py b/src/chat/utils/typo_generator.py index 1f2f9c346..c23c4c319 100644 --- a/src/chat/utils/typo_generator.py +++ b/src/chat/utils/typo_generator.py @@ -2,14 +2,12 @@ 错别字生成器 - 基于拼音和字频的中文错别字生成工具 """ -import itertools +import orjson import math import os import random import time - import jieba -import orjson from collections import defaultdict from pathlib import Path @@ -32,16 +30,11 @@ class ChineseTypoGenerator: 初始化错别字生成器。 Args: - error_rate (float): 控制单个汉字被替换为错别字的基础概率。 - 这个概率会根据词语长度进行调整,词语越长,单个字出错的概率越低。 - min_freq (int): 候选替换字的最小词频阈值。一个汉字的频率低于此值时, - 它不会被选为替换候选字,以避免生成过于生僻的错别字。 - tone_error_rate (float): 在寻找同音字时,有多大的概率会故意使用一个错误的声调来寻找候选字。 - 这可以模拟常见的声调错误,如 "shí" -> "shì"。 - word_replace_rate (float): 控制一个多字词语被整体替换为同音词的概率。 - 例如,“天气” -> “天气”。 - max_freq_diff (int): 允许的原始字与替换字之间的最大归一化频率差异。 - 用于确保替换字与原字的常用程度相似,避免用非常常见的字替换罕见字,反之亦然。 + error_rate (float): 单个汉字被替换为同音字的概率。 + min_freq (int): 候选替换字的最小词频阈值,低于此阈值的字将被忽略。 + tone_error_rate (float): 在选择同音字时,使用错误声调的概率。 + word_replace_rate (float): 整个词语被替换为同音词的概率。 + max_freq_diff (int): 允许的原始字与替换字之间的最大频率差异。 """ self.error_rate = error_rate self.min_freq = min_freq @@ -58,12 +51,11 @@ class ChineseTypoGenerator: def _load_or_create_char_frequency(self): """ 加载或创建汉字频率字典。 - 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载以提高启动速度。 - 否则,通过解析 `jieba` 的内置词典文件 `dict.txt` 来创建。 - 创建过程中,会统计每个汉字的累计频率,并进行归一化处理,然后保存为缓存文件。 + 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载。 + 否则,通过解析 `jieba` 的词典文件来创建,并保存为缓存。 Returns: - dict: 一个将汉字映射到其归一化频率(0-1000范围)的字典。 + dict: 一个将汉字映射到其归一化频率的字典。 """ cache_file = Path("depends-data/char_frequency.json") @@ -100,13 +92,10 @@ class ChineseTypoGenerator: def _create_pinyin_dict(): """ 创建从拼音到汉字的映射字典。 - 该方法会遍历 Unicode 中定义的常用汉字范围 (U+4E00 至 U+9FFF), - 为每个汉字生成带数字声调的拼音(例如 'hao3'),并构建一个从拼音到包含该拼音的所有汉字列表的映射。 - 这个字典是生成同音字和同音词的基础。 + 遍历常用汉字范围,为每个汉字生成带声调的拼音,并构建映射。 Returns: - defaultdict: 一个将拼音字符串映射到汉字字符列表的字典。 - 例如: {'hao3': ['好', '郝', ...]} + defaultdict: 一个将拼音映射到汉字列表的字典。 """ # 定义常用汉字的Unicode范围 chars = [chr(i) for i in range(0x4E00, 0x9FFF)] @@ -115,10 +104,11 @@ class ChineseTypoGenerator: # 为范围内的每个汉字建立拼音到汉字的映射 for char in chars: try: + # 获取带数字声调的拼音 (e.g., 'hao3') py = pinyin(char, style=Style.TONE3) - if py: - pinyin_dict[py].append(char) - except (IndexError, TypeError): + pinyin_dict[py].append(char) + except Exception: + # 忽略无法转换拼音的字符 continue return pinyin_dict @@ -154,28 +144,23 @@ class ChineseTypoGenerator: characters = list(sentence) result = [] for char in characters: + # 忽略所有非中文字符 if self._is_chinese_char(char): - try: - py = pinyin(char, style=Style.TONE3) - if py: - result.append((char, py)) - except (IndexError, TypeError): - continue + # 获取带数字声调的拼音 + py = pinyin(char, style=Style.TONE3) + result.append((char, py)) return result @staticmethod def _get_similar_tone_pinyin(py): """ 为一个给定的拼音生成一个声调错误的相似拼音。 - 例如,输入 'hao3',可能返回 'hao1'、'hao2' 或 'hao4'。 - 此函数用于模拟中文输入时常见的声调错误。 - 对于轻声(拼音末尾无数字声调),会随机分配一个声调。 Args: py (str): 带数字声调的原始拼音 (e.g., 'hao3')。 Returns: - str: 一个声调被随机改变的新拼音。 + str: 一个声调被随机改变的拼音。 """ # 检查拼音是否有效 if not py or len(py) < 1: @@ -201,15 +186,11 @@ class ChineseTypoGenerator: def _calculate_replacement_probability(self, orig_freq, target_freq): """ 根据原始字和目标替换字的频率差异,计算替换概率。 - 这个概率模型遵循以下原则: - 1. 如果目标字比原始字更常用,替换概率为 1.0(倾向于换成更常见的字)。 - 2. 如果频率差异超过 `max_freq_diff` 阈值,替换概率为 0.0。 - 3. 否则,使用指数衰减函数计算概率,频率差异越大,替换概率越低。 - 这使得替换更倾向于选择频率相近的字。 + 频率相近的字有更高的替换概率。 Args: - orig_freq (float): 原始字的归一化频率。 - target_freq (float): 目标替换字的归一化频率。 + orig_freq (float): 原始字的频率。 + target_freq (float): 目标替换字的频率。 Returns: float: 替换概率,介于 0.0 和 1.0 之间。 @@ -229,19 +210,14 @@ class ChineseTypoGenerator: def _get_similar_frequency_chars(self, char, py, num_candidates=5): """ 获取与给定汉字发音相似且频率相近的候选替换字。 - 此方法首先根据 `tone_error_rate` 决定是否寻找声调错误的同音字, - 然后合并声调正确的同音字。接着,根据字频进行过滤和排序: - 1. 移除原始字本身和频率低于 `min_freq` 的字。 - 2. 计算每个候选字的替换概率。 - 3. 按替换概率降序排序,并返回前 `num_candidates` 个候选字。 Args: char (str): 原始汉字。 py (str): 原始汉字的拼音。 - num_candidates (int): 返回的候选字数量上限。 + num_candidates (int): 返回的候选字数量。 Returns: - list or None: 一个包含候选替换字的列表,如果没有找到合适的候选字则返回 None。 + list or None: 一个包含候选替换字的列表,如果没有找到则返回 None。 """ homophones = [] @@ -278,6 +254,7 @@ class ChineseTypoGenerator: if not candidates_with_prob: return None + # 根据替换概率从高到低排序 candidates_with_prob.sort(key=lambda x: x, reverse=True) # 返回概率最高的几个候选字 @@ -292,9 +269,9 @@ class ChineseTypoGenerator: word (str): 输入的词语。 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 def _segment_sentence(sentence): @@ -311,17 +288,14 @@ class ChineseTypoGenerator: def _get_word_homophones(self, word): """ - 获取一个词语的所有同音词。 - 该方法首先获取词语中每个字的拼音,然后为每个字找到所有同音字。 - 接着,使用 `itertools.product` 生成所有可能的同音字组合。 - 最后,过滤掉原始词本身,并只保留在 `jieba` 词典中存在的、有意义的词语。 - 这可以有效避免生成无意义的同音词组合。 + 获取一个词语的同音词。 + 只返回在jieba词典中存在且频率较高的有意义词语。 Args: word (str): 原始词语。 Returns: - List[str]: 一个包含所有有效同音词的列表。 + list: 一个包含同音词的列表。 """ if len(word) <= 1: return [] @@ -336,28 +310,45 @@ class ChineseTypoGenerator: return [] # 如果某个字没有同音字,则无法构成同音词 candidates.append(chars) - all_combinations = itertools.product(*candidates) - homophones = [ - "".join(combo) - for combo in all_combinations - if ("".join(combo) != word and "".join(combo) in jieba.dt.FREQ) - ] + # 生成所有可能的同音字组合 + import itertools - 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): """ 为输入句子生成一个包含错别字的版本。 - 这是核心的错别字生成方法,其流程如下: - 1. 使用 jieba 对输入句子进行分词。 - 2. 遍历每个词语: - a. 如果词语长度大于1,根据 `word_replace_rate` 概率尝试进行整词替换。 - 如果找到了合适的同音词,则替换并跳过后续步骤。 - b. 如果不进行整词替换,则遍历词语中的每个汉字。 - c. 对每个汉字,调用 `_char_replace` 方法,根据 `error_rate` 和词语长度调整后的概率, - 决定是否进行单字替换。 - 3. 将处理后的词语拼接成最终的错别字句子。 - 4. 从所有发生的替换中,随机选择一个作为修正建议返回。 + 该方法会先对句子进行分词,然后根据概率进行整词替换或单字替换。 Args: sentence (str): 原始中文句子。 @@ -366,7 +357,7 @@ class ChineseTypoGenerator: tuple: 包含三个元素的元组: - original_sentence (str): 原始句子。 - typo_sentence (str): 包含错别字的句子。 - - correction_suggestion (Optional[tuple(str, str)]): 一个随机的修正建议,格式为 (错字/词, 正确字/词),或 None。 + - correction_suggestion (str or None): 一个随机的修正建议(可能是正确的字或词),或 None。 """ result = [] typo_info = [] # 用于调试,记录详细的替换信息 @@ -406,56 +397,44 @@ class ChineseTypoGenerator: word_typos.append((typo_word, word)) continue - new_word = "".join( - self._char_replace(char, py, len(word), typo_info, char_typos) for char, py in zip(word, word_pinyin) - ) - result.append(new_word) + # 步骤2: 如果不进行整词替换,则对词中的每个字进行单字替换 + new_word = [] + for char, py in zip(word, word_pinyin, strict=False): + # 词语越长,其中单个字被替换的概率越低 + char_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) + if random.random() < char_error_rate: + similar_chars = self._get_similar_frequency_chars(char, py) + if similar_chars: + typo_char = random.choice(similar_chars) + orig_freq = self.char_frequency.get(char, 0) + typo_freq = self.char_frequency.get(typo_char, 0) + # 根据频率计算最终是否替换 + if random.random() < self._calculate_replacement_probability(orig_freq, typo_freq): + new_word.append(typo_char) + typo_py = pinyin(typo_char, style=Style.TONE3) + typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) + char_typos.append((typo_char, 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 - all_typos = word_typos + char_typos - 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) - if not similar_chars: - return char - - # 从候选列表中随机选择一个 - typo_char = random.choice(similar_chars) - orig_freq = self.char_frequency.get(char, 0) - typo_freq = self.char_frequency.get(typo_char, 0) - - # 根据频率差异再次进行概率判断,决定是否执行替换 - if random.random() >= self._calculate_replacement_probability(orig_freq, typo_freq): - return char - - # 执行替换,并记录相关信息 - typo_py = pinyin(typo_char, style=Style.TONE3) - typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) - char_typos.append((typo_char, char)) - return typo_char - @staticmethod def format_typo_info(typo_info): """ diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 7f3074a81..8d8d04eb3 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -239,6 +239,8 @@ def process_human_text( enable_splitter: 是否启用消息分割器 enable_chinese_typo: 是否启用错字生成器 """ + if isinstance(content, list): + content = "".join(map(str, content)) if not isinstance(content, str): raise ValueError("content 必须是字符串类型") try: