Compare commits

...

1 Commits

Author SHA1 Message Date
Ct201314 6d4db5cec5 feat(skills): add slidecast academic research skill 2026-06-12 20:04:24 +08:00
5 changed files with 475 additions and 0 deletions

65
skills/slidecast/SKILL.md Normal file
View File

@ -0,0 +1,65 @@
---
name: slidecast
version: 1.0.0
description: "论文转 PPT 大纲生成器:把按标题分节的论文文本转成中文 PPT 大纲(标题页 + 各章节页:标题 + 3-5 条要点 + 演讲备注 + 总结页),演讲备注按章节类型智能适配;若装了 python-pptx 可一并导出 .pptx。当用户提到「论文转 PPT」「组会汇报」「答辩讲稿」「paper2ppt」「生成幻灯片大纲」时触发。"
metadata:
requires:
optional_bins: ["python"]
optional_pip: ["python-pptx"]
---
# slidecast论文转 PPT 大纲生成器)
组会前一晚把论文丢进去讲稿骨架就有了。slidecast 把论文切成一页页,每页一个
标题、几条要点、外加一句怎么讲的备注。
## 何时使用本技能
- 把自己的论文转成汇报 PPT
- 把读的文献转成 PPT 给组里讲
- 答辩前快速出一版讲稿
## 与同类工具的区别
同类工具多调 LLM 直接生成幻灯片内容。本技能**纯本地出大纲**:不联网、不调 LLM
按论文结构切分、按信号词提炼要点,生成讲稿骨架。亮点是**演讲备注按章节类型适配**
(方法页提示"讲直觉别陷公式"、结果页提示"先结论后数据"),这是单纯摘要给不出的。
python-pptx 为可选依赖,仅 .pptx 导出需要,未装时照常输出 Markdown 大纲。
## 三个核心能力
| 能力 | 说明 |
|------|------|
| 章节切分 | 按 Markdown 标题或「数字 章节名」识别论文结构 |
| 要点提炼 | 按信号词(提出/结果/表明…)从各节抽 3-5 条要点 |
| 大纲渲染 | 标题页 + 章节页(标题+要点+演讲备注)+ 总结页;可选导出 .pptx |
## 工作流
```bash
# 出 Markdown 大纲
python scripts/cast.py --input paper.md --title "我的汇报" --output slides.md
# 同时导出 pptx需 python-pptx
python scripts/cast.py --input paper.md --pptx slides.pptx
```
| 参数 | 说明 |
|------|------|
| `--input` | 论文文本/Markdown*或用 `--text` |
| `--title` | 汇报标题 |
| `--max-points` | 每页最多要点数(默认 5 |
| `--format` | `markdown`(默认)或 `json` |
| `--output` | Markdown 大纲输出 |
| `--pptx` | 同时导出 .pptx需 python-pptx |
## 注意事项
- 论文用 Markdown 标题(`##`)分节时切分最准;纯文本按「数字 章节名」兜底。
- 要点是从原文抽取的句子,不改写、不编造;`____` 占位需你填。
- .pptx 导出需 `pip install python-pptx`;未装时输出 Markdown 大纲并提示。
## References
- [outline-rules.md](references/outline-rules.md) — 章节切分与要点提炼规则
- [usage.md](references/usage.md) — 输入建议与讲稿打磨流程

View File

@ -0,0 +1,47 @@
# 章节切分与要点提炼规则
## 章节切分
按优先级识别章节边界:
1. **Markdown 标题**`#` / `##` / `###` 开头的行(最准,推荐输入用 Markdown
2. **数字编号标题**`1`、`1.`、`一、` 等开头且较短(< 30 的行
3. **章节名提示**:含「摘要/引言/方法/实验/结果/讨论/结论」等且较短的行。
正文归到最近的章节标题下;开头无标题的内容归入「(引言)」。
## 要点提炼
从每节正文按句切分后打分,取分高的若干句(保持原文顺序):
| 加分项 | 说明 |
|--------|------|
| 含信号词 | 我们/提出/结果/表明/因此/发现/贡献/方法… 每命中 +1 |
| 长度适中 | 10-120 字 +1太短无信息、太长不适合上 slide |
每页默认最多 5 条要点(`--max-points` 可调)。要点是**从原文抽取的原句**,不改写、
不编造。
## 幻灯片结构
| 页 | 内容 |
|----|------|
| 标题页 | 汇报标题 + 汇报人/日期占位 |
| 章节页 | 每节一页:标题 + 要点 + 演讲备注 |
| 总结页 | 主要结论 + 局限 + 致谢 |
## 演讲备注适配
备注按章节类型给不同建议:
| 章节类型 | 备注侧重 |
|---------|---------|
| 引言 | 用例子引出问题30 秒讲清重要性 |
| 方法 | 讲直觉别陷公式,配示意图 |
| 结果/实验 | 先结论后数据,指图讲 |
| 结论/讨论 | 重申贡献,坦诚局限,给方向 |
## 局限
- 要点是抽取式,不做摘要式压缩,复杂长句可能整句上 slide需你精简。
- 切分依赖标题结构,无标题的连续长文切分效果有限。

View File

@ -0,0 +1,50 @@
# 输入建议与讲稿打磨流程
## 输入怎么准备
效果最好的输入是**带 Markdown 标题的论文文本**
```
## 引言
……
## 方法
……
## 实验
……
```
这样章节切分最准。没有标题的纯文本也能用,工具按「数字 章节名」兜底,但建议先
把章节标题补上。
## 推荐流程
1. 把论文(或文献)整理成带标题的文本。
2. 运行生成 Markdown 大纲,通读一遍。
3. 删掉不必要的要点、精简过长句子。
4. 填好 `____` 占位(汇报人、结论、局限)。
5. 需要 .pptx 就加 `--pptx`,导出后在 PowerPoint 里配图、调版式。
## 一页几条要点
- 默认每页 ≤ 5 条,建议讲的时候每页 3-4 条最舒服。
- 内容太多就拆成两页,别把一页塞满。
## 配图建议
工具只出文字骨架,配图靠你:
- 方法页配一张方法示意图(可用 figforge 出)。
- 结果页配关键结果图(可用 figforge 出)。
- 别整页纯文字,讲的时候听众会走神。
## 与其它技能配合
- figforge 出图 → 插到结果/方法页。
- 自己的论文draftsmith 起草 → polishpro 润色 → slidecast 转汇报。
## 局限
- 要点为抽取式,复杂论文仍需人工精简与重组。
- 演讲备注是通用建议,具体讲法结合你的听众调整。

View File

@ -0,0 +1,225 @@
"""slidecast论文转 PPT 大纲生成器。
把一篇论文按标题分节的文本转成一份可直接拿去讲的中文 PPT 大纲每页一个标题
3-5 条要点外加一句演讲备注组会前一晚把论文丢进去讲稿骨架就有了
三个核心能力
1. 章节切分 Markdown 标题# / ##)或「数字 章节名」识别论文结构,把正文
切到各章节名下无显式标题时按段落兜底
2. 要点提炼从每节正文按句抽取要点优先含我们/提出/结果/表明/因此
信号词的句子每页压到 3-5 过长正文自动取最有信息量的若干句
3. 大纲渲染生成标题页 + 各章节页标题 + 要点 + 演讲备注输出 Markdown
大纲若安装了 python-pptx可一并导出 .pptx 文件
纯本地纯标准库出大纲python-pptx 为可选依赖 .pptx 导出需要未安装时给
提示并照常输出 Markdown 大纲它给的是讲稿骨架具体讲什么配什么图由你定
用法
python cast.py --input paper.md --output slides.md
python cast.py --input paper.md --pptx slides.pptx
"""
from __future__ import annotations
import argparse
import json
import re
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
# 要点信号词:含这些词的句子更可能是核心论述
SIGNAL_WORDS = ["我们", "提出", "结果", "表明", "因此", "发现", "贡献", "方法",
"实验", "证明", "显示", "本文", "propose", "result", "show",
"we ", "method", "conclude", "contribution"]
# 常见章节名(用于兜底识别)
SECTION_HINTS = ["摘要", "abstract", "引言", "introduction", "背景", "相关工作",
"related work", "方法", "method", "实验", "experiment", "结果",
"result", "讨论", "discussion", "结论", "conclusion"]
def split_paper(text: str) -> list[dict[str, Any]]:
"""把论文切成章节:[{title, body}]。"""
text = text.lstrip("\ufeff")
lines = text.splitlines()
sections: list[dict[str, Any]] = []
cur_title = None
cur_body: list[str] = []
def flush():
if cur_title is not None or cur_body:
sections.append({"title": cur_title or "(引言)",
"body": "\n".join(cur_body).strip()})
for line in lines:
s = line.strip()
if not s:
continue
# Markdown 标题
m = re.match(r"^#{1,3}\s+(.*)$", s)
# 数字编号标题1 / 1. / 一、 + 短标题
m2 = re.match(r"^((?:\d+[.、]?|[一二三四五六七八九十]+、)\s*\S.{0,30})$", s)
is_hint = any(h in s.lower() for h in SECTION_HINTS) and len(s) < 20
if m:
flush()
cur_title, cur_body = m.group(1).strip(), []
elif (m2 or is_hint) and len(s) < 30:
flush()
cur_title, cur_body = s, []
else:
cur_body.append(s)
flush()
return [sec for sec in sections if sec["body"] or sec["title"]]
def _split_sentences(text: str) -> list[str]:
parts = re.split(r"(?<=[。.!?])\s*", text)
return [p.strip() for p in parts if len(p.strip()) > 4]
def extract_points(body: str, max_points: int = 5) -> list[str]:
"""从章节正文提炼要点。"""
sentences = _split_sentences(body)
if not sentences:
return []
# 按信号词命中数 + 适中长度打分
scored = []
for s in sentences:
score = sum(1 for w in SIGNAL_WORDS if w in s.lower())
length = len(s)
if 10 <= length <= 120:
score += 1
scored.append((score, s))
# 取分高的,但保持原文顺序
top = sorted(scored, key=lambda x: -x[0])[:max_points]
top_set = {s for _, s in top}
ordered = [s for s in sentences if s in top_set]
return ordered[:max_points]
def build_slides(text: str, title: str = "", max_points: int = 5) -> dict[str, Any]:
sections = split_paper(text)
slides = []
# 标题页
slides.append({"type": "title", "title": title or "论文汇报",
"points": ["汇报人____", "日期____"], "note": "开场:一句话说明本文做了什么。"})
for sec in sections:
points = extract_points(sec["body"], max_points=max_points)
if not points and not sec["body"]:
continue
note = _speaker_note(sec["title"], points)
slides.append({"type": "content", "title": sec["title"],
"points": points or ["[本节要点待补]"], "note": note})
# 结尾页
slides.append({"type": "end", "title": "总结与致谢",
"points": ["主要结论____", "局限与未来工作____", "谢谢,欢迎提问"],
"note": "收束:呼应开场,留时间提问。"})
return {"slide_count": len(slides), "slides": slides}
def _speaker_note(section_title: str, points: list[str]) -> str:
t = section_title.lower()
if any(k in t for k in ["方法", "method"]):
return "讲清楚做法的直觉,别陷入公式细节;配一张方法示意图。"
if any(k in t for k in ["结果", "result", "实验", "experiment"]):
return "先说结论再给数据;指着图讲,强调关键对比。"
if any(k in t for k in ["引言", "introduction", "背景"]):
return "用一个具体例子引出问题30 秒内让听众明白为什么重要。"
if any(k in t for k in ["结论", "conclusion", "讨论", "discussion"]):
return "重申贡献,坦诚局限,给出未来方向。"
return "每条要点停顿,给听众消化时间。"
def render_markdown(data: dict[str, Any]) -> str:
lines = [f"# PPT 大纲(共 {data['slide_count']} 页)", "",
"> 由 slidecast 生成。每页一个标题 + 要点 + 演讲备注,`____` 处需你填。", ""]
for i, sl in enumerate(data["slides"], start=1):
lines.append(f"## 第 {i} 页:{sl['title']}")
lines.append("")
for p in sl["points"]:
lines.append(f"- {p}")
lines.append("")
lines.append(f"> 演讲备注:{sl['note']}")
lines.append("")
lines += ["---", "", "由 slidecast 生成。讲稿骨架,配图与具体措辞由你定。"]
return "\n".join(lines)
def export_pptx(data: dict[str, Any], path: Path) -> str:
"""导出 .pptx需 python-pptx"""
try:
from pptx import Presentation
from pptx.util import Pt
except ImportError as exc:
raise RuntimeError(
"导出 .pptx 需要 python-pptx。请先安装pip install python-pptx\n"
"Markdown 大纲不需要它,可直接使用。)"
) from exc
prs = Presentation()
for sl in data["slides"]:
layout = prs.slide_layouts[0] if sl["type"] == "title" else prs.slide_layouts[1]
slide = prs.slides.add_slide(layout)
slide.shapes.title.text = sl["title"]
if len(slide.placeholders) > 1:
body = slide.placeholders[1].text_frame
body.text = sl["points"][0] if sl["points"] else ""
for p in sl["points"][1:]:
para = body.add_paragraph()
para.text = p
# 演讲备注
slide.notes_slide.notes_text_frame.text = sl["note"]
path.parent.mkdir(parents=True, exist_ok=True)
prs.save(str(path))
return str(path)
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(prog="slidecast", description="论文转 PPT 大纲生成器")
p.add_argument("--input", type=Path, help="论文文本/Markdown 文件")
p.add_argument("--text", help="直接传入论文文本")
p.add_argument("--title", default="", help="汇报标题")
p.add_argument("--max-points", type=int, default=5, help="每页最多要点数")
p.add_argument("--format", choices=["markdown", "json"], default="markdown")
p.add_argument("--output", type=Path, help="Markdown 大纲输出")
p.add_argument("--pptx", type=Path, help="同时导出 .pptx需 python-pptx")
args = p.parse_args(argv)
if args.input and args.input.exists():
text = args.input.read_text(encoding="utf-8-sig", errors="replace")
elif args.text:
text = args.text
else:
print("错误:请用 --input 或 --text 提供论文文本。", file=sys.stderr)
return 2
data = build_slides(text, title=args.title, max_points=args.max_points)
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)
if args.pptx:
try:
path = export_pptx(data, args.pptx)
print(f"已导出 {path}")
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
return 3
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,88 @@
"""slidecast 单元测试。"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import pytest
from cast import split_paper, extract_points, build_slides, render_markdown, _speaker_note
PAPER = """# 标题
## 引言
医学分割很重要然而标注稀缺本文提出对比学习方法
## 方法
我们设计了预训练框架该框架学习表征
## 结果
结果表明方法显著优于基线消融证明预训练贡献最大
"""
class TestSplitPaper:
def test_sections(self):
secs = split_paper(PAPER)
titles = [s["title"] for s in secs]
assert "引言" in titles and "方法" in titles and "结果" in titles
def test_numeric_heading(self):
secs = split_paper("1. 引言\n这是引言内容很长一段。\n2. 方法\n这是方法内容。")
titles = [s["title"] for s in secs]
assert any("引言" in t for t in titles)
def test_bom(self):
secs = split_paper("\ufeff## 引言\n内容内容内容。")
assert secs
class TestExtractPoints:
def test_signal_words_prioritized(self):
body = "天气不错。我们提出了一种新方法。结果表明它很好。随便一句废话。"
pts = extract_points(body, max_points=2)
# 含信号词的句子应被选中
joined = "".join(pts)
assert "提出" in joined or "结果" in joined
def test_max_points(self):
body = "".join([f"我们提出方法{i}" for i in range(10)]) + ""
pts = extract_points(body, max_points=3)
assert len(pts) <= 3
def test_empty(self):
assert extract_points("") == []
def test_preserves_order(self):
body = "我们提出A。我们提出B。我们提出C。"
pts = extract_points(body, max_points=3)
assert pts.index("我们提出A。") < pts.index("我们提出B。")
class TestSpeakerNote:
def test_method_note(self):
assert "直觉" in _speaker_note("方法", [])
def test_result_note(self):
assert "结论" in _speaker_note("实验结果", [])
class TestBuildSlides:
def test_has_title_and_end(self):
data = build_slides(PAPER, title="测试汇报")
assert data["slides"][0]["type"] == "title"
assert data["slides"][-1]["type"] == "end"
def test_content_slides(self):
data = build_slides(PAPER)
content = [s for s in data["slides"] if s["type"] == "content"]
assert len(content) >= 3
def test_render(self):
md = render_markdown(build_slides(PAPER, title="X"))
assert "PPT 大纲" in md
assert "演讲备注" in md
assert "第 1 页" in md
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))