
cwc-workshops parse-pptx.ts逐段解读把PPT变成可量化结构数据的关键技巧【免费下载链接】cwc-workshops项目地址: https://gitcode.com/GitHub_Trending/cw/cwc-workshops在 cwc-workshops 开源项目Anthropic 官方 Code with Claude 工作坊资料库中eval-driven-agent-development工作坊用一个评测驱动的思路训练 AI 生成 PPT。整套评测的核心就是 parse-pptx.ts 这一个文件它把 PPT 变成可量化的结构数据——每页有多少形状、多少图片、多少字符、最小字号是多少、标题和正文各说了什么。本文逐段解读这份源码帮你掌握 PPT 评测评分器设计的关键技巧。一、先搞清楚它要解决什么问题这个工作坊的流程是AI Agent 生成.pptx幻灯片 → 用 LibreOffice 渲染成图片 → 用一套两层评分器打分代码评分器code grader直接读 XML 算指标比如是否有页面字号小于 14ptLLM 评审judge看渲染图打分比如版式是否美观。代码评分器要能跑起来前提是把 PPT 变成机器能读的数据。.pptx本质上是一个装着 XML 的 zip 压缩包直接打开文件毫无意义。parse-pptx.ts的全部工作就是完成压缩包 → 每页结构事实这一步转换。文件头部的注释把定位说得很清楚/** * Pure .pptx parsing — no model calls, no scoring policy. * Walks the zip, parses each slides XML, returns per-slide structural * facts (shape/picture counts, text runs, font sizes, title/body split). */翻译过来就是只解析、不调模型、不做评分。这是它最重要的设计原则后面会反复体现。二、输出契约三个接口定义了整个数据字典文件开头定义了三个 TypeScript 接口它们是评分器的数据字典决定了后续任何检查能算什么指标接口作用关键字段SlideMetrics单页的结构统计shapeCount形状数、pictureCount图片数、textChars字符数、fontSizesPt字号列表、emojiCountemoji 数SlideTexts单页的标题与正文title、bodyParsedPptx整份文件的结果exists、validZip、slideCount、perSlide[]、slideTexts[]export interface ParsedPptx { exists: boolean; validZip: boolean; slideCount: number; perSlide: SlideMetrics[]; slideTexts: SlideTexts[]; }这里有个新手容易忽略的技巧exists和validZip两个布尔位。Agent 可能压根没生成文件也可能生成了损坏的文件——这两种失败模式必须区分开。下游的 produced-result.ts 评分器就靠这两个标志分别报告missing没产出和invalid产出损坏。把失败状态建模进数据结构而不是留给下游去猜这是写评测工具的第一课。配套的EMPTY工厂函数统一了两种失败路径的返回值const EMPTY (exists: boolean, validZip: boolean): ParsedPptx ({ exists, validZip, slideCount: 0, perSlide: [], slideTexts: [], });三、入口函数 parsePptx三道关卡 一次遍历主函数 parsePptx 只有 50 行逻辑可以拆成三关一遍历。1. 第一关文件存在吗let buf: Buffer; try { buf await fs.readFile(pptxPath); } catch { return EMPTY(false, false); }读文件失败就立刻短路返回不做任何解析。2. 第二关是合法 zip 吗.pptx是 zip 归档用 JSZip 尝试解压解压失败说明文件损坏let zip: JSZip; try { zip await JSZip.loadAsync(buf); } catch { return EMPTY(true, false); }注意此时返回的是EMPTY(true, false)——文件存在但损坏与上一关的missing精确区分。3. 第三关定位幻灯片并保证顺序幻灯片 XML 固定存放在ppt/slides/slideN.xml但zip 条目顺序不等于幻灯片顺序压缩包的写入顺序是任意的。代码用正则筛出所有幻灯片条目再按文件名里的数字排序const slideEntries Object.keys(zip.files) .filter((n) /^ppt\/slides\/slide\d\.xml$/.test(n)) .sort((a, b) slideIndex(a) - slideIndex(b));一个细节这里匹配的是纯数字命名的slideN.xml天然排除了slideLayouts、slideMasters等目录里结构相似的 XML避免把版式定义误当成幻灯片。4. 遍历一次解析两个提取器排序完成后进入核心循环const parser new XMLParser({ ignoreAttributes: false, attributeNamePrefix: _, }); for (let i 0; i slideEntries.length; i) { const xml await zip.files[slideEntries[i]]!.async(string); const doc parser.parse(xml); perSlide.push(analyzeSlide(i 1, doc)); slideTexts.push(extractSlideTexts(i 1, doc)); }这里藏着两个关键配置ignoreAttributes: falseXML 属性里恰恰装着最需要的数据——字号藏在a:rPr的sz属性里占位符类型藏在p:ph的type属性里。关掉忽略属性默认行为才能拿到它们。attributeNamePrefix: _给属性名加_前缀避免属性和子元素同名时互相覆盖比如sz属性与同名子元素。而一次解析、两个提取器的循环结构体现了职责分离同一份解析好的 DOM 同时喂给analyzeSlide结构统计供代码评分器和extractSlideTexts标题/正文切分供 LLM 评审。每个 XML 只解析一次两个消费者共享结果没有重复 IO。四、analyzeSlide一页幻灯片能榨出什么数字analyzeSlide 负责把一页 DOM 变成SlideMetrics分四步。1. 数形状OOXML 的五种顶层形状PPT 里一切可见元素都挂在p:sld → p:cSld → p:spTree这棵树上。OOXML 定义了五种顶层形状代码把它们全部加起来当作杂乱度信号const spTree pluck(doc, [p:sld, p:cSld, p:spTree]) ?? {}; const shapeCount countShapes(spTree, p:sp) // 文本形状 countShapes(spTree, p:pic) // 图片 countShapes(spTree, p:graphicFrame) // 图表/表格 countShapes(spTree, p:grpSp) // 组合 countShapes(spTree, p:cxnSp); // 连接线只数p:sp文本框就会漏掉图标、图表和组合元素页面的拥挤程度就被低估了。下游的 cluttered-slides.ts 评分器用shapeCount 20判定元素过多、有杂乱风险这个阈值之所以可信正是因为这里数的是全口径。配套的pluck是一个安全的深层取值工具——逐层判空任何一层不存在就返回undefined让调用方可以安全地用?? {}兜底。XML 结构因生成工具而异防御式取值比层层try/catch更轻。2. 收集文字 run 与字号文字统计靠递归遍历整个 DOM收集每一个 DrawingML 文本 runa:rfunction walkRuns(node: unknown, out: { text: string; sizePt?: number }[]): void { ... const t runObj[a:t]; const text typeof t string ? t : t null ? : String(t); const sizeAttr rPr typeof rPr object ? rPr[_sz] : undefined; const sizePt sizeAttr ! null ? Number(sizeAttr) / 100 : undefined; out.push({ text, sizePt }); }两个新手必知的知识点sz属性单位是百分之一磅。sz1800表示 18pt所以代码里要/100换算。字号是可选的——run 可能继承主题字号而没有显式sz属性所以sizePt允许是undefined后续统计时用filter只留真实数字。3. 汇总指标去重排序 emoji 计数const fontSizesPt Array.from( new Set(runs.map((r) r.sizePt).filter((s): s is number typeof s number)), ).sort((a, b) a - b); return { index, shapeCount, pictureCount, textChars: text.length, fontSizesPt, emojiCount: (text.match(EMOJI_RE) ?? []).length, };字号去重 升序排列下游的 small-font-slides.ts 只看数组第一个元素最小字号是否小于 14pt来判断可读性下限。把排序去重做在解析层评分器就只剩一行fontSizesPt[0]! 14。emoji 用 Unicode 属性正则/\p{Extended_Pictographic}/gu一条正则覆盖所有 emoji 码点比维护 emoji 字符表可靠得多。emoji-count.ts 用它统计全 deck 的 emoji 总数。五、标题/正文切分三级启发式让 LLM 评审有干净的输入LLM 评审judge不能直接读 XML——它需要的是人类可读的标题 正文。extractSlideTexts 用三级优先级的启发式挑出哪一块形状是标题占位符类型是title或ctrTitle的第一个形状Office 和 python-pptx 的标准标记否则字号最大 run 所在的形状否则第一个有文本的形状。let titleShape: SlideShape | undefined shapes.find( (s) s.phType title || s.phType ctrTitle, ); if (!titleShape) { const byFont [...shapes].sort((a, b) b.maxSizePt - a.maxSizePt); titleShape byFont[0]; }辅助函数 collectShapes 把每个文本形状压平成三个信号占位符类型藏在p:nvSpPr → p:nvPr → p:phtype属性里、完整文本、最大字号。剩下所有形状的文字拼接起来就是body。这个切分直接喂给 title-body-coherence-judge.ts——对每页发起一次纯文本 LLM 调用让模型给正文是否兑现了标题的承诺打 0-5 分。因为解析层已经把标题和正文切干净了评审调用只需要一次轻量文本请求不用把整页 XML 或渲染图塞给模型又快又便宜。六、输出如何变成评分一行一个指标回到 graders.ts每个评分器就是一个声明式对象grade方法从GraderContext.parsedPptx里取数。因为解析层已经把活干完了每个评分器都是一行表达式// slide-count.ts async grade(ctx) { return ctx.parsedPptx.slideCount; } // cluttered-slides.ts async grade(ctx) { return ctx.parsedPptx.perSlide.filter((s) s.shapeCount 20).length; }这正是文件头注释里Everything a code-check needs to compute a metric lives on ParsedPptx的含义解析层输出即契约——凡是评分器要用的事实全部预先算好放在数据结构里。新增一个指标追加一个 grader 对象即可见 graders/types.ts 的Grader接口解析代码零改动。评分器数据来源判据produced-resultexists/validZip文件存在且可解压slide-countslideCount精确等于 5cluttered-slidesperSlide[].shapeCount超过 20 个形状记为杂乱small-font-slidesperSlide[].fontSizesPt[0]最小字号低于 14ptemoji-countperSlide[].emojiCount全 deck emoji 总数title-body-coherenceslideTexts[]LLM 给标题-正文契合度打分七、值得抄走的四个设计技巧解析与评分彻底分离。parse-pptx.ts不含任何好坏判断阈值、满分线全部在评分器里只输出客观事实。换评测标准时解析层不动写新指标时不用碰 XML 代码。失败状态显式建模。exists/validZip两个布尔位区分没产出与产出损坏评分器可以各自短路而不是拿到空数据后猜原因。重活前置、消费端极薄。排序去重、单位换算、标题切分都在解析层完成评分器退化为一行表达式——可读、可测试、可批量扩展。防御式取值 一次解析多消费者。pluck/walkRuns全程判空容忍不同工具生成的 XML 差异每个 slide XML 只解析一次结构统计和文本提取共享同一份 DOM。想动手体验完整流程可进入 eval-driven-agent-development/ 目录按其 README 安装依赖后运行npm run eval -- technology观察评分卡中每一列背后的数据来源。理解了这份 284 行的解析器你就掌握了让 AI 产出的文档可以被程序量化的完整套路——这正是评测驱动开发Eval-Driven Development的地基。【免费下载链接】cwc-workshops项目地址: https://gitcode.com/GitHub_Trending/cw/cwc-workshops创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考