WPS AI 可以根据主题、文档或大纲生成整套 PPT。用自己的大纲来生成,内容最可控;但生成后可能漏掉章节、多出页面,或者残留“单击此处添加文本”。 这篇做
deck_check.py:生成前检查大纲,生成后对照导出的 .pptx 检查覆盖情况。
先看清成品
- 模拟导出的 PPT:漏掉的“成本与毛利”、多出的“谢谢观看”、第 3 页残留的“单击此处”全部被报出;改写过的标题(“核心数据概览”)仍能对上。
- 修复:标题放在普通文本框里时,第一版把所有章节都判为缺失;现在改用字号最大的文本框作为标题。
- pytest 3 项通过。
合规提醒:WPS AI 需要登录 WPS 账号,在官方客户端或网页版中使用,功能、次数和会员权益以官方说明为准(本文不列价格)。本文不调用任何非官方接口,也不自动化操作 WPS;脚本只在本地检查你自己的文件。把公司文档交给 AI 处理之前,请确认符合公司的数据安全规定。示例数据都是虚构的。
运行说明:box 上没有 WPS 客户端和账号,没有真正调用 WPS AI。本地脚本和测试都在 box 上真实运行过(LibreOffice 25.2.3、poppler-utils、python-pptx 1.0.2、openpyxl 3.1.5)。“AI 生成的”公式、摘要和 PPT 都是我自己写的样例,用来演示核对方法。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
deck_check.py lint |
检查大纲 |
deck_check.py cover |
检查覆盖情况;有缺失或占位文字时退出码为 1 |
outline.md |
示例大纲 |
make_mock_deck.py |
生成模拟导出的 PPT(不是 WPS 的输出) |
test_deck.py |
3 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.9 以上,pip install openpyxl python-pptx pytest |
| LibreOffice | A 篇用于无界面重算公式(apt install libreoffice-calc) |
| poppler-utils | B 篇用于提取 PDF 文字(apt install poppler-utils) |
| WPS Office | 最新版客户端,登录账号;旧版本可以在 ai.wps.cn 升级 |
关键事实
| 事实 | 来源 |
|---|---|
| 入口:WPS 演示 → WPS AI → AI 生成 PPT;可以输入主题、上传文档或粘贴大纲 | WPS 社区官方教程 |
| 生成的大纲可以编辑,也可以调整层级 | WPS 社区官方教程 |
WPS AI 接受什么样的大纲格式,官方教程没有给出规范。这里使用简单的 Markdown(# 标题 / ## 章节 / - 要点),粘贴后请在 WPS 的大纲编辑界面中确认层级。
文件结构
69-wps-ai-C-src/
├── deck_check.py
├── outline.md
├── make_mock_deck.py
└── test_deck.py
第 1 步:示例大纲
# 2026 年第三季度运营汇报
## 核心数据
- 营业收入 1,280 万元,同比增长 12.5%
- 新增注册用户 3.2 万人
## 渠道表现
- 线上渠道收入 760 万元,占比 59.4%
## 成本与毛利
- 营业成本 905 万元,毛利率 29.3%
## 第四季度计划
- 11 月 15 日上线新版会员系统
- 预计营业收入 1,400 万元
第 2 步:检查脚本
"""WPS AI“AI 生成 PPT”可以根据主题、文档或大纲生成整套幻灯片(WPS 社区官方教程)。这个工具做两件事:
1. lint:生成前检查 Markdown 大纲(# 标题 / ## 章节 / - 要点):层级、空章节、要点过长
2. cover:生成并导出 .pptx 后,检查每个章节是否都有对应的幻灯片、多出了哪些页、有没有残留的占位文字
只读本地文件,依赖 python-pptx。"""
import argparse, difflib, re, sys
from pptx import Presentation
PLACEHOLDER = re.compile(r"单击此处|点击此处|添加标题|添加文本|Click to add|Lorem ipsum", re.I)
def parse_outline(md):
title, secs, cur = None, [], None
for n, line in enumerate(md.splitlines(), 1):
if m := re.match(r"^#\s+(.+)", line): title = m.group(1).strip()
elif m := re.match(r"^##\s+(.+)", line): cur = {"title": m.group(1).strip(), "points": [], "line": n}; secs.append(cur)
elif m := re.match(r"^\s*[-*]\s+(.+)", line):
if cur is None: raise SystemExit(f"第 {n} 行:要点出现在任何 ## 章节之前")
cur["points"].append(m.group(1).strip())
elif re.match(r"^#{3,}\s", line): raise SystemExit(f"第 {n} 行:只支持 # 和 ## 两级标题,要点请用 - 列表")
return title, secs
def width(s): return sum(2 if "\u4e00" <= ch <= "\u9fff" else 1 for ch in s)
def lint(md, max_points=6, max_width=40):
title, secs = parse_outline(md); issues = []
if not title: issues.append("缺少 # 总标题")
if not secs: issues.append("没有 ## 章节")
for s in secs:
if not s["points"]: issues.append(f"「{s['title']}」没有要点")
if len(s["points"]) > max_points: issues.append(f"「{s['title']}」有 {len(s['points'])} 个要点,超过 {max_points},考虑拆成两页")
for p in s["points"]:
if width(p) > max_width: issues.append(f"「{s['title']}」的要点太长({width(p)} 宽度):{p[:20]}…")
dup = {t for t in [s["title"] for s in secs] if [s["title"] for s in secs].count(t) > 1}
issues += [f"章节标题重复:{t}" for t in dup]
return issues
def guess_title(slide):
"""没有标题占位符时(很多生成的幻灯片用普通文本框做标题),取字号最大的文本框;没有字号信息时取最上面的。"""
cands = []
for sh in slide.shapes:
if not sh.has_text_frame or not sh.text_frame.text.strip(): continue
sizes = [r.font.size.pt for p in sh.text_frame.paragraphs for r in p.runs if r.font.size]
cands.append((max(sizes, default=0), -(sh.top or 0), sh.text_frame.text.strip().splitlines()[0]))
return max(cands)[2] if cands else ""
def slides(path):
out = []
for i, sl in enumerate(Presentation(path).slides, 1):
title = sl.shapes.title.text_frame.text.strip() if sl.shapes.title is not None and sl.shapes.title.has_text_frame else ""
if not title:
title = guess_title(sl)
texts = [sh.text_frame.text for sh in sl.shapes if sh.has_text_frame]
out.append({"no": i, "title": title, "text": "\n".join(texts)})
return out
def cover(md, pptx_path, cutoff=0.6):
_, secs = parse_outline(md); sl = slides(pptx_path); used, rep = set(), {"missing": [], "matched": [], "extra": [], "placeholder": []}
for s in secs:
best = max(sl, key=lambda x: difflib.SequenceMatcher(None, s["title"], x["title"]).ratio(), default=None)
r = difflib.SequenceMatcher(None, s["title"], best["title"]).ratio() if best else 0
if r >= cutoff: rep["matched"].append((s["title"], best["no"], round(r, 2))); used.add(best["no"])
else: rep["missing"].append(s["title"])
rep["extra"] = [(x["no"], x["title"]) for x in sl if x["no"] not in used and x["no"] != 1]
rep["placeholder"] = [(x["no"], PLACEHOLDER.search(x["text"]).group()) for x in sl if PLACEHOLDER.search(x["text"])]
return rep
def main(argv=None):
ap = argparse.ArgumentParser(); sub = ap.add_subparsers(dest="cmd", required=True)
l = sub.add_parser("lint"); l.add_argument("outline")
c = sub.add_parser("cover"); c.add_argument("outline"); c.add_argument("pptx")
a = ap.parse_args(argv); md = open(a.outline, encoding="utf-8").read()
if a.cmd == "lint":
iss = lint(md); [print("[WARN]", i) for i in iss]; print("大纲 OK" if not iss else f"{len(iss)} 条提醒"); return 1 if iss else 0
r = cover(md, a.pptx)
for t, no, ratio in r["matched"]: print(f"✓ 「{t}」→ 第 {no} 页(相似度 {ratio})")
for t in r["missing"]: print(f"✗ 缺少章节「{t}」")
for no, t in r["extra"]: print(f"+ 第 {no} 页「{t}」不在大纲中")
for no, t in r["placeholder"]: print(f"! 第 {no} 页残留占位文字:{t}")
return 1 if r["missing"] or r["placeholder"] else 0
if __name__ == "__main__":
sys.exit(main())
第 3 步:模拟导出的 PPT
"""生成一个“模拟导出”的 pptx(我用 python-pptx 自己做的,不是 WPS AI 的真实输出):
漏掉“成本与毛利”,多一页“谢谢观看”,第 3 页残留占位文字,章节标题略有改写。"""
from pptx import Presentation
def build(path):
p = Presentation()
def add(title, body, layout=1):
s = p.slides.add_slide(p.slide_layouts[layout]); s.shapes.title.text = title
if layout == 1: s.placeholders[1].text = body
add("2026 年第三季度运营汇报", "", layout=0)
add("核心数据概览", "营业收入 1,280 万元")
add("渠道表现分析", "单击此处添加文本")
add("第四季度工作计划", "上线新版会员系统")
add("谢谢观看", "", layout=0)
p.save(path)
if __name__ == "__main__":
build("mock_deck.pptx"); print("ok")
第 4 步:测试
import pytest
from pptx import Presentation
from pptx.util import Inches, Pt
import deck_check as D, make_mock_deck as M
MD = open("outline.md", encoding="utf-8").read()
def test_lint_ok_and_issues():
assert D.lint(MD) == []
bad = "## 甲\n## 乙\n- " + "很长的要点" * 10 + "\n## 乙\n- x\n"
iss = D.lint(bad)
assert any("总标题" in i for i in iss) and any("「甲」没有要点" in i for i in iss)
assert any("太长" in i for i in iss) and any("重复" in i for i in iss)
with pytest.raises(SystemExit): D.lint("# t\n### 三级\n")
def test_cover_mock(tmp_path):
p = str(tmp_path / "d.pptx"); M.build(p); r = D.cover(MD, p)
assert r["missing"] == ["成本与毛利"] and r["extra"] == [(5, "谢谢观看")] and r["placeholder"] == [(3, "单击此处")]
def test_textbox_titles(tmp_path):
# 调试时发现:标题放在普通文本框里时,shapes.title 为空,所有章节都被判为缺失
p = Presentation()
for t in ["总标题", "核心数据", "渠道表现", "成本与毛利", "第四季度计划"]:
s = p.slides.add_slide(p.slide_layouts[6])
b = s.shapes.add_textbox(Inches(1), Inches(3), Inches(8), Inches(1)); b.text_frame.text = "正文:" + t
b.text_frame.paragraphs[0].runs[0].font.size = Pt(14)
h = s.shapes.add_textbox(Inches(1), Inches(0.5), Inches(8), Inches(1)); h.text_frame.text = t
h.text_frame.paragraphs[0].runs[0].font.size = Pt(36)
f = str(tmp_path / "t.pptx"); p.save(f)
assert D.cover(MD, f)["missing"] == []
第 5 步:运行
$ python deck_check.py lint outline.md
大纲 OK
$ python make_mock_deck.py && python deck_check.py cover outline.md mock_deck.pptx
✓ 「核心数据」→ 第 2 页(相似度 0.8)
✓ 「渠道表现」→ 第 3 页(相似度 0.8)
✓ 「第四季度计划」→ 第 4 页(相似度 0.86)
✗ 缺少章节「成本与毛利」
+ 第 5 页「谢谢观看」不在大纲中
! 第 3 页残留占位文字:单击此处
$ python -m pytest -q
3 passed
怎么检查结果?
- 在 WPS 中把生成的演示文稿另存为 .pptx,再运行 cover。
- 第 1 页(封面)不算作“多出的页”。
- 相似度阈值默认 0.6;标题改写较多时可以调低。
调试示例
标题放在文本框里时,所有章节都被判为缺失。 第一版只读取幻灯片的标题占位符(shapes.title)。我做了一份用普通文本框写标题的测试 PPT(36 号字的标题文本框 + 14 号字的正文文本框),结果:
{'missing': ['核心数据', '渠道表现', '成本与毛利', '第四季度计划'], 'matched': [], 'extra': [(2, ''), (3, ''), (4, ''), (5, '')], ...}
所有页的标题都是空的。很多模板和生成工具会用普通文本框做标题,所以这种情况并不少见。
修复:没有标题占位符时,取字号最大的文本框(字号相同时取位置最上面的)作为标题。修复后 4 个章节全部对上,相似度都是 1.0。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 章节被判为缺失 | 标题改写得太多 | 调低 cutoff,或在大纲中使用更接近的标题 |
| 所有页标题都是空的 | PPT 没有使用标题占位符 | 已自动改用字号最大的文本框;仍然不对时检查文件 |
| 残留占位文字 | 生成后没有填写 | 补写内容或删除该文本框 |
| lint 报“只支持两级标题” | 大纲用了 ### | 改为 - 要点 |
模板规则
- 用自己的大纲生成,内容可控。
- 生成后检查覆盖情况和占位文字。
- 数字和事实以原始材料为准(见 B 篇)。
项目资源
| 用途 | 地址 |
|---|---|
| WPS AI 详细体验教程(WPS 社区官方帖) | https://bbs.wps.cn/topic/2498 |
| WPS AI 官网 | https://ai.wps.cn/ |
大纲在自己手里,生成交给 WPS AI,检查交给脚本。