v0.6-wip: apply_glossary \u5c06\u672f\u8bed\u6838\u67e5\u7ed3\u679c\u56de\u5857\u5230\u6b63\u6587
scripts/apply_glossary.py\uff08\u65b0\u589e\uff09\uff1a - \u4ece glossary \u7684 issue \u5b57\u6bb5\u6293\u53d6\u201c\u539f\u672c\u9519\u8bef / \u6b63\u786e\u5199\u6cd5\u201d\u5bf9\uff0c\u76f4\u63a5\u5728 Markdown \u6b63\u6587\u4e2d\u505a\u5b57\u9762\u66ff\u6362 - \u4fdd\u5b88\u7b56\u7565\uff1a * \u82f1\u6587\u62fc\u5199\u9519\u8bef\uff08high conf\uff09\u76f4\u63a5\u6539 * \u4e2d\u6587\u8bd1\u540d\u9519\u8bef\u4ec5\u5bf9\u201c\u4e13\u6709\u540d\u8bcd\u201d\uff08\u516c\u53f8/\u673a\u6784/\u4ea7\u54c1\uff09\u6539 * \u901a\u7528\u7f29\u5199\uff08PDE/ASGPR/LNP \u7b49\uff09\u6709\u9ed1\u540d\u5355\u62e6\u622a\uff0c\u907f\u514d\u4e0a\u4e0b\u6587\u6b67\u4e49 - dry-run \u6a21\u5f0f\u9884\u89c8 - \u5e42\u7b49 \u5728\u53cc\u9776\u70b9 RNAi \u9879\u76ee\u7684\u6210\u679c\uff1a - build_glossary \u6210\u529f\u6838\u67e5 201/310 \u672f\u8bed\uff0c\u53d1\u73b0\u4e09\u6761\u4e25\u91cd\u9519\u8bef\uff1a * Maywavee \u2192 Mabwell\uff08\u8fc8\u5a01\u751f\u7269\uff09- \u82f1\u6587\u62fc\u5199\u9519\u8bef * Beyotime \u2192 '\u7891\u4e91\u5929' \u4e3a\u9519\u8bef\u8bd1\u540d\uff0c\u5e94\u4e3a '\u5fc5\u8d1d\u7279\u533b\u836f' * Aurigene \u2192 '\u5929\u6d25\u5965\u5229\u6cd5' \u5e94\u4e3a '\u5929\u6d25\u5965\u745e\u82bc\u751f\u7269\u533b\u836f\u6709\u9650\u516c\u53f8' - apply_glossary \u5e72\u51c0\u4fee\u6b63 3 \u5904\uff0c\u6b63\u5728\u518d\u6b21\u751f\u6210 PDF + DOCX - \u6210\u672c\uff1a0.1 \u7f8e\u5143\uff08Haiku + Exa\uff09 \u5df2\u77e5\u9650\u5236\uff1a - Exa/\u4ee3\u7406\u7ec4\u5408\u5728 >6 \u5e76\u53d1\u4e0b\u4f1a\u51fa\u73b0 SSL EOF \u9519\u8bef\uff0c\u5931\u8d25 106 \u6761\u3002\u53ef\u91cd\u8dd1\u6216\u964d\u5230 3 workers\u3002 - \u672a\u6765\u53ef\u5c06 build_glossary \u524d\u79fb\u5230 Phase 2 \u65f6\u8fd0\u884c\uff0c\u6b63\u6587\u751f\u6210\u524d\u5c31\u62e6\u4f4f\u4fe1\u6e90\u4fa7\u9519\u8bef Co-authored-by: User <human>
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""根据 glossary 的核查结果,在 final_zh_polished.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_polished.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())
|
||||
Reference in New Issue
Block a user