text-rewrite 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: text-rewrite
3
+ Version: 0.1.0
4
+ Summary: A high-performance text rewriting and fuzzy phonetic matching engine
5
+ Author-email: Magic Jack <majic31@163.com>
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: flashtext>=2.7
9
+ Requires-Dist: numba>=0.56.0
10
+ Requires-Dist: numpy>=1.21.0
11
+ Requires-Dist: pypinyin>=0.49.0
12
+ Requires-Dist: jieba>=0.42.1
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest>=7.0.0; extra == "test"
15
+
16
+ # text-rewrite 🚀
17
+
18
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
+
21
+ **text-rewrite** 是一个专为工业级 ASR(语音识别)后处理设计的**高性能文本纠错与过滤引擎**。它将极致的计算效率与高精度的语义约束相结合,完美解决了传统 ASR 热词匹配中常见的“跨词误杀”、“谐音错认”以及“高并发性能瓶颈”问题。
22
+
23
+ ## ✨ 核心特性
24
+
25
+ - **极致性能 ($O(N)$ 线性扩展)**:针对万级甚至十万级热词进行专项优化。处理近 2000 字的长文本只需不足 600 毫秒(常规短句 <10ms),绝不会拖垮流式并发服务器。
26
+ - **Entity-Aware 实体约束引擎**:针对容易误杀的极短词(如人名“叶开” vs “也开心”),创新性引入基于 Jieba 词性标注的轻量级 NER 引擎。**内建极速音素预检机制与无 HMM 模式**,将千字长文本的 NER 解析时间从 400ms 暴砍至 5ms。
27
+ - **双音节倒排索引 (Bigram Syllable Index)**:创新性地构建了基于“首位双音节”自适应哈希倒排池。将 10000 个热词在 2000 字长文本上的 DP 候选空间极致压缩了 98.8%,打破长文本下 $K=100\%$ 的魔咒。
28
+ - **Numba Batch DP 加速**:底层基于 Numba JIT 编译的“批量化动态规划(DP)”核心引擎,彻底消除跨语言调度开销,支持纯发音级别的纠错(完美包容平翘舌、前后鼻音、形近音误差)。
29
+ - **微秒级 FlashText 精确匹配**:对于全局安全大词表,底层自动退化为 Aho-Corasick 自动机,做到微秒级无感替换。
30
+ - **Pipeline 乐高式组装**:提供高度可扩展的链式过滤器架构,正则清洗、精准替换、模糊纠错一气呵成。
31
+
32
+ ## 📦 安装依赖
33
+
34
+ 该项目核心依赖于高效计算与轻量级 NLP 组件:
35
+
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+ > **要求**: `numba>=0.56.0`, `numpy>=1.21.0`, `pypinyin>=0.49.0`, `jieba>=0.42.1`
40
+
41
+ ## 🧩 核心过滤器 (Filters) 概览与最佳实践
42
+
43
+ 在真实的工业级落地场景中,最常用的“黄金三剑客”是以下三个过滤器的组合:
44
+
45
+ 1. **`RegexFilter` (打头阵:规则清洗)**
46
+ - **作用**:干脏活累活。负责前置格式规整。
47
+ - **场景**:将全角符号转半角、去除多余空格、清理语气词(“呃”、“啊”、“那个”),以及执行如 `四S -> 4S` 这种高度规律性的文本格式化。
48
+
49
+ 2. **`HotwordFilter` (中坚力量:业务绝对权威)**
50
+ - **作用**:基于 FlashText 的极速精确替换,零误杀,快、准、狠。
51
+ - **场景**:用于承载几万到几十万量级的“黑白名单 / 品牌库 / 敏感词库”(如“蔚来”、“极氪”)。用它拦截掉绝大部分必须 100% 准确的词,避免增加下游模糊匹配的开销和误杀率。
52
+
53
+ 3. **`EntityAwareFuzzyFilter` (最后兜底:长尾智能纠错)**
54
+ - **作用**:收拾残局。
55
+ - **场景**:经过前面精确词表的拦截,剩下的“长尾错别字”(如用户口音导致的“魏来”、“及克”)由它出马,通过发音的编辑距离计算把错别字捞回来。
56
+ - **语法**:`[标签]原词|替换词|阈值`
57
+ - 全局模糊拦截:`欧阳锋|欧阳锋|0.6` (长词,放心全局模糊替换)
58
+ - 短实体约束:`[nr]叶开|叶开|0.8` (`nr`为人名,只有 Jieba 认为是人名时才启动音素级检索)
59
+
60
+ > **注**:除上述三者外,内部模块如 `FuzzyPhonemeFilter`(底层的 Numba DP 发音匹配引擎)和 `JiebaNERFilter`(底层 NER 引擎)主要作为组件被 `EntityAwareFuzzyFilter` 自动编排调用,在常规业务中通常无需直接操作。
61
+
62
+ ## 🚀 快速开始 (Quick Start)
63
+
64
+ 下面是一个将“正则清洗 -> 精确大词表拦截 -> 高危实体模糊纠错”串联起来的完整 Demo:
65
+
66
+ ```python
67
+ from text_rewrite.pipeline import Pipeline
68
+ from text_rewrite.filters.regex import RegexFilter
69
+ from text_rewrite.filters.hotword import HotwordFilter
70
+ from text_rewrite.filters.entity_fuzzy import EntityAwareFuzzyFilter
71
+
72
+ # 1. 配置正则过滤器(清洗语气词)
73
+ regex_rules = {
74
+ r"\b(嗯|啊|哦|那个)\b": ""
75
+ }
76
+ regex_filter = RegexFilter(rules=regex_rules)
77
+
78
+ # 2. 配置精确热词过滤器(处理 10万级 安全大词表)
79
+ exact_filter = HotwordFilter(hotwords={"确定性长词": "替换词"})
80
+
81
+ # 3. 配置实体感知模糊过滤器 (EntityAwareFuzzyFilter)
82
+ # 语法: [标签]目标词|阈值 (省略替换词会自动以目标词作为原词和替换词)
83
+ fuzzy_rules = [
84
+ "张三|0.7", # 无标签:全局极简配置(发音类似于张三的词,如展伞,都会被纠正为张三)
85
+ "[nr]叶开|0.8", # 有标签:严格约束(必须是人名,且发音相似度>=0.8 才纠正)
86
+ "李四|0.7" # 全局极简配置(里死 -> 李四)
87
+ ]
88
+ entity_filter = EntityAwareFuzzyFilter(rules=fuzzy_rules)
89
+
90
+ # 4. 组装 Pipeline 引擎(顺序即执行顺序)
91
+ pipeline = Pipeline()
92
+ pipeline.add_filter(regex_filter)
93
+ pipeline.add_filter(exact_filter)
94
+ pipeline.add_filter(entity_filter)
95
+
96
+ # 5. 真实流式调用
97
+ text = "那个,昨天通知了一下展伞和里死,但是他也开心了,最后通知了也开。"
98
+ result = pipeline.process(text)
99
+
100
+ print(result)
101
+ # 输出: ",昨天通知了一下张三和李四,但是他也开心了,最后通知了也开。"
102
+ ```
103
+
104
+ > **注意**:由于初始化 Numba 引擎以及 JIT 预热需要少量时间,建议在服务启动时**单例初始化** `Pipeline` 实例,在后续流式请求中复用该实例调用 `.process(text)`。
105
+
106
+ ## 📊 性能压测 (Benchmark)
107
+
108
+ 您可以运行自带的压测脚本,体验在注入 **10000** 条工业级比例热词配置下的强悍性能:
109
+ ```bash
110
+ python tests/bench_entity_fuzzy.py
111
+ ```
112
+ * **短句 (约 30 字)**: 端到端平均耗时 **~ 1 ms**
113
+ * **长文 (约 2000 字)**: 端到端平均耗时 **~ 150 ms** (呈现完美的 $O(N)$ 线性扩展,不受十万级词表拖累)
114
+
115
+ ---
116
+ *Built with ❤️ for High-Performance NLP Engineering.*
@@ -0,0 +1,101 @@
1
+ # text-rewrite 🚀
2
+
3
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ **text-rewrite** 是一个专为工业级 ASR(语音识别)后处理设计的**高性能文本纠错与过滤引擎**。它将极致的计算效率与高精度的语义约束相结合,完美解决了传统 ASR 热词匹配中常见的“跨词误杀”、“谐音错认”以及“高并发性能瓶颈”问题。
7
+
8
+ ## ✨ 核心特性
9
+
10
+ - **极致性能 ($O(N)$ 线性扩展)**:针对万级甚至十万级热词进行专项优化。处理近 2000 字的长文本只需不足 600 毫秒(常规短句 <10ms),绝不会拖垮流式并发服务器。
11
+ - **Entity-Aware 实体约束引擎**:针对容易误杀的极短词(如人名“叶开” vs “也开心”),创新性引入基于 Jieba 词性标注的轻量级 NER 引擎。**内建极速音素预检机制与无 HMM 模式**,将千字长文本的 NER 解析时间从 400ms 暴砍至 5ms。
12
+ - **双音节倒排索引 (Bigram Syllable Index)**:创新性地构建了基于“首位双音节”自适应哈希倒排池。将 10000 个热词在 2000 字长文本上的 DP 候选空间极致压缩了 98.8%,打破长文本下 $K=100\%$ 的魔咒。
13
+ - **Numba Batch DP 加速**:底层基于 Numba JIT 编译的“批量化动态规划(DP)”核心引擎,彻底消除跨语言调度开销,支持纯发音级别的纠错(完美包容平翘舌、前后鼻音、形近音误差)。
14
+ - **微秒级 FlashText 精确匹配**:对于全局安全大词表,底层自动退化为 Aho-Corasick 自动机,做到微秒级无感替换。
15
+ - **Pipeline 乐高式组装**:提供高度可扩展的链式过滤器架构,正则清洗、精准替换、模糊纠错一气呵成。
16
+
17
+ ## 📦 安装依赖
18
+
19
+ 该项目核心依赖于高效计算与轻量级 NLP 组件:
20
+
21
+ ```bash
22
+ pip install -r requirements.txt
23
+ ```
24
+ > **要求**: `numba>=0.56.0`, `numpy>=1.21.0`, `pypinyin>=0.49.0`, `jieba>=0.42.1`
25
+
26
+ ## 🧩 核心过滤器 (Filters) 概览与最佳实践
27
+
28
+ 在真实的工业级落地场景中,最常用的“黄金三剑客”是以下三个过滤器的组合:
29
+
30
+ 1. **`RegexFilter` (打头阵:规则清洗)**
31
+ - **作用**:干脏活累活。负责前置格式规整。
32
+ - **场景**:将全角符号转半角、去除多余空格、清理语气词(“呃”、“啊”、“那个”),以及执行如 `四S -> 4S` 这种高度规律性的文本格式化。
33
+
34
+ 2. **`HotwordFilter` (中坚力量:业务绝对权威)**
35
+ - **作用**:基于 FlashText 的极速精确替换,零误杀,快、准、狠。
36
+ - **场景**:用于承载几万到几十万量级的“黑白名单 / 品牌库 / 敏感词库”(如“蔚来”、“极氪”)。用它拦截掉绝大部分必须 100% 准确的词,避免增加下游模糊匹配的开销和误杀率。
37
+
38
+ 3. **`EntityAwareFuzzyFilter` (最后兜底:长尾智能纠错)**
39
+ - **作用**:收拾残局。
40
+ - **场景**:经过前面精确词表的拦截,剩下的“长尾错别字”(如用户口音导致的“魏来”、“及克”)由它出马,通过发音的编辑距离计算把错别字捞回来。
41
+ - **语法**:`[标签]原词|替换词|阈值`
42
+ - 全局模糊拦截:`欧阳锋|欧阳锋|0.6` (长词,放心全局模糊替换)
43
+ - 短实体约束:`[nr]叶开|叶开|0.8` (`nr`为人名,只有 Jieba 认为是人名时才启动音素级检索)
44
+
45
+ > **注**:除上述三者外,内部模块如 `FuzzyPhonemeFilter`(底层的 Numba DP 发音匹配引擎)和 `JiebaNERFilter`(底层 NER 引擎)主要作为组件被 `EntityAwareFuzzyFilter` 自动编排调用,在常规业务中通常无需直接操作。
46
+
47
+ ## 🚀 快速开始 (Quick Start)
48
+
49
+ 下面是一个将“正则清洗 -> 精确大词表拦截 -> 高危实体模糊纠错”串联起来的完整 Demo:
50
+
51
+ ```python
52
+ from text_rewrite.pipeline import Pipeline
53
+ from text_rewrite.filters.regex import RegexFilter
54
+ from text_rewrite.filters.hotword import HotwordFilter
55
+ from text_rewrite.filters.entity_fuzzy import EntityAwareFuzzyFilter
56
+
57
+ # 1. 配置正则过滤器(清洗语气词)
58
+ regex_rules = {
59
+ r"\b(嗯|啊|哦|那个)\b": ""
60
+ }
61
+ regex_filter = RegexFilter(rules=regex_rules)
62
+
63
+ # 2. 配置精确热词过滤器(处理 10万级 安全大词表)
64
+ exact_filter = HotwordFilter(hotwords={"确定性长词": "替换词"})
65
+
66
+ # 3. 配置实体感知模糊过滤器 (EntityAwareFuzzyFilter)
67
+ # 语法: [标签]目标词|阈值 (省略替换词会自动以目标词作为原词和替换词)
68
+ fuzzy_rules = [
69
+ "张三|0.7", # 无标签:全局极简配置(发音类似于张三的词,如展伞,都会被纠正为张三)
70
+ "[nr]叶开|0.8", # 有标签:严格约束(必须是人名,且发音相似度>=0.8 才纠正)
71
+ "李四|0.7" # 全局极简配置(里死 -> 李四)
72
+ ]
73
+ entity_filter = EntityAwareFuzzyFilter(rules=fuzzy_rules)
74
+
75
+ # 4. 组装 Pipeline 引擎(顺序即执行顺序)
76
+ pipeline = Pipeline()
77
+ pipeline.add_filter(regex_filter)
78
+ pipeline.add_filter(exact_filter)
79
+ pipeline.add_filter(entity_filter)
80
+
81
+ # 5. 真实流式调用
82
+ text = "那个,昨天通知了一下展伞和里死,但是他也开心了,最后通知了也开。"
83
+ result = pipeline.process(text)
84
+
85
+ print(result)
86
+ # 输出: ",昨天通知了一下张三和李四,但是他也开心了,最后通知了也开。"
87
+ ```
88
+
89
+ > **注意**:由于初始化 Numba 引擎以及 JIT 预热需要少量时间,建议在服务启动时**单例初始化** `Pipeline` 实例,在后续流式请求中复用该实例调用 `.process(text)`。
90
+
91
+ ## 📊 性能压测 (Benchmark)
92
+
93
+ 您可以运行自带的压测脚本,体验在注入 **10000** 条工业级比例热词配置下的强悍性能:
94
+ ```bash
95
+ python tests/bench_entity_fuzzy.py
96
+ ```
97
+ * **短句 (约 30 字)**: 端到端平均耗时 **~ 1 ms**
98
+ * **长文 (约 2000 字)**: 端到端平均耗时 **~ 150 ms** (呈现完美的 $O(N)$ 线性扩展,不受十万级词表拖累)
99
+
100
+ ---
101
+ *Built with ❤️ for High-Performance NLP Engineering.*
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "text-rewrite"
7
+ version = "0.1.0"
8
+ description = "A high-performance text rewriting and fuzzy phonetic matching engine"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [
12
+ { name = "Magic Jack", email = "majic31@163.com" }
13
+ ]
14
+ dependencies = [
15
+ "flashtext>=2.7",
16
+ "numba>=0.56.0",
17
+ "numpy>=1.21.0",
18
+ "pypinyin>=0.49.0",
19
+ "jieba>=0.42.1"
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ test = [
24
+ "pytest>=7.0.0"
25
+ ]
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["."]
29
+ include = ["text_rewrite*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .pipeline import Pipeline
2
+
3
+ __all__ = ["Pipeline"]
@@ -0,0 +1,9 @@
1
+ from .base import BaseFilter
2
+ from .hotword import HotwordFilter
3
+ from .regex import RegexFilter
4
+ from .ner import NERFilter
5
+ from .jieba_ner import JiebaNERFilter
6
+ from .fuzzy_phoneme import FuzzyPhonemeFilter
7
+ from .entity_fuzzy import EntityAwareFuzzyFilter
8
+
9
+ __all__ = ["BaseFilter", "HotwordFilter", "RegexFilter", "NERFilter", "JiebaNERFilter", "FuzzyPhonemeFilter", "EntityAwareFuzzyFilter"]
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class BaseFilter(ABC):
4
+ """
5
+ Abstract base class for all text filters.
6
+ """
7
+ def __init__(self, name: str = None):
8
+ self.name = name or self.__class__.__name__
9
+
10
+ @abstractmethod
11
+ def process(self, text: str) -> str:
12
+ """
13
+ Process the input text and return the filtered text.
14
+ """
15
+ pass
@@ -0,0 +1,187 @@
1
+ import logging
2
+ import re
3
+ from typing import List
4
+ from collections import defaultdict
5
+ from .base import BaseFilter
6
+ from .fuzzy_phoneme import FuzzyPhonemeFilter
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class EntityAwareFuzzyFilter(BaseFilter):
10
+ """
11
+ SOTA Entity-Aware Fuzzy Phoneme Filter.
12
+ Combines high-speed global phoneme matching with Jieba-based NER tagging
13
+ to correct ASR homophone errors accurately and efficiently.
14
+ """
15
+ def __init__(self, name: str = "EntityAwareFuzzyFilter", rules: List[str] = None):
16
+ """
17
+ :param rules: A list of rule strings.
18
+ Format: `[tag]word1:threshold1|word2:replacement2:threshold2`
19
+ tag, replacement, and threshold are optional.
20
+ Example: "[nr]叶开:0.8|张三:0.7", "李四:0.7", "欧阳锋"
21
+ """
22
+ super().__init__(name=name)
23
+ self.global_hotwords = {}
24
+ self.global_thresholds = {}
25
+ self.tagged_hotwords = defaultdict(dict)
26
+ self.tagged_thresholds = defaultdict(dict)
27
+
28
+ self.global_filter = None
29
+ self.tagged_filters = {}
30
+ self.ner_filter = None
31
+
32
+ if rules:
33
+ self.add_rules(rules)
34
+
35
+ def add_rules(self, rules: List[str]):
36
+ tag_pattern = re.compile(r'^\[([^\]]+)\]')
37
+ for rule in rules:
38
+ self._parse_rule(rule, tag_pattern)
39
+
40
+ self._rebuild_filters()
41
+ self._setup_ner_filter()
42
+
43
+ def _parse_rule(self, rule: str, tag_pattern: re.Pattern):
44
+ rule = rule.strip()
45
+ if not rule:
46
+ return
47
+
48
+ tag_match = tag_pattern.match(rule)
49
+ tag = tag_match.group(1) if tag_match else None
50
+
51
+ if tag_match:
52
+ rule = rule[tag_match.end():]
53
+
54
+ parts = [p.strip() for p in rule.split('|')]
55
+ for part in parts:
56
+ if not part:
57
+ continue
58
+
59
+ subparts = [sp.strip() for sp in part.split(':')]
60
+ word = subparts[0]
61
+ replacement = word
62
+ threshold = 0.6
63
+
64
+ if len(subparts) == 2:
65
+ try:
66
+ threshold = float(subparts[1])
67
+ except ValueError:
68
+ replacement = subparts[1]
69
+ elif len(subparts) >= 3:
70
+ replacement = subparts[1]
71
+ try:
72
+ threshold = float(subparts[2])
73
+ except ValueError:
74
+ pass
75
+
76
+ if tag:
77
+ self.tagged_hotwords[tag][word] = replacement
78
+ self.tagged_thresholds[tag][word] = threshold
79
+ else:
80
+ self.global_hotwords[word] = replacement
81
+ self.global_thresholds[word] = threshold
82
+
83
+ def _rebuild_filters(self):
84
+ # Rebuild global filter
85
+ if self.global_hotwords:
86
+ self.global_filter = FuzzyPhonemeFilter(hotwords=self.global_hotwords, threshold=0.6)
87
+ self.global_filter.custom_thresholds = self.global_thresholds
88
+
89
+ # Rebuild tagged filters
90
+ for tag, hw_dict in self.tagged_hotwords.items():
91
+ f = FuzzyPhonemeFilter(hotwords=hw_dict, threshold=0.6)
92
+ f.custom_thresholds = self.tagged_thresholds[tag]
93
+ self.tagged_filters[tag] = f
94
+
95
+ def _setup_ner_filter(self):
96
+ # Init JiebaNERFilter if tagged rules exist
97
+ if not self.tagged_filters:
98
+ return
99
+
100
+ import functools
101
+
102
+ # Cache the processing of individual words to avoid redundant phoneme DP searches
103
+ # Since tagged filters don't change state during processing, caching per tag is safe.
104
+ word_cache = {}
105
+ for tag in self.tagged_filters:
106
+ word_cache[tag] = functools.lru_cache(maxsize=4096)(self.tagged_filters[tag].process)
107
+
108
+ def jieba_callback(terms, text):
109
+ new_text = text
110
+
111
+ # Keep track of character offsets manually since jieba
112
+ # splits continuously.
113
+ offset = 0
114
+ entities = []
115
+ for term in terms:
116
+ word = term.word
117
+ tag_str = term.flag
118
+
119
+ if tag_str in self.tagged_filters:
120
+ entities.append({
121
+ 'word': word,
122
+ 'tag': tag_str,
123
+ 'start': offset,
124
+ 'end': offset + len(word)
125
+ })
126
+ offset += len(word)
127
+
128
+ # Process from back to front to avoid index shifting after string replacement
129
+ for ent in reversed(entities):
130
+ word = ent['word']
131
+ tag = ent['tag']
132
+ start = ent['start']
133
+ end = ent['end']
134
+
135
+ # Fuzzy phoneme search on the isolated entity word (Cached)
136
+ corrected = word_cache[tag](word)
137
+ if corrected != word:
138
+ new_text = new_text[:start] + corrected + new_text[end:]
139
+
140
+ return new_text
141
+
142
+ from .jieba_ner import JiebaNERFilter
143
+ self.ner_filter = JiebaNERFilter(replace_callback=jieba_callback)
144
+ self.ner_filter._load_model() # Pre-load it
145
+
146
+ # Build prechecker
147
+ all_tagged_hw = {}
148
+ all_tagged_thresholds = {}
149
+ for tag, hw_dict in self.tagged_hotwords.items():
150
+ all_tagged_hw.update(hw_dict)
151
+ all_tagged_thresholds.update(self.tagged_thresholds[tag])
152
+
153
+ if all_tagged_hw:
154
+ self.ner_prechecker = FuzzyPhonemeFilter(hotwords=all_tagged_hw, threshold=0.6)
155
+ self.ner_prechecker.custom_thresholds = all_tagged_thresholds
156
+ else:
157
+ self.ner_prechecker = None
158
+
159
+ def process(self, text: str) -> str:
160
+ """
161
+ Execute SOTA cascading replacement.
162
+ 1. Global Numba match (fastest, for long words or safe thresholds).
163
+ 2. Jieba NER constraint match (for tags like 'nr').
164
+ """
165
+ if not text:
166
+ return text
167
+
168
+
169
+ # Step 1: Global substitution
170
+ if self.global_filter:
171
+ text = self.global_filter.process(text)
172
+
173
+ # Step 2: Tag-constrained substitution
174
+ if self.ner_filter:
175
+ # 预检:如果不可能有带标签的热词出现,直接跳过 jieba 分词
176
+ skip_ner = False
177
+ if hasattr(self, 'ner_prechecker') and self.ner_prechecker:
178
+ from text_rewrite.utils.algo_phoneme import get_phoneme_info
179
+ inp = get_phoneme_info(text)
180
+ cands = self.ner_prechecker.rag.index.get_candidates(inp)
181
+ if not cands:
182
+ skip_ner = True
183
+
184
+ if not skip_ner:
185
+ text = self.ner_filter.process(text)
186
+
187
+ return text
@@ -0,0 +1,130 @@
1
+ import logging
2
+ from typing import Dict
3
+ from .base import BaseFilter
4
+ from ..utils.algo_phoneme import get_phoneme_info, get_phoneme_seq
5
+ from ..utils.rag_fast import FastRAG
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class FuzzyPhonemeFilter(BaseFilter):
10
+ """
11
+ Fuzzy Phoneme Filter using Numba JIT DP acceleration.
12
+ Handles exact and fuzzy phoneme matches to correct ASR homophone errors.
13
+ """
14
+ def __init__(self, name: str = "FuzzyPhonemeFilter", hotwords: Dict[str, str] = None, threshold: float = 0.6):
15
+ """
16
+ :param threshold: The similarity threshold (0.0 to 1.0) for a phoneme sequence to match.
17
+ 0.6 is a good default for allowing some ASR errors.
18
+ """
19
+ super().__init__(name=name)
20
+ self.rag = FastRAG(threshold=threshold)
21
+ self.hotword_replacements = {}
22
+ # Cache hw_info to avoid recomputing
23
+ self._hw_info_cache = {}
24
+
25
+ # Optional mapping of original word -> specific threshold
26
+ self.custom_thresholds = {}
27
+
28
+ if hotwords:
29
+ self.add_hotwords(hotwords)
30
+
31
+ def add_hotwords(self, hotwords: Dict[str, str], custom_thresholds: Dict[str, float] = None):
32
+ """
33
+ Add hotwords dynamically.
34
+ :param hotwords: Dictionary mapping the phonetic target (e.g., "张三") to the replacement string (e.g., "张三").
35
+ :param custom_thresholds: Optional dictionary mapping target word to its specific threshold.
36
+ """
37
+ if custom_thresholds:
38
+ self.custom_thresholds.update(custom_thresholds)
39
+
40
+ hw_dict = {}
41
+ for original, replacement in hotwords.items():
42
+ phonemes = get_phoneme_seq(original)
43
+ hw_dict[original] = phonemes
44
+ self.hotword_replacements[original] = replacement
45
+ self._hw_info_cache[original] = [p.info for p in phonemes]
46
+
47
+ self.rag.add_hotwords(hw_dict)
48
+ self.rag.numba_searcher.build_cache(self._hw_info_cache)
49
+
50
+ def process(self, text: str) -> str:
51
+ """
52
+ Process text by converting to phonemes, searching for fuzzy matches,
53
+ mapping indices back, and replacing.
54
+ """
55
+ if not text:
56
+ return text
57
+
58
+ # 1. Get input phonemes with char_start/char_end
59
+ input_phonemes = get_phoneme_info(text)
60
+ if not input_phonemes:
61
+ return text
62
+
63
+ # 2. Get candidates from inverted index to avoid full scan
64
+ candidates = self.rag.index.get_candidates(input_phonemes)
65
+ if not candidates:
66
+ return text
67
+
68
+ input_info = [p.info for p in input_phonemes]
69
+
70
+ # Find the absolute minimum threshold to query Numba, post-filter later
71
+ min_thresh = self.rag.threshold
72
+ if self.custom_thresholds:
73
+ min_thresh = min(min_thresh, min(self.custom_thresholds.values()))
74
+
75
+ # 3. Perform fine-grained Numba Substring DP search
76
+ replacements = []
77
+
78
+ # Pre-encode input_info ONCE to avoid O(N_candidates) encoding overhead
79
+ inp_codes, inp_langs, _, inp_is_ws, inp_is_we = self.rag.numba_searcher.encode_input_vecs(input_info)
80
+
81
+ candidate_keys = [hw_key for hw_key, _ in candidates]
82
+
83
+ batch_res = self.rag.numba_searcher.search_batch_with_encoded_input(
84
+ candidate_keys, inp_codes, inp_langs, inp_is_ws, inp_is_we, threshold=min_thresh
85
+ )
86
+
87
+ for hw_key, res_list in batch_res.items():
88
+ for score, start_idx, end_idx in res_list:
89
+ # Apply word-specific threshold
90
+ if score >= self.custom_thresholds.get(hw_key, self.rag.threshold):
91
+ replacements.append({
92
+ 'score': score,
93
+ 'start_idx': start_idx,
94
+ 'end_idx': end_idx,
95
+ 'replacement': self.hotword_replacements[hw_key]
96
+ })
97
+
98
+ if not replacements:
99
+ return text
100
+
101
+ # 4. Resolve overlaps (greedy: highest score, then longest span)
102
+ replacements.sort(key=lambda x: (-x['score'], -(x['end_idx'] - x['start_idx'])))
103
+
104
+ final_reps = []
105
+ used_indices = set()
106
+ for r in replacements:
107
+ r_set = set(range(r['start_idx'], r['end_idx']))
108
+ if not r_set.intersection(used_indices):
109
+ final_reps.append(r)
110
+ used_indices.update(r_set)
111
+
112
+ # 5. Execute replacements from back to front to avoid index shifting issues
113
+ final_reps.sort(key=lambda x: x['start_idx'], reverse=True)
114
+
115
+ result_text = text
116
+ for r in final_reps:
117
+ # Map phoneme index to char index
118
+ # end_idx is exclusive in phoneme array
119
+ start_p = r['start_idx']
120
+ end_p = r['end_idx'] - 1
121
+
122
+ if start_p >= len(input_phonemes) or end_p >= len(input_phonemes) or start_p > end_p:
123
+ continue
124
+
125
+ char_start = input_phonemes[start_p].char_start
126
+ char_end = input_phonemes[end_p].char_end
127
+
128
+ result_text = result_text[:char_start] + r['replacement'] + result_text[char_end:]
129
+
130
+ return result_text
@@ -0,0 +1,50 @@
1
+ from typing import Dict, List
2
+ from .base import BaseFilter
3
+
4
+ try:
5
+ from flashtext import KeywordProcessor
6
+ except ImportError:
7
+ KeywordProcessor = None
8
+
9
+
10
+ class HotwordFilter(BaseFilter):
11
+ """
12
+ Hotword Filter for high-performance exact word replacements.
13
+ Uses Aho-Corasick algorithm via flashtext library.
14
+ Ideal for massive dictionaries (100k+ words).
15
+ """
16
+ def __init__(self, name: str = "HotwordFilter", hotwords: Dict[str, str] = None, case_sensitive: bool = False):
17
+ super().__init__(name=name)
18
+ if KeywordProcessor is None:
19
+ raise ImportError("flashtext library is required for HotwordFilter. Run `pip install flashtext`.")
20
+
21
+ self.keyword_processor = KeywordProcessor(case_sensitive=case_sensitive)
22
+ if hotwords:
23
+ self.add_hotwords(hotwords)
24
+
25
+ def add_hotwords(self, hotwords: Dict[str, str]):
26
+ """
27
+ Add hotword replacements dynamically.
28
+ :param hotwords: Dictionary mapping original words to target replacement words.
29
+ Note: flashtext format is { 'replacement': ['word1', 'word2'] } for add_keywords_from_dict.
30
+ Since we get { 'word': 'replacement' }, we iterate or transform it.
31
+ """
32
+ for original_word, replacement_word in hotwords.items():
33
+ self.keyword_processor.add_keyword(original_word, replacement_word)
34
+
35
+ def remove_hotwords(self, words: List[str]):
36
+ """
37
+ Remove hotwords from the trie dynamically.
38
+ """
39
+ for word in words:
40
+ self.keyword_processor.remove_keyword(word)
41
+
42
+ def process(self, text: str) -> str:
43
+ """
44
+ Process text by efficiently finding and replacing all matching hotwords.
45
+ """
46
+ if not text:
47
+ return text
48
+
49
+ # flashtext operates in O(N) where N is text length, extremely fast.
50
+ return self.keyword_processor.replace_keywords(text)