AI 生成 PPTX:11 页课件编译出 429 个形状

AI 生成 PPTX:11 页课件编译出 429 个形状

2026 年「让 Agent 写 PPT」成了显学:官方 pptx skill 收敛到「先大纲、再出结构化中间表示、交给渲染器出文件」,Marp、Slidev 这类 code-first 方案也被翻出来。我用同一思路做了份 11 页试讲课件,73.8 KiB 源码编译出 429 个原生形状。

一、先看产物:一条流水线吐出了什么

整套东西不是「一句话生成幻灯片」那种黑盒,而是一条能反复改、能版本回滚的工程流水线。实测产物清单如下。

产物 数量 体积 说明
slides/*.slide 课件源码 11 个 / 1068 行 73.8 KiB 类 JSX 的声明式 DSL
DSL 组件节点 Box 367 + Text 257 + CodeBlock 5 + SVG 6 --- 6 个 SVG 全部手写
ppt/slides/*.xml 编译产物 11 页 435.5 KiB 未压缩,是源码的 5.90 倍
打包后 PPTX 429 个形状 68.0 KiB zip 把 XML 压到 9.9%
preview/ 出图 11 张 PNG 566.7 KiB 平均每页 52.8 KiB,用于人眼验收
STORY.md 设计文档 11 页规则表 7.6 KiB 节奏、版式、字数预算
试讲逐字稿 11 段 / 340 s 8.3 KiB 与每页一一对应

一句话概括体量关系:73.8 KiB 源码 → 435.5 KiB OOXML → zip 压到 44.3 KiB → 落盘 68.0 KiB 的 PPTX。

二、技术选型:三条出片路线怎么选

路线 代表做法 产物 文字可编辑 我的判断
图像合成 每页整张图再拼进 PPTX 位图 否 好看,但字号改不了、文字选不中,教学课件要反复改,直接否决
命令式代码 手写 PptxGenJS / python-pptx,逐个 addShape 原生形状 是 可控,但所有坐标要人肉算,11 页 429 个形状会写疯
声明式 DSL 写 <Box> <Text>,编译器负责布局与出形状 原生形状 是 选它:写的是意图,不是坐标

决定性的差别在最后一步能不能改。试讲课件要按评委反馈反复调字号、挪色块,图像路线每改一次就重出一遍图,而 DSL 路线改一行源码重新编译即可。

三、核心原理一:DSL 节点到 OOXML 形状的映射

这是整条链路最值得讲的一层。我把 11 页逐个拆开数了一遍:

页 Box 带 background 的 Box Text SVG <p:sp> <p:pic> 文本字符
P1 20 3 19 1 26 1 187
P2 36 14 24 0 42 0 292
P3 33 14 22 0 37 0 352
P4 27 11 17 2 29 2 248
P5 48 13 33 1 51 1 419
P6 28 13 25 0 41 0 523
P7 56 14 43 0 65 0 320
P8 38 15 26 0 48 0 396
P9 26 9 20 0 32 0 374
P10 36 16 21 0 38 0 340
P11 19 6 7 2 14 2 118
合计 367 128 257 6 423 6 3569

三条实测规律:

  1. 1 个 <Text> 换 1 个可编辑文本框 。11 页里 8 页的「带文字形状数」与 DSL 的 <Text> 数量完全相等,P6 / P8 / P9 多出的 1~3 个来自 CodeBlock 内部的行标签,不是误差。
  2. 367 个 Box 里只有 128 个(34.9%)会落成矩形 ,判定条件是有没有写 background 。剩下 239 个 <Box style={``{ height: 20 }} /> 是纯粹的布局占位撑高度/撑间距,编译后一个形状都不留。
  3. 6 个手写 SVG 原样以 .svg 进 ppt/media/,不是先光栅化成 PNG,矢量保真;并且以 STORED 方式存包(原始字节 = 压缩后字节),因为 SVG 本身是文本,再压一次收益极小。

整体折算下来:平均每 2.49 行 DSL 产出 1 个形状,每个形状烧掉约 1040 字节 XML,每页平均 39.0 个形状。

顺带看一眼样式侧的落地情况。全篇几何体只有 rect 和 roundRect 两种,roundRect 共 90 个,对应 DSL 里 85 处 borderRadius,圆角没有丢。border 也是一样:DSL 里 42 处描边声明,OOXML 里恰好 42 条带属性的 <a:ln>,一条不差,另外 125 个无边框形状补的是空 <a:ln/> 节点。字体只用三款------Microsoft YaHei 1184 次、Menlo 204 次(代码块)、Consolas 80 次(数字与变量名),没有出现字体回退失败导致的方块字。颜色共 26 种、填充 927 次,用得最多的是正文深灰 #1A2230(153 次)。

四、核心原理二:同一份尺寸要走三套单位

DSL 里写的是前端熟悉的 px,落到 OOXML 全部换单位,这三套换算一旦记错,手写 XML 修图必然全歪。

维度 DSL 里写的 OOXML 里的值 换算关系
画布宽 width: '1280px' <a:ext cx="12192000"> 1 px = 9525 EMU
画布高 height: '720px' <a:ext cy="6858000"> 同上
字号 fontSize: 58 sz="4350" 1 px = 0.75 pt,sz 单位是 1/100 pt,即 px × 75
字距 letterSpacing: 3 spc="225" 同上,px × 75

所以 DSL 里的 58px 大标题,在 PowerPoint 里显示为 43.5 pt。编译器还会按字符类型切 run:全篇 628 个 run 中 lang="zh-CN" 349 个、lang="en-US" 279 个,中文与英文/数字分开打语言标记,字体回退和拼写检查才不会串。

五、核心原理三:5.9 倍膨胀,两成来自源码内嵌

435.5 KiB / 73.8 KiB = 5.90 倍,这个数一开始让我以为是形状骨架太重。查下去发现另有原因:每页的完整 DSL 源码被 HTML 转义后塞进了 <p:cNvPr id="1" descr="...">。

11 页 descr 合计 87,356 字节,占全部 slide XML 的 19.6% ,单页转义后是原文的 1.15~1.18 倍(&lt; &gt; &#xA; 这些实体在吃体积)。扣掉 descr 后 XML 仍是源码的 4.75 倍,剩下的才是形状骨架税。

这么干是有道理的:descr 让 PPTX 自带可反解的源码,下一轮 Agent 接着改时能读回原始意图,不用猜哪个形状对应哪段 DSL。代价就是下面这条坑。

六、验收闭环:渲染成图,再让「另一双眼睛」看一遍

这条流水线一共分四层,缺任何一层质量都会塌:

  1. 大纲层 :先写 STORY.md,把受众、目标、页数、每页功能和节奏定下来,人工过一遍;
  2. 内容层 :把大纲翻译成 slides/*.slide,这一步只做内容决策,不碰坐标;
  3. 编译层:确定性渲染器把 DSL 变成 OOXML,不做任何「自由发挥」;
  4. 验收层 :每页渲成 PNG 放 preview/,由人(或另一个不看生成上下文的 Agent)对照清单挑毛病,只修出问题的页。

第 4 层是最容易省、也最不该省的一层。原因很实在:文字溢出、元素重叠、对比度不够这三类缺陷,只有在渲染成像素之后才存在 ,看 DSL 源码永远发现不了------源码里 height: 54 和 fontSize: 19 两个数单独看都合理,但真渲出来字可能顶到边框。这次 11 页全部出图,合计 566.7 KiB,平均每页 52.8 KiB,最大的 P3 是 59,740 B,最小的 P11 是 33,202 B。

还有一条纪律:生成者和验收者必须是两边。写这页的对象脑子里已经有「它应该长什么样」,会本能地给自己的排版缺陷找理由;换一个没有生成上下文的校验者,才会老老实实按清单逐条查。这也是 2026 年几套成熟方案共同的做法。

七、关键代码

结束页的 DSL 长这样,注意 svg 是内联手写的,Box 有 background 才会变成矩形:

jsx 复制代码
// slides/11.slide ------ 结束页
<Slide style={{ width: '1280px', height: '720px', background: '#FFFFFF',
                padding: 0, fontFamily: 'Microsoft YaHei' }}>
    {/* 背景装饰:低透明循环环,SVG 手写,编译后保留矢量 */}
    <Box style={{ position: 'absolute', top: 30, left: 660, opacity: 0.07 }}>
        <svg width={620} height={620} viewBox='0 0 620 620'>
            <path d='M 310 60 A 250 250 0 1 1 486.8 133.2' fill='none'
                  stroke='#1E4FA8' strokeWidth='36' strokeLinecap='round' />
            <polygon points='486.8,133.2 441.5,122.0 475.5,88.0' fill='#1E4FA8' />
        </svg>
    </Box>

    <Text style={{ fontSize: 58, fontWeight: 'bold', color: '#0E3F8C' }}>
        请各位评委老师批评指正
    </Text>
    {/* 只有带 background 的 Box 会落成 PPT 里的矩形,其余都是布局占位 */}
    <Box style={{ width: 100, height: 3, background: '#1E4FA8' }} />
