Files
deep_research/scripts/apply_glossary.py

261 lines
9.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""根据 glossary 的核查结果,在 final_zh.md 上做精确的文本替换。
原理:
- build_glossary.py 会给每条术语标 `zh`(正确中文)和 `issue`(有发现问题)
- 本脚本扫描所有 `issue` 非空且 `confidence in {high, medium}` 的条目
- 对这些条目,在正文中把「当前错误译名」替换为「正确译名」
- 同时处理英文拼写错误(例如 Maywavee → Mabwell
规则:
- 安全第一:只做字面替换,不做上下文改写
- 明确可见:每一处替换都打印出来,方便 diff
- 幂等:多次跑结果一致
- 支持 --dry-run 预览
用法:
uv run python scripts/apply_glossary.py <project_slug>
uv run python scripts/apply_glossary.py <project_slug> --dry-run
uv run python scripts/apply_glossary.py <project_slug> --min-confidence high
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
@dataclass
class Correction:
"""一条要应用的修正。"""
kind: str # "en" (英文拼写) / "zh" (中文译名)
wrong: str # 当前文中的错误写法
correct: str # 正确写法
term_key: str # glossary 里这条的 key(人工 debug 用)
confidence: str # high / medium / low
reason: str # 为什么要改(从 issue 字段提取)
_ISSUE_WRONG_EN_RE = re.compile(
r"(?:'|\"|term\s*)([A-Z][A-Za-z0-9_\- ]{2,40})['\"]*\s*(?:为|是)[^。]*?"
r"(?:拼写错误|拼写有误|应为|正确英文名为|正确为|正确拼写为)",
)
_CORRECT_EN_RE = re.compile(
r"(?:正确英文名为|正确为|正确拼写为|应为|正确写法为)\s*['\"]?([A-Z][A-Za-z0-9_\- ]{2,40})['\"]?",
)
_WRONG_ZH_RE = re.compile(
r"current_zh\s*['\"]?([\u4e00-\u9fff][^'\"。,;]{1,40})['\"]?\s*(?:为|是|存在)[^。]*?(?:错误|错译|误译|不准确|应为|应该)",
)
def resolve_project(arg: str) -> Path:
p = Path(arg)
if p.is_dir():
return p
cand = Path.cwd() / "projects" / arg
if cand.is_dir():
return cand
raise SystemExit(f"project not found: {arg}")
def load_glossary(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
# 中文替换的保守性:只对"公司名、机构名、产品名"类这种具体化的词做替换
# 规避常见缩写词在不同上下文有不同含义的情况(如 PDE = 磷酸二酯酶 OR 允许日暴露量)
_AMBIGUOUS_ABBREVS = {
"PDE", "ASGPR", "BLA", "IND", "NDA", "CMC", "API", "QC", "QA",
"ADC", "CRO", "CDMO", "CMO", "GMP", "PK", "PD", "TRL",
"FDA", "EMA", "NMPA", "ICH", "WHO", "CFDA", "CDE",
"LNP", "RNP", "AAV", "mRNA", "RNAi", "siRNA", "ASO",
"DNA", "RNA", "cDNA", "dsRNA", "ssRNA",
"OTP", "HCV", "HBV", "HPV", "HIV",
}
def _looks_like_proper_noun(term_key: str) -> bool:
"""判断这条术语是否是"专有名词"(公司/机构/产品/药物名)。"""
# 全大写 2-4 字符缩写 → 视作通用缩写,跳过(歧义风险高)
if term_key.upper() == term_key and 2 <= len(term_key) <= 5:
return False
if term_key in _AMBIGUOUS_ABBREVS:
return False
# 其他情况:视作专有名词
return True
def extract_corrections(glossary: dict, min_conf: str) -> list[Correction]:
"""从 glossary 抽出可应用的修正。
保守策略:
- 英文拼写错误(term_key 本身错):高信度直接修
- 中文译名错误(issue 里说 current_zh 错):仅对"专有名词"类(公司/产品/机构)修
- 缩写/通用术语(PDE/ASGPR 等):不自动修,避免上下文歧义
"""
rank = {"high": 3, "medium": 2, "low": 1}
threshold = rank.get(min_conf, 2)
corrections: list[Correction] = []
for term_key, entry in glossary.items():
if not isinstance(entry, dict):
continue
conf = entry.get("confidence", "low")
if rank.get(conf, 0) < threshold:
continue
issue = entry.get("issue", "")
zh = entry.get("zh", "")
en_full = entry.get("en_full", "")
if not issue:
continue
# 1. 英文拼写错误:key 本身错了,en_full 是对的
if (
en_full
and term_key != en_full
and re.search(r"拼写错误|拼写有误|spelled|should be", issue, re.IGNORECASE)
and term_key[0].isupper() # 通常是公司/产品名
):
# 排除太短的(容易误伤)或包含空格的原始 key
if len(term_key) >= 4 and len(en_full) >= 4:
corrections.append(
Correction(
kind="en",
wrong=term_key,
correct=en_full.split("(")[0].strip(), # 去掉括号内的法人全称
term_key=term_key,
confidence=conf,
reason=issue[:200],
)
)
# 2. 中文译名错误:只对"专有名词"修(排除缩写/通用术语的歧义风险)
if not _looks_like_proper_noun(term_key):
continue
m = _WRONG_ZH_RE.search(issue)
if m and zh:
wrong_zh = m.group(1).strip()
if wrong_zh != zh and len(wrong_zh) >= 2:
corrections.append(
Correction(
kind="zh",
wrong=wrong_zh,
correct=zh,
term_key=term_key,
confidence=conf,
reason=issue[:200],
)
)
return corrections
def apply_corrections(text: str, corrections: list[Correction]) -> tuple[str, list[tuple[Correction, int]]]:
"""返回 (新文本, [(correction, 替换次数)])。"""
results: list[tuple[Correction, int]] = []
new_text = text
for c in corrections:
# 英文术语用 \b 边界;中文直接替换
if c.kind == "en":
pattern = r"\b" + re.escape(c.wrong) + r"\b"
new_text, count = re.subn(pattern, c.correct, new_text)
else:
count = new_text.count(c.wrong)
if count:
new_text = new_text.replace(c.wrong, c.correct)
results.append((c, count))
return new_text, results
def main() -> int:
parser = argparse.ArgumentParser(description="用 glossary 修正正文术语")
parser.add_argument("project", help="项目 slug 或路径")
parser.add_argument(
"--input", default="phase4/final_zh.md",
help="待修正的 Markdown",
)
parser.add_argument(
"--output", default=None,
help="输出路径(默认原地覆盖)",
)
parser.add_argument(
"--glossary", default="phase4/glossary.json",
help="glossary.json 路径",
)
parser.add_argument(
"--min-confidence",
choices=["high", "medium", "low"],
default="medium",
help="只应用 >= 此置信度的修正(默认 medium",
)
parser.add_argument(
"--dry-run", action="store_true",
help="只打印会发生的修改,不实际写入",
)
args = parser.parse_args()
project_root = resolve_project(args.project)
input_path = project_root / args.input
output_path = project_root / (args.output or args.input)
glossary_path = project_root / args.glossary
if not input_path.exists():
raise SystemExit(f"输入文件不存在:{input_path}")
if not glossary_path.exists():
raise SystemExit(f"glossary 不存在:{glossary_path}")
glossary = load_glossary(glossary_path)
corrections = extract_corrections(glossary, args.min_confidence)
print(f"Project: {project_root.name}")
print(f"Input: {input_path.relative_to(project_root)}")
print(f"Output: {output_path.relative_to(project_root)}")
print(f"Glossary: {glossary_path.relative_to(project_root)} ({len(glossary)} terms)")
print(f"Min confidence: {args.min_confidence}")
print(f"候选修正: {len(corrections)} 条")
print()
if not corrections:
print("没有需要应用的修正。")
return 0
original = input_path.read_text(encoding="utf-8")
new_text, results = apply_corrections(original, corrections)
applied = [(c, n) for c, n in results if n > 0]
skipped = [(c, n) for c, n in results if n == 0]
print(f"=== 已应用 {len(applied)} 条修正(共替换 {sum(n for _, n in applied)} 处)===")
for c, n in sorted(applied, key=lambda x: -x[1]):
marker = "EN" if c.kind == "en" else "ZH"
print(f" [{marker}][{c.confidence}] {c.wrong!r}{c.correct!r} (×{n})")
print(f" 原因: {c.reason[:120]}")
if skipped:
print(f"\n=== 跳过 {len(skipped)} 条(正文未出现该错误写法)===")
for c, _ in skipped[:10]:
marker = "EN" if c.kind == "en" else "ZH"
print(f" [{marker}] {c.wrong!r}{c.correct!r} (0 hit)")
if len(skipped) > 10:
print(f" …还有 {len(skipped) - 10} 条")
if args.dry_run:
print("\n(--dry-run:未写入)")
return 0
output_path.write_text(new_text, encoding="utf-8")
delta = len(new_text) - len(original)
print(f"\n✓ 已写入 {output_path.relative_to(project_root)} (字符变化 {delta:+d})")
return 0
if __name__ == "__main__":
sys.exit(main())