Skip to content

Commit c388e2f

Browse files
committed
feat(text_processor): 优化文本规范化功能,增强对缩写和特殊格式的处理,修复edge-tts因版本过低生成语音失败问题
1 parent 8314321 commit c388e2f

3 files changed

Lines changed: 78 additions & 6 deletions

File tree

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ fastapi>=0.68.0,<1.0.0
22
uvicorn>=0.35.0,<1.0.0
33
jinja2>=3.0.0,<4.0.0
44
python-multipart>=0.0.20,<1.0.0
5-
edge-tts>=7.0.0,<8.0.0
5+
edge-tts>=7.2.7,<8.0.0
66
PyMuPDF>=1.26.0,<2.0.0
77
nltk>=3.9.0,<4.0.0
88
translators>=6.0.0,<7.0.0

tests/test_text_processor.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ def test_normalize_text(self):
88
text = "This is a test sentence。Some people may wonder,“How long does it take to form a habit?”This is a another test sentence!"
99
normalized_text = normalize_text(text)
1010
self.assertEqual(normalized_text, 'This is a test sentence. Some people may wonder, "How long does it take to form a habit?" This is a another test sentence!')
11+
12+
# 回归测试:专有缩写和小数不应被错误插入空格
13+
edge_text = "by C. Liu, et al., we compare TLS 1.3 in detail."
14+
normalized_edge_text = normalize_text(edge_text)
15+
self.assertIn("et al.,", normalized_edge_text)
16+
self.assertIn("TLS 1.3", normalized_edge_text)
17+
1118
ja_text = '今日はいい天気だ...でも“急に雨が降り出した!”と彼は叫んだ. 明日の予定は? (キャンプを中止する) みんなで“楽しみにしていた”イベントだったのに…本当に残念ですね。'
1219
normalized_text = normalize_text(ja_text, 'japanese')
1320
self.assertEqual(normalized_text, '今日はいい天気だ…でも『急に雨が降り出した!』と彼は叫んだ。明日の予定は?(キャンプを中止する)みんなで『楽しみにしていた』イベントだったのに…本当に残念ですね。')
@@ -47,6 +54,43 @@ def test_get_sentences_lang(self):
4754
for lang, text in texts.items():
4855
sentences = get_sentences(text, lang)
4956
self.assertEqual(len(sentences), 2)
57+
58+
def test_get_sentences_with_abbreviation_and_decimal(self):
59+
"""回归测试:缩写 + 小数场景不应导致误拆分"""
60+
init_nltk()
61+
text = "This was reported by C. Liu, et al., in TLS 1.3 research. The result is stable."
62+
sentences = get_sentences(text, 'english')
63+
self.assertEqual(len(sentences), 2)
64+
self.assertIn("et al.,", sentences[0])
65+
self.assertIn("TLS 1.3", sentences[0])
66+
67+
def test_normalize_text_special_cases(self):
68+
"""更多特殊情况:缩写链、版本号、URL、邮箱、数字格式"""
69+
text = (
70+
"In the U.S.A.we use v3.10 and TLS 1.3.This is common!"
71+
"Contact me at test@example.com,and check https://example.com/docs."
72+
"The budget is 1,000.25 at 12:30."
73+
)
74+
normalized = normalize_text(text)
75+
76+
self.assertIn("U.S.A.", normalized)
77+
self.assertIn("v3.10", normalized)
78+
self.assertIn("TLS 1.3", normalized)
79+
self.assertIn("test@example.com", normalized)
80+
self.assertIn("https://example.com/docs", normalized)
81+
self.assertIn("1,000.25", normalized)
82+
self.assertIn("12:30", normalized)
83+
self.assertIn("This is common! Contact", normalized)
84+
85+
def test_get_sentences_with_acronym_and_url(self):
86+
"""回归测试:缩写链与URL不应导致误拆分"""
87+
init_nltk()
88+
text = "In the U.S.A. we test TLS 1.3. Read more at https://example.com/docs. This is final."
89+
sentences = get_sentences(text, 'english')
90+
self.assertEqual(len(sentences), 3)
91+
self.assertIn("U.S.A.", sentences[0])
92+
self.assertIn("TLS 1.3", sentences[0])
93+
self.assertIn("https://example.com/docs.", sentences[1])
5094

5195

5296
if __name__ == '__main__':

utils/text_processor.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,29 @@ def normalize_text(text: str, lang: str = 'english') -> str:
145145
# 中文符号替换为英文符号
146146
for chinese_punct, english_punct in chinese_to_english_punctuation.items():
147147
text = text.replace(chinese_punct, english_punct)
148+
149+
# 先保护高风险片段,避免后续正则误插空格导致语义破坏
150+
protected_tokens = []
151+
152+
def _protect(pattern: str):
153+
nonlocal text
154+
155+
def _repl(match: re.Match) -> str:
156+
protected_tokens.append(match.group(0))
157+
return f"__PROTECTED_TOKEN_{len(protected_tokens)-1}__"
158+
159+
text = re.sub(pattern, _repl, text)
160+
161+
_protect(r'https?://[^\s<>"\']+|www\.[^\s<>"\']+')
162+
_protect(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}')
163+
_protect(r'\b(?:[A-Za-z]\.){2,}')
164+
_protect(r'\b\d+(?:\.\d+){1,}\b')
148165

149-
# 在标点符号后添加空格(除了引号和括号类符号)
150-
# 处理句号、感叹号、问号后需要空格的情况
151-
text = re.sub(r'([.!?])([^\s"])', r'\1 \2', text)
166+
# 在句末符号后按需补空格:
167+
# 1) 仅在更像“句子边界”时补空格(例如后续是大写开头或引号/括号)
168+
# 2) 避免打断缩写链(U.S.A.)、版本号/小数(3.10 / 1.3)
169+
text = re.sub(r'([!?])(?=[^\s"\'\)\]\},.;:!?])', r'\1 ', text)
170+
text = re.sub(r'(?<!\d)\.(?!\d)(?=[A-Z"\'\(\[])', r'. ', text)
152171

153172
# 处理句号、感叹号、问号在引号内的情况,在引号后添加空格
154173
text = re.sub(r'([.!?])(["\'])', r'\1\2', text) # 先确保标点和引号紧挨着
@@ -157,8 +176,12 @@ def normalize_text(text: str, lang: str = 'english') -> str:
157176
# 处理括号类符号
158177
text = re.sub(r'([(){}\[\]<>])', r' \1 ', text)
159178

160-
# 处理逗号、分号、冒号等符号
161-
text = re.sub(r'([,;:])', r'\1 ', text)
179+
# 处理逗号、分号、冒号等符号:
180+
# 1) 千分位数字(1,000)不加空格
181+
# 2) 时间格式(12:30)不加空格
182+
text = re.sub(r'(?<=\D),(?=\S)', r', ', text)
183+
text = re.sub(r';(?=\S)', r'; ', text)
184+
text = re.sub(r'(?<!\d):(?=\S)', r': ', text)
162185

163186
# 处理开头引号前的空格
164187
text = re.sub(r'\s+"\s+', r' "', text)
@@ -171,5 +194,10 @@ def normalize_text(text: str, lang: str = 'english') -> str:
171194

172195
# 去除前后空白符
173196
text = text.strip()
197+
198+
# 还原被保护片段
199+
if lang != 'japanese':
200+
for i, token in enumerate(protected_tokens):
201+
text = text.replace(f"__PROTECTED_TOKEN_{i}__", token)
174202

175203
return text

0 commit comments

Comments
 (0)