From d28cc4a2a2499b703b4308bb12ae74b6dd75c406 Mon Sep 17 00:00:00 2001 From: Ct201314 <1195214305@qq.com> Date: Fri, 12 Jun 2026 20:03:26 +0800 Subject: [PATCH] feat(skills): add bilingua academic research skill --- skills/bilingua/SKILL.md | 62 ++++++ .../bilingua/references/sentence-splitting.md | 44 +++++ skills/bilingua/references/usage.md | 33 ++++ skills/bilingua/scripts/bilingua.py | 187 ++++++++++++++++++ skills/bilingua/tests/test_bilingua.py | 76 +++++++ 5 files changed, 402 insertions(+) create mode 100644 skills/bilingua/SKILL.md create mode 100644 skills/bilingua/references/sentence-splitting.md create mode 100644 skills/bilingua/references/usage.md create mode 100644 skills/bilingua/scripts/bilingua.py create mode 100644 skills/bilingua/tests/test_bilingua.py diff --git a/skills/bilingua/SKILL.md b/skills/bilingua/SKILL.md new file mode 100644 index 0000000..ba485dd --- /dev/null +++ b/skills/bilingua/SKILL.md @@ -0,0 +1,62 @@ +--- +name: bilingua +version: 1.0.0 +description: "英文论文中英对照阅读骨架生成器:把英文文本智能断句(正确处理 Dr./et al./e.g. 缩写与 0.92、p<0.05 小数点不误切),生成原文/译文逐句对照的 Markdown 表格,并提取高频术语生成术语表以统一译名。当用户提到「中英对照」「逐句对照读论文」「论文翻译骨架」「英文文献精读」「术语表」时触发。" +metadata: + requires: + optional_bins: ["python"] +--- + +# bilingua(中英对照阅读骨架生成器) + +英文论文读得慢,不是你菜,是没人给你逐句配中文。bilingua 把英文切成一句一句, +生成原文/译文逐句对照表,并锁定全文术语,让你对照着读、稳定地回填译文。 + +## 何时使用本技能 + +- 啃外文文献,想中英对照着读 +- 某句不确定意思,想逐句对着看 +- 想把论文整理成可反复看的 Markdown 对照笔记 + +## 与同类工具的区别 + +同类阅读器多直接调翻译/LLM 输出译文。本技能**只搭骨架、不臆造翻译**:纯本地、 +纯标准库,负责把英文「拆成可对照的结构」并「提取统一术语」,译文列留空由你或 +翻译工具回填。它的核心价值在**高质量断句**——正确处理学术文本里高频的缩写 +(Dr./et al./e.g./i.e./Fig./vs.)和小数点(0.92、p < 0.05),不在这些地方误切。 + +## 三个核心能力 + +| 能力 | 说明 | +|------|------| +| 智能断句 | 句末标点切分 + 缩写/小数点保护,避免误切 | +| 中英对照骨架 | 每句一行,原文 \| 译文占位,逐句回填 | +| 术语提取 | 高频缩略语 + 名词短语候选 → 术语表统一译名 | + +## 工作流 + +```bash +python scripts/bilingua.py --input paper.txt --output reading.md +python scripts/bilingua.py --text "Your English paragraph." --format json +``` + +| 参数 | 说明 | +|------|------| +| `--input` | 英文文本文件(*或用 `--text`) | +| `--text` | 直接传入英文文本 | +| `--term-top` | 术语表最多条数(默认 15) | +| `--format` | `markdown`(默认)或 `json` | +| `--output` | 输出文件 | + +建议流程:先填术语表统一译名,再逐句回填译文,保证同一术语全文译法一致。 + +## 注意事项 + +- 工具不产出译文,不调用任何翻译服务;译文由用户或外部工具回填。 +- 断句针对英文学术文本优化;对中文混排或代码块文本效果有限。 +- 自动剥离文件 BOM,避免首句残留乱码字符。 + +## References + +- [sentence-splitting.md](references/sentence-splitting.md) — 断句规则与缩写表 +- [usage.md](references/usage.md) — 使用流程与术语表填写建议 diff --git a/skills/bilingua/references/sentence-splitting.md b/skills/bilingua/references/sentence-splitting.md new file mode 100644 index 0000000..bd7a1ce --- /dev/null +++ b/skills/bilingua/references/sentence-splitting.md @@ -0,0 +1,44 @@ +# 断句规则与缩写表 + +## 断句基本规则 + +以 `.`、`!`、`?` 为候选句末标点,满足以下条件才真正切分: + +1. 标点后(跳过紧跟的引号/括号)是空格,且下一个非空字符为大写字母或引号/左括号; +2. 或已到文本结尾。 + +这样能避免在句中缩写、小数点处误切。 + +## 缩写保护 + +句点前的单词若属于缩写表,则不切。当前缩写表(小写、去点比对): + +``` +dr mr mrs ms prof vs etc eg ie al fig figs eq eqs ref refs +no vol pp cf approx i.e e.g et al ca st jr sr +``` + +此外,单个字母 + 句点(如人名首字母 `J.`)也不切。可在 `bilingua.py` 的 +`ABBREVIATIONS` 中扩充。 + +## 小数点保护 + +句点两侧都是数字时(如 `0.92`、`3.14`、`p < 0.05` 中的 `0.05`),视为小数点,不切。 + +## 验证用例 + +下面这句包含 4 个陷阱,应切成 1 句而非多句: + +``` +Dr. Wang et al. reported a Dice score of 0.92 on the MRI dataset (p < 0.05). +``` + +- `Dr.` → 缩写,不切 +- `et al.` → 缩写,不切 +- `0.92` → 小数点,不切 +- `0.05` → 小数点,不切 + +## 局限 + +- 针对英文学术文本优化;中英混排、含大量代码或公式的文本可能切分不准。 +- 极端不规范的标点(如句末无空格直接接下一句)可能漏切。 diff --git a/skills/bilingua/references/usage.md b/skills/bilingua/references/usage.md new file mode 100644 index 0000000..3c04653 --- /dev/null +++ b/skills/bilingua/references/usage.md @@ -0,0 +1,33 @@ +# 使用流程与术语表填写 + +## 推荐流程 + +1. 把英文(段落或全文)存成 .txt,或用 `--text` 直接传入。 +2. 运行生成对照骨架 Markdown。 +3. **先填术语表**:给高频术语定一个统一译名(如 CNN→卷积神经网络)。 +4. **再逐句回填译文**:对照术语表翻译每一句,保证同词同译。 + +## 为什么先定术语 + +学术翻译最容易出的问题是同一个词前后译法不一(一会儿「分割」一会儿「分隔」)。 +先把术语表的译名敲定,逐句翻译时照着填,全文译名就统一了。 + +## 配合翻译工具 + +工具本身不产出译文。译文列可以: + +- 自己逐句翻译填入; +- 或把原文列丢给翻译工具,把结果贴回译文列; +- 或交给 Agent 按术语表逐句翻译。 + +无论哪种,术语表都作为「译名约束」先行。 + +## 输出说明 + +- Markdown:术语表 + 逐句对照两张表,`____` 为待填占位。 +- JSON:`sentences`(每条含 en/zh)+ `terms`,便于程序化处理或导入其它工具。 + +## 参数提示 + +- `--term-top` 控制术语表条数,长文可调大(如 30)。 +- 原文中的 `|` 会被自动转义,避免破坏 Markdown 表格。 diff --git a/skills/bilingua/scripts/bilingua.py b/skills/bilingua/scripts/bilingua.py new file mode 100644 index 0000000..d080205 --- /dev/null +++ b/skills/bilingua/scripts/bilingua.py @@ -0,0 +1,187 @@ +"""bilingua:英文论文中英对照阅读骨架生成器。 + +把一段(或一篇)英文文本,切成一句一句,生成「原文 | 译文」逐句对照的 Markdown +表格骨架,译文列留待填写(由你或翻译工具补全),并自动提取高频术语,生成全文 +统一的术语表,避免同一个词前后译法不一。 + +三个核心能力: +1. 智能断句——按句末标点切分,同时正确处理常见缩写(Dr./et al./e.g./Fig./ + vs./i.e. 等)和小数点(如 3.14、p < 0.05),避免在这些位置误切。 +2. 中英对照骨架——每句一行,左列原文、右列译文占位,段落之间保留分隔,方便 + 逐句对照阅读与回填译文。 +3. 术语提取——统计高频的大写开头词与名词短语候选,列出术语表,供全文统一译名。 + +本工具纯本地、纯标准库:不调用任何翻译 API 或在线服务,只负责把英文「拆成可 +对照的结构」并「锁定术语」。译文回填由用户掌控,工具不臆造翻译。 + +用法: + python bilingua.py --input paper.txt --output reading.md + python bilingua.py --text "Your English paragraph." --format json +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from collections import Counter +from pathlib import Path +from typing import Any + +if hasattr(sys.stdout, "reconfigure"): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + + +# 常见缩写:其后的句点不应触发断句 +ABBREVIATIONS = { + "dr", "mr", "mrs", "ms", "prof", "vs", "etc", "eg", "ie", "al", "fig", + "figs", "eq", "eqs", "ref", "refs", "no", "vol", "pp", "cf", "approx", + "i.e", "e.g", "et al", "ca", "st", "jr", "sr", +} + +# 英文常见功能词,不作为术语候选 +COMMON_WORDS = { + "the", "a", "an", "and", "or", "but", "of", "to", "in", "on", "at", "for", + "with", "by", "from", "as", "is", "are", "was", "were", "be", "been", "being", + "this", "that", "these", "those", "it", "we", "our", "their", "its", "they", + "which", "who", "whom", "can", "could", "may", "might", "will", "would", + "however", "therefore", "thus", "moreover", "furthermore", "we", "i", "he", "she", +} + + +def split_sentences(text: str) -> list[str]: + """把英文文本切成句子,正确处理缩写与小数点。""" + text = text.lstrip("\ufeff") # 剥离可能的 UTF-8 BOM + text = re.sub(r"\s+", " ", text.strip()) + if not text: + return [] + + sentences: list[str] = [] + start = 0 + i = 0 + n = len(text) + while i < n: + ch = text[i] + if ch in ".!?": + # 小数点:句点两侧都是数字,不切 + if ch == "." and 0 < i < n - 1 and text[i - 1].isdigit() and text[i + 1].isdigit(): + i += 1 + continue + # 缩写:句点前的单词(小写化、去点)属于缩写集合,不切 + if ch == ".": + prev_word = re.findall(r"([A-Za-z.]+)$", text[start:i]) + token = prev_word[0].lower().rstrip(".") if prev_word else "" + if token in ABBREVIATIONS or (len(token) == 1 and token.isalpha()): + i += 1 + continue + # 句末标点后须跟空格+大写或引号或结尾,才算真正断句 + j = i + 1 + while j < n and text[j] in '"\')]': + j += 1 + if j >= n or (text[j] == " " and (j + 1 >= n or text[j + 1].isupper() or text[j + 1] in '"\'(')): + sentences.append(text[start:j].strip()) + start = j + i = j + continue + i += 1 + if start < n: + tail = text[start:].strip() + if tail: + sentences.append(tail) + return [s for s in sentences if s] + + +def extract_terms(text: str, top: int = 15) -> list[dict[str, Any]]: + """提取高频术语候选:大写开头词 + 连续大写词组(缩略语)。""" + # 缩略语:连续 2+ 大写字母(如 CNN, MRI, GAN) + acronyms = re.findall(r"\b[A-Z]{2,}\b", text) + # 专有/术语候选:大写开头的多词短语 + phrases = re.findall(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2})\b", text) + # 普通高频实词 + words = [w.lower() for w in re.findall(r"\b[a-z]{4,}\b", text.lower()) + if w.lower() not in COMMON_WORDS] + + counter: Counter[str] = Counter() + for a in acronyms: + counter[a] += 3 # 缩略语权重高 + for p in phrases: + counter[p] += 2 + for w in words: + counter[w] += 1 + + terms = [] + for term, freq in counter.most_common(top): + if freq >= 2: + terms.append({"term": term, "count": freq, "translation": ""}) + return terms + + +def build_reading(text: str, term_top: int = 15) -> dict[str, Any]: + """生成对照骨架数据。""" + sentences = split_sentences(text) + terms = extract_terms(text, top=term_top) + return { + "sentence_count": len(sentences), + "sentences": [{"en": s, "zh": ""} for s in sentences], + "terms": terms, + } + + +def render_markdown(data: dict[str, Any]) -> str: + lines = [ + "# 中英对照阅读", + "", + f"共 {data['sentence_count']} 句。下表左列为原文,右列译文待填。", + "", + "## 术语表", + "", + "先统一术语译名,再逐句翻译,避免同词异译。", + "", + "| 术语 | 出现次数 | 译名(待填) |", + "|------|:------:|------|", + ] + for t in data["terms"]: + lines.append(f"| {t['term']} | {t['count']} | {t['translation'] or '____'} |") + lines += ["", "## 逐句对照", "", "| # | 原文 (EN) | 译文 (中) |", "|:--:|------|------|"] + for i, s in enumerate(data["sentences"], start=1): + en = s["en"].replace("|", "\\|") + lines.append(f"| {i} | {en} | {s['zh'] or '____'} |") + lines += ["", "---", "", "由 bilingua 生成。工具只搭对照骨架与术语表,译文由你或翻译工具回填,不臆造翻译。"] + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(prog="bilingua", description="英文论文中英对照阅读骨架生成器") + p.add_argument("--input", type=Path, help="英文文本文件") + p.add_argument("--text", help="直接传入英文文本") + p.add_argument("--term-top", type=int, default=15, help="术语表最多条数") + p.add_argument("--format", choices=["markdown", "json"], default="markdown") + p.add_argument("--output", type=Path) + args = p.parse_args(argv) + + if args.input: + text = args.input.read_text(encoding="utf-8", errors="replace") + elif args.text: + text = args.text + else: + print("错误:请用 --input 或 --text 提供英文文本。", file=sys.stderr) + return 2 + + data = build_reading(text, term_top=args.term_top) + out = (json.dumps(data, ensure_ascii=False, indent=2) if args.format == "json" + else render_markdown(data)) + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(out, encoding="utf-8") + print(f"已写入 {args.output}") + else: + print(out) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/bilingua/tests/test_bilingua.py b/skills/bilingua/tests/test_bilingua.py new file mode 100644 index 0000000..5a271b3 --- /dev/null +++ b/skills/bilingua/tests/test_bilingua.py @@ -0,0 +1,76 @@ +"""bilingua 单元测试。重点覆盖断句难点(缩写、小数点)。""" +from __future__ import annotations +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +import pytest +from bilingua import split_sentences, extract_terms, build_reading, render_markdown + + +class TestSplitSentences: + def test_basic(self): + s = split_sentences("This is one. This is two. This is three.") + assert len(s) == 3 + + def test_decimal_not_split(self): + s = split_sentences("The score was 0.92 on average. It improved.") + assert len(s) == 2 + assert "0.92" in s[0] + + def test_pvalue_not_split(self): + s = split_sentences("The result was significant (p < 0.05). We confirmed it.") + assert len(s) == 2 + + def test_abbreviation_dr(self): + s = split_sentences("Dr. Wang led the study. He published it.") + assert len(s) == 2 + assert "Dr. Wang" in s[0] + + def test_et_al(self): + s = split_sentences("Smith et al. proposed a method. It worked well.") + assert len(s) == 2 + + def test_eg_ie(self): + s = split_sentences("Some methods (e.g., CNN) are common. Others (i.e., GAN) are newer.") + assert len(s) == 2 + + def test_question_exclaim(self): + s = split_sentences("Why does it work? Nobody knows! We investigate.") + assert len(s) == 3 + + def test_empty(self): + assert split_sentences("") == [] + + def test_bom_stripped(self): + s = split_sentences("\ufeffFirst sentence here. Second one.") + assert not s[0].startswith("\ufeff") + assert len(s) == 2 + + +class TestExtractTerms: + def test_acronyms(self): + terms = extract_terms("The CNN and MRI were used. CNN improved MRI analysis. CNN is key.") + names = [t["term"] for t in terms] + assert "CNN" in names + + def test_filters_common(self): + terms = extract_terms("the the the and and with with from from") + assert terms == [] or all(t["term"] not in {"the", "and"} for t in terms) + + +class TestBuildAndRender: + def test_build(self): + d = build_reading("Contrastive learning helps. The CNN model is strong. CNN wins again.") + assert d["sentence_count"] == 3 + assert all("en" in s and "zh" in s for s in d["sentences"]) + + def test_render(self): + md = render_markdown(build_reading("Dr. Lee reported 0.95 accuracy. It is high.")) + assert "中英对照阅读" in md + assert "术语表" in md + assert "逐句对照" in md + assert "Dr. Lee" in md + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-v"]))