Compare commits

...

1 Commits

Author SHA1 Message Date
dengzilong.zero
a6dd578fc8 refactor: strengthen lark slides quality workflow 2026-07-19 04:52:36 +08:00
18 changed files with 886 additions and 252 deletions

View File

@@ -1,285 +1,131 @@
---
name: lark-slides
version: 1.0.0
description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill不要因为域名不是飞书而回退到 WebFetch路由依据是 URL 路径模式和 token而不是域名。不负责云文档内容编辑(走 lark-doc、云文档里的独立画板对象走 lark-whiteboard注意 slide 内嵌的流程图/架构图仍属本 skill、上传或下载普通文件走 lark-drive。"
description: "飞书幻灯片:创建、读取和编辑 Slides支持从计划到 XML、图片/图标/图表、局部替换、真实截图验收与视觉基线评测。用户要求新建或修改飞书幻灯片、给出 /slides/ 或 /wiki/ 链接(包括 doubao.com 对应路径)、需要 PPT 版式、图片、图表、流程图、截图验证或视觉质量对标时使用。不负责云文档正文编辑、独立画板或普通文件上传下载。"
metadata:
requires:
bins: ["lark-cli"]
cliHelp: "lark-cli slides --help"
---
# slides (v1)
# Lark Slides
**CRITICAL — 全局硬约束PPT 的尺寸是 960x540,确保主体内容在页面边界内。**
Create structured, editable presentations—not raster slide images. The page canvas is **960×540**.
**CRITICAL — 图片至关重要:必须有意识的主动多用图片!素材图使用生图工具和搜图工具,缺图时用生图工具生成配图补足;背景图必须使用生图工具,且生图指令中必须明确要求不要出现任何文字。**
## Rule Priority
**CRITICAL — 防文本溢出:所有承载突出信息和密集文字的 `<content>` 必须设置 `autoFit="normal-auto-fit"`,字号会在框内自动缩排以防溢出。**
When rules conflict, use this order:
## Quick Reference
1. User intent and supplied content.
2. `references/slides_xml_schema_definition.xml` and `lark-cli schema` for protocol/API truth.
3. This workflow's structural, safety, and quality gates.
4. Page-specific plan and asset strategy.
5. Visual preferences and examples.
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|----------|----------|-----------------|
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md``visual-planning.md``asset-planning.md``slides +create` |
| 从模板创建或编辑已有本地 PPTX | 导入 PPTX 为 Slides | `lark-slides-pptx-template-workflows.md` |
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide``lark-slides-replace-slide.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token用 shortcut 回读全文 XML 或读取单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `slides +xml-get``xml_presentation.slide.get` |
| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面,一次不超过 10 页 | `slides +screenshot``lark-slides-screenshot.md` |
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`,或 `+create --slides``@./path` 占位符 |
| 绘制图表 | 原生图表用 `<chart>`,其他用 `<shape>` + `<line>`,只有复杂 Mermaid、SVG 用 `<whiteboard>` | `xml-schema-quick-ref.md``slides_chart_demo.xml` |
| 绘制表格 | 优先用 `rect``text` 模拟,其他用 `<table>` | `xml-schema-quick-ref.md` |
| 使用图标 | 禁止盲猜 `iconType`,必须先检索 IconPark再写 `<icon iconType="...">`,图标必须填充颜色并和背景有足够对比,禁止使用 emoji 图标 | `iconpark_tool.py search → resolve``iconpark.md` |
| 创建失败、空白页、3350001、布局异常 | 先回读状态,再按排障清单修复,不假设原操作原子成功 | `troubleshooting.md``validation-checklist.md` |
Do not claim SOTA from a local render or a single VLM judgment.
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),认证、权限和全局参数均以 lark-shared 为准。**
## Non-Negotiable Output Contract
**CRITICAL — 生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。**
- Keep slide-authored titles, body text, labels, data, page numbers, and navigation as native XML elements. Evidence assets may contain inherited text, but it must not carry the slide's title or key conclusion; add a native annotation/caption when needed. Set `autoFit="normal-auto-fit"` on prominent or dense text.
- Use `--as user` unless the user explicitly requests bot identity.
- Use `<img>` only with a Drive `file_token` or a creation-time `@./local-path` placeholder. Never use an external image URL.
- Use a **no-text** image asset for material-rich, editorial, product, or atmospheric visuals. Keep all slide copy outside that image.
- Do not use a full-page generated raster as the final deck. It is a benchmark/reference artifact only.
- Use a meaningful native chart, semantic diagram, screenshot, or high-value asset as each formal page's visual focus. Small icons, repeated cards, or decorative shape stacks do not satisfy this alone.
- Run static diagnostics before writes. Resolve every error and review every warning.
- For every new or materially rebuilt page: validate static XML; content-render it when it has no local `@` image placeholder; then create/update the real presentation and inspect a live screenshot when screenshot service is available. A local placeholder page uses static → live-create → live screenshot as its equivalent path.
**CRITICAL — 新建演示文稿或大幅改写页面时MUST 先生成 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。先创建对应目录规划层规则和中间产物生命周期见 [planning-layer.md](references/planning-layer.md)。仅替换一个标题、插入一个块等小型已有页编辑可豁免。**
## Choose the Visual Route
**CRITICAL — 新建演示文稿或大幅改写页面时,生成 XML 前 MUST 读取 [visual-planning.md](references/visual-planning.md),确保 `layout_type`、`visual_focus`、`text_density` 实际改变页面几何、主视觉和文本量。**
| Page need | Use |
|---|---|
| Material, scene, product, editorial abstraction, cover atmosphere | Generate/find one **no-text** asset; reserve 3545% or an intentional full-bleed region; compose editable XML around it. |
| Process, architecture, comparison, data explanation | Native `chart`, semantic `icon`, `whiteboard` SVG/Mermaid, shapes, and connectors. Make the diagram itself the visual focus. |
| Existing screenshot, figure, or user asset | Upload it, crop deliberately, add concise native annotations, and retain a fallback plan. |
| Asset unavailable | Render the explicit `fallback_if_missing`; never leave an empty image frame or replace it with dense bullets. |
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](references/asset-planning.md)。**
Do not try to mimic a photorealistic object by stacking many shapes. Use an asset when material fidelity is the point; use SVG/shape diagrams when semantics and editability are the point.
**CRITICAL — 将完整 `<slide>` XML 提交给 `slides +create --slides`、`xml_presentation.slide create` 或 `slides +replace-pages` 之前MUST 先把待提交 XML 保存到本地文件并运行 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py)`summary.error_count` 必须为 0 才能调用接口。**
## Required Reads
**CRITICAL — 创建或大幅改写后MUST 按 [validation-checklist.md](references/validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素、检查空白/破损页、明显溢出、布局风险。**
Read these before acting:
**CRITICAL — 创建前自检或失败排障时MUST 按 [troubleshooting.md](references/troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](references/lark-slides-replace-slide.md)(块级替换/插入,不动页序);已有 Slides 的多页大改优先用 [`+replace-pages`](references/lark-slides-replace-pages.md) 在原 presentation 内批量重建页面,避免 `slides +create` 生成新链接。选择 action 和完整读-改-写流程见 [`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)。
## 身份选择
飞书幻灯片通常是用户自己的内容资源。**默认应优先显式使用 `--as user`(用户身份)执行 slides 相关操作**,始终显式指定身份。
- **`--as user`(推荐)**:以当前登录用户身份创建、读取、管理演示文稿。执行前先完成用户授权:
```bash
lark-cli auth login --domain slides
```
- **`--as bot`**:仅在用户明确要求以应用身份操作,或需要让 bot 持有/创建资源时使用。使用 bot 身份时,要额外确认 bot 是否真的有目标演示文稿的访问权限。
**执行规则**
1. 创建、读取、增删 slide、按用户给出的链接继续编辑已有 PPT默认都先用 `--as user`
2. 如果出现权限不足,先检查当前是否误用了 bot 身份;不要默认回退到 bot。
3. 只有在用户明确要求"用应用身份 / bot 身份操作",或当前工作流就是 bot 创建资源后再做协作授权时,才切换到 `--as bot`
## 执行前必做
> **重要**`references/slides_xml_schema_definition.xml` 是此 skill 唯一正确的 XML 协议来源;其他 md 仅是对它和 CLI schema 的摘要。
高频只读:
- [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md)
- [planning-layer.md](references/planning-layer.md)(新建 / 大幅改写)
- [visual-planning.md](references/visual-planning.md)(新建 / 大幅改写)
- [asset-planning.md](references/asset-planning.md)(新建 / 大幅改写)
- [validation-checklist.md](references/validation-checklist.md)(创建 / 大幅改写后)
按需再读:
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)
- 编辑:[`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)、[`lark-slides-replace-slide.md`](references/lark-slides-replace-slide.md)、[`lark-slides-replace-pages.md`](references/lark-slides-replace-pages.md)
- 截图:[`lark-slides-screenshot.md`](references/lark-slides-screenshot.md)
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
- 图标:[`iconpark.md`](references/iconpark.md)、[`scripts/iconpark_tool.py`](scripts/iconpark_tool.py)
- 排障:[`troubleshooting.md`](references/troubleshooting.md)
- 完整协议:[`slides_xml_schema_definition.xml`](references/slides_xml_schema_definition.xml)
| Situation | Read |
|---|---|
| Always | `../lark-shared/SKILL.md`, `references/xml-schema-quick-ref.md` |
| New deck or major rebuild | `references/planning-layer.md`, `references/visual-planning.md`, `references/asset-planning.md` |
| Asset-first or raw-image visual comparison | `references/visual-ceiling.md` |
| Create/replace/read media | the matching `lark-slides-*.md` reference from the table below |
| Validation | `references/validation-checklist.md`, `references/quality-loop.md` |
| Benchmark/SOTA request | `references/external-evaluation.md` |
## Workflow
> **这是演示文稿,不是文档。** 每页 slide 是独立的视觉画面,信息密度要适当,排版要留白。
### 1. Decide scope and route
### Design Ideas
- Existing presentation, small edit: read the target page and use block-level replacement.
- Existing presentation, multi-page rebuild: use `+replace-pages` to preserve the original link.
- New deck or major rewrite: create `.lark-slides/plan/<task-id>/slide_plan.json` first.
不要生成无设计感的幻灯片。纯白背景 + 标题 + bullets 只能作为极简临时稿,不能作为正式交付。
The plan must name the audience, page role, `layout_type`, `key_message`, `visual_focus`, `text_density`, `asset_need`, visual system, and fallback for every planned asset.
开始写 XML 前,先在 `slide_plan.json` 里确定 deck 级视觉策略:
### 2. Make the visual system explicit
- **主题化配色**:配色必须服务本次主题、行业和受众,不要默认蓝色商务风。如果把同一套颜色换到另一个完全不同主题仍然成立,说明配色不够具体。
- **主次比例**:选择 1 个主色承担约 60-70% 视觉权重1-2 个辅助色承担结构和分区1 个强调色只用于关键数字、结论或行动点。不要让所有颜色权重相同。
- **背景一致性**:先确定全 deck 的背景策略,默认保持同一明暗基调和底色体系;只有分节、转场或强调页才有意改变背景,并必须通过相同主色、纹理、边栏或 motif 让变化看起来属于同一套设计。无论深浅,都要保证正文、图标和线条对比充足。
- **统一 motif**:选择一个可复用视觉母题贯穿全文,例如粗侧边栏、圆形图标底、半出血图片区、编号节点、卡片左上角色块或大号数字。不要每页换一套装饰语言。
Choose one background strategy, one reusable motif, primary/secondary/accent color roles, and typography limits. Vary page geometry by page role; do not vary decorative language arbitrarily.
每页至少要有一个视觉元素:图片、图标、图表、表格、流程、对比结构、大号数字、示意图或由 shape 组成的抽象视觉。文本框本身不算主视觉。
Before XML, verify that each page answers:
可优先考虑这些页面形态:
1. What is the dominant visual and why does it clarify the key message?
2. Is it large enough to be noticed before supporting copy?
3. Is all user-facing copy native and editable?
4. Is the layout visibly different from other page roles?
- **双栏结构**:左文右图或左图右文,视觉区域占 35-45% 宽度。
- **图标行**:图标在色块或圆形底中,右侧是短标题和一句解释。
- **2x2 / 2x3 网格**:适合能力、模块、风险、行动项,每格内容保持同等层级。
- **半出血视觉**:图片或抽象形状占据左/右半屏,文字覆盖或贴边排布。
- **大数字卡片**:关键指标用 60-72pt 数字,下面配 10-14pt 标签。
- **对比列**before/after、方案 A/B、问题/解法用左右并列,标题和基线严格对齐。
- **时间线/流程图**:步骤用节点和箭头表达,流程方向必须一眼可见。
### 3. Build assets and XML
字体和间距建议:
- For generated images, request no text and reserve the intended copy region in the prompt. Place it with `<img src="@./path">` only in `+create --slides`, or upload first and use the returned token.
- For icons, run `scripts/iconpark_tool.py search` before choosing `iconType`; set an explicit visible fill.
- Use native `<chart>` for supported data charts. Use `<whiteboard>` only for complex SVG/Mermaid diagrams.
- Keep non-background content inside the canvas and maintain readable margins, contrast, and text fit.
- Save XML locally before submission and run `scripts/xml_text_overlap_lint.py`. Do not call the API while errors remain.
- 标题 36-44pt关键结论可更大正文 14-18pt注释 10-12pt。
- 正文默认左对齐;只在封面、结尾或大号数字场景中使用居中。
- 页面边距至少 40px内容块之间保持 24-40px 间距,并在同一 deck 内保持一致。
- 卡片内边距要真实留出空间,不要让文字贴边;对齐 shape 和文字时要考虑文本框 padding。
### 4. Create or edit
常见错误必须避免:
- Use `slides +create --as user --slides` for simple new pages, including creation-time local image upload.
- For complex XML, Chinese-heavy content, or difficult escaping: create the presentation, inspect the API schema, then add pages through `xml_presentation.slide create` with `jq`-built payloads.
- For a local edit, prefer `+replace-slide`; for a multi-page rewrite, prefer `+replace-pages`.
- After any partial failure, read current state before retrying. Do not assume creation was atomic.
- 不要所有页面复用同一种标题 + 三 bullets 版式。
- 不要用低对比文字或低对比图标,例如浅灰字压在浅色背景上。
- 不要让装饰线穿过文字,或让页脚、来源、编号挤压主体内容。
- 不要把素材缺失表现为空白图片框;必须按 `fallback_if_missing` 生成 XML-native 视觉。
- 不要留下模板占位文案、示例公司名、示例日期或与用户主题无关的原模板内容。
- 不要使用 emoji。
- 不要为了画出一个具象物体而堆叠 3 个以上仅用于拟形的 shape。
### 5. Verify live output
### 创建方式选择
1. Run static diagnostics and retain the report.
2. Render changed XML to PNG when supported.
3. Read back presentation XML; confirm page count, image tokens, key text, and primary elements.
4. Screenshot every changed live page; repair clipping, weak contrast, bad crop, or weak visual focus.
| 场景 | 推荐方式 |
|------|----------|
| 简单 XML1-3 页、结构简单、几乎无复杂中文和特殊字符) | `slides +create --slides '[...]'` 一步创建 |
| 复杂 XML多页、含中文、大段文本、复杂布局、嵌套引号、特殊字符较多 | **两步创建**:先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide create` 逐页添加 |
| 已有 PPT 继续追加或插入页面 | 使用 `xml_presentation.slide create`,必要时配合 `before_slide_id` |
### 6. Evaluate visual ceiling when requested
> [!WARNING]
> `--slides '[...]'` 的风险点主要在 shell 参数传递,而不是单纯页数。即使只有 1 页,只要 XML 足够复杂,也建议使用两步创建法。
For raw-image comparison, use the same brief:
> [!IMPORTANT]
> `slides +create --slides` 底层会逐页创建,不是原子操作。中途失败时先记录 `xml_presentation_id`,回读确认当前状态,再继续修复或追加。
- raw baseline: one raster page per slide;
- candidate: asset-first structured XML with native text/layout;
- compare as a simultaneous contact sheet or balanced A/B pair, never only as a sequential image list;
- record visual polish, hierarchy, consistency, text fidelity, and editability separately.
### 生成流程
A local tie or win supports only that declared comparison. SOTA requires a public/declared task set, named baselines, repeated position-balanced evaluation, raw artifacts, and preferably human preference evidence.
```text
Step 1: 需求澄清 & 读取知识
- 澄清主题、受众、页数、风格;若用户上传 PPTX 作为模板,按顶部『用户自定义模板』规则处理
- 读取 xml-schema-quick-ref.md新建 / 大幅改写时还要读取 planning-layer.md、visual-planning.md、asset-planning.md
## Reference Map
Step 2: 生成大纲 → 用户确认 → 写入 slide_plan.json
- 生成结构化大纲供用户确认
- 新建 / 大幅改写必须先创建目录并写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`
- plan 字段、路径命名和 `asset_need` 结构按 planning-layer.md / asset-planning.md 执行
| Need | Reference |
|---|---|
| Planning / page geometry / assets | `planning-layer.md`, `visual-planning.md`, `asset-planning.md` |
| Image/media workflow | `lark-slides-media-upload.md`, `lark-slides-create.md` |
| Editing and replacement | `lark-slides-edit-workflows.md`, `lark-slides-replace-slide.md`, `lark-slides-replace-pages.md` |
| Readback and screenshots | `lark-slides-xml-get.md`, `lark-slides-screenshot.md` |
| XML protocol | `references/xml-schema-quick-ref.md`, `references/slides_xml_schema_definition.xml` |
| Icons | `iconpark.md`, `scripts/iconpark_tool.py` |
| Diagnostics and recovery | `validation-checklist.md`, `quality-loop.md`, `troubleshooting.md` |
| Visual ceiling and claims | `visual-ceiling.md`, `external-evaluation.md` |
Step 3: 按 slide_plan.json 生成 XML → 创建
- 逐页消费 plankey_message 定主结论layout_type 定几何visual_focus 定主视觉text_density 定文本量
- 缺少真实素材时必须用 `fallback_if_missing` 生成 XML-native 兜底视觉;不要留空
- 调用创建或整页替换接口前,先保存待提交 XML 并运行 xml_text_overlap_lint.pyerror_count 不为 0 必须先修
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
## Delivery Evidence
Step 4: 审查 & 交付
- 创建完成后,必须用 `slides +xml-get` 读取全文 XML并按 validation-checklist.md 做显式验证记录
- 失败或部分成功按 troubleshooting.md 处理;局部问题优先用 `+replace-slide` 修正
- 没问题 → 交付:告知用户演示文稿 ID 和访问方式
```
### jq 命令模板(编辑已有 PPT 时使用)
新建 PPT 推荐用 `+create --slides`。以下 jq 模板适用于向已有演示文稿追加页面的场景,可以避免手动转义双引号:
```bash
# 追加到末尾
lark-cli slides xml_presentation.slide create \
--as user \
--params '{"xml_presentation_id":"YOUR_ID"}' \
--data "$(jq -n --arg content '<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="BACKGROUND_COLOR"/></fill></style>
<data>
<!-- 在这里放置 shape、line、table、chart 等元素 -->
</data>
</slide>' '{slide:{content:$content}}')"
# 插到指定页之前before_slide_id 必须在 --data body 里,与 slide 同级
# ⚠️ 不要把 before_slide_id 写进 --params —— CLI 会当未知 query 参数静默下发,服务端忽略,新页跑到末尾
lark-cli slides xml_presentation.slide create \
--as user \
--params '{"xml_presentation_id":"YOUR_ID"}' \
--data "$(jq -n --arg content '<slide ...>...</slide>' --arg before 'TARGET_SLIDE_ID' \
'{slide:{content:$content}, before_slide_id:$before}')"
```
> 渐变色必须使用 `rgba()` 格式并带百分比停靠点,如 `linear-gradient(135deg,rgba(15,23,42,1) 0%,rgba(56,97,140,1) 100%)`。使用 `rgb()` 或省略停靠点会导致服务端回退为白色。
### 大纲模板
生成大纲时使用以下格式,交给用户确认:
```text
[PPT 标题] — [定位描述],面向 [目标受众]
页面结构N 页):
1. 封面页:[标题文案]
2. [页面主题][要点1]、[要点2]、[要点3]
3. [页面主题][要点描述]
...
N. 结尾页:[结尾文案]
风格:[配色方案][排版风格]
```
## 核心概念
### URL 格式与 Token
| URL 格式 | 示例 | Token 类型 | 处理方式 |
|----------|------|-----------|----------|
| `/slides/` | `https://example.larkoffice.com/slides/xxxxxxxxxxxxx` | `xml_presentation_id` | URL 路径中的 token 直接作为 `xml_presentation_id` 使用 |
| `/wiki/` | `https://example.larkoffice.com/wiki/wikcnxxxxxxxxx` | `wiki_token` | ⚠️ **不能直接使用**,需要先查询获取真实的 `obj_token` |
> `+replace-slide` 和 `+media-upload` shortcut 会自动解析以上两种 URL直接调用原生 API 时仍需手动解析 wiki 链接。
### Wiki 链接特殊处理(关键!)
知识库链接(`/wiki/TOKEN`)不能直接当 `xml_presentation_id`。直接调用原生 API 前,先查询 wiki 节点,确认 `node.obj_type == "slides"`,再用 `node.obj_token` 作为真实 presentation ID。
```bash
lark-cli wiki spaces get_node --as user --params '{"token":"wiki_token"}'
```
Shortcut `+replace-slide``+media-upload` 会自动解析 `/wiki/` URL手动调用 `xml_presentations.*` / `xml_presentation.slide.*` 时才需要自己做这一步。
### 资源关系
```text
Wiki Space (知识空间)
└── Wiki Node (知识库节点, obj_type: slides)
└── obj_token → xml_presentation_id
Slides (演示文稿)
├── xml_presentation_id (演示文稿唯一标识)
├── revision_id (版本号)
└── Slide (幻灯片页面)
└── slide_id (页面唯一标识)
```
## Shortcuts 与 API
Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`)。有 Shortcut 的操作优先使用。
| Shortcut | 说明 |
|----------|------|
| [`+create`](references/lark-slides-create.md) | 创建 PPT可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
| [`+xml-get`](references/lark-slides-xml-get.md) | 读取全文或单页 XML并可保存到本地文件避免终端输出被截断 |
| [`+media-upload`](references/lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
| [`+replace-slide`](references/lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
| [`+replace-pages`](references/lark-slides-replace-pages.md) | 在原演示文稿内批量重建多个页面:先创建新页到旧页前,再删除旧页;适合已有 Slides 的多页大改,不新建链接 |
没有 Shortcut 覆盖时使用原生 API。高频资源`slides +xml-get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
```bash
lark-cli schema slides.<resource>.<method> # 调用 API 前必须先查看参数结构
lark-cli slides <resource> <method> [flags] # 调用 API
```
> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
## 核心规则
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;模板、风格和大纲只能作为规划输入,不能绕过规划层
2. **创建流程**:简单短 XML1-3 页、结构简单、特殊字符少)可用 `slides +create --slides '[...]'` 一步创建;复杂内容、含图片/中文大段文本/嵌套引号/较多特殊字符,或超过 10 页时,默认先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide.create` 逐页添加
3. **`<slide>` 直接子元素只有 `<style>``<data>``<note>`**:文本和图形必须放在 `<data>`
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内
5. **保存关键 ID**:后续操作需要 `xml_presentation_id``slide_id``revision_id`
6. **删除谨慎**:删除操作不可逆,且至少保留一页幻灯片
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide``block_replace` / `block_insert`),不要整页重建;已有 Slides 的多页整页重建用 `+replace-pages`,不要用 `slides +create` 新建整份 PPT只有没有 shortcut 覆盖的特殊单页整页操作才手动 `slide.create` + `slide.delete`
8. **`<img src>` 只能用上传到飞书 drive 的 `file_token`,禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 在 PPT 里通常不显示或显示破图。流程必须是「先把图存到本地 → 用 `slides +media-upload` 上传或 `+create --slides``@./path` 占位符自动上传 → 拿 `file_token` 写进 `<img src>`」。如果用户给了网图链接,先 `curl`/下载到 CWD 内再走上传流程,不要直接把外链 URL 塞进 `src`。**图片最大 20 MB**slides upload API 不支持分片上传)。
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。
Return the presentation URL/ID, changed page IDs, static report status, live screenshot locations, and any unresolved tradeoffs. Do not call a deck visually accepted merely because the create API returned success.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Run a manifest-driven structural and renderability benchmark for SML slides."""
from __future__ import annotations
import argparse
import json
import re
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from run import lint_xml, relative_to_cwd, run_cli
from xml_text_overlap_lint import extract_elements
def load_manifest(path: Path) -> dict[str, Any]:
manifest = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(manifest.get("cases"), list) or not manifest["cases"]:
raise ValueError("manifest must contain a non-empty cases list")
return manifest
def validate_case(case: dict[str, Any], manifest_dir: Path) -> tuple[Path, dict[str, Any]]:
if not isinstance(case.get("id"), str) or not isinstance(case.get("xml"), str):
raise ValueError("every case needs string id and xml fields")
xml_path = (manifest_dir / case["xml"]).resolve()
xml = xml_path.read_text(encoding="utf-8")
static = lint_xml(xml, str(xml_path))
slide_issues = [issue for slide in static["slides"] for issue in slide["issues"]]
elements = [element for slide_xml in [xml] for element in extract_elements(slide_xml)]
checks = case.get("checks", {})
failures: list[str] = []
if static["summary"]["error_count"]:
failures.append(f"{static['summary']['error_count']} static error(s)")
if static["summary"]["warning_count"] and not checks.get("allow_warnings", False):
failures.append(f"{static['summary']['warning_count']} unresolved warning(s)")
if len(elements) < checks.get("min_elements", 0):
failures.append(f"expected at least {checks['min_elements']} elements, got {len(elements)}")
present_kinds = {element["kind"] for element in elements}
for kind in checks.get("required_kinds", []):
if kind not in present_kinds:
failures.append(f"missing required visual kind: {kind}")
for text in checks.get("required_text", []):
if text not in xml:
failures.append(f"missing required text: {text}")
local_media = re.findall(r'<img\b[^>]*\bsrc="@([^"\\]+)"', xml)
for raw_path in local_media:
media_path = Path(raw_path)
if media_path.is_absolute() or ".." in media_path.parts:
failures.append(f"unsafe local image placeholder: @{raw_path}")
elif not (Path.cwd() / media_path).is_file():
failures.append(f"local image placeholder not found: @{raw_path}")
return xml_path, {
"id": case["id"],
"static": static,
"element_count": len(elements),
"kinds": sorted(present_kinds),
"issues": slide_issues,
"requires_live_media_upload": bool(local_media),
"failures": failures,
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Benchmark SML slide fixtures with static and rendered evidence.")
parser.add_argument("--manifest", required=True, type=Path)
parser.add_argument("--mode", choices=("static", "render", "live"), default="static")
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--title", default="[Eval] Lark Slides benchmark")
parser.add_argument("--confirm-write", action="store_true", help="Required before --mode live creates a presentation.")
options = parser.parse_args(argv)
manifest_path = options.manifest.resolve()
manifest = load_manifest(manifest_path)
output_dir = options.output_dir or Path(".lark-slides/eval-runs") / ("benchmark-" + datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ"))
output_dir.mkdir(parents=True, exist_ok=True)
cases: list[dict[str, Any]] = []
xml_paths: list[Path] = []
for case in manifest["cases"]:
xml_path, result = validate_case(case, manifest_path.parent)
xml_paths.append(xml_path)
if options.mode == "render" and result["requires_live_media_upload"]:
result["render_skipped_reason"] = "local_image_placeholder_requires_live_create"
result["screenshots"] = []
elif options.mode == "render" and not result["failures"]:
rendered = run_cli(
[
"slides", "+screenshot", "--as", "user", "--content", "@" + relative_to_cwd(xml_path),
"--output-dir", relative_to_cwd(output_dir), "--output-name", result["id"],
]
)
result["screenshots"] = [item["path"] for item in rendered["data"]["screenshots"]]
else:
result["screenshots"] = []
cases.append(result)
presentation: dict[str, Any] | None = None
if options.mode == "live":
if not options.confirm_write:
parser.error("--mode live requires --confirm-write because it creates a presentation")
if not any(case["failures"] for case in cases):
created = run_cli(
["slides", "+create", "--as", "user", "--title", options.title, "--slides", json.dumps([path.read_text(encoding="utf-8") for path in xml_paths])]
)
presentation = created["data"]
screenshot = run_cli(
[
"slides", "+screenshot", "--as", "user", "--presentation", presentation["xml_presentation_id"],
*[flag for slide_id in presentation["slide_ids"] for flag in ("--slide-id", slide_id)],
"--output-dir", relative_to_cwd(output_dir),
]
)
for case, item in zip(cases, screenshot["data"]["screenshots"]):
case["screenshots"] = [item["path"]]
passed = sum(
not case["failures"]
and (
options.mode == "static"
or bool(case["screenshots"])
or (options.mode == "render" and "render_skipped_reason" in case)
)
for case in cases
)
report = {
"name": manifest.get("name", manifest_path.stem),
"mode": options.mode,
"summary": {
"case_count": len(cases),
"passed": passed,
"pass_rate": passed / len(cases),
"rendered": sum(bool(case["screenshots"]) for case in cases) if options.mode == "render" else None,
"render_skipped": sum("render_skipped_reason" in case for case in cases) if options.mode == "render" else None,
},
"cases": cases,
"limits": "This benchmark proves declared structural requirements and renderer/live evidence. Local @ image placeholders skip content render and require live-create evidence. It does not establish human preference or global state of the art.",
}
if presentation is not None:
report["presentation"] = presentation
(output_dir / "benchmark.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if passed == len(cases) else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent))
import benchmark # noqa: E402
SLIDE = '''<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><shape type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/><shape type="text" topLeftX="80" topLeftY="80" width="600" height="80"><content autoFit="normal-auto-fit" fontSize="36"><p>Expected claim</p></content></shape></data></slide>'''
class BenchmarkTest(unittest.TestCase):
def test_static_benchmark_enforces_manifest_contract(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
(root / "slide.xml").write_text(SLIDE, encoding="utf-8")
(root / "cases.json").write_text(
json.dumps({"name": "test", "cases": [{"id": "case", "xml": "slide.xml", "checks": {"min_elements": 2, "required_kinds": ["shape"], "required_text": ["Expected claim"]}}]}),
encoding="utf-8",
)
output_dir = root / "out"
self.assertEqual(benchmark.main(["--manifest", str(root / "cases.json"), "--output-dir", str(output_dir)]), 0)
report = json.loads((output_dir / "benchmark.json").read_text(encoding="utf-8"))
self.assertEqual(report["summary"]["case_count"], 1)
self.assertEqual(report["summary"]["passed"], 1)
self.assertEqual(report["summary"]["pass_rate"], 1)
def test_static_benchmark_fails_missing_required_text(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
(root / "slide.xml").write_text(SLIDE, encoding="utf-8")
(root / "cases.json").write_text(json.dumps({"cases": [{"id": "case", "xml": "slide.xml", "checks": {"required_text": ["missing"]}}]}), encoding="utf-8")
self.assertEqual(benchmark.main(["--manifest", str(root / "cases.json"), "--output-dir", str(root / "out")]), 1)
def test_render_benchmark_requires_and_records_a_screenshot(self) -> None:
with tempfile.TemporaryDirectory(dir=Path.cwd()) as temp_dir:
root = Path(temp_dir)
(root / "slide.xml").write_text(SLIDE, encoding="utf-8")
(root / "cases.json").write_text(json.dumps({"cases": [{"id": "case", "xml": "slide.xml"}]}), encoding="utf-8")
with patch.object(benchmark, "run_cli", return_value={"data": {"screenshots": [{"path": "/tmp/case.png"}]}}):
self.assertEqual(benchmark.main(["--manifest", str(root / "cases.json"), "--mode", "render", "--output-dir", str(root / "out")]), 0)
report = json.loads((root / "out" / "benchmark.json").read_text(encoding="utf-8"))
self.assertEqual(report["cases"][0]["screenshots"], ["/tmp/case.png"])
def test_render_benchmark_skips_local_image_until_live_create(self) -> None:
with tempfile.TemporaryDirectory(dir=Path.cwd()) as temp_dir:
root = Path(temp_dir)
(Path.cwd() / "hero.png").write_bytes(b"fixture")
image_slide = '<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><img src="@./hero.png" topLeftX="0" topLeftY="0" width="960" height="540"/></data></slide>'
(root / "slide.xml").write_text(image_slide, encoding="utf-8")
(root / "cases.json").write_text(json.dumps({"cases": [{"id": "case", "xml": "slide.xml"}]}), encoding="utf-8")
with patch.object(benchmark, "run_cli") as run_cli:
self.assertEqual(benchmark.main(["--manifest", str(root / "cases.json"), "--mode", "render", "--output-dir", str(root / "out")]), 0)
run_cli.assert_not_called()
report = json.loads((root / "out" / "benchmark.json").read_text(encoding="utf-8"))
self.assertEqual(report["cases"][0]["render_skipped_reason"], "local_image_placeholder_requires_live_create")
(Path.cwd() / "hero.png").unlink()
def test_static_benchmark_fails_missing_local_image(self) -> None:
with tempfile.TemporaryDirectory(dir=Path.cwd()) as temp_dir:
root = Path(temp_dir)
image_slide = '<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><img src="@./missing.png" topLeftX="0" topLeftY="0" width="960" height="540"/></data></slide>'
(root / "slide.xml").write_text(image_slide, encoding="utf-8")
(root / "cases.json").write_text(json.dumps({"cases": [{"id": "case", "xml": "slide.xml"}]}), encoding="utf-8")
self.assertEqual(benchmark.main(["--manifest", str(root / "cases.json"), "--output-dir", str(root / "out")]), 1)
def test_live_benchmark_requires_explicit_write_confirmation(self) -> None:
with tempfile.TemporaryDirectory(dir=Path.cwd()) as temp_dir:
root = Path(temp_dir)
(root / "slide.xml").write_text(SLIDE, encoding="utf-8")
(root / "cases.json").write_text(json.dumps({"cases": [{"id": "case", "xml": "slide.xml"}]}), encoding="utf-8")
with self.assertRaises(SystemExit):
benchmark.main(["--manifest", str(root / "cases.json"), "--mode", "live", "--output-dir", str(root / "out")])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,15 @@
{
"name": "lark-slides-quality-core",
"fixture_assets": [
{
"path": "skills/lark-slides/eval/assets/quality-loop-hero-v1.png",
"source": "OpenAI image generation, user-authorized local evaluation fixture",
"purpose": "No-text cover focal visual; all slide copy remains native XML."
}
],
"cases": [
{"id": "cover", "xml": "cases/cover_with_image.xml", "checks": {"min_elements": 8, "required_kinds": ["img", "shape"], "required_text": ["QUALITY SYSTEM / 2026", "经得起看见"]}},
{"id": "comparison", "xml": "cases/comparison.xml", "checks": {"min_elements": 12, "required_kinds": ["shape"], "required_text": ["QUALITY EVIDENCE", "截图验证通过"]}},
{"id": "architecture", "xml": "cases/architecture.xml", "checks": {"min_elements": 20, "required_kinds": ["shape"], "required_text": ["QUALITY EVIDENCE", "截图验收"]}}
]
}

View File

@@ -0,0 +1,36 @@
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="rgba(10, 22, 40, 1)"/></fill></style>
<data>
<shape type="rect" topLeftX="0" topLeftY="0" width="12" height="540"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="56" topLeftY="48" width="570" height="58"><content autoFit="normal-auto-fit" textType="title" fontSize="38" color="rgba(248, 250, 252, 1)" bold="true"><p>从需求到可验证交付</p></content></shape>
<shape type="text" topLeftX="58" topLeftY="112" width="550" height="44"><content autoFit="normal-auto-fit" fontSize="17" color="rgba(190, 205, 224, 1)"><p>把视觉验收嵌入 XML 生成链路,而不是在交付后被动补救。</p></content></shape>
<shape type="rect" topLeftX="56" topLeftY="198" width="174" height="176"><fill><fillColor color="linear-gradient(145deg,rgba(37, 91, 141, 1) 0%,rgba(19, 52, 85, 1) 68%,rgba(10, 31, 58, 1) 100%)"/></fill><border color="rgba(103, 232, 249, 0.72)" width="1"/></shape>
<shape type="rect" topLeftX="56" topLeftY="198" width="174" height="6"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="76" topLeftY="226" width="134" height="28"><content autoFit="normal-auto-fit" fontSize="15" color="rgba(181, 255, 88, 1)" bold="true" textAlign="center"><p>01 · PLAN</p></content></shape>
<shape type="text" topLeftX="76" topLeftY="270" width="134" height="35"><content autoFit="normal-auto-fit" fontSize="20" color="rgba(248, 250, 252, 1)" bold="true" textAlign="center"><p>设计意图</p></content></shape>
<icon iconType="iconpark/Edit/plan.svg" topLeftX="124" topLeftY="326" width="38" height="26"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></icon>
<shape type="right-arrow" topLeftX="236" topLeftY="274" width="28" height="28"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="rect" topLeftX="270" topLeftY="198" width="174" height="176"><fill><fillColor color="linear-gradient(145deg,rgba(37, 91, 141, 1) 0%,rgba(19, 52, 85, 1) 68%,rgba(10, 31, 58, 1) 100%)"/></fill><border color="rgba(103, 232, 249, 0.72)" width="1"/></shape>
<shape type="rect" topLeftX="270" topLeftY="198" width="174" height="6"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="290" topLeftY="226" width="134" height="28"><content autoFit="normal-auto-fit" fontSize="15" color="rgba(181, 255, 88, 1)" bold="true" textAlign="center"><p>02 · BUILD</p></content></shape>
<shape type="text" topLeftX="290" topLeftY="270" width="134" height="35"><content autoFit="normal-auto-fit" fontSize="20" color="rgba(248, 250, 252, 1)" bold="true" textAlign="center"><p>XML 诊断</p></content></shape>
<icon iconType="iconpark/Edit/code.svg" topLeftX="338" topLeftY="326" width="38" height="26"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></icon>
<shape type="right-arrow" topLeftX="450" topLeftY="274" width="28" height="28"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="rect" topLeftX="484" topLeftY="198" width="178" height="176"><fill><fillColor color="linear-gradient(145deg,rgba(37, 91, 141, 1) 0%,rgba(19, 52, 85, 1) 68%,rgba(10, 31, 58, 1) 100%)"/></fill><border color="rgba(103, 232, 249, 0.72)" width="1"/></shape>
<shape type="rect" topLeftX="484" topLeftY="198" width="178" height="6"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="504" topLeftY="226" width="138" height="28"><content autoFit="normal-auto-fit" fontSize="15" color="rgba(181, 255, 88, 1)" bold="true" textAlign="center"><p>03 · SEE</p></content></shape>
<shape type="text" topLeftX="504" topLeftY="270" width="138" height="35"><content autoFit="normal-auto-fit" fontSize="20" color="rgba(248, 250, 252, 1)" bold="true" textAlign="center"><p>截图验收</p></content></shape>
<icon iconType="iconpark/Edit/screenshot-one.svg" topLeftX="554" topLeftY="326" width="38" height="26"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></icon>
<shape type="rect" topLeftX="700" topLeftY="70" width="206" height="322"><fill><fillColor color="linear-gradient(160deg,rgba(248, 253, 255, 1) 0%,rgba(223, 240, 250, 1) 100%)"/></fill><border color="rgba(181, 255, 88, 0.82)" width="1"/></shape>
<shape type="rect" topLeftX="700" topLeftY="70" width="206" height="8"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="722" topLeftY="108" width="160" height="26"><content autoFit="normal-auto-fit" fontSize="12" color="rgba(20, 54, 82, 1)" bold="true"><p>QUALITY EVIDENCE</p></content></shape>
<shape type="text" topLeftX="720" topLeftY="150" width="164" height="78"><content autoFit="normal-auto-fit" fontSize="25" color="rgba(10, 22, 40, 1)" bold="true"><p>先看见,</p><p>再交付。</p></content></shape>
<shape type="text" topLeftX="720" topLeftY="274" width="164" height="46"><content autoFit="normal-auto-fit" fontSize="13" color="rgba(71, 100, 126, 1)"><p>每一个 warning 都要有截图证据。</p></content></shape>
<shape type="rect" topLeftX="720" topLeftY="330" width="164" height="1"><fill><fillColor color="rgba(103, 149, 183, 0.45)"/></fill></shape>
<shape type="ellipse" topLeftX="720" topLeftY="344" width="14" height="14"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="744" topLeftY="340" width="126" height="22"><content autoFit="normal-auto-fit" fontSize="11" color="rgba(71, 100, 126, 1)" bold="true"><p>TRACEABLE OUTPUT</p></content></shape>
<line startX="56" startY="444" endX="906" endY="444"><border color="rgba(74, 103, 132, 1)" width="1"/></line>
<shape type="text" topLeftX="58" topLeftY="466" width="600" height="24"><content autoFit="normal-auto-fit" fontSize="13" color="rgba(190, 205, 224, 1)"><p>PLAN · BUILD · RENDER · REVIEW · REPAIR</p></content></shape>
<shape type="text" topLeftX="816" topLeftY="466" width="90" height="24"><content autoFit="normal-auto-fit" fontSize="13" color="rgba(181, 255, 88, 1)" bold="true" textAlign="right"><p>03 / 03</p></content></shape>
</data>
</slide>

View File

@@ -0,0 +1,24 @@
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="rgba(246, 249, 252, 1)"/></fill></style>
<data>
<shape type="rect" topLeftX="0" topLeftY="0" width="960" height="76"><fill><fillColor color="rgba(10, 22, 40, 1)"/></fill></shape>
<shape type="rect" topLeftX="0" topLeftY="0" width="12" height="76"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="42" topLeftY="20" width="700" height="34"><content autoFit="normal-auto-fit" fontSize="26" color="rgba(248, 250, 252, 1)" bold="true"><p>从“能生成”到“能交付”</p></content></shape>
<shape type="rect" topLeftX="56" topLeftY="136" width="370" height="280"><fill><fillColor color="rgba(231, 238, 246, 1)"/></fill></shape>
<shape type="rect" topLeftX="56" topLeftY="136" width="370" height="6"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="86" topLeftY="172" width="300" height="30"><content autoFit="normal-auto-fit" fontSize="16" color="rgba(71, 100, 126, 1)" bold="true"><p>ONLY XML SUCCESS</p></content></shape>
<shape type="text" topLeftX="84" topLeftY="222" width="300" height="52"><content autoFit="normal-auto-fit" fontSize="28" color="rgba(10, 22, 40, 1)" bold="true"><p>接口返回成功</p></content></shape>
<shape type="ellipse" topLeftX="342" topLeftY="166" width="44" height="44"><fill><fillColor color="rgba(255, 255, 255, 0.78)"/></fill></shape>
<shape type="text" topLeftX="342" topLeftY="176" width="44" height="22"><content autoFit="normal-auto-fit" fontSize="12" color="rgba(71, 100, 126, 1)" bold="true" textAlign="center"><p>01</p></content></shape>
<shape type="text" topLeftX="86" topLeftY="304" width="286" height="54"><content autoFit="normal-auto-fit" fontSize="16" color="rgba(71, 100, 126, 1)"><p>未检查内容是否被裁切,是否真的形成视觉层级。</p></content></shape>
<shape type="rect" topLeftX="534" topLeftY="136" width="370" height="280"><fill><fillColor color="rgba(10, 22, 40, 1)"/></fill></shape>
<shape type="rect" topLeftX="534" topLeftY="136" width="370" height="6"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="564" topLeftY="172" width="300" height="30"><content autoFit="normal-auto-fit" fontSize="16" color="rgba(181, 255, 88, 1)" bold="true"><p>QUALITY EVIDENCE</p></content></shape>
<shape type="text" topLeftX="562" topLeftY="222" width="300" height="52"><content autoFit="normal-auto-fit" fontSize="28" color="rgba(248, 250, 252, 1)" bold="true"><p>截图验证通过</p></content></shape>
<shape type="ellipse" topLeftX="820" topLeftY="166" width="44" height="44"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="820" topLeftY="176" width="44" height="22"><content autoFit="normal-auto-fit" fontSize="12" color="rgba(10, 22, 40, 1)" bold="true" textAlign="center"><p>02</p></content></shape>
<shape type="text" topLeftX="564" topLeftY="304" width="286" height="54"><content autoFit="normal-auto-fit" fontSize="16" color="rgba(190, 205, 224, 1)"><p>静态诊断、渲染截图、真实页面回读构成可复现证据。</p></content></shape>
<shape type="right-arrow" topLeftX="452" topLeftY="258" width="56" height="36"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="428" topLeftY="320" width="104" height="22"><content autoFit="normal-auto-fit" fontSize="11" color="rgba(71, 100, 126, 1)" bold="true" textAlign="center"><p>ADD EVIDENCE</p></content></shape>
</data>
</slide>

View File

@@ -0,0 +1,14 @@
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="rgba(10, 22, 40, 1)"/></fill></style>
<data>
<img src="@./skills/lark-slides/eval/assets/quality-loop-hero-v1.png" topLeftX="0" topLeftY="0" width="960" height="540"/>
<shape type="rect" topLeftX="0" topLeftY="0" width="14" height="540"><fill><fillColor color="rgba(181, 255, 88, 1)"/></fill></shape>
<shape type="text" topLeftX="66" topLeftY="82" width="470" height="30"><content autoFit="normal-auto-fit" fontSize="15" color="rgba(181, 255, 88, 1)" bold="true"><p>QUALITY SYSTEM / 2026</p></content></shape>
<shape type="text" topLeftX="62" topLeftY="138" width="480" height="118"><content autoFit="normal-auto-fit" textType="title" fontSize="46" color="rgba(248, 250, 252, 1)" bold="true"><p>让每一页</p><p>经得起看见</p></content></shape>
<shape type="text" topLeftX="66" topLeftY="282" width="420" height="48"><content autoFit="normal-auto-fit" fontSize="18" color="rgba(215, 228, 242, 1)"><p>从 XML 合法到视觉可交付的 Slides 质量闭环。</p></content></shape>
<line startX="66" startY="384" endX="420" endY="384"><border color="rgba(180, 211, 235, 0.55)" width="1"/></line>
<shape type="text" topLeftX="66" topLeftY="402" width="420" height="26"><content autoFit="normal-auto-fit" fontSize="14" color="rgba(215, 228, 242, 1)"><p>PLAN · BUILD · RENDER · REVIEW</p></content></shape>
<shape type="round-rect" topLeftX="694" topLeftY="398" width="196" height="44"><fill><fillColor color="rgba(10, 22, 40, 0.72)"/></fill><border color="rgba(181, 255, 88, 0.75)" width="1"/></shape>
<shape type="text" topLeftX="710" topLeftY="408" width="164" height="22"><content autoFit="normal-auto-fit" fontSize="12" color="rgba(181, 255, 88, 1)" bold="true" textAlign="center"><p>01 / DESIGN SYSTEM</p></content></shape>
</data>
</slide>

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Repeatable quality loop for a single SML slide.
Static mode is safe and local. Render mode asks the Slides renderer for a PNG.
Live mode creates a disposable presentation and must be explicitly confirmed.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
from xml_text_overlap_lint import lint_xml # noqa: E402
def relative_to_cwd(path: Path) -> str:
try:
return str(path.resolve().relative_to(Path.cwd().resolve()))
except ValueError as error:
raise ValueError(f"path must be inside the current working directory: {path}") from error
def run_cli(arguments: list[str]) -> dict[str, Any]:
environment = {
**os.environ,
"LARKSUITE_CLI_NO_UPDATE_NOTIFIER": "1",
"LARKSUITE_CLI_NO_SKILLS_NOTIFIER": "1",
}
completed = subprocess.run(
["lark-cli", *arguments], text=True, capture_output=True, check=False, env=environment
)
output = completed.stdout if completed.returncode == 0 else completed.stderr
try:
payload = json.loads(output)
except json.JSONDecodeError as error:
raise RuntimeError(f"lark-cli returned non-JSON output: {output.strip()}") from error
if completed.returncode != 0 or not payload.get("ok"):
raise RuntimeError(json.dumps(payload, ensure_ascii=False))
return payload
def write_review_template(path: Path, report: dict[str, Any]) -> None:
screenshot_paths = report.get("screenshots", [])
presentation_id = report.get("presentation", {}).get("xml_presentation_id", "not created")
lines = [
"# Slide Review",
"",
f"- Presentation: `{presentation_id}`",
f"- Static errors: `{report['static']['summary']['error_count']}`",
f"- Static warnings: `{report['static']['summary']['warning_count']}`",
"",
"## Screenshot evidence",
*((f"- `{item}`" for item in screenshot_paths) if screenshot_paths else ["- No screenshot was created."]),
"",
"## Score each 0-2 and name the exact XML repair when below 2",
"",
"| Dimension | Score | Evidence / repair |",
"|---|---:|---|",
"| Message hierarchy (title, claim, support) | | |",
"| Visual focus and composition | | |",
"| Text fit, contrast, and scanability | | |",
"| Alignment, spacing, and canvas safety | | |",
"| Theme consistency and asset rendering | | |",
"",
"Pass only when every dimension is 2 and static errors are 0. Warnings require either a repair or a screenshot-based justification.",
]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def run_static(xml_path: Path) -> dict[str, Any]:
return lint_xml(xml_path.read_text(encoding="utf-8"), str(xml_path))
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Run the lark-slides static/render/live quality loop.")
parser.add_argument("--input", required=True, type=Path, help="A single <slide> XML file inside the current directory.")
parser.add_argument("--mode", choices=("static", "render", "live"), default="static")
parser.add_argument("--title", default="Slides quality-loop evaluation")
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--confirm-write", action="store_true", help="Required before --mode live creates a presentation.")
options = parser.parse_args(argv)
xml_path = options.input.resolve()
output_dir = options.output_dir or Path(".lark-slides/eval-runs") / datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
output_dir.mkdir(parents=True, exist_ok=True)
static = run_static(xml_path)
report: dict[str, Any] = {"mode": options.mode, "input": str(xml_path), "static": static, "screenshots": []}
if static["summary"]["error_count"]:
report["status"] = "blocked_by_static_errors"
elif options.mode == "static":
report["status"] = "static_passed"
else:
input_argument = "@" + relative_to_cwd(xml_path)
output_argument = relative_to_cwd(output_dir)
if options.mode == "render":
rendered = run_cli(
["slides", "+screenshot", "--as", "user", "--content", input_argument, "--output-dir", output_argument]
)
report["screenshots"] = [item["path"] for item in rendered["data"]["screenshots"]]
report["status"] = "rendered"
else:
if not options.confirm_write:
parser.error("--mode live requires --confirm-write because it creates a presentation")
created = run_cli(["slides", "+create", "--as", "user", "--title", options.title, "--slides", json.dumps([xml_path.read_text(encoding="utf-8")])])
presentation = created["data"]
slide_id = presentation["slide_ids"][0]
screenshot = run_cli(
[
"slides", "+screenshot", "--as", "user", "--presentation", presentation["xml_presentation_id"],
"--slide-id", slide_id, "--output-dir", output_argument,
]
)
report["presentation"] = presentation
report["screenshots"] = [item["path"] for item in screenshot["data"]["screenshots"]]
report["status"] = "live_created_and_rendered"
(output_dir / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
write_review_template(output_dir / "review.md", report)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if not static["summary"]["error_count"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import run # noqa: E402
VALID_SLIDE = '''<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><shape type="text" topLeftX="60" topLeftY="60" width="840" height="100"><content autoFit="normal-auto-fit" textType="title" fontSize="36"><p>Quality loop</p></content></shape></data></slide>'''
class QualityLoopTest(unittest.TestCase):
def test_static_mode_writes_report_and_review_template(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
xml_path = root / "slide.xml"
output_dir = root / "out"
xml_path.write_text(VALID_SLIDE, encoding="utf-8")
exit_code = run.main(["--input", str(xml_path), "--mode", "static", "--output-dir", str(output_dir)])
self.assertEqual(exit_code, 0)
report = json.loads((output_dir / "report.json").read_text(encoding="utf-8"))
self.assertEqual(report["status"], "static_passed")
self.assertEqual(report["static"]["summary"]["error_count"], 0)
self.assertIn("Pass only when", (output_dir / "review.md").read_text(encoding="utf-8"))
def test_static_errors_block_render_or_live(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
xml_path = root / "bad.xml"
output_dir = root / "out"
xml_path.write_text("<slide><data><shape></data></slide>", encoding="utf-8")
exit_code = run.main(["--input", str(xml_path), "--mode", "render", "--output-dir", str(output_dir)])
self.assertEqual(exit_code, 1)
report = json.loads((output_dir / "report.json").read_text(encoding="utf-8"))
self.assertEqual(report["status"], "blocked_by_static_errors")
if __name__ == "__main__":
unittest.main()

View File

@@ -2,7 +2,7 @@
新建演示文稿或大幅改写页面时,在写入 `slide_plan.json` 前后都可以参考本文件。目标是让 agent 主动识别有价值的图、图标、图表、流程图、时序图、架构图、装饰图案、截图或示意图需求,同时保持 deck 在没有真实素材时也能完整执行。
本文件定义轻量资产规划。不要把它理解成素材采集流程
本文件定义轻量资产规划;素材采集属于 XML 前的 asset-realization 阶段。计划阶段不执行搜索、下载或上传,完成计划后可在用户授权且工具可用时落实真实或生成资产
## Core Rules
@@ -10,6 +10,7 @@
- Asset needs must serve the page's `key_message` and `visual_focus`. Do not add decorative assets that do not clarify the page.
- Prefer a few high-value asset plans over one asset on every page. For a 6-page technical or business deck, plan assets on at least 3 pages when the content allows.
- If a real local asset already exists or the user provides one, it can be used through the normal media-upload workflow. Still keep `fallback_if_missing` in the plan.
- For `generated_visual`, record `source: "generated"`, a no-text prompt intent, and the intended copy-safe region. Generate it only in the asset-realization stage when authorized and available; otherwise use the fallback and report that no raster asset was used.
- Do not leave blank image boxes in final XML. If the asset is missing, render the fallback visual.
## JSON Shape
@@ -25,6 +26,21 @@ Use an object for one planned asset, or an array when a page genuinely needs mul
}
```
For a generated focal visual, use this additional contract:
```json
{
"asset_type": "generated_visual",
"purpose": "Create a material-rich cover focal point without baking in slide copy.",
"source": "generated",
"prompt_intent": "no-text blue glass quality loop on the right",
"copy_safe_region": "left_45_percent",
"realization_status": "planned",
"suggested_query": "quality loop editorial abstract visual",
"fallback_if_missing": "Render a semantic quality-loop SVG with native labels."
}
```
For a page without a meaningful asset need, use:
```json
@@ -46,6 +62,7 @@ For a page without a meaningful asset need, use:
- `infographic`: composed visual explanation, usually combining labels, numbers, and simple shapes.
- `screenshot`: product UI, terminal output, workflow state, or page capture.
- `flow_diagram`: process, sequence, decision tree, or mechanism diagram.
- `generated_visual`: no-text editorial, material, atmospheric, or product-context visual used as a focal asset; native XML owns all copy.
- `none`: explicitly no asset needed.
Do not invent new asset types unless the user asks for a special visual format. If a need is close to these types, choose the closest one and explain the detail in `purpose`.
@@ -61,7 +78,9 @@ Match asset type to slide role:
- `big-number` layout often works with `chart` or `infographic`, but only if it supports the metric.
- `image-left-text-right` and `image-right-text-left` can use `screenshot`, `paper_figure`, `logo`, or `infographic`; if missing, use a large placeholder diagram or stylized panel.
`suggested_query` is only a future lookup hint. Write it as a short phrase a human or later workflow could search, but do not execute the search unless the user separately requests real assets.
`suggested_query` is only a planning hint. Do not execute it during planning. After the plan is accepted, asset realization may search or generate only when the user has authorized that source/tool; otherwise execute `fallback_if_missing` and report the limitation.
For `generated_visual`, require `source`, `prompt_intent`, `copy_safe_region`, and `realization_status` (`planned`, `acquired`, or `fallback`). Record the resulting local path/token or fallback reason in the deck's `assets_used` state.
For `asset_type: "chart"`:

View File

@@ -0,0 +1,45 @@
# External Evaluation and SOTA Claims
Use this reference only when the user asks for a benchmark claim, model comparison, leaderboard result, or “SOTA”. A successful local render is not evidence for any of those claims.
## Claim Levels
| Claim | Minimum evidence | Allowed wording |
|---|---|---|
| Local regression pass | Versioned fixtures, static checks, rendered screenshots, and live XML readback all pass | “Passed the local quality benchmark.” |
| External metric result | A named public evaluator, pinned version/configuration, and saved raw output | “Scored X under <benchmark>/<configuration>.” |
| Better than a named baseline | Same inputs, same evaluator/configuration, raw scores for both systems, and a predeclared comparison rule | “Outperformed <baseline> on this evaluation.” |
| SOTA | Public benchmark coverage, comparable named-system results, blind or human-preference evaluation, reproducible artifacts, and a statistically defensible win | “SOTA on <benchmark/split/metric>.” |
Do not replace a missing comparison with an adjective such as “SOTA”, “best”, or “leading”.
## SlidesGen-Bench-Compatible Protocol
SlidesGen-Bench evaluates rendered slides across content, aesthetics, and editability. For a comparable run:
1. Pin the benchmark commit, model versions, metric configuration, and evaluator prompts.
2. Generate the same task set and source materials as every named baseline. Cover the benchmark scenarios, not only a handpicked demo.
3. Save raw slide images, source-to-slide mappings, static reports, and editable XML for every result.
4. Compute image-based aesthetics metrics on all rendered pages.
5. Run its content and visual VLM evaluation with a disclosed judge model and credentials authorized by the user. Use blinded candidate names for pairwise/ELO comparisons.
6. Run editability tests: apply the same editing instructions to each generated deck and verify that the edit succeeds without breaking unrelated content.
7. Report per-scenario scores, aggregate scores, variance/confidence intervals, failures, costs, and latency. Averages without raw artifacts are not enough.
## Required Artifacts
Keep these outside committed source unless the user asks to publish them:
- benchmark manifest and pinned evaluator version;
- generated slide XML/PPT artifacts and screenshots;
- raw evaluator JSON and configuration;
- per-case pass/fail, latency, and cost ledger;
- blinded preference or ELO judgments;
- comparison table that names every baseline and split.
## Current Local Boundary
`skills/lark-slides/eval/cases.json` is a three-archetype regression set (cover, comparison, architecture). Its 100% result proves only that the declared local structural contracts, renderer calls, and live readback pass. It is deliberately not a SOTA benchmark.
The canonical `cases.json` is asset-first: its cover uses a no-text fixture asset while copy remains native XML. A local preference win against a declared baseline can support only the named local comparison; it does not establish external-system superiority or SOTA.
A direct image-model PPT baseline is a different comparator: it must generate each page as a complete raster slide from the same brief, then be inserted unchanged as a full-page image. A structured `lark-slides` deck may use generated focal assets, but it must keep copy and layout as editable XML elements. A blind win over that baseline supports “preferred to the evaluated raw-image baseline on this local brief,” not SOTA.

View File

@@ -203,13 +203,13 @@ Do not hard-code a page number just because a previous deck used that pattern. P
## Asset Planning
`asset_need` is metadata. It can describe a desired figure, diagram, chart, icon, logo, screenshot, or fallback shape-based visual, but it must not require web search, local download, or media upload.
`asset_need` is planning metadata. It must not perform web search, local download, upload, or image generation during planning. After the plan is accepted, the asset-realization stage may obtain an authorized real/generated asset or execute `fallback_if_missing`.
Use an object for one planned asset, an array for multiple real needs, or `asset_type: "none"` when no asset is useful. Each planned asset must include:
- `asset_type`: one of `paper_figure`, `architecture_diagram`, `icon`, `logo`, `chart`, `infographic`, `screenshot`, `flow_diagram`, or `none`.
- `asset_type`: one of `paper_figure`, `architecture_diagram`, `icon`, `logo`, `chart`, `infographic`, `screenshot`, `flow_diagram`, `generated_visual`, or `none`.
- `purpose`: why this asset helps the page's key message.
- `suggested_query`: short future lookup hint only; do not execute it unless separately requested.
- `suggested_query`: short future lookup or generation hint only; do not execute it during planning.
- `fallback_if_missing`: concrete XML-native visual plan using shapes, labels, tables, whiteboard diagrams, or placeholder panels.
- `chart_contract`: when `asset_type` is `chart` and the visual is a supported standard data chart, set this optional slide-level field so generation is locked to native `<chart>`.
@@ -229,7 +229,7 @@ Before writing each slide XML, map the plan fields to concrete decisions:
- `layout_type` determines the coordinate structure and element types. Use `visual-planning.md` for concrete layout rules.
- `visual_focus` determines the largest visual region or emphasized object.
- `text_density` caps visible text volume.
- `asset_need` informs placeholder diagrams, icons, charts, screenshots, or shape-based fallback visuals only. Missing real assets must use `fallback_if_missing`, not blank regions.
- `asset_need` informs authorized asset realization and, when unavailable, placeholder diagrams, icons, charts, screenshots, or shape-based fallback visuals. Missing real assets must use `fallback_if_missing`, not blank regions.
- `chart_contract` locks supported standard data charts to native `<chart>` output. Manual approximations are allowed only when the planned chart type is unsupported by `<chart>` or when the visual is explicitly non-data/decorative.
After creating the PPT, fetch the presentation and verify:

View File

@@ -0,0 +1,99 @@
# Quality Loop
Use this loop for every new deck and every page whose layout is materially rebuilt. XML validity is necessary, not visual acceptance.
## Contract
1. Write `slide_plan.json` and one `<slide>` XML file per planned page.
2. Run the static gate. Resolve every `error`; explicitly review every `warning`.
3. Content-render each changed page when it has no local `@` image placeholder. For a local placeholder, use static validation then live creation, because content render cannot upload that image.
4. Review available rendered/live PNGs against the five dimensions below. Record the exact XML repair for every score below 2.
5. Create or update the real presentation, read back XML, and screenshot the live page when the screenshot service is available. If screenshots are unavailable, retain the error plus XML/readback evidence and do not claim screenshot acceptance.
6. Re-run the same checks after a repair. Do not claim visual acceptance from XML alone.
## Commands
All paths must be relative to the repository working directory.
```bash
# Local XML diagnostics only; no network or write.
python3 skills/lark-slides/eval/run.py \
--input .lark-slides/plan/<deck>/slide-01.xml --mode static
# Server-side render of XML; no presentation is created.
python3 skills/lark-slides/eval/run.py \
--input .lark-slides/plan/<deck>/slide-01.xml --mode render
# Explicitly create a presentation and screenshot its actual rendered page.
python3 skills/lark-slides/eval/run.py \
--input .lark-slides/plan/<deck>/slide-01.xml --mode live \
--confirm-write --title "<evaluation title>"
```
Each run writes `.lark-slides/eval-runs/<timestamp>/report.json` and `review.md`. Treat `report.json` as machine evidence and `review.md` as the human/model visual-review record.
## Regression Benchmark
Use the tracked core fixtures to stop a later change from silently degrading cover, comparison, or architecture pages:
```bash
# Proves structural contracts and static safety for all cases.
python3 skills/lark-slides/eval/benchmark.py \
--manifest skills/lark-slides/eval/cases.json --mode static
# Only use for manifests without local @ image placeholders.
python3 skills/lark-slides/eval/benchmark.py \
--manifest skills/lark-slides/eval/cases.json --mode render
# Creates one three-page live deck and screenshots each actual page.
python3 skills/lark-slides/eval/benchmark.py \
--manifest skills/lark-slides/eval/cases.json --mode live \
--confirm-write --title "<evaluation title>"
```
The canonical `cases.json` is asset-first and its cover uses a local image placeholder: run `static` then `live`; render mode records that page as skipped and requires live evidence. A 100% pass rate proves only declared local contracts and available render/live evidence, not global SOTA.
## Image-Assisted Candidate Comparison
When a native-shape composition stalls visually, make a second candidate that keeps the same message, copy, and slide structure but replaces only the focal visual with one licensed/generated raster asset. This isolates whether imagery improves the deck rather than rewarding a different story.
1. Generate or obtain the asset under the user-approved image workflow. Keep the original prompt and saved asset path.
2. Put the selected asset under `skills/lark-slides/eval/assets/` for a tracked regression fixture, or keep it under the deck's own assets directory for a one-off deck.
3. Use `<img src="@./relative/path.png">` only with `slides +create`; it uploads the local file and replaces it with a Drive token. Do not use external URLs.
4. Run static validation, then use **live** benchmark mode. `+screenshot --content` does not upload local `@` image placeholders, so it is not sufficient evidence for image fixtures.
5. Blind-compare rendered screenshots: same prompt, same judge, candidate labels hidden, and an explicit pass rule. Preserve the raw judge JSON and token/cost ledger outside committed source.
The canonical `skills/lark-slides/eval/cases.json` already exercises this asset-first route. Keep generated run outputs, judge JSON, and raw baselines under `.lark-slides/`, not committed source; only a small, licensed/provenanced fixture asset belongs under `eval/assets/`.
## Structured-Skill vs Raw-Image Baseline
Do not confuse an image-assisted structured deck with a raw image-generation baseline: the former is still a `lark-slides` result because its layout, text, and editable elements are assembled by the Skill. When testing whether the Skill adds value beyond an image model, compare these two distinct artifacts:
| Artifact | Construction | What it tests |
|---|---|---|
| Raw-image baseline | One generated raster image per page, inserted as a full-slide `<img>` | Image model's direct visual output, including its text/layout limits |
| Structured Skill deck | Native `<shape>`, `<table>`, `<icon>`, or `<whiteboard>` elements, optionally with a generated focal asset | The Skill's layout system, text fidelity, cross-page consistency, and editability |
Use identical slide briefs and preserve both three-page screenshot sets. Blind the deck labels and score visual quality plus `text_fidelity` (complete, accurate, readable copy). Use a simultaneous contact sheet or balanced A/B order; inspect the raw baseline's XML separately. A page containing only one `<img>` cannot support a targeted text edit, while the structured deck must retain its text blocks as distinct elements.
An image-model baseline win does not make the Skill weak; it identifies the visual pattern to improve. A structured-Skill win supports only the narrow result “preferred to this raw-image baseline on this declared local brief.” It is not a global SOTA claim.
## Review Rubric
Score every dimension 0, 1, or 2. A page passes only with all scores at 2.
| Dimension | 2 means | Typical XML repair |
|---|---|---|
| Message hierarchy | Claim is readable before support text; one clear title and one focal conclusion | Increase title prominence, shorten body, move support text |
| Visual focus | The focal region is visibly dominant and supports the claim | Resize/reposition image, chart, or diagram; reduce competing cards |
| Scanability | Text fits, contrast is clear, and labels can be read at normal slide size | Split content, use `autoFit`, increase contrast, simplify labels |
| Composition | Alignment, whitespace, and canvas edges look intentional | Normalize coordinates/gaps; repair overlaps and clipping |
| System consistency | Palette, motif, and asset rendering match the deck plan | Reuse color roles/motif; replace broken or placeholder assets |
## Static Diagnostics
- `error`: blocks creation. Includes XML syntax/schema violations, invalid icons, table canvas overflow, and text overlap.
- `warning`: does not block, but requires screenshot review and either an XML repair or an explicit visual justification. Includes visible non-image content clipping and estimated text-height risk.
- `info`: records a recoverable discrepancy, such as table declared vs resolved dimensions.
For `bbox_overlap`, use `bboxes.left`, `bboxes.right`, and `intersection` instead of guessing which coordinate to move. For `text_may_overflow`, use `estimated.line_count` and `estimated.required_height` to decide whether to shorten, split, resize, or enable `autoFit`.

View File

@@ -36,7 +36,7 @@ python3 skills/lark-slides/scripts/xml_text_overlap_lint.py --input <presentatio
通过标准:
- `summary.error_count == 0`。任何 error 都必须先修复再提交接口。
- 当前工具检查 XML well-formed、SXSD tag/attr 支持情况、IconPark icon 类型和 icon 填充可见性、文本元素之间的明显重叠,以及 whiteboard 容器与外部 sibling 元素的可疑边界重叠;它不检查越界、文本高度不足、图文压盖、表格/图表压盖或底部拥挤。
- 当前工具检查 XML well-formed、SXSD tag/attr 支持情况、IconPark icon 类型和 icon 填充可见性、文本元素之间的明显重叠whiteboard 容器与外部 sibling 元素的可疑边界重叠、表格越界/尺寸不一致,以及非图片元素越界和估算文本高度风险。后两项是 warning必须由截图复核它仍不能可靠判断图文压盖、对比度、视觉层级或底部拥挤。
- 该工具不能替代页数核对、关键内容核对或真实视觉验收。
常见 code 的处理方向:
@@ -52,6 +52,8 @@ python3 skills/lark-slides/scripts/xml_text_overlap_lint.py --input <presentatio
| `icon_transparent_fill_color` | `<icon>``fillColor` 是透明色,不满足视觉可见性要求 | 改成与背景有足够对比的非透明颜色 |
| `bbox_overlap` | 文本元素的估算绘制区域明显重叠 | 拉开文本坐标、缩小文本框/字号,或改成明确的分栏/分组结构 |
| `whiteboard_external_overlap` | whiteboard 容器 bbox 与外部 sibling 元素跨边界重叠 | 按 lint `hint` 缩小或移动 whiteboard / 外部元素;若接受该风险,最终必须以截图 QA 或等价渲染视觉检查为准 |
| `content_out_of_canvas` | 可见的非图片元素可能被画布裁切 | 检查 `overflow` 和截图;移动/缩小元素,或记录这是可接受的视觉裁切 |
| `text_may_overflow` | 估算文本高度大于声明文本框高度 | 缩短或拆分文本、增大框,或设置 `autoFit="normal-auto-fit"` 后截图复核 |
## Page Count And Structure

View File

@@ -0,0 +1,43 @@
# Visual Ceiling Rules
Use this reference when the deck must approach or exceed a strong image-generated visual baseline. The target is not a flatter XML imitation of a raster slide. The target is a structured deck that keeps image-level visual focus while retaining native text, layout, and editing controls.
## Golden Rules: Verifiable Form
| Rule | Pass condition | Failure signal |
|---|---|---|
| Visual focus is real | Every page has one dominant focus tied to its key message: image, chart, diagram, semantic SVG, large metric, or intentional text-led statement | Tiny icons or repeated cards are the only visual objects |
| Asset has a job | Each planned asset clarifies the page key message, not just decorates it | Removing the asset changes nothing about the slide meaning |
| Images are text-free | Generated backgrounds/focal art contain no slide copy, labels, or page numbers | Copy is baked into an image and cannot be edited reliably |
| XML owns structure | Title, body, labels, data, and page navigation are native XML elements | A full-slide `<img>` is the final deliverable |
| Layout varies deliberately | Page roles result in distinct geometry and rhythm | Cover, comparison, and process pages are all title plus cards |
| Render proof decides | Static proof is mandatory; use content render when supported and live screenshot when available | XML validity is used as a proxy for visual quality |
## Asset-First, Structure-First Composition
1. In `slide_plan.json`, name the `visual_focus` and `asset_need` before writing XML.
2. Use a real or generated **no-text** asset for material-rich imagery, cover atmosphere, product context, or editorial abstraction. Prompt the asset with a reserved copy region; do not ask the image model to render the title.
3. Put the asset in a deliberate 3545% (or intentional full-bleed) region. Keep slide copy and labels as native `<shape type="text">` elements.
4. When an image does not clarify the message, use one semantic SVG/whiteboard, native chart, or diagram at visual-focus scale. Do not stack many small shapes solely to imitate photorealistic depth.
5. Reuse palette, edge treatment, crop logic, and one motif across the deck. Vary page geometry—not the visual language—between page roles.
## Raw-Image Baseline Protocol
A raw image model may be used as a visual ceiling reference, but never as the final structured deck.
- Build the raw baseline from the same slide brief, one full-page raster per page.
- Build the Skill candidate with the same message, but native text and layout plus optional no-text assets.
- Compare with a simultaneous contact sheet or balanced A/B presentation. Do not judge one deck only after another: sequential VLM inputs can favor the most recently shown deck.
- Record visual polish, information hierarchy, cross-slide consistency, text fidelity, and editability separately.
- A local tie means the visual ceiling is approached; it is not SOTA. Require repeated wins across a declared task set and named baselines before making a stronger claim.
## Preflight Gate
Before creation, answer `yes` to all of these:
1. Is the dominant visual large enough to be noticed before the supporting text?
2. Is that visual either a meaningful asset or a semantic diagram, rather than decorative filler?
3. Are all user-facing words editable native text rather than pixels?
4. Does this page's geometry visibly differ from the other page roles in the deck?
5. If `asset_need` is not `none`, does the plan specify an XML-native fallback if the asset is unavailable?
6. Will the evaluation use live screenshots when available and a position-balanced comparison if a baseline is involved?

View File

@@ -1077,6 +1077,76 @@ def detect_table_layout_size_mismatches(elements: list[dict[str, Any]]) -> list[
return issues
def bbox(element: dict[str, Any]) -> dict[str, int | float]:
return {key: element[key] for key in ("x", "y", "width", "height")}
def intersection_bbox(left: dict[str, Any], right: dict[str, Any]) -> dict[str, int | float]:
x = max(left["x"], right["x"])
y = max(left["y"], right["y"])
width = max(min(left["x"] + left["width"], right["x"] + right["width"]) - x, 0)
height = max(min(left["y"] + left["height"], right["y"] + right["height"]) - y, 0)
return {"x": x, "y": y, "width": width, "height": height, "area": width * height}
def detect_content_out_of_canvas(
elements: list[dict[str, Any]], slide_width: int | float, slide_height: int | float
) -> list[dict[str, Any]]:
"""Report visible content that is clipped, but allow image bleed as a deliberate treatment."""
issues: list[dict[str, Any]] = []
for element in (element for element in elements if element["kind"] != "img"):
overflow = {
"left": max(-element["x"], 0),
"top": max(-element["y"], 0),
"right": max(element["x"] + element["width"] - slide_width, 0),
"bottom": max(element["y"] + element["height"] - slide_height, 0),
}
if not any(overflow.values()):
continue
issues.append(
{
"level": "warning",
"code": "content_out_of_canvas",
"elements": [element["id"]],
"canvas": {"width": slide_width, "height": slide_height},
"bbox": bbox(element),
"overflow": overflow,
"message": f'{element["id"]} extends outside the {slide_width:g}x{slide_height:g} canvas',
"hint": "Move or resize the visible element inside the canvas, or verify the intentional crop in a rendered screenshot.",
}
)
return issues
def detect_text_capacity_risks(elements: list[dict[str, Any]]) -> list[dict[str, Any]]:
issues: list[dict[str, Any]] = []
for element in elements:
visual_bbox = estimate_text_visual_bbox(element)
if visual_bbox is None:
continue
estimated_lines = estimate_text_line_count(element)
required_height = estimated_lines * element["fontSize"] * 1.2
if required_height <= element["height"]:
continue
issues.append(
{
"level": "warning",
"code": "text_may_overflow",
"elements": [element["id"]],
"bbox": bbox(element),
"estimated": {
"line_count": estimated_lines,
"required_height": required_height,
"font_size": element["fontSize"],
"auto_fit": element.get("autoFit"),
},
"message": f'{element["id"]} needs about {required_height:g}px for {estimated_lines} line(s), but has {element["height"]:g}px',
"hint": "Shorten or split the text, enlarge the box, or set content autoFit=\"normal-auto-fit\" and verify the rendered slide.",
}
)
return issues
def lint_slide(
slide_xml: str, slide_number: int, slide_width: int | float = 960, slide_height: int | float = 540
) -> dict[str, Any]:
@@ -1085,6 +1155,8 @@ def lint_slide(
*detect_whiteboard_external_overlaps(elements, slide_width, slide_height),
*detect_table_out_of_canvas(elements, slide_width, slide_height),
*detect_table_layout_size_mismatches(elements),
*detect_content_out_of_canvas(elements, slide_width, slide_height),
*detect_text_capacity_risks(elements),
]
for index, left in enumerate(elements):
@@ -1097,7 +1169,10 @@ def lint_slide(
"level": "error",
"code": "bbox_overlap",
"elements": [left["id"], right["id"]],
"bboxes": {"left": bbox(left), "right": bbox(right)},
"intersection": intersection_bbox(left, right),
"message": f'{left["id"]} overlaps {right["id"]}',
"hint": "Separate the text boxes or reduce their text density; use the bboxes and intersection to choose the smallest safe coordinate change.",
}
)

View File

@@ -522,7 +522,11 @@ class XmlTextOverlapLintTest(unittest.TestCase):
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["code"], "bbox_overlap")
self.assertEqual(issue["bboxes"]["left"], {"x": 80, "y": 80, "width": 300, "height": 60})
self.assertEqual(issue["intersection"], {"x": 80, "y": 80, "width": 300, "height": 60, "area": 18000})
self.assertIn("coordinate", issue["hint"])
def test_lint_xml_detects_current_itinerary_cjk_caption_occlusion(self) -> None:
result = xml_text_overlap_lint.lint_xml(
@@ -598,10 +602,10 @@ class XmlTextOverlapLintTest(unittest.TestCase):
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["source", "target"])
overlap = next(issue for issue in result["slides"][0]["issues"] if issue["code"] == "bbox_overlap")
self.assertEqual(overlap["elements"], ["source", "target"])
def test_lint_xml_does_not_check_bounds_or_text_height(self) -> None:
def test_lint_xml_reports_bounds_and_text_height_risks_as_warnings(self) -> None:
result = xml_text_overlap_lint.lint_xml(
"""
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -619,7 +623,12 @@ class XmlTextOverlapLintTest(unittest.TestCase):
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 2)
issues = {issue["code"]: issue for issue in result["slides"][0]["issues"]}
self.assertEqual(issues["content_out_of_canvas"]["elements"], ["shape-2"])
self.assertEqual(issues["content_out_of_canvas"]["overflow"]["right"], 160)
self.assertEqual(issues["text_may_overflow"]["elements"], ["shape-1"])
self.assertGreater(issues["text_may_overflow"]["estimated"]["required_height"], 20)
def test_lint_xml_allows_template_style_bleed_and_text_over_images(self) -> None:
result = xml_text_overlap_lint.lint_xml(