</Slide>

「执行过程追踪」那张表也是 Box + Text 一行行拼出来的,没有用任何表格原语:

jsx 复制代码
// slides/07.slide ------ 追踪表的一行,斑马纹靠手写 background 交替
<Box style={{ width: '100%', height: 54, background: '#F0F5FC',
              flexDirection: 'row', alignItems: 'center',
              borderBottomWidth: 1, borderBottomStyle: 'solid',
              borderBottomColor: '#E5E7EB' }}>
    <Box style={{ width: 200, paddingLeft: 20 }}>
        <Text style={{ fontSize: 19, color: '#4A5568' }}>循环开始前</Text>
    </Box>
    <Box style={{ width: 150 }}>
        <Text style={{ fontSize: 19, color: '#8B97A8', fontFamily: 'Consolas' }}>---</Text>
    </Box>
    <Box style={{ width: 200 }}>
        <Text style={{ fontSize: 19, fontWeight: 'bold',
                       color: '#1E4FA8', fontFamily: 'Consolas' }}>0</Text>
    </Box>
</Box>

代码演示页用 CodeBlock 组件,它带自己的语法主题与等宽字体,不是普通 Text:

jsx 复制代码
// slides/06.slide ------ 代码块组件
<CodeBlock
    code={`# 例:计算 1 + 2 + ... + 100
total = 0              # ① 建:累加器清零

for i in range(1, 101):    # ② 取:i 依次是 1..100
    total = total + i      #    加:把当前的 i 累加进去

print(total)           # ③ 用:循环结束后再取结果`}
    language='python'
    width={744}
    fontSize={21}
    theme='github-dark'
