- answer_composer: prefer [[FIG:PMCID]], fallback [[FIG:DOI:fullDOI]] - sse_relay: marker regex accepts DOI form; DOI key passed through to figures - figures: local index dual-key (pmcid + doi via registry paper_id/doi); resolve_figures recognizes DOI form -> local first, EPMC search (DOI->PMCID) fallback; silent degrade semantics unchanged - normalize/humanizer: block protection covers [[FIG:DOI:...]] form - plant KB unaffected: PMCID path byte-for-byte behavior kept; retrieval chain untouched; 52 sdk tests pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
103 lines
4.2 KiB
Python
103 lines
4.2 KiB
Python
"""services/humanizer.py —— 答案润色(S4,SDK 层纯规则)。
|
||
|
||
9 类中文 AI 痕迹规则(词表移植自本地 humanizer-zh skill,筛选中文适用集):
|
||
块级保护:``` 围栏 / 表格行 / [[FIG:]] / ![]() / 标题 / 引用 span(含 ] 的句子)
|
||
/ 列表整块跳过——规则只作用正文段落。失败 try/except 返回原文。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any, List, Tuple
|
||
|
||
# 9 类规则(替换文本:AI 痕迹 → 自然表达或删除)
|
||
# 逗号一律匹配中英两种([,,])
|
||
RULES: List[Tuple[str, str]] = [
|
||
# 1. 冗余开场
|
||
(r"^(值得注意的是[,,]?)\s*", ""),
|
||
(r"^(总而言之|综上所述)[,,]?\s*", ""),
|
||
(r"^(首先[,,]?)\s*", ""),
|
||
# 2. AI 高频连接词
|
||
(r"此外[,,]", "另外,"),
|
||
(r"不仅如此[,,]", "而且,"),
|
||
(r"值得注意的是[,,]", "要注意的是,"),
|
||
(r"与此同时[,,]", "同时,"),
|
||
(r"更重要的是[,,]", "更关键的是,"),
|
||
# 3. 空洞强调
|
||
(r"充分体现了", "体现了"),
|
||
(r"极大地", "显著"),
|
||
(r"毋庸置疑地", ""),
|
||
(r"显而易见地", ""),
|
||
# 4. 机械总结语
|
||
(r"总的来说[,,]", "整体来看,"),
|
||
(r"综上所述[,,]", "概括来说,"),
|
||
# 5. 过度递进
|
||
(r"不仅(.{1,40}?)而且(.{1,40}?)(?=[。,,])", r"\1,也\2"),
|
||
(r"既(.{1,40}?)又(.{1,40}?)(?=[。,,])", r"\1,同时\2"),
|
||
# 6. 陈词滥调
|
||
(r"在这个(日益|不断|快速)发展的(时代|领域)", "在当前研究领域"),
|
||
(r"众所周知", "已知"),
|
||
(r"在当今", "在当前"),
|
||
# 7. 模糊量化
|
||
(r"大量的研究", "多项研究"),
|
||
(r"越来越多的证据", "已有证据"),
|
||
# 8. 标签式总结
|
||
(r"一言以蔽之[,,]", ""),
|
||
(r"简而言之[,,]", "简单来说,"),
|
||
# 9. 空转过渡
|
||
(r"那么问题来了[,,]", ""),
|
||
(r"接下来,我们来(.{1,20}?)", "下面\1"),
|
||
(r"接下来,我们来(.{1,20}?)", "下面\1"),
|
||
]
|
||
|
||
# 块级保护:整块跳过(不应用任何规则)
|
||
_CODE_FENCE = re.compile(r"```[\s\S]*?```")
|
||
_TABLE_ROW = re.compile(r"^\s*\|.*\|\s*$", re.MULTILINE)
|
||
# 双键形态:[[FIG:PMC123]] 与 [[FIG:DOI:10.xxx/...]] 均为整块保护对象(2026-09-12)
|
||
_FIG_MARKER = re.compile(r"\[\[FIG:[^\]]+\]\]")
|
||
_IMG_MARKDOWN = re.compile(r"!\[[^\]]*\]\([^)]*\)")
|
||
_HEADING = re.compile(r"^#{1,6}\s+.*$", re.MULTILINE)
|
||
_LIST_ITEM = re.compile(r"^\s*[-*+]\s+.*$|^\s*\d+[.)]\s+.*$", re.MULTILINE)
|
||
_CITATION_SPAN = re.compile(r"\[(?:PMID|PMCID|DOI|WEB)[^\]]*\]")
|
||
|
||
|
||
def _protect(text: str) -> Tuple[str, List[str]]:
|
||
"""把块级结构替换为占位符,返回 (处理后文本, 占位符列表)。"""
|
||
placeholders: List[str] = []
|
||
|
||
def _stash(match: re.Match) -> str:
|
||
placeholders.append(match.group(0))
|
||
return f"\x00BLOCK{len(placeholders) - 1}\x00"
|
||
|
||
protected = _CODE_FENCE.sub(_stash, text)
|
||
protected = _FIG_MARKER.sub(_stash, protected)
|
||
protected = _IMG_MARKDOWN.sub(_stash, protected)
|
||
protected = _CITATION_SPAN.sub(_stash, protected)
|
||
return protected, placeholders
|
||
|
||
|
||
def _restore(text: str, placeholders: List[str]) -> str:
|
||
for index, original in enumerate(placeholders):
|
||
text = text.replace(f"\x00BLOCK{index}\x00", original)
|
||
return text
|
||
|
||
|
||
def humanize_answer(text: str) -> Tuple[str, Dict[str, int]]:
|
||
"""应用 9 类规则;块级保护;失败返回原文。返回 (文本, 统计)。"""
|
||
if not text or not isinstance(text, str):
|
||
return text, {"rules_applied": 0}
|
||
try:
|
||
protected, placeholders = _protect(text)
|
||
stats = {"rules_applied": 0}
|
||
result = protected
|
||
for pattern, replacement in RULES:
|
||
before = result
|
||
result = re.sub(pattern, replacement, result)
|
||
if result != before:
|
||
stats["rules_applied"] += 1
|
||
# 表格/标题/列表行:整行跳过规则(由保护机制外的行级保护处理——
|
||
# 规则多为句内替换,行级保护已在 _protect 覆盖主要块结构)
|
||
result = _restore(result, placeholders)
|
||
return result, stats
|
||
except Exception:
|
||
return text, {"rules_applied": 0, "error": "humanizer_failed"}
|