feat(skills): add bilingua academic research skill

This commit is contained in:
Ct201314 2026-06-12 20:03:26 +08:00
parent b45241dcda
commit d28cc4a2a2
5 changed files with 402 additions and 0 deletions

62
skills/bilingua/SKILL.md Normal file
View File

@ -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) — 使用流程与术语表填写建议

View File

@ -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` → 小数点,不切
## 局限
- 针对英文学术文本优化;中英混排、含大量代码或公式的文本可能切分不准。
- 极端不规范的标点(如句末无空格直接接下一句)可能漏切。

View File

@ -0,0 +1,33 @@
# 使用流程与术语表填写
## 推荐流程
1. 把英文(段落或全文)存成 .txt或用 `--text` 直接传入。
2. 运行生成对照骨架 Markdown。
3. **先填术语表**:给高频术语定一个统一译名(如 CNN→卷积神经网络
4. **再逐句回填译文**:对照术语表翻译每一句,保证同词同译。
## 为什么先定术语
学术翻译最容易出的问题是同一个词前后译法不一(一会儿「分割」一会儿「分隔」)。
先把术语表的译名敲定,逐句翻译时照着填,全文译名就统一了。
## 配合翻译工具
工具本身不产出译文。译文列可以:
- 自己逐句翻译填入;
- 或把原文列丢给翻译工具,把结果贴回译文列;
- 或交给 Agent 按术语表逐句翻译。
无论哪种,术语表都作为「译名约束」先行。
## 输出说明
- Markdown术语表 + 逐句对照两张表,`____` 为待填占位。
- JSON`sentences`(每条含 en/zh+ `terms`,便于程序化处理或导入其它工具。
## 参数提示
- `--term-top` 控制术语表条数,长文可调大(如 30
- 原文中的 `|` 会被自动转义,避免破坏 Markdown 表格。

View File

@ -0,0 +1,187 @@
"""bilingua英文论文中英对照阅读骨架生成器。
把一段或一篇英文文本切成一句一句生成原文 | 译文逐句对照的 Markdown
表格骨架译文列留待填写由你或翻译工具补全并自动提取高频术语生成全文
统一的术语表避免同一个词前后译法不一
三个核心能力
1. 智能断句按句末标点切分同时正确处理常见缩写Dr./et al./e.g./Fig./
vs./i.e. 和小数点 3.14p < 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())

View File

@ -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"]))