/>

增量修订靠 .slidep/state.json 记每页的版本与 commit,这是能反复改的基础:

json 复制代码
{
  "filePath": "...\\Python循环结构-试讲课件.pptx",
  "lastCommit": {
    "01": { "revisionId": "103cd8dc...", "version": 1,  "at": "2026-09-08 12:36:59.786" },
    "10": { "revisionId": "44b21f5c...", "version": 10, "at": "2026-09-08 12:46:51.559" },
    "11": { "revisionId": "7a0bcb39...", "version": 12, "at": "2026-09-08 12:48:00.375" }
  }
}

上面所有数字都是把 PPTX 当 zip 打开后数出来的,脚本核心就这几行:

python 复制代码
# tools/_measure_pptworker.py(节选)------ 所有结论均来自真实文件
import zipfile, re
z = zipfile.ZipFile(PPTX)
x = z.read('ppt/slides/slide11.xml').decode('utf-8', 'ignore')
print('sp 形状:', len(re.findall(r'<p:sp>', x)),
      'pic 图片:', len(re.findall(r'<p:pic>', x)),
      '原生表格:', len(re.findall(r'<a:tbl>', x)))   # 实测为 0
descr = re.search(r'<p:cNvPr id="1" name="" descr="(.*?)"/>', x, re.S).group(1)
print('源码内嵌:', len(descr), 'B')

八、把设计规则写成可校验的表

STORY.md 最有价值的地方是它把「排版感觉」写成了能跑校验的规则。实测下来全部命中:

  • hero 页(视觉重页)3 个:P1 / P3 / P11,占 3/11 = 27.3%,落在 20--30% 预算区间
  • 节奏序列 peak 4 个、valley 5 个、transition 2 个,最长连续 valley = 2,没出现连续三页低谷
  • 任意两个 hero 之间至少隔 1 页,实测相邻 hero 对数 = 0
  • 全部 11 页的关键数字都在文档里留了手算校验:5050、4950、2550、50005000

九、踩坑记录

1. 「表格」根本不是表格。 全篇 <a:tbl> 数量是 0 ,<p:graphicFrame> 是 0,图表引用也是 0。P7 那张追踪表是 43 个独立文本框拼出来的,P7 的 XML 也因此成为最重的一页(58,641 B)。后果是在 PowerPoint 里没法整表选中、改列宽要一个个拖、排序完全做不了。想导出成真表格,得在 DSL 层加 <Table> 原语。

2. 纯布局 Box 选不中。 367 个 Box 里 239 个没有 background,编译后消失。有次我想给某块区域加底色,在 PowerPoint 里怎么点都选不到它------因为它压根不存在,得回 DSL 里补 background 再编译。

3. 单位换算按 100 倍算会全错。 第一次手改 XML 时我把 fontSize: 58 写成了 sz="5800",实际是 4350。记住 px × 75,不是 × 100(1 px = 0.75 pt)。

