forked from Gitlink/gitlink-cli
feat(skills): add bilingua academic research skill
This commit is contained in:
parent
b45241dcda
commit
d28cc4a2a2
|
|
@ -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) — 使用流程与术语表填写建议
|
||||
|
|
@ -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` → 小数点,不切
|
||||
|
||||
## 局限
|
||||
|
||||
- 针对英文学术文本优化;中英混排、含大量代码或公式的文本可能切分不准。
|
||||
- 极端不规范的标点(如句末无空格直接接下一句)可能漏切。
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# 使用流程与术语表填写
|
||||
|
||||
## 推荐流程
|
||||
|
||||
1. 把英文(段落或全文)存成 .txt,或用 `--text` 直接传入。
|
||||
2. 运行生成对照骨架 Markdown。
|
||||
3. **先填术语表**:给高频术语定一个统一译名(如 CNN→卷积神经网络)。
|
||||
4. **再逐句回填译文**:对照术语表翻译每一句,保证同词同译。
|
||||
|
||||
## 为什么先定术语
|
||||
|
||||
学术翻译最容易出的问题是同一个词前后译法不一(一会儿「分割」一会儿「分隔」)。
|
||||
先把术语表的译名敲定,逐句翻译时照着填,全文译名就统一了。
|
||||
|
||||
## 配合翻译工具
|
||||
|
||||
工具本身不产出译文。译文列可以:
|
||||
|
||||
- 自己逐句翻译填入;
|
||||
- 或把原文列丢给翻译工具,把结果贴回译文列;
|
||||
- 或交给 Agent 按术语表逐句翻译。
|
||||
|
||||
无论哪种,术语表都作为「译名约束」先行。
|
||||
|
||||
## 输出说明
|
||||
|
||||
- Markdown:术语表 + 逐句对照两张表,`____` 为待填占位。
|
||||
- JSON:`sentences`(每条含 en/zh)+ `terms`,便于程序化处理或导入其它工具。
|
||||
|
||||
## 参数提示
|
||||
|
||||
- `--term-top` 控制术语表条数,长文可调大(如 30)。
|
||||
- 原文中的 `|` 会被自动转义,避免破坏 Markdown 表格。
|
||||
|
|
@ -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())
|
||||
|
|
@ -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"]))
|
||||
Loading…
Reference in New Issue