DeepTutor PPTX 技能实战:借助 python-pptx 读取、创建与编辑 PowerPoint 演示文稿
这份指南完整拆解 DeepTutor 内置的 pptx 技能(deeptutor/skills/builtin/pptx/SKILL.md),它让 Agent 能够在沙箱 exec shell 中直接处理用户上传的 .pptx/.ppt 演示文稿:从读取文字与备注、按大纲批量建页,到保留格式的占位符替换、原位换图、插入表格与图表,乃至用 LibreOffice 导出 PDF/图片和最后的原始 OOXML 兜底手段。读完你会掌握一套在 DeepTutor 体系内对 PowerPoint 文件进行“读写改验”全链路操作的实战方法,并理解其背后的 python-pptx 对象模型与 OOXML 包结构。
技能定位:何时触发、在哪里执行
pptx 技能是 DeepTutor 随产品内置(builtin)的一组 SKILL 包之一,与 docx、pdf、xlsx、skill-creator 并列存放于 deeptutor/skills/builtin/ 目录。它的 YAML frontmatter 定义了触发条件与运行前置:
name: pptx
description: Read, create, or edit PowerPoint .pptx decks — build slides from an outline,
extract slide text/speaker notes, edit shapes/tables/charts, replace images, or
export to PDF/images. Use whenever a .pptx (or .ppt) file is an input or output,
or the user mentions a deck, slides, or a presentation.
tags:
- tool
- office
requires:
sandbox: shell
几个关键点需要结合仓库机制来理解:
- 何时触发:description 约定“只要用户提到 deck、slides、presentation,或输入/输出涉及
.pptx/.ppt,就应使用本技能”。这与 DeepTutor 的技能加载机制一致——按照 skill-creator 的说明,系统提示词只携带每个技能的name + description,模型在任务匹配时才用read_skill读取完整正文。 - 运行环境:
requires.sandbox: shell表示它依赖 shell 执行沙箱,即 DeepTutor 的 exec_tool。所有工作在沙箱execshell 的当前工作区目录中完成(上传的文件就落在那里);Agent 通常写一段 Python heredoc 或临时.py脚本并执行。 - 交付物:
exec结束后,工具结果里会出现 Generated artifacts(下载卡片),Agent 需在最终回答中引用其 URL,用户即可下载生成的演示文稿。这一机制由 deeptutor/services/sandbox/artifacts.py 落地:沙箱产物会以"Generated artifacts (now saved — shown to the user as download cards)"的形式返回给模型,前端随后把回答中逐字出现的产物文件名自动转成可点击下载链接。 - 依赖预装:技能正文声称 python-pptx “preinstalled”,仓库证据确实如此——requirements/cli.txt 与 requirements/dev.txt 均固定了
python-pptx>=1.0.0,Dockerfile.runner 也把python-docx、python-pptx、openpyxl等办公库一并装入运行时镜像。
心智模型:.pptx 的对象层次与计量单位
在写任何代码前,先建立正确的模型,能避免 90% 的“对着 API 硬试”:
- 一个演示文稿(presentation)由若干幻灯片(slides)组成;每张幻灯片基于某个版式(layout)构建;版式归属于幻灯片母版(slide master)。版式通过
idx和类型定义占位符(placeholders)(如标题 title、正文 body、图片 picture)。 - 一张幻灯片上承载各种形状(shapes):占位符、文本框、图片、表格、图表。
- 带文字的 shape 向下暴露
.text_frame→.paragraphs→.runs。run(文本片断)是携带格式的最小单元(字体、字号、加粗、颜色都挂在 run 上)。 - 所有尺寸单位为 EMU。工具类统一从
from pptx.util import Inches, Pt, Emu导入,用Inches(1)/Pt(18)表达尺寸而无需手算 EMU。
这也是“为什么改文字必须精确到 run 级”的理论基础:直接整体赋值 text_frame.text 会把格式折叠成单个 run,丢失行内样式(详见下文“编辑既有演示文稿”)。
读取与信息提取:把任意 deck 变成结构化文本
技能给出的读取骨架覆盖了“页数/尺寸 → 逐页逐形状 → 表格 → 备注”四层信息:
from pptx import Presentation
prs = Presentation("deck.pptx")
print(len(prs.slides), prs.slide_width, prs.slide_height) # EMU dims
for i, slide in enumerate(prs.slides, 1):
print(f"--- slide {i} (layout: {slide.slide_layout.name}) ---")
for shape in slide.shapes:
if shape.has_text_frame:
print(shape.text_frame.text) # \n-joined paragraphs
elif shape.has_table:
for row in shape.table.rows:
print([c.text for c in row.cells])
if slide.has_notes_slide:
notes = slide.notes_slide.notes_text_frame.text
if notes:
print("NOTES:", notes)
使用建议:
- 通过
slide.placeholders遍历,可查看每个占位符的idx与其placeholder_format.type——当你不确定某模板“第二块占位符到底是正文还是副标题”时,先打印它。 - 若只需纯文本,直接跨页收集
shape.text_frame.text即可(输出为段落级\n连接),不必关心表格、图片等富元素。 slide_width/slide_height返回的是 EMU 值(如 16:9 的 12192000×6858000),需要换算时除以 914400 得到英寸。
从大纲创建演示文稿
创建的关键第一步是先枚举模板的版式——不同模板的 layout 索引并不固定。python-pptx 默认模板的常见映射是:layout 0 = Title(标题页)、1 = Title+Content(标题+内容)、5 = Title Only、6 = Blank,但务必先运行下面这段核实:
from pptx import Presentation
from pptx.util import Inches, Pt
prs = Presentation() # or Presentation("template.pptx") to inherit a theme
for idx, lay in enumerate(prs.slide_layouts):
print(idx, lay.name, [(p.placeholder_format.idx, p.name) for p in lay.placeholders])
模板选择上,技能特别指出两种打开方式的差异:Presentation() 使用内置默认模板,而 Presentation("template.pptx") 会继承该模板的主题与版式——要做带客户品牌色/公司模板的 deck 时务必用后者。
标题页与要点页
# Title slide
s = prs.slides.add_slide(prs.slide_layouts[0])
s.shapes.title.text = "My Deck"
s.placeholders[1].text = "Subtitle" # idx from the listing above
# Title + bullets
s = prs.slides.add_slide(prs.slide_layouts[1])
s.shapes.title.text = "Agenda"
tf = s.placeholders[1].text_frame
tf.text = "First point" # first paragraph
for line, lvl in [("Second", 0), ("Sub-point", 1)]:
p = tf.add_paragraph()
p.text = line
p.level = lvl
prs.save("out.pptx")
tf.text = "First point" 会先建立第一个段落,随后用 add_paragraph() 追加,并通过 p.level 控制层级——项目符号与缩进由版式自动渲染。
技能在此有一条铁律:文字永远通过占位符/shape 写入,绝不手工书写项目符号字符(•);缩进和圆点样式交给版式基于 paragraph.level 生成,手工画 • 会在更换主题或 Word/PPT 重排时彻底错乱。
补充文本框与图片
任何幻灯片都可以脱离占位符自由放置元素:
tb = s.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1))
r = tb.text_frame.paragraphs[0].add_run()
r.text = "Hi"
r.font.size = Pt(28)
r.font.bold = True
s.shapes.add_picture("logo.png", Inches(0.5), Inches(0.5), height=Inches(1)) # omit w to keep ratio
注意 add_picture 的用法细节:只给 height(或只给 width)时,python-pptx 会按原图纵横比自动缩放另一维,这是保持 logo、截图不变形的基本功。
编辑既有演示文稿:逐 run 替换,而非整体覆盖
“模板填空”是办公自动化的高频场景。技能强调:必须在 run 层级编辑以保留原 run 的格式——重写 text_frame.text 会把段落折叠成单个 run,丢掉行内格式。
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
for para in shape.text_frame.paragraphs:
for run in para.runs:
if "{{NAME}}" in run.text:
run.text = run.text.replace("{{NAME}}", "Frank")
删除形状/占位符需要下沉到底层 lxml 元素:sp = shape._element; sp.getparent().remove(sp)。技能给出一条实战忠告:如果模板提供的插槽多于实际数据,应把多余 shape 整个删掉,而不是留下空的占位符——空占位符在渲染时往往显示为“click to add text”的灰字,破坏成品观感。
原位替换图片(保持尺寸与位置)
python-pptx 没有直接的“替换图片”setter。技能给出的做法是直接替换相关 image part 的字节:先读取 picture 的 r:embed rId(位于其 <a:blip> 元素上),再覆写该 part 的 blob:
from pptx.oxml.ns import qn
for shape in slide.shapes:
if shape.shape_type == 13: # MSO_SHAPE_TYPE.PICTURE
blip = shape._element.find(".//" + qn("a:blip"))
rid = blip.get(qn("r:embed"))
with open("new.png", "rb") as f:
shape.part.related_part(rid)._blob = f.read()
其底层原理是:图片在 OOXML 里是独立的媒体 part(见 ppt/media/),而 shape 只是通过 relationship + r:embed 引用它;只要新图片与原图尺寸/宽高比相近(或接受拉伸),换 blob 即可保留原有大小与位置属性。
表格与图表
from pptx.util import Inches
tbl = s.shapes.add_table(
rows=2, cols=2, left=Inches(1), top=Inches(1), width=Inches(6), height=Inches(2)
).table
tbl.cell(0, 0).text = "Header"
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
cd = CategoryChartData()
cd.categories = ["Q1", "Q2", "Q3"]
cd.add_series("Sales", (4.5, 5.5, 6.2))
s.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED, Inches(1), Inches(1), Inches(8), Inches(4.5), cd)
图表走“数据先行”的模式:先用 CategoryChartData 构造类别轴与系列数据,再通过 add_chart 指定图表类型与位置尺寸,python-pptx 会生成内嵌图表及其工作簿。
视觉设计规范(仅当用户要“精修版”而非“数据倾倒”)
技能将设计约束限定在“用户明确想要精致 deck”的场景,避免在纯数据输出时画蛇添足,其要点可直接作为检查清单:
- 配色:选取与主题相关的主色板 + 一个强调色,不要默认用通用蓝;一种颜色应占主导;标题/封底页用深色,内容页用浅色。
- 版式变化:跨页轮换双栏、数据统计块(stat callout)、引言、章节分隔等不同 layout——从头到尾重复同一个“标题+圆点”版式会被视为低质量产出。多数页面最好有视觉元素(图/表/形状),而非只有标题和要点。
- 字号体系:标题 36–44pt,节标题 20–24pt,正文 14–16pt;标题和行内标签加粗;正文左对齐,只有标题居中;页面边距保持 ≥ 0.5in。
- 溢出防护:设置
text_frame.word_wrap = True,警惕替换后的长文本溢出。渲染后(见下节导出)务必“批判性”地目检图片,默认自己存在重叠/溢出/对比度问题并修复,再宣告完成。
导出 PDF / 图片(可选,依赖 LibreOffice)
视觉验收依赖真实渲染。技能约定的导出链路如下,任何一步缺工具都要显式告知并跳过,绝不擅自 pip install(沙箱关闭网络出口,安装也不可行):
command -v soffice >/dev/null || { echo "soffice not available — cannot export"; exit 0; }
soffice --headless --convert-to pdf out.pptx
command -v pdftoppm >/dev/null && pdftoppm -jpeg -r 150 out.pdf slide && ls -1 "$PWD"/slide-*.jpg
要点:
pdftoppm(poppler-utils)把 PDF 按 150 DPI 逐页转成slide-*.jpg,Agent 再用自身的图像查看能力读取这些 JPG 路径做真实目检。- 每次编辑后都要重新转换——PDF 不会自动反映
.pptx的变化,忘记重跑就相当于在检查过期版本。 - 若
soffice或pdftoppm缺失,明确说明并跳过导出,而不是找替代方案硬来。
兼容旧格式:.ppt → .pptx
旧版 .ppt 是另一套二进制格式,python-pptx 无法直接打开。处理方式是在有 soffice 时先转换:
command -v soffice >/dev/null && soffice --headless --convert-to pptx old.ppt || echo "need soffice for .ppt"
若环境无 LibreOffice,只能如实告知用户该 .ppt 无法被处理——这是二进制格式边界,不是代码能力问题。
进阶兜底:直接操作原始 OOXML
技能明确把 raw OOXML 定位为“最后的 resort”,仅用于 python-pptx 表达不了的场景,例如精确保真的幻灯片复制、渐变填充、主题色修改、非类型化 XML 元素。在此之前优先利用 python-pptx 已暴露的 shape._element(lxml 元素)做外科手术式的小修,而不是整体解包重拼。
包结构地图
.pptx 本质是 ZIP,关键 part 的分布如下:
- 幻灯片顺序声明在
ppt/presentation.xml的<p:sldIdLst>; - 幻灯片本体在
ppt/slides/slideN.xml,其关系在ppt/slides/_rels/slideN.xml.rels; - 版式/母版分别在
ppt/slideLayouts、ppt/slideMasters; - 媒体资源统一放
ppt/media/; - 所有 part 的类型在
[Content_Types].xml中登记。
手工增改 part 的致命红线
一旦手工增删 part,任何一条被破坏,PowerPoint 都会报“文件损坏”:
- 每个新 part 都必须在
[Content_Types].xml声明:幻灯片用<Override>,媒体扩展名(如 png/jpeg)用<Default>。 - 所有跨 part 引用必须走
_rels/*.rels的<Relationship>,XML 里的r:id必须可解析。新增一张幻灯片 = 写入该 part 及其.rels+ 增加 content-type override + 在presentation.xml.rels加<Relationship>+ 在<p:sldIdLst>加<p:sldId>。 - ID 唯一性:
<p:sldId>的 id、单张幻灯片内形状 id(<p:cNvPr id=...>)不得重复;sldLayoutId/sldMasterId全局唯一。 - 空白保留:任何
<a:t>若首尾含空格,必须加xml:space="preserve",否则 PowerPoint 会吞掉空格。 - 解析/序列化:用 lxml 或
defusedxml,严禁朴素的字符串拼接把命名空间搞乱、或把“格式化缩进”变成真实文本节点。
最小文本替换:zip 手术
ZIP 成员无法原地覆写,正确姿势是重建归档、只替换一个 part:
import zipfile
target = "ppt/slides/slide1.xml"
with zipfile.ZipFile("in.pptx") as zin:
xml = zin.read(target).decode().replace("Old title", "New title")
with zipfile.ZipFile("out.pptx", "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.namelist():
zout.writestr(item, xml.encode() if item == target else zin.read(item))
这段代码遍历原归档所有成员,仅对目标 XML 写出修改后的字节,其余成员原样重写,从而把破坏面控制在最小。
交付前验证:把“能打开”变成硬性门槛
技能要求在宣告完成前完成多层自检,这与 docx 技能(deeptutor/skills/builtin/docx/SKILL.md)的“Verify before returning”哲学一脉相承:
- 重新打开输出文件:
Presentation("out.pptx")并断言幻灯片数与关键文本——能干净重开就排除了绝大多数损坏情况; - 对“要好看”的 deck,导出并目检(见导出节)——把渲染图当作存在重叠/溢出/对比度问题的默认前提来审查;
- 排查模板遗留文字:检查版式/模板中残留的占位符示例文本(
xxxx、lorem、[insert ...]等)是否被带入成品,发现即修复。
小结:把 SKILL.md 变成可复用的作业流程
回顾整份 pptx/SKILL.md,它实际封装了一条完整且自洽的作业流水线:读(枚举 + 提取)→ 建(从大纲 + 版式)→ 改(run 级替换 + 原位换图 + 表格图表)→ 美(设计规范)→ 验(导出渲染 + 重开断言)→ 兜底(raw OOXML 手术)。每一步都配套了可直接运行的 python-pptx 代码和明确的环境降级策略(无 soffice 就说 no,绝不 pip install)。
这条流水线的执行载体并非一次性脚本,而是 DeepTutor 的技能机制:SKILL.md 作为可复用的程序性知识包存放在 deeptutor/skills/builtin/,由 deeptutor/services/skill/service.py 负责运行时加载与解析(builtin 层位于 deeptutor/skills/builtin/<name>/SKILL.md),按需读取、按描述匹配触发。理解这套结构后,你既可以把它当作一份 python-pptx 速查手册直接套用,也可以参照 skill-creator 的 frontmatter 规范,沉淀出属于自己的“报告生成”“幻灯片批量制作”等专属技能包。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00