4. descr 与形状会脱节。 因为源码被内嵌进 PPTX,一旦有人在 PowerPoint 里手工拖过形状,descr 里的 DSL 就和实际布局不一致了,下一轮 Agent 读回源码再编译会把手工改动冲掉。要么全程只改 DSL,要么接受手工改动会在下次编译时丢失。

5. 版本号断号。 .slidep/state.json 记了 11 页,版本号范围是 1...12,但 11 是缺失的 ------中间有一次提交被覆盖。想做「回退到上一版」的功能时,按 version - 1 取会取空。

6. SVG 缓存留了孤儿。 .cache/media/images/ 下有 7 个 SVG,只有 6 个被打进 PPTX,剩下的 5e7acd15....svg(176 B)成了孤儿。清理产物时要按引用表删,不能一股脑拷目录。

7. 逐字稿时间预算超标。 表格里 11 段相加是 340 秒(5:40),而文档标题写的是「总计 5:00」,超了 40 秒、13.3%。试讲卡时严格的话,必须按文档提示把 P2、P7 当提纲页快速带过。

十、实测数据汇总

指标 实测值
源码 → slide XML 膨胀 5.90 倍(435.5 KiB / 73.8 KiB)
其中 descr 内嵌占比 19.6%(87,356 B)
zip 压缩率 9.9%(435.5 KiB → 44.3 KiB)
形状总数 / 平均每页 429 / 39.0
文本形状占比 59.9%(257 个)
产出效率 每 2.49 行 DSL → 1 个形状
颜色种类 / 填充总次数 26 种 / 927 次
字号档位 21 档,13 px ~ 38 px
编译节奏 11 分 01 秒出 11 页,平均 60.1 s/页

顺带一个提醒:全篇最小字号是 13 px(9.75 pt),只出现 1 次。在 1280 px 画布上 13 px 只占 1.0% 宽度,投到教室投影基本看不清,试讲课件建议正文不低于 18 px。

十一、小结与下一步

回看这套做法,真正省时间的不只是「生成」,而是把课件变成了一份可 diff、可回滚、可校验的工程文件 :设计规则写在 STORY.md 里能跑校验,每页改动记在 state.json 里能定位,出图放在 preview/ 里能人眼验收。

对要自己搭这套流程的同学,我的建议是按顺序做三件事,别一上来就追求「一句话出片」:

  • 先把规则写出来:hero 占比、连续低谷页数、每页字数预算、配色数量上限,写成一张能跑校验的表,比事后靠眼睛挑有效得多;
  • 再把单位换算刻进肌肉:px × 9525 得 EMU、px × 75 得 sz,这两个数错了,后面所有手工微调都是白工;
  • 最后一定留出验收位:渲染成图、换一双眼睛看,这一步的成本远低于在 PPT 里一个个拖形状。

这三条做完,「AI 生成 PPT」才从演示效果变成能反复用的生产方式。我下一步想补的是 <Table> 表格原语(解决第 1 条坑),以及把 descr 拆成外部 sidecar 文件,把那 19.6% 的体积还回来。

完整工程已整理好,含 11 页 DSL 源码、STORY.md 设计文档、5 分钟逐字稿和上文这个实测脚本,需要的同学评论区扣「源码」,我看到会一一回复;也欢迎关注我,后面会把课件工程化这个系列继续更下去。

相关推荐
leisoo80971 小时前
股票筹码分布怎么用获利比例成本区间与集中度实战 IG50免费开源股票数据API接口
开发语言·jvm·数据库·python·开源
ControlM1 小时前
从官方 CDN 里扒出 TRAE (TraeCode) 历史版本安装包
python·逆向·trae
tianyuanwo1 小时前
Python 属性查找陷阱:从 `AttributeError: ‘X‘ object has no attribute ‘_children‘` 说起
python
程序猿阿森1 小时前
混合精度详解:FP16 vs BF16,Loss Scaling 与 PyTorch 实战
人工智能·pytorch·python
benchmark_cc1 小时前
A股、港股、美股日K如何拼接?处理不同交易日造成的时间轴错位
大数据·python·pandas·量化交易·股票数据·quantdash
龙腾-虎跃1 小时前
AI-Vue3-python-flask-Blog 全栈博客项目深度解析:从零搭建你的 AI 博客
人工智能·python·flask
余槐i2 小时前
无慢查询 RT 却飙升:cProfile 定位 FastAPI 事件循环阻塞
后端·python·性能优化·fastapi·asyncio
Xiu Yan2 小时前
Python 数据分析:数据分析步骤
开发语言·python·数据分析
vilya2 小时前
把 Python 塞进 APK:Chaquopy 打包实践
android·python