forked from Gitlink/gitlink-cli
190 lines
7.8 KiB
Python
190 lines
7.8 KiB
Python
"""draftsmith:论文段落起草器。
|
||
|
||
对着你的结果要点、图表说明、甚至中文草稿,起草成结构合理的 IMRaD 论文段落
|
||
(Introduction / Methods / Results / Discussion)。它不替你编数据,而是把你给的
|
||
零散要点,套进学术写作的句式骨架,搭出一段像样的初稿,让你在「有结果却憋不出
|
||
正文」时有个起点。
|
||
|
||
三个核心能力:
|
||
1. 章节句式骨架——为引言/方法/结果/讨论四个核心章节各准备一套学术句式模板
|
||
(研究背景→空白→本文工作;数据→方法→统计;发现→对比→意义),把要点填进去。
|
||
2. 要点编织——把用户提供的 bullet 要点,用连接词与句式组织成连贯段落,而不是
|
||
简单罗列;自动补「研究表明/结果显示/与…一致」等学术过渡语。
|
||
3. 占位提醒——凡是需要具体数字、引用、对象的地方留明确占位([填具体数值]、
|
||
[引用]),提醒你补全,绝不编造数据或参考文献。
|
||
|
||
纯本地模板引擎:不联网、不调用 LLM,仅用 Python 标准库把要点结构化为段落。它给
|
||
的是「初稿骨架」,学术判断与事实填充由你完成。
|
||
|
||
用法:
|
||
python draft.py --section results --points "实验组准确率更高" "差异显著" --output draft.md
|
||
python draft.py --spec paper.json --output draft.md
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
if hasattr(sys.stdout, "reconfigure"):
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
SECTIONS = ["introduction", "methods", "results", "discussion"]
|
||
SECTION_CN = {
|
||
"introduction": "引言", "methods": "方法",
|
||
"results": "结果", "discussion": "讨论",
|
||
}
|
||
|
||
# 各章节的句式骨架:用于把要点编织成段落的开头/过渡/收束语
|
||
SECTION_TEMPLATES = {
|
||
"introduction": {
|
||
"opening": "近年来,{topic}受到广泛关注。",
|
||
"transitions": ["然而,", "尽管已有进展,", "值得注意的是,"],
|
||
"gap": "然而,[现有方法的不足/尚未解决的问题]仍有待研究。",
|
||
"contribution": "为此,本文[提出/研究/分析]……,主要贡献包括:",
|
||
"hint": "引言应:交代背景重要性 → 指出研究空白 → 引出本文工作与贡献。",
|
||
},
|
||
"methods": {
|
||
"opening": "本研究采用[数据来源/实验设置]开展。",
|
||
"transitions": ["具体而言,", "在此基础上,", "随后,"],
|
||
"gap": "",
|
||
"contribution": "所有统计分析采用[检验方法],显著性水平设为 α = 0.05。",
|
||
"hint": "方法应:可复现地交代数据、流程、参数、统计方法;用过去时、被动语态。",
|
||
},
|
||
"results": {
|
||
"opening": "结果显示,[主要发现]。",
|
||
"transitions": ["此外,", "进一步地,", "与之相比,"],
|
||
"gap": "",
|
||
"contribution": "上述结果在[图/表 X]中汇总([填统计量、p 值、效应量])。",
|
||
"hint": "结果应:客观陈述发现 + 对应图表 + 统计证据;不解读意义(留给讨论)。",
|
||
},
|
||
"discussion": {
|
||
"opening": "本研究表明,[核心结论]。",
|
||
"transitions": ["这一发现与[文献]一致,", "与既有工作不同,", "需要指出的是,"],
|
||
"gap": "本研究存在以下局限:[样本/范围/方法的限制]。",
|
||
"contribution": "未来工作可[改进方向]。",
|
||
"hint": "讨论应:呼应发现的意义 → 与文献对比 → 承认局限 → 展望。",
|
||
},
|
||
}
|
||
|
||
|
||
def weave_points(section: str, points: list[str]) -> str:
|
||
"""把要点编织成段落。"""
|
||
tpl = SECTION_TEMPLATES[section]
|
||
transitions = tpl["transitions"]
|
||
sentences: list[str] = []
|
||
for i, pt in enumerate(points):
|
||
pt = pt.strip().rstrip("。.")
|
||
if not pt:
|
||
continue
|
||
if i == 0:
|
||
sentences.append(f"{pt}。")
|
||
else:
|
||
conn = transitions[(i - 1) % len(transitions)]
|
||
sentences.append(f"{conn}{pt}。")
|
||
return "".join(sentences)
|
||
|
||
|
||
def draft_section(section: str, points: list[str], topic: str = "该问题") -> dict[str, Any]:
|
||
"""起草单个章节。"""
|
||
if section not in SECTION_TEMPLATES:
|
||
raise ValueError(f"未知章节:{section},应为 {SECTIONS}")
|
||
tpl = SECTION_TEMPLATES[section]
|
||
body = weave_points(section, points) if points else ""
|
||
|
||
paragraph_parts = []
|
||
if section == "introduction":
|
||
paragraph_parts.append(tpl["opening"].format(topic=topic))
|
||
if body:
|
||
paragraph_parts.append(body)
|
||
# 章节特定收束
|
||
if section == "introduction":
|
||
paragraph_parts.append(tpl["gap"])
|
||
paragraph_parts.append(tpl["contribution"])
|
||
elif section == "methods":
|
||
paragraph_parts.append(tpl["contribution"])
|
||
elif section == "results":
|
||
paragraph_parts.append(tpl["contribution"])
|
||
elif section == "discussion":
|
||
paragraph_parts.append(tpl["gap"])
|
||
paragraph_parts.append(tpl["contribution"])
|
||
|
||
return {
|
||
"section": section,
|
||
"section_cn": SECTION_CN[section],
|
||
"paragraph": "".join(paragraph_parts),
|
||
"hint": tpl["hint"],
|
||
}
|
||
|
||
|
||
def draft_paper(spec: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""按 spec 起草多个章节。spec = {topic, sections: {results: [...], ...}}"""
|
||
topic = spec.get("topic", "该问题")
|
||
out = []
|
||
for section in SECTIONS:
|
||
pts = spec.get("sections", {}).get(section)
|
||
if pts is not None:
|
||
out.append(draft_section(section, pts, topic=topic))
|
||
return out
|
||
|
||
|
||
def render_markdown(drafts: list[dict[str, Any]], topic: str = "") -> str:
|
||
lines = ["# 论文段落初稿"]
|
||
if topic:
|
||
lines.append(f"\n主题:{topic}")
|
||
lines += ["",
|
||
"> 由 draftsmith 起草。这是把要点套进学术句式的初稿骨架,"
|
||
"`[...]` 处需你填具体数值、引用与对象,工具不编造数据。", ""]
|
||
for d in drafts:
|
||
lines.append(f"## {d['section_cn']}({d['section']})")
|
||
lines.append("")
|
||
lines.append(d["paragraph"])
|
||
lines.append("")
|
||
lines.append(f"> 写作提示:{d['hint']}")
|
||
lines.append("")
|
||
lines += ["---", "", "由 draftsmith 生成。初稿仅供起步,事实与表达需你核定。"]
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
p = argparse.ArgumentParser(prog="draftsmith", description="论文段落起草器")
|
||
p.add_argument("--section", choices=SECTIONS, help="起草单个章节")
|
||
p.add_argument("--points", nargs="+", help="要点列表(配合 --section)")
|
||
p.add_argument("--topic", default="该问题", help="研究主题(用于引言开头)")
|
||
p.add_argument("--spec", type=Path, help="多章节 JSON:{topic, sections:{results:[...]}}")
|
||
p.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
||
p.add_argument("--output", type=Path)
|
||
args = p.parse_args(argv)
|
||
|
||
if args.spec and args.spec.exists():
|
||
spec = json.loads(args.spec.read_text(encoding="utf-8-sig"))
|
||
drafts = draft_paper(spec)
|
||
topic = spec.get("topic", "")
|
||
elif args.section:
|
||
drafts = [draft_section(args.section, args.points or [], topic=args.topic)]
|
||
topic = args.topic
|
||
else:
|
||
print("错误:请用 --section + --points,或用 --spec 提供多章节要点。", file=sys.stderr)
|
||
return 2
|
||
|
||
out = (json.dumps(drafts, ensure_ascii=False, indent=2) if args.format == "json"
|
||
else render_markdown(drafts, topic))
|
||
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())
|