简介:Paper2Slides 是一款面向科研人员、教师和学生的开源演示稿生成工具,可将 PDF、Word、Excel、Markdown 等多种文档一键转换为专业的幻灯片或学术海报,自动完成排版与设计。它基于 RAG 技术对文档内容进行精准抽取和索引,确保每页演示材料都能对应原始出处,避免信息偏差,同时提供多样化主题、自然语言样式定制、断点续作和实时预览,适合科研展示、会议汇报与教学演示。压缩包共含 96 个文件,大小 18.89MB,其中以 52 个 Python 核心源码、13 个 JSX 前端组件、6 个 Shell 管理脚本为主,另附示例 PDF、预览图和说明文档,目录结构清晰,便于在 Python 环境中快速部署和二次开发。目前项目已有 146 人浏览学习。完整源码涵盖后端 API、前端交互界面、提示词模板和一键启停脚本,并附带多种主题的真实输出样例,帮助读者快速掌握从论文解析、内容抽取、样式定制到最终渲染的完整链路,可显著缩短制作演示材料的时间。
1. Paper2Slides:把 PDF/Word 一键变成 PPT 和海报,这套源码到底怎么落地
Paper2Slides 解决的痛点很具体:一篇几十页的 PDF 论文或 Word 方案,人工提炼成演讲稿往往要耗掉半天,而内容漏提、排版返工是常态。我拿到这套源码后,核对了它最核心的三层能力——文档解析、内容抽取、自动排版。它对 PDF 和 Word 的版面做了结构化拆解,结合规则打分抽取标题和关键段落,最后通过模板把内容渲染到 PPT 和海报里。适合三类人用:经常做论文汇报的研究生、帮客户改方案的咨询顾问、需要批量生成课程讲义的老师。这套源码不是现成的在线网站,而是一套可以自行部署、二次开发的本地工具链。往下我从源码里真实存在的模块和参数讲起,把每一步执行细节和坑位都摊开讲。
2. 文档解析层:PyMuPDF 和 python-docx 怎么把版面拆干净
2.1 为什么选 PyMuPDF 而不是 pdfplumber:解析速度与版面还原的取舍
拆 PDF 的关键,是把「视觉上的段落」还原成「结构化的文本块」。源码里用 PyMuPDF(fitz)做主解析器,我看完第一反应是这对的。PyMuPDF 的page.get_text("dict")能直接拿到每个文本块的坐标、字号、字体名,这对后续判断标题层级至关重要。pdfplumber 也能做类似事情,但它对复杂版面处理更细致,代价是慢——几十页论文跑下来,PyMuPDF 三秒完成,pdfplumber 可能要三十秒。对于批量转换场景,速度优势太明显了,所以我一般会直接沿用 PyMuPDF。
import fitz # PyMuPDF def parse_pdf_blocks(pdf_path: str, skip_small_text: float = 8.0) -> list[dict]: """按块提取 PDF 文本及版面信息 Args: pdf_path: 输入的 PDF 文件路径 skip_small_text: 滤掉字号小于该值的块,默认 8pt,用来去页眉页脚 Returns: blocks: 每个块包含 text / size / bbox / page 等字段 """ doc = fitz.open(pdf_path) blocks = [] for page_no, page in enumerate(doc): page_dict = page.get_text("dict") # 获取页面结构化数据 for block in page_dict.get("blocks", []): if block.get("type") != 0: # 0 代表文本块,1 代表图片块 continue for line in block.get("lines", []): for span in line.get("spans", []): text = span.get("text", "").strip() if not text: continue if span.get("size", 0) < skip_small_text: continue blocks.append({ "page": page_no + 1, "text": text, "size": round(span.get("size", 0), 1), "bbox": span.get("bbox", []), # x0, y0, x1, y1 "font": span.get("font", ""), }) doc.close() return blocks这段代码的要点在span层级。PyMuPDF 的层级结构是 page → blocks → lines → spans,一个 span 通常对应一段连续属性(同字号、同字体)的文字。我按 span 拆,保留的是最细粒度的信息,后续做标题检测时才不会把同字号但不同内容的文字混在一起。skip_small_text这个参数很有用,论文页脚页码一般在 7pt 左右,设置 8pt 能直接滤掉。真实场景里,如果你处理的文档正文本身就用小字号排版,这个值要降到 6.5,否则会误删正文。
2.2 Word 侧解析:python-docx 的段落与表格结构还原
PDF 解析拿到的是坐标和字号,Word 解析要更省心一点,因为 Word 本身就是流式文档,天然带段落和样式结构。源码里用 python-docx 遍历document.paragraphs和document.tables,把标题样式(Heading 1/2)映射成层级标记,把普通正文段落直接加入内容池。这里最容易漏的是表格里的文字,很多人只遍历 paragraphs 忘了 tables,结果表格内容全部丢失。
from docx import Document def parse_docx(docx_path: str) -> list[dict]: """解析 Word 文档,保留标题层级与表格内容""" doc = Document(docx_path) items = [] for para in doc.paragraphs: text = para.text.strip() if not text: continue style_name = para.style.name # 如 "Heading 1" / "Normal" level = 0 if style_name and style_name.lower().startswith("heading"): level = int(style_name.split()[-1]) items.append({ "type": "heading" if level > 0 else "body", "level": level, "text": text, }) for table in doc.tables: for row in table.rows: row_text = " | ".join(cell.text.strip() for cell in row.cells) if row_text.strip(" |"): items.append({"type": "table", "level": 0, "text": row_text}) return items这个函数返回的items直接对接后续的内容抽取模块。style_name.split()[-1]是把 "Heading 1" 拆成数字层级的关键写法,注意如果文档用了「标题 1」这种中文样式名,程序要改成判断style_name.startswith("标题")。表格转成cell1 | cell2的文本形式,是为了后续在 PPT 里能快速切成表格或分栏列表。Word 里还有一种情况是「正文段落但用户手动加粗放大冒充标题」,python-docx 在paragraph.style之外拿不到有效的字号信息时,需要靠run.font.size做补充判断,否则标题层级的准确率会肉眼可见地下降。
3. 内容抽取:标题检测、关键段打分和 OCR 兜底
3.1 标题检测与关键段打分:阈值设置直接决定幻灯片质量
内容抽取是整个 Paper2Slides 的决策核心。普通文本块进来之后,源码先按字号和字体判断是否属于标题:字号是全文最大、且字体为粗体时直接判定为一级标题;字号第二档算二级标题。这一步看着简单,实际最坑——论文里的图表注释、基金项目说明经常也是大字号,容易把「摘要」「致谢」误判成正文。我建议在源码默认逻辑上做一处改动:只认「行首位置小于页面宽度 15%」的块为标题,因为正常标题都是左对齐且顶格排的,页眉页脚虽然也可能顶格,但它们已经在上一步被skip_small_text滤掉了。判完标题后,关键段打分用一个加权函数:含方法关键词(we propose / 我们提出 / experiment)的段落加 2 分,长度在 80 到 200 字之间加 1 分,含数字或百分比结果再加 1 分。最后每页按得分取前 N 段进幻灯片,N 默认是 3。
SCORE_KEYWORDS = [ "propose", "we present", "experiment", "result", "conclusion", "我们", "实验", "结果表明", "提出", "实现" ] def score_paragraph(text: str, font_size: float, max_size: float) -> float: """给段落打分,分高者优先进入幻灯片""" score = 0.0 t = text.lower() # 关键词命中 for kw in SCORE_KEYWORDS: if kw in t: score += 2.0 break # 同段只加一次,避免关键词堆叠刷分 # 长度适中的段落更适合做要点 length = len(text) if 80 <= length <= 200: score += 1.0 # 包含数字和百分比,说明有数据支撑,优先展示 has_number = any(ch.isdigit() for ch in text) has_percent = "%" in text or "%" in text if has_number: score += 0.5 if has_percent: score += 0.5 # 字号越接近标题,越可能是核心结论 if font_size >= max_size * 0.8: score += 1.0 return score这个分段计分逻辑,基本上决定了一张幻灯片里能看到什么。SCORE_KEYWORDS是中英文混杂的,因为论文和方案文档混用双语的情况非常多。max_size * 0.8是经验值:如果全文最大字号是 18pt,那 14.4pt 以上的文字大概率是结论或者强调句,给 1 分很合理。但这里有个明显边界——纯英文论文里数字和百分比的权重很低,公式多的论文抽出来往往是摘要原句,所以实话说这版打分规则对「实验型论文」更友好,纯理论类论文要自己加大prove / theorem / 定义这类词。
3.2 OCR 兜底:扫描版 PDF 的图片文字提取与中文识别
扫描版 PDF 在源码里是个独立分支,它的存在是为了应对那些「页面全是图片」的论文。PyMuPDF 对扫描版输出的文本块几乎为空,需要走 OCR 通道。源码的默认链路是:PyMuPDF 先把 PDF 页面渲染成高分辨率 PNG,再用 PaddleOCR 做文字识别。这里有两个硬性参数,zoom决定渲染分辨率,我一般固定设 2.0,也就是 144 DPI;低于 2.0 时,中文小五号字体的识别准确率会明显下滑,高于 3.0 又会让每页处理时间翻倍。
import fitz from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) def ocr_pdf_page(page) -> list[dict]: """把单个 PDF 页面转成图片后走 OCR,返回带坐标的文本块""" zoom = 2.0 mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat, alpha=False) img_path = "/tmp/page.png" pix.save(img_path) result = ocr.ocr(img_path, cls=True) text_blocks = [] for line in result[0]: if line is None: continue box = line[0] # 四个角点坐标 text = line[1][0] # 识别出的文本 conf = line[1][1] # 置信度 if conf < 0.7: continue text_blocks.append({ "text": text, "bbox": [box[0][0], box[0][1], box[2][0], box[2][1]], "size": None, # OCR 结果没有字号,由后续宽度估算 }) return text_blocksOCR 分支跑一轮的耗时大概是直接解析的 20 倍,所以我在源码基础上加了一个前置判断:先统计页面里图片块面积占整页的比例,超过 60% 才走 OCR。这个判断能省掉大量纯文本 PDF 的无效 OCR。use_angle_cls=True是开方向分类,处理扫描件里颠倒的页面非常有效,但会增加 5% 的耗时,权衡下来值得开。OCR 出来的块没有字号属性,只有盒子的宽高,后续判断标题时只能用「盒高大于同行盒高均值 1.5 倍」这种近似逻辑,准确率会比基于字号的差一截,这是目前这套源码里最明显的能力边界。
4. 自动排版:模板 JSON 到 PPT 和海报的渲染管线
4.1 模板 JSON 的字段约定:坐标、字体、色板和内容槽位
内容抽完之后是排版,排版不写死逻辑,而是读一份模板 JSON。源码里的模板结构是这样:全局定义画布宽高和默认字体,再定义一个slots数组,每个 slot 是一个内容占位区,指定了坐标、尺寸、对齐方式和允许的内容类型。这样做的好处是换肤不动代码,改 JSON 就能套用新的视觉风格。我见过不少现成的 pptx 生成库,模板靠修改 pptx 源文件实现,但 Paper2Slides 直接走 JSON,比起改 pptx,更透明也更好调试。
{ "canvas": {"width": 1920, "height": 1080}, "default_font": {"name": "Microsoft YaHei", "size": 20, "color": "#333333"}, "theme": {"primary": "#2F5496", "accent": "#C00000", "background": "#FFFFFF"}, "slots": [ {"id": "title", "type": "text", "x": 80, "y": 60, "w": 1760, "h": 120, "align": "left", "font_size": 44, "font_bold": true, "max_len": 40}, {"id": "bullet_list", "type": "list", "x": 120, "y": 220, "w": 1700, "h": 700, "align": "left", "font_size": 24, "max_items": 4}, {"id": "stat_badge", "type": "key_value", "x": 80, "y": 960, "w": 500, "h": 80, "font_size": 28} ] }模板字段的核心是「内容槽位」而不是「绝对内容」。max_len和max_items是给上一层的抽取模块用的,告诉它标题最多放多少字、列表最多放几行。我在模板里会刻意把bullet_list的max_items设成 4,强行控制单页信息密度。theme里的primary和accent会被渲染成标题色、强调色,调色时改这两个值就行。最大的坑是字体名:在 Linux 服务器上跑,Microsoft YaHei不存在,渲染出来的 PPT 打开会是默认字体,我通常改成Noto Sans CJK SC或者做一层「目标机器字体名」的映射。
4.2 从 JSON 到 PPT/海报:渲染脚本、对齐策略与文字溢出处理
模板解析完后是两条渲染路径:PPT 走 python-pptx,海报走 PIL。两条路径读同一个 JSON,但是绘制逻辑完全不同。PPT 路径的核心是add_textbox+set_text_frame,它拿到 slot 参数,按坐标创建文本框;海报路径则用ImageDraw直接往画布上画文字。为了防止文字溢出,我在源码逻辑里补了一道防线——按字号估算字符串像素宽度,如果超过 slot 宽度,就逐字递减直到装下为止。
from pptx import Presentation from pptx.util import Inches, Pt def render_ppt_slot(slide, slot: dict, content: str, theme: dict): """在指定幻灯片上渲染一个文本槽位""" left = Inches(slot["x"] / 96) # 模板坐标单位是像素,96 DPI 换算成英寸 top = Inches(slot["y"] / 96) width = Inches(slot["w"] / 96) height = Inches(slot["h"] / 96) textbox = slide.shapes.add_textbox(left, top, width, height) tf = textbox.text_frame tf.word_wrap = True tf.clear() p = tf.paragraphs[0] p.text = content p.font.size = Pt(slot.get("font_size", 20)) p.font.bold = slot.get("font_bold", False) p.font.color.rgb = theme.get("primary", 0x2F5496) return textbox这段代码没什么魔法,关键在坐标单位换算。模板 JSON 里我习惯以 96 DPI 像素为基准,python-pptx 的接口却用 Inches 和 Pt,所以每次渲染前都要做一次像素到英寸的除法。word_wrap = True必须开,不开的话长文本会横向溢出文本框。字体颜色theme.get("primary", 0x2F5496)的写法是从 JSON 里取色板的主题色,这样模板换色时渲染代码完全不动。PPT 路径还有一个隐藏行为:tf.paragraphs[0]默认第一段是空的,clear()一下再赋值,不然会出现标题上方多出一行空白的毛病。海报渲染的路径不同,PIL 的textbbox要先量文本尺寸再居中,如果同一条内容在 PPT 和海报里显示效果不一致,十有八九是两边字体渲染宽度算得不一样。
5. 避坑:PDF 解析和 PPT 生成的五个高频翻车现场
5.1 双栏论文被当成单栏读,段落顺序错乱
现象:双栏论文生成的 PPT 里,左栏后半段接上了右栏开头的文字,语义完全断裂。
原因:PyMuPDF 的get_text("dict")默认按页面物理顺序输出块,双栏版面的物理顺序是「左栏上半 + 右栏上半」,而不是人眼阅读的「左栏整条 + 右栏整条」。
解决:解析后增加一步按bbox[1](y 坐标)和bbox[0](x 坐标)排序的逻辑:同一 y 带内的块先排,x 小于页面一半的归左栏,大于一半的归右栏。对 A4 页面,栏界值取 595/2 像素,用页宽的一半做阈值比较稳。
5.2 同一句被跨行拆成多个片段,抽取时重复计分
现象:一段正常的文字被 split 成三四段,每段只有十几个字,关键段打分时因为均不超过 80 字,得分极低,结果幻灯片里缺了论文最核心的方法描述。
原因:PDF 行的切分是按渲染行的,一个段落如果因为对齐折行,在lines层级就是独立行,源码没有做行合并。
解决:在输出blocks前做一次「行合并」——当相邻两行的bbox左右边界近似相等、字体名一致、且行间距小于当前字号 1.6 倍时,判定为同一段落,用空格拼接。我顺手把拼接后的段落长度上限放宽到 500 字,避免把整个摘要拼成一大坨。
5.3 公式和特殊符号变成乱码或直接丢失
现象:生成 PPT 后公式位置出现空字符串或字体变框,尤其希腊字母和数学符号。
原因:PyMuPDF 能提取公式区域的字符,但公式通常由特定字体渲染,提取后文本是 Unicode 数学符号;python-pptx 写入时如果默认字体不支持该字符集,显示就成了方框。
解决:渲染前做一层字符过滤——检测文本里的数学字母数字符号区间,把这些字符包的字体名在后面追加"Cambria Math",显示时优先走该字体。更粗暴但有效的方案是:检测到公式占比超过段落 30% 时,整段不进入 PPT 文本,而是用 PDF 页面截图代替。
5.4 Word 表格列宽不一致导致 PPT 内容错位
现象:Word 表格有七列,PPT 渲染时却按五列均分,数据串行,数字和表头对不上。
原因:python-docx 的table.rows[i].cells返回的是当前行的单元格列表,但表格可能存在合并单元格或列宽不均,直接按列表顺序渲染就会错位。
解决:在解析 Word 表格时计算每一列的实际起始坐标,用cell.width累加出列偏移量;渲染 PPT 时按偏移量比例设置表格列宽,而不是均分。如果源表格存在合并单元格,我建议直接放弃表格形态,把每行渲染成「标签:值」的扁平列表,信息不丢可读性反而更好。
5.5 OCR 识别后中文标点变成英文标点
现象:OCR 出来的文字里逗号、句号、引号全部变成半角,PPT 里排版参差不齐。
原因:PaddleOCR 识别中文时标点符号默认映射到英文半角字符,这不算 bug,但对中文排版来说,全角标点的宽度占位明显不同,导致换行位置和后端对齐计算全部偏掉。
解决:在 OCR 分支后加一个标点转换映射表,把,、.、?在全角中文上下文中替换成,。?——判断很简单,当前字符两侧都是 CJK 字符就做替换,否则不变。这个清洗函数每次做 OCR 批量转换后我都会强制跑一遍,已经成了肌肉记忆。
6. 验证与调试:命令行检查、可视化预览和批量回归
排版完的脚本最怕「单张对、批量错」。我习惯用一套三层验证法,第一层是对单页输出做数据校验。Paper2Slides 的源码里有一个validate_slide()函数,逐项检查每个文本块是否越界、是否重叠、字号是否小于模板允许的最小值。命令行跑起来长这样:
python validate_slides.py --input ./output/paper.pptx --report ./report.json这个脚本会把每页的异常项写进 JSON,比如{"slide": 3, "type": "overflow", "slot_id": "bullet_list"}。第二层是可视化预览——把 PPT 每页用 LibreOffice 转成 PNG,拼在一张长图上快速翻看,这一步能发现坐标越界检查发现不了的视觉问题:
soffice --headless --convert-to pdf ./output/paper.pptx pdftoppm -png -r 80 ./output/paper.pdf ./preview/page-r 80是 80 DPI 预览分辨率,够看清排版轮廓又不会太慢。我每次拿到新的输入文档,都会强制走一遍这个预览流程,往下一翻就能看到哪页标题孤悬在底部、哪页文字挤成了长条。第三层是批量回归——准备一个测试文档集,包含双栏论文、扫描版 PDF、带复杂表格的 Word、纯中文方案,跑完后对比新旧版本生成结果,重点看两个指标:关键段命中率有没有下降、文字溢出页数有没有增加。这套回归跑完,才敢放心批量转换。
从那以后,我每次拿到新文档类型都会强制走一遍「解析 → 打分 → 渲染 → 预览」四步流程,哪怕只是多了一个新字体,也要跑一次回归确认没有把排版带崩。Paper2Slides 这套源码的价值就在于此:它把文档到演示稿的流程拆成了可调试的独立阶段,每一层都能单独验证和替换,这让它从一个黑匣子变成了一套我能掌控的流水线。希望这篇笔记能帮你把这条流水线真正跑起来,少踩我踩过的那些坑。
本文还有配套的精品资源,点击获取