Sync third-party and MCP marketplace plugins

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot
2026-08-09 00:02:05 +08:00
parent c9acffa399
commit 7c9f7d0efc
241 changed files with 9964 additions and 7534 deletions
+10 -10
View File
@@ -42,8 +42,8 @@
"repo": "https://github.com/JuliusBrussee/caveman.git",
"ref": "main",
"adapter": "codex-plugin",
"commit": "ec83e5bace4c20484d704dea21e12fc4eb94e9aa",
"syncedAt": "2026-08-06T16:00:02Z"
"commit": "11ddc0c9813c8f75365cd5be2f753df08712f154",
"syncedAt": "2026-08-08T16:00:01Z"
},
{
"id": "taste-skill",
@@ -60,8 +60,8 @@
"repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "9846e22ce52c723554742860a0dbd3e5cf19b573",
"syncedAt": "2026-08-06T16:00:02Z"
"commit": "6261bd89f72d794aea491482cc2acfd8dc3d63e2",
"syncedAt": "2026-08-08T16:00:01Z"
},
{
"id": "frontend-slides",
@@ -78,8 +78,8 @@
"repo": "https://github.com/op7418/guizang-ppt-skill.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "e8357b315471f04e17551988826011974bfff779",
"syncedAt": "2026-08-06T16:00:02Z"
"commit": "c91369c449d34755d320a8b81d0734000d99d1ab",
"syncedAt": "2026-08-08T16:00:01Z"
},
{
"id": "html-ppt",
@@ -96,8 +96,8 @@
"repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "c0305342cf5c558e7242dba64cfc207874a5134a",
"syncedAt": "2026-08-06T16:00:02Z"
"commit": "eb095bfcc6b625fd8754e9f5d053ec4220865370",
"syncedAt": "2026-08-08T16:00:01Z"
},
{
"id": "next-skills",
@@ -105,8 +105,8 @@
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"adapter": "skill-collection",
"commit": "0ae8c72462952df163f1b1e0726641bc5b40dc93",
"syncedAt": "2026-08-06T16:00:02Z"
"commit": "a677cf66af002fbdcf49a982ef435b03554817cc",
"syncedAt": "2026-08-08T16:00:01Z"
}
]
}
@@ -2,8 +2,8 @@
"sourceId": "caveman",
"repo": "https://github.com/JuliusBrussee/caveman.git",
"ref": "main",
"commit": "ec83e5bace4c20484d704dea21e12fc4eb94e9aa",
"commit": "11ddc0c9813c8f75365cd5be2f753df08712f154",
"adapter": "codex-plugin",
"sourcePath": "plugins/caveman",
"syncedAt": "2026-08-06T16:00:02Z"
"syncedAt": "2026-08-08T16:00:01Z"
}
@@ -11,7 +11,7 @@
![Kimi work Gold Sponsor](https://img.shields.io/static/v1?label=Kimi%20work&message=Gold%20Sponsor&color=111111&style=flat-square)
![Cola Skill Gold Sponsor](https://img.shields.io/static/v1?label=Cola%20Skill&message=Gold%20Sponsor&color=E4353F&style=flat-square)
An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages.
An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages, with a complete rehearsal and presenter mode built in.
It ships with two visual systems:
@@ -59,6 +59,7 @@ Other useful prompts:
Turn this Markdown file into an editorial magazine-style presentation.
Create a 21:9 social cover from the core idea of this deck.
Redesign this product screenshot as a 16:10 slide visual.
Add speaker notes and planned timing to this deck, then help me rehearse it in presenter mode.
```
## Sponsors and Supporters
@@ -88,6 +89,7 @@ Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platform
- 🎨 **Curated theme presets**: 5 electronic-ink themes for Style A, 4 Swiss anchor-color themes for Style B
- 🖼 **Optional Codex image flow**: generate documentary photos, infographics, flow diagrams, system maps, and UI scenes with GPT-Image 2.0 / GPT-M 2.0, then insert them at template-safe ratios
- 📰 **Social covers**: generate 21:9 WeChat cover images, 1:1 share cards, 3:4 Xiaohongshu covers, video thumbnails, and related variants
- 🎤 **Presenter mode**: dual-window audience output, 16:9 current/next previews, speaker notes, timing and rehearsal, auto advance, laser pointer, circles, and live recovery controls
- 📴 **Low-power static mode**: press `B` to turn WebGL / canvas animation into static visuals
- 📄 **Single HTML file** — no build, no server, open directly in the browser
@@ -107,12 +109,13 @@ Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platform
| Deck visuals | In Codex, generate photos, infographics, flow diagrams, system maps, or UI scenes |
| Social covers | Generate 21:9 main covers, 1:1 share cards, 3:4 vertical covers, and video thumbnails from the same idea |
| Screenshot normalization | Redesign raw screenshots into template-safe ratios before inserting them into slides |
| Live talk / rehearsal | Ask the agent to generate notes and timing, then press the bottom-right `P` to enter presenter mode |
## Why HTML decks
- **Agent-native editing**: HTML / CSS is plain text, so agents can read, edit, and validate it directly.
- **Higher visual density than Markdown**: precise layout, positioning, motion, interactivity, and cover formats.
- **Lightweight delivery**: one HTML file can be opened, presented, sent, screenshotted, or recorded.
- **Lightweight delivery**: one HTML file can be opened, presented, sent, screenshotted, or recorded, with presenter tools included.
- **Better quality gates**: the Swiss validator can catch layout drift, unsafe image placement, centered body titles, and SVG text traps.
- **One visual system across outputs**: decks, generated visuals, covers, and screenshot redesigns can share the same style rules.
@@ -162,6 +165,7 @@ Once installed, Claude Code auto-detects the skill. Trigger phrases:
- "Electronic ink slides for my talk"
- "Create a 21:9 WeChat cover from this article"
- "Create a 1:1 share card from this deck"
- "Add speaker notes to this deck and help me rehearse it in presenter mode"
## Workflow
@@ -172,12 +176,59 @@ The skill is a structured workflow; the agent walks you through each step:
3. **Copy template** — Style A uses `assets/template.html`; Style B uses `assets/template-swiss.html`
4. **Fill content** — create a rhythm plan, then choose and adapt the matching layout skeletons
5. **Optional image generation** — in Codex, ask whether to use GPT-Image 2.0 / GPT-M 2.0 images, then insert them at page-appropriate ratios
6. **Self-check** — match against `references/checklist.md`; P0 issues must all pass; Swiss decks must also pass the layout validator
7. **Preview** — open the HTML in a browser
8. **Iterate**use inline styles to tune font size, height, spacing
6. **Generate speaker notes** — derive each slide's purpose, talking points, transition, and planned duration from the outline; do not invent missing live details
7. **Self-check** — match against `references/checklist.md`; P0 issues must all pass; run the Swiss and presenter validators when applicable
8. **Preview**open the HTML in a browser
9. **Rehearse / present** — press the bottom-right `P`, verify audience sync, and record actual timing
10. **Iterate** — adjust content, font size, height, and spacing from rehearsal results
Full spec in [`SKILL.md`](./SKILL.md).
## Presenter mode
Both templates ship with the same presenter runtime. Open a deck and press the bottom-right `P` to enter presenter view; the browser opens a clean audience window at the same time. Core features run entirely inside the local HTML and browser—no live-caption service, cloud relay, phone remote, or AI coaching service is required.
**Presenter view example**
![Presenter view with current and next slides, speaker notes, timing controls, and audience status](https://github.com/user-attachments/assets/95db62b5-65bb-40af-ac0b-1d415682af88)
### What the speaker sees
- **Current and next slides**: stacked vertically and always kept at `16:9`; small windows scale the whole slide without cropping or reflowing its text
- **Grid navigation**: replace the preview area with an inline overview, choose a slide, then return immediately to the two previews while the audience follows
- **Structured notes**: title, purpose, talking points, and transition are required; interaction, delivery, advance cue, fallback, and pronunciation appear only when supplied by the outline
- **Progress and health**: current / total slide count, completion percentage, plus audience states for connecting, synced, unsynced, frozen, disconnected, or popup blocked
### Timing, rehearsal, and auto advance
- The footer separates elapsed, current-slide, and remaining / overtime values, with explicit “Start timer / Resume timer / Reset timer” controls
- Rehearsal mode records actual time per slide and a local session summary; it does not grade the speaker with AI
- Auto advance is off by default and only runs when the outline provides per-slide seconds or the user enables a global interval
- Auto advance pauses while the grid, settings, or annotation tool is open, when the page is hidden, or when the audience is paused or out of sync
### Live tools and recovery
- Laser pointer, circle annotation, and clear actions sync to the audience window
- Black screen, white screen, or freeze the audience; unfreezing catches it up to the presenter's current slide
- Closing the audience window or losing its heartbeat produces a visible “Disconnected” state; “Reopen audience” is always available
- Exiting presenter mode closes the audience window when allowed, or leaves a clear “Presentation ended” fallback
- Preflight checks cover popups, fullscreen, fonts, images / video, and `16:9` previews, while reminding the speaker to verify HDMI adapters and projectors manually
Common shortcuts: `← / →` navigate, `Home / End` jump to first or last, `G` opens the grid, `L` laser, `C` circle, `B / W` black or white screen, `F` freezes the audience, and `?` opens the full shortcut list.
Ask an agent to prepare presenter mode with:
```text
Use this outline to add a purpose, talking points, transition, and planned duration to every slide. Do not invent interaction or live details that were not provided, then run the presenter-mode validator.
```
See [`references/presenter-mode.md`](./references/presenter-mode.md) for the full notes contract and runtime behavior. Validate with:
```bash
node scripts/validate-presenter-mode.mjs path/to/index.html
node scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30
```
## Style B Swiss
The Swiss theme is a strict layout system, not just a CSS skin.
@@ -258,7 +309,9 @@ guizang-ppt-skill/
│ ├── template-swiss.html ← Style B Swiss template
│ └── screenshot-backgrounds/ ← bundled WebP screenshot backgrounds: 5 style-a / 4 style-b
├── scripts/
── validate-swiss-deck.mjs ← Swiss layout validator
── validate-swiss-deck.mjs ← Swiss layout validator
│ ├── validate-presenter-mode.mjs ← speaker notes, timing, and runtime validator
│ └── check-presenter-runtime-sync.mjs ← cross-template presenter runtime drift check
└── references/
├── components.md ← component catalog (type, color, grid, icons, callout, stat, pipeline)
├── layouts.md ← 10 layout skeletons (paste-ready)
@@ -268,6 +321,7 @@ guizang-ppt-skill/
├── themes-swiss.md ← 4 Swiss anchor-color themes
├── image-prompts.md ← GPT-Image 2.0 / GPT-M 2.0 image types, ratios, and base prompts
├── screenshot-framing.md ← CleanShot X-style screenshot framing semantics
├── presenter-mode.md ← notes contract, rehearsal, audience output, and live tools
└── checklist.md ← quality checklist (P0 / P1 / P2 / P3 tiers)
```
@@ -341,6 +395,12 @@ Yes. Style A layouts can be extended in `references/layouts.md`. Style B is stri
**Is Codex image generation required?**
No. Decks work without generated images. The image flow is only used when you need photos, infographics, UI scenes, or covers.
**Does presenter mode require internet access or another service?**
No. Dual-window sync, notes, timing, rehearsal, auto advance, and annotations run locally in the browser. It does not provide live captions, phone control, or AI rehearsal scoring.
**Why does closing the audience window show “Disconnected”?**
The presenter uses acknowledgements and heartbeats to verify the software link. Closing the window or losing its heartbeat produces “Disconnected”; use “Reopen audience” to recover. The browser cannot verify a physical HDMI or projector cable, so the speaker still needs a visual check.
**How do I update the skill?**
Run the install command again, or run `git pull` inside your local skill directory.
@@ -351,6 +411,8 @@ Bugs, layout issues, new layout requests — Issues and PRs welcome. Prioritize:
- Add new classes to `template.html` first; don't let `layouts.md` reference undefined classes
- When changing `template-swiss.html`, update `layouts-swiss.md` and `swiss-layout-lock.md` together
- When adding Swiss rules, update `scripts/validate-swiss-deck.mjs`
- Presenter runtime changes must land in both templates and pass `scripts/check-presenter-runtime-sync.mjs`; CI rejects CSS / JavaScript drift
- Presenter-note or live-behavior changes must also update `references/presenter-mode.md`, `references/checklist.md`, and `scripts/validate-presenter-mode.mjs`
- Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier
- New theme colors go into `themes.md` with a recommended use case
@@ -13,7 +13,7 @@
> 🌏 **English version: [README.en.md](./README.en.md)**
一个适配 Claude Code / Codex 等 Agent 环境的网页 PPT 技能,用于生成**单文件 HTML 横向翻页 PPT**、PPT 配图和多平台封面。
一个适配 Claude Code / Codex 等 Agent 环境的网页 PPT 技能,用于生成**单文件 HTML 横向翻页 PPT**、PPT 配图和多平台封面,并内置完整的排练与演讲者模式
内置两套视觉系统:
@@ -61,6 +61,7 @@ npx skills add https://github.com/op7418/guizang-ppt-skill --skill guizang-ppt-s
帮我把这份 Markdown 做成杂志风演讲 PPT。
基于这份 PPT 的核心观点,生成一张公众号 21:9 头图。
把这张产品截图重新设计成适合 PPT 的 16:10 配图。
给这份 PPT 补齐演讲备注和每页计划时长,然后用演讲者模式帮我排练。
```
## 赞助与支持
@@ -90,6 +91,7 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、*
- 🎨 **主题色预设**:Style A 5 套电子墨水主题,Style B 4 套瑞士高饱和锚点色
- 🖼 **Codex 可选配图流程**:可用 GPT-Image 2.0 / GPT-M 2.0 生成纪实照片、信息图、流程图、系统关系图、UI 情景图,并按模板比例插入
- 📰 **多平台封面**:可用同一套视觉规则生成公众号 21:9、公众号分享卡 1:1、小红书 3:4、视频号横版等封面
- 🎤 **演讲者模式**:双窗口观众屏、当前页/下一页 16:9 预览、演讲备注、计时排练、自动翻页、激光笔、圈选和现场故障恢复
- 📴 **低性能静态模式**:按 `B` 可关闭 WebGL / canvas 动画,让动态内容退回静态背景
- 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开
@@ -109,12 +111,13 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、*
| PPT 配图 | 在 Codex 中用 GPT-Image 2.0 / GPT-M 2.0 生成照片、信息图、流程图、UI 情景图 |
| 多平台封面 | 从同一份内容生成公众号 21:9、1:1 分享卡、小红书 3:4、视频号横版封面 |
| 截图统一风格 | 把原始截图重新生成到模板需要的比例,再插入 PPT |
| 现场演讲 / 排练 | 让 Agent 从大纲生成备注和时长,按右下角 `P` 进入演讲者模式 |
## 为什么是 HTML PPT
- **更适合 Agent 生成和修改**:HTML / CSS 是文本,Agent 能直接读、改、验证。
- **表现力比 Markdown 更高**:可以做精细排版、空间定位、动画、交互和响应式封面。
- **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图。
- **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图,演讲者工具也随文件一起交付
- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐、危险 SVG,并在可用时用 Playwright 后验测量超出、底部空白、nav 安全线和标题间距。
- **更适合视觉内容链路**:同一套主题能覆盖 PPT、配图、封面和截图再设计。
@@ -164,6 +167,7 @@ git clone https://github.com/op7418/guizang-ppt-skill.git ~/.claude/skills/guiza
- "electronic ink 风格演讲 slides"
- "基于这篇文章做一张公众号 21:9 封面"
- "基于这份 PPT 生成一张 1:1 分享卡"
- "给这份 PPT 补齐演讲备注,并用演讲者模式帮我排练"
## 使用流程
@@ -174,12 +178,59 @@ Skill 本身是结构化工作流,Agent 会逐步引导:
3. **拷贝模板** — Style A 用 `assets/template.html`,Style B 用 `assets/template-swiss.html`
4. **填充内容** — 先做主题节奏表,再从对应 layout 骨架里挑、粘、改文案
5. **可选配图** — 在 Codex 中询问是否用 GPT-Image 2.0 / GPT-M 2.0 生成配图,再按页面比例插入
6. **自检**对照 `references/checklist.md`,P0 级问题必须全过;瑞士风还要运行版式校验器
7. **预览**浏览器直接打开
8. **迭代**inline style 改字号/高度/间距
6. **生成演讲备注**从大纲提取每页目的、讲述要点、转场和计划时长;用户没提供的信息不猜测
7. **自检**对照 `references/checklist.md`,P0 级问题必须全过;瑞士风和演讲模式分别运行对应校验器
8. **预览**浏览器直接打开
9. **排练 / 演讲** — 按右下角 `P` 进入演讲者模式,检查观众屏同步并记录实际时长
10. **迭代** — 根据排练结果调整内容、字号、高度和间距
详细说明见 [`SKILL.md`](./SKILL.md)。
## 演讲者模式
两套模板都内置同一套演讲者运行时。打开 deck 后,点击右下角 `P` 即可进入演讲者视图;浏览器会同时打开一个干净的观众屏。所有核心能力都在本地 HTML 和浏览器里完成,不依赖实时字幕、云端中继、手机遥控或 AI 教练服务。
**演讲者视图示例**
![演讲者视图示例:当前页、下一页、演讲备注、计时控制与观众屏状态](https://github.com/user-attachments/assets/95db62b5-65bb-40af-ac0b-1d415682af88)
### 演讲时能看到什么
- **当前页与下一页**:上下排列并始终保持 `16:9`,小屏时整页等比缩放,不裁切、不挤压文字
- **宫格选页**:在预览区原位切换总览,点选页面后立即回到双预览,观众屏同步跟随
- **结构化备注**:标题、本页目的、讲述要点和转场必填;互动、语气、翻页时机、备用方案和读音等信息只在大纲提供时显示
- **进度与状态**:显示当前页 / 总页数、完成百分比,以及观众屏的连接中、已同步、未同步、已冻结、未连接或弹窗被拦截状态
### 时间、排练与自动翻页
- 底栏分别显示已进行、本页和剩余 / 超时时间,按钮明确区分“开始计时 / 继续计时 / 重置计时”
- 排练模式记录每页实际时长和整场汇总,数据保存在本地浏览器,不会做 AI 评分
- 自动翻页默认关闭;只有大纲明确给出页面停留秒数,或用户在设置中开启全局间隔时才启用
- 打开宫格、设置、圈选工具,页面隐藏,或观众屏暂停 / 失去同步时,自动翻页会暂停
### 现场工具与故障恢复
- 激光笔、圈选和清除会同步到观众屏
- 一键黑屏、白屏,或冻结观众屏;恢复后自动追平演讲者当前页
- 观众窗口关闭或心跳超时后明确显示“未连接”,可随时点击“重新打开观众屏”
- 退出演讲时自动关闭观众窗口;浏览器不允许自关时,观众端显示“演示已结束”
- 演前检查覆盖弹窗、全屏、字体、图片 / 视频和 `16:9` 预览,并提醒人工确认 HDMI、转接器和投影仪
常用快捷键:`← / →` 翻页,`Home / End` 跳首页或尾页,`G` 宫格,`L` 激光笔,`C` 圈选,`B / W` 黑屏或白屏,`F` 冻结观众屏,`?` 查看完整快捷键。
让 Agent 生成或补齐演讲模式时,可以直接说:
```text
根据这份大纲给每一页补齐演讲目的、讲述要点、转场和计划时长。没有提供的互动或现场信息不要猜,然后运行演讲模式校验器。
```
演讲备注契约和完整行为说明见 [`references/presenter-mode.md`](./references/presenter-mode.md)。校验命令:
```bash
node scripts/validate-presenter-mode.mjs path/to/index.html
node scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30
```
## Style B 瑞士风
瑞士风是这次新增的结构化主题。它不是"换一套 CSS",而是一套更严格的版式系统。
@@ -259,7 +310,9 @@ guizang-ppt-skill/
│ ├── template-swiss.html ← Style B 瑞士国际主义模板
│ └── screenshot-backgrounds/ ← 截图美化内置背景(WebP):style-a 5 套 / style-b 4 套
├── scripts/
── validate-swiss-deck.mjs ← 瑞士风版式校验器
── validate-swiss-deck.mjs ← 瑞士风版式校验器
│ ├── validate-presenter-mode.mjs ← 演讲备注、时长和运行时校验器
│ └── check-presenter-runtime-sync.mjs ← 两套模板演讲者运行时防漂移检查
└── references/
├── components.md ← 组件手册(字体、色、网格、图标、callout、stat、pipeline)
├── layouts.md ← 10 种页面布局骨架(可直接粘贴)
@@ -269,6 +322,7 @@ guizang-ppt-skill/
├── themes-swiss.md ← 4 套瑞士风锚点色
├── image-prompts.md ← GPT-Image 2.0 / GPT-M 2.0 配图类型、比例和基础提示词
├── screenshot-framing.md ← CleanShot X 式截图适配语义
├── presenter-mode.md ← 演讲备注契约、排练、观众屏与现场工具说明
└── checklist.md ← 质量检查清单(P0 / P1 / P2 / P3 分级)
```
@@ -342,6 +396,12 @@ guizang-ppt-skill/
**Codex 配图是必须的吗?**
不是。没有配图也能生成 PPT。配图流程只在需要照片、信息图、UI 情景图或封面时使用。
**演讲者模式需要联网或额外服务吗?**
不需要。双窗口同步、备注、计时、排练、自动翻页和标注都在本地浏览器完成。它不会提供实时字幕、手机遥控或 AI 排练评分。
**为什么关掉观众窗口后显示“未连接”?**
演讲者端会通过观众屏确认和心跳判断软件链路。窗口关闭或心跳超时会显示“未连接”,点击“重新打开观众屏”即可恢复。浏览器无法判断 HDMI 或投影仪线缆是否真的接通,现场仍需目视确认。
**怎么更新到最新版?**
重新运行安装命令,或在本地 skill 目录执行 `git pull`
@@ -352,6 +412,8 @@ Bug、排版问题、新布局需求——欢迎开 Issue 或 PR。改动请优
-`template.html` 里补类,不要让 layouts.md 使用未定义的类
-`template-swiss.html` 里补类时,同步更新 `layouts-swiss.md``swiss-layout-lock.md`
- 瑞士风新增规则后,同步更新 `scripts/validate-swiss-deck.mjs`
- 演讲者运行时必须同时更新两套模板,并运行 `scripts/check-presenter-runtime-sync.mjs`;CI 会拦截两份模板的 CSS / JavaScript 漂移
- 演讲备注或现场行为变化时,同步更新 `references/presenter-mode.md``references/checklist.md``scripts/validate-presenter-mode.mjs`
- 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别
- 新主题色进 `themes.md` 并给出适合的场景
@@ -2,8 +2,8 @@
"sourceId": "guizang-ppt-skill",
"repo": "https://github.com/op7418/guizang-ppt-skill.git",
"ref": "main",
"commit": "e8357b315471f04e17551988826011974bfff779",
"commit": "c91369c449d34755d320a8b81d0734000d99d1ab",
"adapter": "claude-skill",
"sourcePath": ".",
"syncedAt": "2026-08-06T16:00:02Z"
"syncedAt": "2026-08-08T16:00:01Z"
}
@@ -0,0 +1,24 @@
name: Presenter runtime sync
on:
pull_request:
paths:
- "assets/template*.html"
- "scripts/check-presenter-runtime-sync.mjs"
- ".github/workflows/presenter-runtime-sync.yml"
push:
branches: [main]
paths:
- "assets/template*.html"
- "scripts/check-presenter-runtime-sync.mjs"
- ".github/workflows/presenter-runtime-sync.yml"
jobs:
presenter-runtime-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: node scripts/check-presenter-runtime-sync.mjs
@@ -11,7 +11,7 @@
![Kimi work Gold Sponsor](https://img.shields.io/static/v1?label=Kimi%20work&message=Gold%20Sponsor&color=111111&style=flat-square)
![Cola Skill Gold Sponsor](https://img.shields.io/static/v1?label=Cola%20Skill&message=Gold%20Sponsor&color=E4353F&style=flat-square)
An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages.
An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages, with a complete rehearsal and presenter mode built in.
It ships with two visual systems:
@@ -59,6 +59,7 @@ Other useful prompts:
Turn this Markdown file into an editorial magazine-style presentation.
Create a 21:9 social cover from the core idea of this deck.
Redesign this product screenshot as a 16:10 slide visual.
Add speaker notes and planned timing to this deck, then help me rehearse it in presenter mode.
```
## Sponsors and Supporters
@@ -88,6 +89,7 @@ Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platform
- 🎨 **Curated theme presets**: 5 electronic-ink themes for Style A, 4 Swiss anchor-color themes for Style B
- 🖼 **Optional Codex image flow**: generate documentary photos, infographics, flow diagrams, system maps, and UI scenes with GPT-Image 2.0 / GPT-M 2.0, then insert them at template-safe ratios
- 📰 **Social covers**: generate 21:9 WeChat cover images, 1:1 share cards, 3:4 Xiaohongshu covers, video thumbnails, and related variants
- 🎤 **Presenter mode**: dual-window audience output, 16:9 current/next previews, speaker notes, timing and rehearsal, auto advance, laser pointer, circles, and live recovery controls
- 📴 **Low-power static mode**: press `B` to turn WebGL / canvas animation into static visuals
- 📄 **Single HTML file** — no build, no server, open directly in the browser
@@ -107,12 +109,13 @@ Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platform
| Deck visuals | In Codex, generate photos, infographics, flow diagrams, system maps, or UI scenes |
| Social covers | Generate 21:9 main covers, 1:1 share cards, 3:4 vertical covers, and video thumbnails from the same idea |
| Screenshot normalization | Redesign raw screenshots into template-safe ratios before inserting them into slides |
| Live talk / rehearsal | Ask the agent to generate notes and timing, then press the bottom-right `P` to enter presenter mode |
## Why HTML decks
- **Agent-native editing**: HTML / CSS is plain text, so agents can read, edit, and validate it directly.
- **Higher visual density than Markdown**: precise layout, positioning, motion, interactivity, and cover formats.
- **Lightweight delivery**: one HTML file can be opened, presented, sent, screenshotted, or recorded.
- **Lightweight delivery**: one HTML file can be opened, presented, sent, screenshotted, or recorded, with presenter tools included.
- **Better quality gates**: the Swiss validator can catch layout drift, unsafe image placement, centered body titles, and SVG text traps.
- **One visual system across outputs**: decks, generated visuals, covers, and screenshot redesigns can share the same style rules.
@@ -162,6 +165,7 @@ Once installed, Claude Code auto-detects the skill. Trigger phrases:
- "Electronic ink slides for my talk"
- "Create a 21:9 WeChat cover from this article"
- "Create a 1:1 share card from this deck"
- "Add speaker notes to this deck and help me rehearse it in presenter mode"
## Workflow
@@ -172,12 +176,59 @@ The skill is a structured workflow; the agent walks you through each step:
3. **Copy template** — Style A uses `assets/template.html`; Style B uses `assets/template-swiss.html`
4. **Fill content** — create a rhythm plan, then choose and adapt the matching layout skeletons
5. **Optional image generation** — in Codex, ask whether to use GPT-Image 2.0 / GPT-M 2.0 images, then insert them at page-appropriate ratios
6. **Self-check** — match against `references/checklist.md`; P0 issues must all pass; Swiss decks must also pass the layout validator
7. **Preview** — open the HTML in a browser
8. **Iterate**use inline styles to tune font size, height, spacing
6. **Generate speaker notes** — derive each slide's purpose, talking points, transition, and planned duration from the outline; do not invent missing live details
7. **Self-check** — match against `references/checklist.md`; P0 issues must all pass; run the Swiss and presenter validators when applicable
8. **Preview**open the HTML in a browser
9. **Rehearse / present** — press the bottom-right `P`, verify audience sync, and record actual timing
10. **Iterate** — adjust content, font size, height, and spacing from rehearsal results
Full spec in [`SKILL.md`](./SKILL.md).
## Presenter mode
Both templates ship with the same presenter runtime. Open a deck and press the bottom-right `P` to enter presenter view; the browser opens a clean audience window at the same time. Core features run entirely inside the local HTML and browser—no live-caption service, cloud relay, phone remote, or AI coaching service is required.
**Presenter view example**
![Presenter view with current and next slides, speaker notes, timing controls, and audience status](https://github.com/user-attachments/assets/95db62b5-65bb-40af-ac0b-1d415682af88)
### What the speaker sees
- **Current and next slides**: stacked vertically and always kept at `16:9`; small windows scale the whole slide without cropping or reflowing its text
- **Grid navigation**: replace the preview area with an inline overview, choose a slide, then return immediately to the two previews while the audience follows
- **Structured notes**: title, purpose, talking points, and transition are required; interaction, delivery, advance cue, fallback, and pronunciation appear only when supplied by the outline
- **Progress and health**: current / total slide count, completion percentage, plus audience states for connecting, synced, unsynced, frozen, disconnected, or popup blocked
### Timing, rehearsal, and auto advance
- The footer separates elapsed, current-slide, and remaining / overtime values, with explicit “Start timer / Resume timer / Reset timer” controls
- Rehearsal mode records actual time per slide and a local session summary; it does not grade the speaker with AI
- Auto advance is off by default and only runs when the outline provides per-slide seconds or the user enables a global interval
- Auto advance pauses while the grid, settings, or annotation tool is open, when the page is hidden, or when the audience is paused or out of sync
### Live tools and recovery
- Laser pointer, circle annotation, and clear actions sync to the audience window
- Black screen, white screen, or freeze the audience; unfreezing catches it up to the presenter's current slide
- Closing the audience window or losing its heartbeat produces a visible “Disconnected” state; “Reopen audience” is always available
- Exiting presenter mode closes the audience window when allowed, or leaves a clear “Presentation ended” fallback
- Preflight checks cover popups, fullscreen, fonts, images / video, and `16:9` previews, while reminding the speaker to verify HDMI adapters and projectors manually
Common shortcuts: `← / →` navigate, `Home / End` jump to first or last, `G` opens the grid, `L` laser, `C` circle, `B / W` black or white screen, `F` freezes the audience, and `?` opens the full shortcut list.
Ask an agent to prepare presenter mode with:
```text
Use this outline to add a purpose, talking points, transition, and planned duration to every slide. Do not invent interaction or live details that were not provided, then run the presenter-mode validator.
```
See [`references/presenter-mode.md`](./references/presenter-mode.md) for the full notes contract and runtime behavior. Validate with:
```bash
node scripts/validate-presenter-mode.mjs path/to/index.html
node scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30
```
## Style B Swiss
The Swiss theme is a strict layout system, not just a CSS skin.
@@ -258,7 +309,9 @@ guizang-ppt-skill/
│ ├── template-swiss.html ← Style B Swiss template
│ └── screenshot-backgrounds/ ← bundled WebP screenshot backgrounds: 5 style-a / 4 style-b
├── scripts/
── validate-swiss-deck.mjs ← Swiss layout validator
── validate-swiss-deck.mjs ← Swiss layout validator
│ ├── validate-presenter-mode.mjs ← speaker notes, timing, and runtime validator
│ └── check-presenter-runtime-sync.mjs ← cross-template presenter runtime drift check
└── references/
├── components.md ← component catalog (type, color, grid, icons, callout, stat, pipeline)
├── layouts.md ← 10 layout skeletons (paste-ready)
@@ -268,6 +321,7 @@ guizang-ppt-skill/
├── themes-swiss.md ← 4 Swiss anchor-color themes
├── image-prompts.md ← GPT-Image 2.0 / GPT-M 2.0 image types, ratios, and base prompts
├── screenshot-framing.md ← CleanShot X-style screenshot framing semantics
├── presenter-mode.md ← notes contract, rehearsal, audience output, and live tools
└── checklist.md ← quality checklist (P0 / P1 / P2 / P3 tiers)
```
@@ -341,6 +395,12 @@ Yes. Style A layouts can be extended in `references/layouts.md`. Style B is stri
**Is Codex image generation required?**
No. Decks work without generated images. The image flow is only used when you need photos, infographics, UI scenes, or covers.
**Does presenter mode require internet access or another service?**
No. Dual-window sync, notes, timing, rehearsal, auto advance, and annotations run locally in the browser. It does not provide live captions, phone control, or AI rehearsal scoring.
**Why does closing the audience window show “Disconnected”?**
The presenter uses acknowledgements and heartbeats to verify the software link. Closing the window or losing its heartbeat produces “Disconnected”; use “Reopen audience” to recover. The browser cannot verify a physical HDMI or projector cable, so the speaker still needs a visual check.
**How do I update the skill?**
Run the install command again, or run `git pull` inside your local skill directory.
@@ -351,6 +411,8 @@ Bugs, layout issues, new layout requests — Issues and PRs welcome. Prioritize:
- Add new classes to `template.html` first; don't let `layouts.md` reference undefined classes
- When changing `template-swiss.html`, update `layouts-swiss.md` and `swiss-layout-lock.md` together
- When adding Swiss rules, update `scripts/validate-swiss-deck.mjs`
- Presenter runtime changes must land in both templates and pass `scripts/check-presenter-runtime-sync.mjs`; CI rejects CSS / JavaScript drift
- Presenter-note or live-behavior changes must also update `references/presenter-mode.md`, `references/checklist.md`, and `scripts/validate-presenter-mode.mjs`
- Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier
- New theme colors go into `themes.md` with a recommended use case
@@ -13,7 +13,7 @@
> 🌏 **English version: [README.en.md](./README.en.md)**
一个适配 Claude Code / Codex 等 Agent 环境的网页 PPT 技能,用于生成**单文件 HTML 横向翻页 PPT**、PPT 配图和多平台封面。
一个适配 Claude Code / Codex 等 Agent 环境的网页 PPT 技能,用于生成**单文件 HTML 横向翻页 PPT**、PPT 配图和多平台封面,并内置完整的排练与演讲者模式
内置两套视觉系统:
@@ -61,6 +61,7 @@ npx skills add https://github.com/op7418/guizang-ppt-skill --skill guizang-ppt-s
帮我把这份 Markdown 做成杂志风演讲 PPT。
基于这份 PPT 的核心观点,生成一张公众号 21:9 头图。
把这张产品截图重新设计成适合 PPT 的 16:10 配图。
给这份 PPT 补齐演讲备注和每页计划时长,然后用演讲者模式帮我排练。
```
## 赞助与支持
@@ -90,6 +91,7 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、*
- 🎨 **主题色预设**:Style A 5 套电子墨水主题,Style B 4 套瑞士高饱和锚点色
- 🖼 **Codex 可选配图流程**:可用 GPT-Image 2.0 / GPT-M 2.0 生成纪实照片、信息图、流程图、系统关系图、UI 情景图,并按模板比例插入
- 📰 **多平台封面**:可用同一套视觉规则生成公众号 21:9、公众号分享卡 1:1、小红书 3:4、视频号横版等封面
- 🎤 **演讲者模式**:双窗口观众屏、当前页/下一页 16:9 预览、演讲备注、计时排练、自动翻页、激光笔、圈选和现场故障恢复
- 📴 **低性能静态模式**:按 `B` 可关闭 WebGL / canvas 动画,让动态内容退回静态背景
- 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开
@@ -109,12 +111,13 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、*
| PPT 配图 | 在 Codex 中用 GPT-Image 2.0 / GPT-M 2.0 生成照片、信息图、流程图、UI 情景图 |
| 多平台封面 | 从同一份内容生成公众号 21:9、1:1 分享卡、小红书 3:4、视频号横版封面 |
| 截图统一风格 | 把原始截图重新生成到模板需要的比例,再插入 PPT |
| 现场演讲 / 排练 | 让 Agent 从大纲生成备注和时长,按右下角 `P` 进入演讲者模式 |
## 为什么是 HTML PPT
- **更适合 Agent 生成和修改**:HTML / CSS 是文本,Agent 能直接读、改、验证。
- **表现力比 Markdown 更高**:可以做精细排版、空间定位、动画、交互和响应式封面。
- **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图。
- **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图,演讲者工具也随文件一起交付
- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐、危险 SVG,并在可用时用 Playwright 后验测量超出、底部空白、nav 安全线和标题间距。
- **更适合视觉内容链路**:同一套主题能覆盖 PPT、配图、封面和截图再设计。
@@ -164,6 +167,7 @@ git clone https://github.com/op7418/guizang-ppt-skill.git ~/.claude/skills/guiza
- "electronic ink 风格演讲 slides"
- "基于这篇文章做一张公众号 21:9 封面"
- "基于这份 PPT 生成一张 1:1 分享卡"
- "给这份 PPT 补齐演讲备注,并用演讲者模式帮我排练"
## 使用流程
@@ -174,12 +178,59 @@ Skill 本身是结构化工作流,Agent 会逐步引导:
3. **拷贝模板** — Style A 用 `assets/template.html`,Style B 用 `assets/template-swiss.html`
4. **填充内容** — 先做主题节奏表,再从对应 layout 骨架里挑、粘、改文案
5. **可选配图** — 在 Codex 中询问是否用 GPT-Image 2.0 / GPT-M 2.0 生成配图,再按页面比例插入
6. **自检**对照 `references/checklist.md`,P0 级问题必须全过;瑞士风还要运行版式校验器
7. **预览**浏览器直接打开
8. **迭代**inline style 改字号/高度/间距
6. **生成演讲备注**从大纲提取每页目的、讲述要点、转场和计划时长;用户没提供的信息不猜测
7. **自检**对照 `references/checklist.md`,P0 级问题必须全过;瑞士风和演讲模式分别运行对应校验器
8. **预览**浏览器直接打开
9. **排练 / 演讲** — 按右下角 `P` 进入演讲者模式,检查观众屏同步并记录实际时长
10. **迭代** — 根据排练结果调整内容、字号、高度和间距
详细说明见 [`SKILL.md`](./SKILL.md)。
## 演讲者模式
两套模板都内置同一套演讲者运行时。打开 deck 后,点击右下角 `P` 即可进入演讲者视图;浏览器会同时打开一个干净的观众屏。所有核心能力都在本地 HTML 和浏览器里完成,不依赖实时字幕、云端中继、手机遥控或 AI 教练服务。
**演讲者视图示例**
![演讲者视图示例:当前页、下一页、演讲备注、计时控制与观众屏状态](https://github.com/user-attachments/assets/95db62b5-65bb-40af-ac0b-1d415682af88)
### 演讲时能看到什么
- **当前页与下一页**:上下排列并始终保持 `16:9`,小屏时整页等比缩放,不裁切、不挤压文字
- **宫格选页**:在预览区原位切换总览,点选页面后立即回到双预览,观众屏同步跟随
- **结构化备注**:标题、本页目的、讲述要点和转场必填;互动、语气、翻页时机、备用方案和读音等信息只在大纲提供时显示
- **进度与状态**:显示当前页 / 总页数、完成百分比,以及观众屏的连接中、已同步、未同步、已冻结、未连接或弹窗被拦截状态
### 时间、排练与自动翻页
- 底栏分别显示已进行、本页和剩余 / 超时时间,按钮明确区分“开始计时 / 继续计时 / 重置计时”
- 排练模式记录每页实际时长和整场汇总,数据保存在本地浏览器,不会做 AI 评分
- 自动翻页默认关闭;只有大纲明确给出页面停留秒数,或用户在设置中开启全局间隔时才启用
- 打开宫格、设置、圈选工具,页面隐藏,或观众屏暂停 / 失去同步时,自动翻页会暂停
### 现场工具与故障恢复
- 激光笔、圈选和清除会同步到观众屏
- 一键黑屏、白屏,或冻结观众屏;恢复后自动追平演讲者当前页
- 观众窗口关闭或心跳超时后明确显示“未连接”,可随时点击“重新打开观众屏”
- 退出演讲时自动关闭观众窗口;浏览器不允许自关时,观众端显示“演示已结束”
- 演前检查覆盖弹窗、全屏、字体、图片 / 视频和 `16:9` 预览,并提醒人工确认 HDMI、转接器和投影仪
常用快捷键:`← / →` 翻页,`Home / End` 跳首页或尾页,`G` 宫格,`L` 激光笔,`C` 圈选,`B / W` 黑屏或白屏,`F` 冻结观众屏,`?` 查看完整快捷键。
让 Agent 生成或补齐演讲模式时,可以直接说:
```text
根据这份大纲给每一页补齐演讲目的、讲述要点、转场和计划时长。没有提供的互动或现场信息不要猜,然后运行演讲模式校验器。
```
演讲备注契约和完整行为说明见 [`references/presenter-mode.md`](./references/presenter-mode.md)。校验命令:
```bash
node scripts/validate-presenter-mode.mjs path/to/index.html
node scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30
```
## Style B 瑞士风
瑞士风是这次新增的结构化主题。它不是"换一套 CSS",而是一套更严格的版式系统。
@@ -259,7 +310,9 @@ guizang-ppt-skill/
│ ├── template-swiss.html ← Style B 瑞士国际主义模板
│ └── screenshot-backgrounds/ ← 截图美化内置背景(WebP):style-a 5 套 / style-b 4 套
├── scripts/
── validate-swiss-deck.mjs ← 瑞士风版式校验器
── validate-swiss-deck.mjs ← 瑞士风版式校验器
│ ├── validate-presenter-mode.mjs ← 演讲备注、时长和运行时校验器
│ └── check-presenter-runtime-sync.mjs ← 两套模板演讲者运行时防漂移检查
└── references/
├── components.md ← 组件手册(字体、色、网格、图标、callout、stat、pipeline)
├── layouts.md ← 10 种页面布局骨架(可直接粘贴)
@@ -269,6 +322,7 @@ guizang-ppt-skill/
├── themes-swiss.md ← 4 套瑞士风锚点色
├── image-prompts.md ← GPT-Image 2.0 / GPT-M 2.0 配图类型、比例和基础提示词
├── screenshot-framing.md ← CleanShot X 式截图适配语义
├── presenter-mode.md ← 演讲备注契约、排练、观众屏与现场工具说明
└── checklist.md ← 质量检查清单(P0 / P1 / P2 / P3 分级)
```
@@ -342,6 +396,12 @@ guizang-ppt-skill/
**Codex 配图是必须的吗?**
不是。没有配图也能生成 PPT。配图流程只在需要照片、信息图、UI 情景图或封面时使用。
**演讲者模式需要联网或额外服务吗?**
不需要。双窗口同步、备注、计时、排练、自动翻页和标注都在本地浏览器完成。它不会提供实时字幕、手机遥控或 AI 排练评分。
**为什么关掉观众窗口后显示“未连接”?**
演讲者端会通过观众屏确认和心跳判断软件链路。窗口关闭或心跳超时会显示“未连接”,点击“重新打开观众屏”即可恢复。浏览器无法判断 HDMI 或投影仪线缆是否真的接通,现场仍需目视确认。
**怎么更新到最新版?**
重新运行安装命令,或在本地 skill 目录执行 `git pull`
@@ -352,6 +412,8 @@ Bug、排版问题、新布局需求——欢迎开 Issue 或 PR。改动请优
-`template.html` 里补类,不要让 layouts.md 使用未定义的类
-`template-swiss.html` 里补类时,同步更新 `layouts-swiss.md``swiss-layout-lock.md`
- 瑞士风新增规则后,同步更新 `scripts/validate-swiss-deck.mjs`
- 演讲者运行时必须同时更新两套模板,并运行 `scripts/check-presenter-runtime-sync.mjs`;CI 会拦截两份模板的 CSS / JavaScript 漂移
- 演讲备注或现场行为变化时,同步更新 `references/presenter-mode.md``references/checklist.md``scripts/validate-presenter-mode.mjs`
- 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别
- 新主题色进 `themes.md` 并给出适合的场景
@@ -448,9 +448,10 @@ cp "<SKILL_ROOT>/assets/template-swiss.html" "项目/XXX/ppt/index.html"
```bash
node <SKILL_ROOT>/scripts/validate-presenter-mode.mjs path/to/index.html
node <SKILL_ROOT>/scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30
node <SKILL_ROOT>/scripts/check-presenter-runtime-sync.mjs
```
会拦截缺失/重复页面 ID、备注与页面错位、必填字段或可选字段类型错误、完整时间计划超出 90% 预算,以及计时、排练、自动翻页、标注、演前检查和观众屏恢复控件缺失。
第一个脚本会拦截缺失/重复页面 ID、备注与页面错位、必填字段或可选字段类型错误、完整时间计划超出 90% 预算,以及计时、排练、自动翻页、标注、演前检查和观众屏恢复控件缺失。第二个脚本会拦截两套模板之间的演讲者 CSS / JS 漂移。
#### 4.0.1 · 先量后改:超出 / 空白 / 标题间距
@@ -563,7 +563,7 @@
body.ppt-audience #hint,body.ppt-audience #nav,
body.ppt-preview #hint,body.ppt-preview #nav{display:none!important}
body.ppt-audience{cursor:none}
body.ppt-preview{pointer-events:none}
body.ppt-preview{pointer-events:none}body.ppt-preview #deck{transition:none!important}
body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint,
body.ppt-presenter>.bg{visibility:hidden}
#ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif}
@@ -583,9 +583,9 @@
.ppt-stage-label{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34}
.ppt-annotation-tools{display:flex;align-items:center;gap:5px;margin-left:auto}.ppt-annotation-tools .ppt-ui-btn{padding:6px 9px;font-size:12px}.ppt-annotation-tools .ppt-ui-btn.is-active{color:#111318;background:#ff5b63;border-color:#ff5b63}.ppt-annotation-tools .ppt-grid-btn.is-active{color:#111318;background:#edf1f5;border-color:#edf1f5}.ppt-annotation-tools .ppt-clear-btn{min-width:0}.ppt-grid-btn{display:inline-flex;align-items:center;gap:6px}.ppt-grid-icon{display:grid;grid-template-columns:repeat(2,4px);gap:2px}.ppt-grid-icon i{width:4px;height:4px;background:currentColor}
.ppt-frame-viewport{position:relative;display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b}
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}#ppt-next-card.is-end .ppt-frame{visibility:hidden}#ppt-next-card.is-end .ppt-frame-viewport::after{content:"演示结束";position:absolute;color:#727a86;font:650 14px/1 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em}
.ppt-annotation-layer{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;width:0;height:0;pointer-events:none;touch-action:none}.ppt-annotation-layer.is-active{pointer-events:auto;cursor:crosshair}
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}#ppt-audience-cover.is-ended{display:grid!important;place-items:center;background:#101216!important;color:#f5f6f7;font:650 clamp(24px,3vw,46px)/1.2 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em}
.ppt-preview-stack.is-overview>.ppt-stage-card{display:none}.ppt-presenter-overview{grid-row:1/-1;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}.ppt-presenter-overview[hidden]{display:none}.ppt-overview-head{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;border-bottom:1px solid #292d34}.ppt-overview-head strong{font-size:15px}.ppt-overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;overflow:auto;padding:14px}.ppt-overview-page{appearance:none;min-height:112px;display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:13px;border:1px solid #343943;border-radius:10px;background:#111318;color:#eef1f4;text-align:left;cursor:pointer}.ppt-overview-page:hover{background:#1d2128;border-color:#636b78}.ppt-overview-page.is-current{border-color:#84a8ff;box-shadow:inset 0 0 0 1px #84a8ff}.ppt-overview-page-no{color:#8f97a3;font:600 12px/1 var(--mono,monospace)}.ppt-overview-page-title{font-size:14px;font-weight:650;line-height:1.35}.ppt-overview-page-meta{display:flex;justify-content:space-between;gap:8px;color:#7f8793;font-size:11px}
.ppt-notes-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-label{margin-bottom:6px;color:#848c98;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.ppt-notes-title{font-size:21px;font-weight:680;line-height:1.32}.ppt-purpose-block{margin-top:14px;padding:11px 12px;border-left:3px solid #84a8ff;background:#111318}.ppt-purpose-copy{color:#c9ced6;font-size:14px;line-height:1.55}.ppt-notes-draft-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-top:15px;padding-top:13px;border-top:1px solid #2d3139}.ppt-notes-draft-head .ppt-notes-label{margin:0 0 4px}.ppt-note-meta{color:#8f97a3;font-size:12px}
.ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-notes-tools .ppt-ui-btn{padding:7px 9px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif}.ppt-note-editor:focus{box-shadow:inset 0 0 0 1px #3d4654}
@@ -600,6 +600,7 @@
@media(max-width:1100px){.ppt-presenter-bottom{grid-template-columns:minmax(292px,1fr) auto minmax(86px,.6fr);gap:12px}.ppt-time-stat{min-width:82px;padding:0 11px}.ppt-time-value{font-size:20px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:60px;padding:8px 9px}}
@media(max-width:900px){.ppt-presenter-top{padding:12px 16px;flex-wrap:wrap}.ppt-presenter-top .ppt-presenter-meta{gap:6px}.ppt-presenter-top .ppt-ui-btn{padding:8px 9px}.ppt-presenter-main,#ppt-presenter[data-layout="slides"] .ppt-presenter-main,#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-saved{display:none}.ppt-presenter-bottom{grid-template-columns:minmax(270px,1fr) auto 74px;gap:8px;padding:9px 16px}.ppt-time-stat{min-width:0;padding:0 9px}.ppt-time-value{font-size:18px}.ppt-time-hint{max-width:78px;overflow:hidden;text-overflow:ellipsis}.ppt-controls,.ppt-session-actions{gap:5px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:52px;padding:7px 8px;font-size:12px}.ppt-page-state strong{font-size:14px}.ppt-dialog-grid{grid-template-columns:1fr}.ppt-segmented.screen{grid-template-columns:repeat(2,1fr)}}
@media(max-height:700px){.ppt-presenter-top{padding-top:10px;padding-bottom:10px}.ppt-presenter-main{padding-top:10px;padding-bottom:10px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(88px,16vh,118px);gap:10px}.ppt-stage-label{padding:7px 10px}.ppt-notes-head{padding:12px 14px}.ppt-purpose-block{margin-top:10px;padding:8px 10px}.ppt-notes-draft-head{margin-top:10px;padding-top:9px}.ppt-presenter-bottom{padding-top:8px;padding-bottom:8px}.ppt-control-stack{gap:5px}.ppt-session-actions{padding-top:5px}}
/* ============ /演讲者模式 ============ */
/* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */
#overview [data-anim]{opacity:1!important;transform:none!important}
@@ -1602,7 +1603,7 @@ window.__pptPresenter=(()=>{
function noteAt(i){
const id=slideId(i);
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`${i+1}`,purpose:'待补充本页目的',talk:['待补充讲述要点'],transition:'待补充转场'};
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`${i+1}`,purpose:'',talk:[],transition:''};
}
function slideLabel(i){return `${i+1} / ${total} · ${noteAt(i).title||slideId(i)}`;}
function escapeHTML(value){return String(value??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));}
@@ -1616,7 +1617,7 @@ window.__pptPresenter=(()=>{
if(n.transition)parts.push(`【转场】\n${n.transition}`);
const optional=[['舞台提示',n.cue],['互动',n.interaction],['表达提示',n.delivery],['翻页时机',n.advance],['备用方案',n.fallback],['读音提醒',n.pronunciation]];
optional.forEach(([label,value])=>{const text=valueLines(value);if(text)parts.push(`${label}\n${text}`);});
return parts.join('\n\n');
return parts.join('\n\n')||'—';
}
function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;}
function loadNoteValue(id,i,n){
@@ -1634,6 +1635,14 @@ window.__pptPresenter=(()=>{
url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1));
return url.href;
}
function postPreviewGoto(frame,i){
try{frame.contentWindow?.postMessage({__guizangPptSync:1,session,role:'presenter',type:'preview-goto',index:i},'*');}catch(e){}
}
function updatePreviewFrame(frame,i){
frame.dataset.slide=String(i);
if(!frame.dataset.previewBound){frame.dataset.previewBound='1';frame.addEventListener('load',()=>postPreviewGoto(frame,Number(frame.dataset.slide)));}
if(!frame.getAttribute('src'))frame.src=frameUrl('preview',i);else postPreviewGoto(frame,i);
}
function plannedMsAt(i){
const minutes=Number(noteAt(i).minutes);
return Number.isFinite(minutes)&&minutes>0?minutes*60000:null;
@@ -1756,7 +1765,7 @@ window.__pptPresenter=(()=>{
<div class="ppt-presenter-top"><div><div class="ppt-presenter-brand">演讲者视图</div><div class="ppt-presenter-meta"><span id="ppt-plan"></span><span id="ppt-section" hidden></span></div></div><div class="ppt-presenter-meta"><span id="ppt-sync" class="ppt-sync" data-state="disconnected" title="表示观众页面的软件同步状态,不代表 HDMI 等物理线缆状态">观众屏 · 未连接</span><button id="ppt-auto-toggle" class="ppt-ui-btn ppt-auto-state" type="button">自动翻页 · 关闭</button><button id="ppt-preflight" class="ppt-ui-btn" type="button">检查</button><button id="ppt-settings" class="ppt-ui-btn" type="button">设置</button><button id="ppt-reopen" class="ppt-ui-btn" type="button">重新打开观众屏</button><button id="ppt-exit" class="ppt-ui-btn" type="button">退出</button></div></div>
<div class="ppt-presenter-main"><div class="ppt-preview-stack"><div class="ppt-stage-card"><div class="ppt-stage-label"><span>当前页</span><span class="ppt-annotation-tools"><button id="ppt-grid" class="ppt-ui-btn ppt-grid-btn" type="button" aria-pressed="false" title="宫格选页(Esc"><span class="ppt-grid-icon" aria-hidden="true"><i></i><i></i><i></i><i></i></span>宫格</button><button class="ppt-ui-btn" data-ppt-tool="laser" type="button" title="激光笔(L">激光</button><button class="ppt-ui-btn" data-ppt-tool="circle" type="button" title="圈选(C">圈选</button><button id="ppt-ink-clear" class="ppt-ui-btn ppt-clear-btn" type="button" title="清除标记(X">清除</button></span><span id="ppt-current-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe><canvas id="ppt-presenter-ink" class="ppt-annotation-layer" aria-label="演讲标注层"></canvas></div></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></div><div id="ppt-presenter-overview" class="ppt-presenter-overview" hidden><div class="ppt-overview-head"><strong>选择页面</strong><button id="ppt-overview-close" class="ppt-ui-btn" type="button">返回预览</button></div><div id="ppt-presenter-overview-grid" class="ppt-overview-grid"></div></div></div>
<div class="ppt-notes-card"><div class="ppt-notes-head"><div class="ppt-notes-label">标题</div><div id="ppt-notes-title" class="ppt-notes-title"></div><div class="ppt-purpose-block"><div class="ppt-notes-label">本页目的</div><div id="ppt-notes-purpose" class="ppt-purpose-copy"></div></div><div class="ppt-notes-draft-head"><div><div class="ppt-notes-label">草稿(备注)</div><div id="ppt-note-meta" class="ppt-note-meta"></div></div><span class="ppt-notes-tools"><span id="ppt-saved" class="ppt-saved">已保存</span><button id="ppt-note-smaller" class="ppt-ui-btn" type="button" title="缩小备注字号">A</button><button id="ppt-note-larger" class="ppt-ui-btn" type="button" title="放大备注字号">A+</button></span></div></div><textarea id="ppt-note-editor" class="ppt-note-editor" spellcheck="false" aria-label="本页演讲备注"></textarea></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-time-dashboard"><div class="ppt-time-stat"><span class="ppt-time-label">已进行</span><strong id="ppt-clock" class="ppt-time-value">00:00</strong><span class="ppt-time-hint">总时长</span></div><div class="ppt-time-stat"><span class="ppt-time-label">本页</span><strong id="ppt-slide-clock" class="ppt-time-value">00:00</strong><span id="ppt-slide-plan" class="ppt-time-hint">计划 —</span></div><div id="ppt-slide-status-box" class="ppt-time-stat"><span id="ppt-slide-status-label" class="ppt-time-label">计划</span><strong id="ppt-slide-status" class="ppt-time-value">—</strong><span id="ppt-slide-status-hint" class="ppt-time-hint">未设置</span></div></div><div class="ppt-control-stack"><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-session-actions"><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置</button><button id="ppt-rehearsal" class="ppt-ui-btn" type="button">排练</button></div></div><div class="ppt-page-state"><strong id="ppt-page-count"></strong><span id="ppt-page-percent"></span></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-time-dashboard"><div class="ppt-time-stat"><span class="ppt-time-label">已进行</span><strong id="ppt-clock" class="ppt-time-value">00:00</strong><span class="ppt-time-hint">总时长</span></div><div class="ppt-time-stat"><span class="ppt-time-label">本页</span><strong id="ppt-slide-clock" class="ppt-time-value">00:00</strong><span id="ppt-slide-plan" class="ppt-time-hint">计划 —</span></div><div id="ppt-slide-status-box" class="ppt-time-stat"><span id="ppt-slide-status-label" class="ppt-time-label">计划</span><strong id="ppt-slide-status" class="ppt-time-value">—</strong><span id="ppt-slide-status-hint" class="ppt-time-hint">未设置</span></div></div><div class="ppt-control-stack"><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-session-actions"><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始计时</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置计时</button><button id="ppt-rehearsal" class="ppt-ui-btn" type="button">排练</button></div></div><div class="ppt-page-state"><strong id="ppt-page-count"></strong><span id="ppt-page-percent"></span></div></div>
<div id="ppt-dialog" class="ppt-dialog" hidden><div class="ppt-dialog-panel" role="dialog" aria-modal="true"><div class="ppt-dialog-head"><div id="ppt-dialog-title" class="ppt-dialog-title"></div><button id="ppt-dialog-close" class="ppt-ui-btn" type="button">关闭</button></div><div id="ppt-dialog-body" class="ppt-dialog-body"></div></div></div>
</div>`);
document.getElementById('ppt-reopen').onclick=reopenAudience;
@@ -1790,8 +1799,9 @@ window.__pptPresenter=(()=>{
if(mode!=='presenter')return;mountShell();
const n=noteAt(idx),next=Math.min(idx+1,total-1),id=slideId(idx),editor=document.getElementById('ppt-note-editor');
const currentFrame=document.getElementById('ppt-current-frame'),nextFrame=document.getElementById('ppt-next-frame');
if(currentFrame.dataset.slide!==String(idx)){currentFrame.src=frameUrl('preview',idx);currentFrame.dataset.slide=String(idx);}
if(nextFrame.dataset.slide!==String(next)){nextFrame.src=frameUrl('preview',next);nextFrame.dataset.slide=String(next);}
if(currentFrame.dataset.slide!==String(idx))updatePreviewFrame(currentFrame,idx);
if(nextFrame.dataset.slide!==String(next))updatePreviewFrame(nextFrame,next);
document.getElementById('ppt-next-card').classList.toggle('is-end',idx===total-1);
document.getElementById('ppt-current-label').textContent=`${idx+1} / ${total}`;
document.getElementById('ppt-next-label').textContent=idx===total-1?'已到尾页':`${next+1} / ${total}`;
document.getElementById('ppt-notes-title').textContent=n.title||`${idx+1}`;
@@ -1828,7 +1838,7 @@ window.__pptPresenter=(()=>{
if(plan)plan.textContent=totalPlan?`计划 ${Number(totalPlan/60000).toFixed(1)} 分钟 · 预计 ${clockTime(Date.now()+Math.max(0,totalPlan-totalUsed))}`:'计划 —';
const section=sectionInfo(idx),sectionEl=document.getElementById('ppt-section');
if(sectionEl){sectionEl.hidden=!section;sectionEl.textContent=section?`${section.name} · ${section.position}/${section.count}${section.planned?` · 剩余 ${durationText(Math.max(0,section.planned-section.used))}`:''}`:'';}
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续':'开始');
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续计时':'开始计时');
const rehearsalButton=document.getElementById('ppt-rehearsal');if(rehearsalButton){rehearsalButton.textContent=rehearsalActive?'结束排练':'排练';rehearsalButton.classList.toggle('ppt-rehearsal-on',rehearsalActive);}
renderAutoState();
}
@@ -1898,8 +1908,13 @@ window.__pptPresenter=(()=>{
}
function applyAudienceScreen(value){
const cover=document.getElementById('ppt-audience-cover');if(!cover)return;
cover.classList.remove('is-ended');cover.textContent='';
if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none';
}
function showAudienceEnded(){
const cover=document.getElementById('ppt-audience-cover');if(cover){cover.style.display='';cover.style.background='';cover.textContent='演示已结束';cover.classList.add('is-ended');}
setTimeout(()=>{try{window.close();}catch(e){}},120);
}
function setScreenMode(value){
const next=['normal','black','white','frozen'].includes(value)?value:'normal',wasFrozen=audienceFrozen;
screenMode=next;audienceFrozen=next==='frozen';emit({type:'screen',mode:next});
@@ -1959,6 +1974,7 @@ window.__pptPresenter=(()=>{
return;
}
if(mode!=='audience')return;
if(message.type==='bye'){showAudienceEnded();return;}
if(message.type==='screen'){applyAudienceScreen(message.mode);return;}
if(message.type==='laser'&&Number(message.index)===idx){audienceLaser=message.point||null;audienceLaserHideAt=Date.now()+650;renderInks();return;}
if(message.type==='circle'&&Number(message.index)===idx&&message.circle){audienceCircles.push(message.circle);renderInks();return;}
@@ -1968,14 +1984,18 @@ window.__pptPresenter=(()=>{
emit({type:'ack',index:idx,seq:Number(message.seq)});
}
}
function receivePreview(message){
if(!message||message.__guizangPptSync!==1||message.session!==session||message.role!=='presenter'||message.type!=='preview-goto')return;
go(Number(message.index),{force:true});
}
function sendGoto(force=false){
if(mode!=='presenter'||audienceFrozen)return;if(force||lastAckIndex!==idx)seq++;
emit({type:'goto',index:idx,seq,screenMode,circles});updateSyncStatus();
}
function syncState(){
if(popupBlocked)return'blocked';if(audienceWindow&&audienceWindow.closed)return'disconnected';if(audienceFrozen)return'frozen';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':(audienceOpenedAt?'unsynced':'disconnected');
if(Date.now()-lastAckAt>2600||lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';return'synced';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':'disconnected';
if(Date.now()-lastAckAt>2600)return'disconnected';if(lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';return'synced';
}
function updateSyncStatus(){
const el=document.getElementById('ppt-sync');if(!el)return;const state=syncState();
@@ -1994,7 +2014,7 @@ window.__pptPresenter=(()=>{
window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience();
}
function exitPresenter(){
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);emit({type:'bye'});mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);emit({type:'bye'});audienceWindow=null;audienceOpenedAt=0;lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
}
function initAudience(){
mode='audience';document.body.classList.add('ppt-audience');document.title=`观众屏 · ${originalTitle}`;
@@ -2002,7 +2022,7 @@ window.__pptPresenter=(()=>{
const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize();
initTransport();emit({type:'ready',index:idx});
}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});addEventListener('message',e=>{if(e.source===parent)receivePreview(e.data);});}
function onSlideChange(nextIndex){
if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();updateShell();sendGoto();
}
@@ -2013,9 +2033,9 @@ window.__pptPresenter=(()=>{
if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;}
if(mode!=='presenter')return false;
const key=e.key?.toLowerCase();
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
const dialog=document.getElementById('ppt-dialog');
if(!dialog?.hidden){if(e.key==='Escape'){e.preventDefault();closeDialog();}return true;}
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else togglePresenterOverview();return true;}
if(presenterOverviewOn)return true;
if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;}
@@ -2044,6 +2064,7 @@ window.__pptPresenter=(()=>{
},250);
return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,toggleOverview:togglePresenterOverview,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,presenterOverviewOn,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})};
})();
/* =============== /演讲者模式 / 观众屏同步 =============== */
addEventListener('keydown',e=>{
if(window.__pptPresenter?.handleKey(e))return;
@@ -207,7 +207,7 @@
body.ppt-audience #hint,body.ppt-audience #nav,
body.ppt-preview #hint,body.ppt-preview #nav{display:none!important}
body.ppt-audience{cursor:none}
body.ppt-preview{pointer-events:none}
body.ppt-preview{pointer-events:none}body.ppt-preview #deck{transition:none!important}
body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint,
body.ppt-presenter>.bg{visibility:hidden}
#ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif}
@@ -227,9 +227,9 @@
.ppt-stage-label{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34}
.ppt-annotation-tools{display:flex;align-items:center;gap:5px;margin-left:auto}.ppt-annotation-tools .ppt-ui-btn{padding:6px 9px;font-size:12px}.ppt-annotation-tools .ppt-ui-btn.is-active{color:#111318;background:#ff5b63;border-color:#ff5b63}.ppt-annotation-tools .ppt-grid-btn.is-active{color:#111318;background:#edf1f5;border-color:#edf1f5}.ppt-annotation-tools .ppt-clear-btn{min-width:0}.ppt-grid-btn{display:inline-flex;align-items:center;gap:6px}.ppt-grid-icon{display:grid;grid-template-columns:repeat(2,4px);gap:2px}.ppt-grid-icon i{width:4px;height:4px;background:currentColor}
.ppt-frame-viewport{position:relative;display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b}
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}#ppt-next-card.is-end .ppt-frame{visibility:hidden}#ppt-next-card.is-end .ppt-frame-viewport::after{content:"演示结束";position:absolute;color:#727a86;font:650 14px/1 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em}
.ppt-annotation-layer{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;width:0;height:0;pointer-events:none;touch-action:none}.ppt-annotation-layer.is-active{pointer-events:auto;cursor:crosshair}
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}#ppt-audience-cover.is-ended{display:grid!important;place-items:center;background:#101216!important;color:#f5f6f7;font:650 clamp(24px,3vw,46px)/1.2 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em}
.ppt-preview-stack.is-overview>.ppt-stage-card{display:none}.ppt-presenter-overview{grid-row:1/-1;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}.ppt-presenter-overview[hidden]{display:none}.ppt-overview-head{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;border-bottom:1px solid #292d34}.ppt-overview-head strong{font-size:15px}.ppt-overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;overflow:auto;padding:14px}.ppt-overview-page{appearance:none;min-height:112px;display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:13px;border:1px solid #343943;border-radius:10px;background:#111318;color:#eef1f4;text-align:left;cursor:pointer}.ppt-overview-page:hover{background:#1d2128;border-color:#636b78}.ppt-overview-page.is-current{border-color:#84a8ff;box-shadow:inset 0 0 0 1px #84a8ff}.ppt-overview-page-no{color:#8f97a3;font:600 12px/1 var(--mono,monospace)}.ppt-overview-page-title{font-size:14px;font-weight:650;line-height:1.35}.ppt-overview-page-meta{display:flex;justify-content:space-between;gap:8px;color:#7f8793;font-size:11px}
.ppt-notes-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-label{margin-bottom:6px;color:#848c98;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.ppt-notes-title{font-size:21px;font-weight:680;line-height:1.32}.ppt-purpose-block{margin-top:14px;padding:11px 12px;border-left:3px solid #84a8ff;background:#111318}.ppt-purpose-copy{color:#c9ced6;font-size:14px;line-height:1.55}.ppt-notes-draft-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-top:15px;padding-top:13px;border-top:1px solid #2d3139}.ppt-notes-draft-head .ppt-notes-label{margin:0 0 4px}.ppt-note-meta{color:#8f97a3;font-size:12px}
.ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-notes-tools .ppt-ui-btn{padding:7px 9px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif}.ppt-note-editor:focus{box-shadow:inset 0 0 0 1px #3d4654}
@@ -244,6 +244,7 @@
@media(max-width:1100px){.ppt-presenter-bottom{grid-template-columns:minmax(292px,1fr) auto minmax(86px,.6fr);gap:12px}.ppt-time-stat{min-width:82px;padding:0 11px}.ppt-time-value{font-size:20px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:60px;padding:8px 9px}}
@media(max-width:900px){.ppt-presenter-top{padding:12px 16px;flex-wrap:wrap}.ppt-presenter-top .ppt-presenter-meta{gap:6px}.ppt-presenter-top .ppt-ui-btn{padding:8px 9px}.ppt-presenter-main,#ppt-presenter[data-layout="slides"] .ppt-presenter-main,#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-saved{display:none}.ppt-presenter-bottom{grid-template-columns:minmax(270px,1fr) auto 74px;gap:8px;padding:9px 16px}.ppt-time-stat{min-width:0;padding:0 9px}.ppt-time-value{font-size:18px}.ppt-time-hint{max-width:78px;overflow:hidden;text-overflow:ellipsis}.ppt-controls,.ppt-session-actions{gap:5px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:52px;padding:7px 8px;font-size:12px}.ppt-page-state strong{font-size:14px}.ppt-dialog-grid{grid-template-columns:1fr}.ppt-segmented.screen{grid-template-columns:repeat(2,1fr)}}
@media(max-height:700px){.ppt-presenter-top{padding-top:10px;padding-bottom:10px}.ppt-presenter-main{padding-top:10px;padding-bottom:10px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(88px,16vh,118px);gap:10px}.ppt-stage-label{padding:7px 10px}.ppt-notes-head{padding:12px 14px}.ppt-purpose-block{margin-top:10px;padding:8px 10px}.ppt-notes-draft-head{margin-top:10px;padding-top:9px}.ppt-presenter-bottom{padding-top:8px;padding-bottom:8px}.ppt-control-stack{gap:5px}.ppt-session-actions{padding-top:5px}}
/* ============ /演讲者模式 ============ */
/* ============================================================
============ LAYOUTS API · 面向 agent 的类(v2============
@@ -779,7 +780,7 @@ window.__pptPresenter=(()=>{
function noteAt(i){
const id=slideId(i);
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,purpose:'待补充本页目的',talk:['待补充讲述要点'],transition:'待补充转场'};
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,purpose:'',talk:[],transition:''};
}
function slideLabel(i){return `${i+1} / ${total} · ${noteAt(i).title||slideId(i)}`;}
function escapeHTML(value){return String(value??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));}
@@ -793,7 +794,7 @@ window.__pptPresenter=(()=>{
if(n.transition)parts.push(`【转场】\n${n.transition}`);
const optional=[['舞台提示',n.cue],['互动',n.interaction],['表达提示',n.delivery],['翻页时机',n.advance],['备用方案',n.fallback],['读音提醒',n.pronunciation]];
optional.forEach(([label,value])=>{const text=valueLines(value);if(text)parts.push(`【${label}】\n${text}`);});
return parts.join('\n\n');
return parts.join('\n\n')||'—';
}
function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;}
function loadNoteValue(id,i,n){
@@ -811,6 +812,14 @@ window.__pptPresenter=(()=>{
url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1));
return url.href;
}
function postPreviewGoto(frame,i){
try{frame.contentWindow?.postMessage({__guizangPptSync:1,session,role:'presenter',type:'preview-goto',index:i},'*');}catch(e){}
}
function updatePreviewFrame(frame,i){
frame.dataset.slide=String(i);
if(!frame.dataset.previewBound){frame.dataset.previewBound='1';frame.addEventListener('load',()=>postPreviewGoto(frame,Number(frame.dataset.slide)));}
if(!frame.getAttribute('src'))frame.src=frameUrl('preview',i);else postPreviewGoto(frame,i);
}
function plannedMsAt(i){
const minutes=Number(noteAt(i).minutes);
return Number.isFinite(minutes)&&minutes>0?minutes*60000:null;
@@ -933,7 +942,7 @@ window.__pptPresenter=(()=>{
<div class="ppt-presenter-top"><div><div class="ppt-presenter-brand">演讲者视图</div><div class="ppt-presenter-meta"><span id="ppt-plan"></span><span id="ppt-section" hidden></span></div></div><div class="ppt-presenter-meta"><span id="ppt-sync" class="ppt-sync" data-state="disconnected" title="表示观众页面的软件同步状态,不代表 HDMI 等物理线缆状态">观众屏 · 未连接</span><button id="ppt-auto-toggle" class="ppt-ui-btn ppt-auto-state" type="button">自动翻页 · 关闭</button><button id="ppt-preflight" class="ppt-ui-btn" type="button">检查</button><button id="ppt-settings" class="ppt-ui-btn" type="button">设置</button><button id="ppt-reopen" class="ppt-ui-btn" type="button">重新打开观众屏</button><button id="ppt-exit" class="ppt-ui-btn" type="button">退出</button></div></div>
<div class="ppt-presenter-main"><div class="ppt-preview-stack"><div class="ppt-stage-card"><div class="ppt-stage-label"><span>当前页</span><span class="ppt-annotation-tools"><button id="ppt-grid" class="ppt-ui-btn ppt-grid-btn" type="button" aria-pressed="false" title="宫格选页(Esc"><span class="ppt-grid-icon" aria-hidden="true"><i></i><i></i><i></i><i></i></span>宫格</button><button class="ppt-ui-btn" data-ppt-tool="laser" type="button" title="激光笔(L">激光</button><button class="ppt-ui-btn" data-ppt-tool="circle" type="button" title="圈选(C">圈选</button><button id="ppt-ink-clear" class="ppt-ui-btn ppt-clear-btn" type="button" title="清除标记(X">清除</button></span><span id="ppt-current-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe><canvas id="ppt-presenter-ink" class="ppt-annotation-layer" aria-label="演讲标注层"></canvas></div></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></div><div id="ppt-presenter-overview" class="ppt-presenter-overview" hidden><div class="ppt-overview-head"><strong>选择页面</strong><button id="ppt-overview-close" class="ppt-ui-btn" type="button">返回预览</button></div><div id="ppt-presenter-overview-grid" class="ppt-overview-grid"></div></div></div>
<div class="ppt-notes-card"><div class="ppt-notes-head"><div class="ppt-notes-label">标题</div><div id="ppt-notes-title" class="ppt-notes-title"></div><div class="ppt-purpose-block"><div class="ppt-notes-label">本页目的</div><div id="ppt-notes-purpose" class="ppt-purpose-copy"></div></div><div class="ppt-notes-draft-head"><div><div class="ppt-notes-label">草稿(备注)</div><div id="ppt-note-meta" class="ppt-note-meta"></div></div><span class="ppt-notes-tools"><span id="ppt-saved" class="ppt-saved">已保存</span><button id="ppt-note-smaller" class="ppt-ui-btn" type="button" title="缩小备注字号">A</button><button id="ppt-note-larger" class="ppt-ui-btn" type="button" title="放大备注字号">A+</button></span></div></div><textarea id="ppt-note-editor" class="ppt-note-editor" spellcheck="false" aria-label="本页演讲备注"></textarea></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-time-dashboard"><div class="ppt-time-stat"><span class="ppt-time-label">已进行</span><strong id="ppt-clock" class="ppt-time-value">00:00</strong><span class="ppt-time-hint">总时长</span></div><div class="ppt-time-stat"><span class="ppt-time-label">本页</span><strong id="ppt-slide-clock" class="ppt-time-value">00:00</strong><span id="ppt-slide-plan" class="ppt-time-hint">计划 —</span></div><div id="ppt-slide-status-box" class="ppt-time-stat"><span id="ppt-slide-status-label" class="ppt-time-label">计划</span><strong id="ppt-slide-status" class="ppt-time-value"></strong><span id="ppt-slide-status-hint" class="ppt-time-hint">未设置</span></div></div><div class="ppt-control-stack"><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-session-actions"><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置</button><button id="ppt-rehearsal" class="ppt-ui-btn" type="button">排练</button></div></div><div class="ppt-page-state"><strong id="ppt-page-count"></strong><span id="ppt-page-percent"></span></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-time-dashboard"><div class="ppt-time-stat"><span class="ppt-time-label">已进行</span><strong id="ppt-clock" class="ppt-time-value">00:00</strong><span class="ppt-time-hint">总时长</span></div><div class="ppt-time-stat"><span class="ppt-time-label">本页</span><strong id="ppt-slide-clock" class="ppt-time-value">00:00</strong><span id="ppt-slide-plan" class="ppt-time-hint">计划 —</span></div><div id="ppt-slide-status-box" class="ppt-time-stat"><span id="ppt-slide-status-label" class="ppt-time-label">计划</span><strong id="ppt-slide-status" class="ppt-time-value"></strong><span id="ppt-slide-status-hint" class="ppt-time-hint">未设置</span></div></div><div class="ppt-control-stack"><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-session-actions"><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始计时</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置计时</button><button id="ppt-rehearsal" class="ppt-ui-btn" type="button">排练</button></div></div><div class="ppt-page-state"><strong id="ppt-page-count"></strong><span id="ppt-page-percent"></span></div></div>
<div id="ppt-dialog" class="ppt-dialog" hidden><div class="ppt-dialog-panel" role="dialog" aria-modal="true"><div class="ppt-dialog-head"><div id="ppt-dialog-title" class="ppt-dialog-title"></div><button id="ppt-dialog-close" class="ppt-ui-btn" type="button">关闭</button></div><div id="ppt-dialog-body" class="ppt-dialog-body"></div></div></div>
</div>`);
document.getElementById('ppt-reopen').onclick=reopenAudience;
@@ -967,8 +976,9 @@ window.__pptPresenter=(()=>{
if(mode!=='presenter')return;mountShell();
const n=noteAt(idx),next=Math.min(idx+1,total-1),id=slideId(idx),editor=document.getElementById('ppt-note-editor');
const currentFrame=document.getElementById('ppt-current-frame'),nextFrame=document.getElementById('ppt-next-frame');
if(currentFrame.dataset.slide!==String(idx)){currentFrame.src=frameUrl('preview',idx);currentFrame.dataset.slide=String(idx);}
if(nextFrame.dataset.slide!==String(next)){nextFrame.src=frameUrl('preview',next);nextFrame.dataset.slide=String(next);}
if(currentFrame.dataset.slide!==String(idx))updatePreviewFrame(currentFrame,idx);
if(nextFrame.dataset.slide!==String(next))updatePreviewFrame(nextFrame,next);
document.getElementById('ppt-next-card').classList.toggle('is-end',idx===total-1);
document.getElementById('ppt-current-label').textContent=`${idx+1} / ${total}`;
document.getElementById('ppt-next-label').textContent=idx===total-1?'已到尾页':`${next+1} / ${total}`;
document.getElementById('ppt-notes-title').textContent=n.title||`第 ${idx+1} 页`;
@@ -1005,7 +1015,7 @@ window.__pptPresenter=(()=>{
if(plan)plan.textContent=totalPlan?`计划 ${Number(totalPlan/60000).toFixed(1)} 分钟 · 预计 ${clockTime(Date.now()+Math.max(0,totalPlan-totalUsed))}`:'计划 —';
const section=sectionInfo(idx),sectionEl=document.getElementById('ppt-section');
if(sectionEl){sectionEl.hidden=!section;sectionEl.textContent=section?`${section.name} · ${section.position}/${section.count}${section.planned?` · 剩余 ${durationText(Math.max(0,section.planned-section.used))}`:''}`:'';}
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续':'开始');
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续计时':'开始计时');
const rehearsalButton=document.getElementById('ppt-rehearsal');if(rehearsalButton){rehearsalButton.textContent=rehearsalActive?'结束排练':'排练';rehearsalButton.classList.toggle('ppt-rehearsal-on',rehearsalActive);}
renderAutoState();
}
@@ -1075,8 +1085,13 @@ window.__pptPresenter=(()=>{
}
function applyAudienceScreen(value){
const cover=document.getElementById('ppt-audience-cover');if(!cover)return;
cover.classList.remove('is-ended');cover.textContent='';
if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none';
}
function showAudienceEnded(){
const cover=document.getElementById('ppt-audience-cover');if(cover){cover.style.display='';cover.style.background='';cover.textContent='演示已结束';cover.classList.add('is-ended');}
setTimeout(()=>{try{window.close();}catch(e){}},120);
}
function setScreenMode(value){
const next=['normal','black','white','frozen'].includes(value)?value:'normal',wasFrozen=audienceFrozen;
screenMode=next;audienceFrozen=next==='frozen';emit({type:'screen',mode:next});
@@ -1136,6 +1151,7 @@ window.__pptPresenter=(()=>{
return;
}
if(mode!=='audience')return;
if(message.type==='bye'){showAudienceEnded();return;}
if(message.type==='screen'){applyAudienceScreen(message.mode);return;}
if(message.type==='laser'&&Number(message.index)===idx){audienceLaser=message.point||null;audienceLaserHideAt=Date.now()+650;renderInks();return;}
if(message.type==='circle'&&Number(message.index)===idx&&message.circle){audienceCircles.push(message.circle);renderInks();return;}
@@ -1145,14 +1161,18 @@ window.__pptPresenter=(()=>{
emit({type:'ack',index:idx,seq:Number(message.seq)});
}
}
function receivePreview(message){
if(!message||message.__guizangPptSync!==1||message.session!==session||message.role!=='presenter'||message.type!=='preview-goto')return;
go(Number(message.index),{force:true});
}
function sendGoto(force=false){
if(mode!=='presenter'||audienceFrozen)return;if(force||lastAckIndex!==idx)seq++;
emit({type:'goto',index:idx,seq,screenMode,circles});updateSyncStatus();
}
function syncState(){
if(popupBlocked)return'blocked';if(audienceWindow&&audienceWindow.closed)return'disconnected';if(audienceFrozen)return'frozen';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':(audienceOpenedAt?'unsynced':'disconnected');
if(Date.now()-lastAckAt>2600||lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';return'synced';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':'disconnected';
if(Date.now()-lastAckAt>2600)return'disconnected';if(lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';return'synced';
}
function updateSyncStatus(){
const el=document.getElementById('ppt-sync');if(!el)return;const state=syncState();
@@ -1171,7 +1191,7 @@ window.__pptPresenter=(()=>{
window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience();
}
function exitPresenter(){
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);emit({type:'bye'});mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);emit({type:'bye'});audienceWindow=null;audienceOpenedAt=0;lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
}
function initAudience(){
mode='audience';document.body.classList.add('ppt-audience');document.title=`观众屏 · ${originalTitle}`;
@@ -1179,7 +1199,7 @@ window.__pptPresenter=(()=>{
const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize();
initTransport();emit({type:'ready',index:idx});
}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});addEventListener('message',e=>{if(e.source===parent)receivePreview(e.data);});}
function onSlideChange(nextIndex){
if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();updateShell();sendGoto();
}
@@ -1190,9 +1210,9 @@ window.__pptPresenter=(()=>{
if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;}
if(mode!=='presenter')return false;
const key=e.key?.toLowerCase();
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
const dialog=document.getElementById('ppt-dialog');
if(!dialog?.hidden){if(e.key==='Escape'){e.preventDefault();closeDialog();}return true;}
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else togglePresenterOverview();return true;}
if(presenterOverviewOn)return true;
if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;}
@@ -1221,6 +1241,7 @@ window.__pptPresenter=(()=>{
},250);
return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,toggleOverview:togglePresenterOverview,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,presenterOverviewOn,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})};
})();
/* =============== /演讲者模式 / 观众屏同步 =============== */
addEventListener('keydown',e=>{
if(window.__pptPresenter?.handleKey(e))return;
@@ -1,7 +1,7 @@
# 多主题统一管理整合方案
> 基于对 template.html / template-swiss.html 两份模板与全部 references 的逐行对比分析(2026-07)。
> 本文档是**方案建议**,尚未实施;按阶段推进,每个阶段独立可交付、可回滚。
> 本文档是**方案建议**,尚未整体实施;按阶段推进,每个阶段独立可交付、可回滚。2026-08-07 已先为演讲者 CSS / JS 加入边界标记、字节级同步脚本和 GitHub Actions,但尚未提取共享源文件。
## 现状诊断
@@ -52,6 +52,10 @@
- **a. 构建脚本**(推荐):`scripts/build-templates.mjs``src/runtime.js` + 各主题 `src/style-*.css` + 皮肤 HTML 组装成各 template*.html。模板仍是完整单文件,但源头唯一。
- **b. 文档约定**:不引入构建,把 runtime 段落用 `<!-- SHARED-RUNTIME v3 -->` 注释标记,改动时用脚本校验各份一致。成本低但漂移风险仍在。
当前演讲者模式已先落地 Phase 1b 的防漂移护栏:`scripts/check-presenter-runtime-sync.mjs``.github/workflows/presenter-runtime-sync.yml`。它不替代长期的共享源提取,但可以防止修改只落到其中一份模板。
如果未来合并 Bento 分支,以 `main` 上的演讲者运行时为上游,采用 `main → bento` 的合并方向;不要用旧 Bento 模板反向覆盖 `main`
### Phase 2 · 公共 Token 契约
在各模板间统一**命名和语义**(值可以不同):
@@ -16,15 +16,18 @@
- 演讲入口只放在右下角现有快捷控制区,显示 `P 演讲模式`
- 演讲者主体只做“左侧预览 + 右侧备注”两栏;当前页在上、下一页在下,避免形成挤压当前页的三栏布局。
- 两个预览 iframe 必须严格保持 `16:9`,容器不足时等比缩小并留边,不得裁切或拉伸;小屏优先压缩下一页预览高度。
- 底栏固定为“左时间、中控制、右页码进度”:左侧分组显示已进行/本页/剩余或超时,中间两行放翻页和会话控制,右侧只放页码与百分比;不要重复当前页标题。
- 底栏固定为“左时间、中控制、右页码进度”:左侧分组显示已进行/本页/剩余或超时,中间两行放翻页和会话控制,右侧只放页码与百分比;不要重复当前页标题。会话控制使用“开始计时 / 继续计时 / 重置计时 / 排练”等明确动作名,不使用孤立的“开始”或“重置”。
- 翻页控制顺序固定为 `首页 / 上一页 / 下一页 / 尾页`;尾页时最后一个按钮变为“重新开始”。自动翻页放在右上角状态栏。
- `宫格` 放在当前页标题旁,打开时替换当前/下一页预览区;选中页面后回到预览,不要弹出突兀的全屏总览。
- 右侧卡片依次显示标题、本页目的和草稿备注;设置里的开关用胶囊 switch,时间间隔用美化后的加减 stepper。
- 观众屏必须显示软件链路状态:`连接中 / 已同步 / 未同步 / 未连接 / 弹窗被拦截`,并始终提供“重新打开观众屏”。
- 演讲者必须能看到总时长、本页时长、计划/剩余/超时;缺失 `minutes` 时显示横杠,不猜测。
- 临时新增页面没有对应备注时,界面 fallback 只显示 `—`,不显示“待补充”;校验器仍要报告页面/备注数量不一致。
- 排练模式记录每页实际时长,本地保留最近 5 次;只做数据汇总,不做 AI 教练评分。
- 自动翻页默认关闭,`minutes``autoAdvanceSeconds` 必须分开;总览、圈选、设置、页面隐藏、观众屏暂停或失同步时倒计必须暂停。
- 必须提供激光笔、圈选、清除、黑屏、白屏和冻结/恢复;标注坐标要归一化同步到观众屏。
- 预览 iframe 只初次加载,翻页改用 `postMessage` 同步页码;尾页的下一页预览显示“演示结束”,不重复当前页。
- 退出演讲必须处理 `bye`:脚本打开的观众窗口自动关闭,无法自关时显示“演示已结束”;心跳超时必须显示“未连接”而不是永久停在“未同步”。
- 演前检查必须覆盖观众屏、弹窗、全屏、字体、图片/视频和 16:9;同时明确提醒它不能检测物理线缆。
- 备注里的 `section / cue / interaction / delivery / advance / fallback / pronunciation / autoAdvanceSeconds` 都是可选字段;用户大纲没有就整段隐藏。
- 每页必须有稳定、唯一的 `data-slide-id`;`SPEAKER_NOTES` 数量、顺序和 ID 必须逐页一致。
@@ -25,6 +25,7 @@
- 大纲明确写了停顿、提问、Demo、视频、语气、翻页点或备用路径:分别写入对应的可选字段。
- 用户明确要求某页自动停留多少秒:才写 `autoAdvanceSeconds`
- 没有提供的信息不猜测、不写“待补充”到演讲界面,直接省略字段。界面对缺失时长显示横杠,其他可选模块整段隐藏。
- 如果页面在未运行校验的情况下临时新增,且没有对应备注,演讲者界面只显示中性横杠 `—`,不显示“待补充”或虚构内容;校验器仍应报告备注与页面不一致。
`minutes` 是讲述计划,`autoAdvanceSeconds` 是播放行为,二者必须分开。不得因为一页建议讲 1 分钟,就默认 60 秒后自动翻页。
@@ -92,14 +93,16 @@ window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
- 点击后当前窗口进入演讲者视图,并打开独立观众屏。
- 主体保持两栏:左侧预览、右侧备注;左侧当前页在上、下一页在下。
- 当前页与下一页 iframe 始终严格保持 `16:9`。空间不足时留边并整体等比缩小,不得裁切、压扁或让页面内部文字重新排版。
- 预览 iframe 只在初次挂载时加载 HTML,翻页时通过 `postMessage` 发送页码,不反复改写 `src` 重载整份演示稿。
- 小屏优先缩小下一页预览,把空间留给当前页。
- 底栏分成三段:左侧是 `已进行 / 本页 / 剩余或超时` 三组时间,中间是两行控制(第一行 `首页 / 上一页 / 下一页 / 尾页`,第二行 `开始或暂停 / 重置 / 排练`),右侧只显示 `第几页 / 总页数` 和完成百分比。不要在底栏重复当前页标题。
- 底栏分成三段:左侧是 `已进行 / 本页 / 剩余或超时` 三组时间,中间是两行控制(第一行 `首页 / 上一页 / 下一页 / 尾页`,第二行 `开始计时或暂停 / 重置计时 / 排练`),右侧只显示 `第几页 / 总页数` 和完成百分比。不要在底栏重复当前页标题。计时按钮必须明确写成“开始计时 / 继续计时 / 重置计时”,避免让用户误以为是重置整场演示。
- `自动翻页` 放在右上角状态栏,不占用底部翻页区。
- `宫格` 放在“当前页”标题旁。打开后直接用宫格替换当前页/下一页预览区,不要弹出突兀的全屏层;卡片显示页码、标题、章节和进度,点击页面后立即回到当前页/下一页预览。`ESC` 切换宫格。
- 右侧卡片依次显示 `标题 / 本页目的 / 草稿(备注)`,避免把标题或页面目的混进草稿正文。
- 演讲者可从“均衡 / 页面优先 / 备注优先”三种布局选择。
- 备注编辑按 `data-slide-id` 存入 `localStorage`,显示保存状态;切页后备注滚动回顶部,并允许调整字号。
- `Home` / `End`、方向键、PageUp/PageDown 与可见按钮保持一致;光标在备注编辑器中时不得触发翻页。
- 对话框已打开时,`?` 不得覆盖当前对话框;只有 `Escape` 关闭当前对话框。尾页的“下一页”预览显示“演示结束”,不重复当前页。
- 设置面板使用可辨识的卡片层级;开关使用胶囊 switch,数值间隔使用带减号、数值和加号的 stepper,不直接暴露浏览器原生复选框或粗糙数字输入框。
## 6. 时间控制与排练
@@ -134,13 +137,15 @@ window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
- **连接中**:观众窗口已打开,尚未收到确认。
- **已同步**:观众页确认的页码与演讲者页相同,心跳有效。
- **未同步**确认超时或两端页码不同。
- **未同步**观众页心跳仍有效,但两端页码或序列号不同。
- **已冻结**:观众屏故意保留原页,不是同步故障。
- **未连接**:观众窗口未打开已关闭。
- **未连接**:观众窗口未打开已关闭,或心跳已超时。即使某些内置浏览器的 `window.closed` 代理不可靠,心跳超时后也必须落到“未连接”
- **弹窗被拦截**:浏览器阻止打开观众窗口。
“重新打开观众屏”必须始终可用,并在恢复后立即发送当前页。
演讲者退出演讲模式时必须发送 `bye`。由脚本打开的观众窗口尝试自动关闭;如果浏览器不允许自关,则保留深色“演示已结束”遮罩,不得继续停留在最后一页假装演示仍在进行。
浏览器只能确认观众**页面/窗口**的软件同步状态,不能检测 HDMI、转接器或投影仪线缆是否物理断开。现场仍应目视确认外接屏。
## 10. 演前检查和快捷键
@@ -174,6 +179,6 @@ node <SKILL_ROOT>/scripts/validate-presenter-mode.mjs path/to/index.html
node <SKILL_ROOT>/scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30
```
浏览器实测至少包含:进入演讲模式、弹窗允许/拦截、前后翻页、内嵌宫格替换预览与选页返回、首页/尾页、尾页重新开始、观众窗口关闭后的状态变化、重新打开后恢复同步、备注保存、计时、排练记录、自动翻页暂停/恢复、激光笔、圈选、清除、黑屏、白屏、冻结、设置面板与演前检查。
浏览器实测至少包含:进入演讲模式、弹窗允许/拦截、前后翻页且预览 iframe 不重载、内嵌宫格替换预览与选页返回、首页/尾页、尾页预览结束态、尾页重新开始、观众窗口关闭后显示“未连接”、重新打开后恢复同步、退出演讲后观众屏关闭或显示结束遮罩、备注保存、计时、排练记录、自动翻页暂停/恢复、激光笔、圈选、清除、黑屏、白屏、冻结、设置面板与演前检查。
至少在一组常用尺寸和一组小屏尺寸下检查:当前页/下一页上下排列,两个 iframe 宽高比均为 `16:9`,且没有超出各自容器。
@@ -0,0 +1,39 @@
#!/usr/bin/env node
import {readFileSync} from 'node:fs';
import {dirname,resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');
const files=['assets/template.html','assets/template-swiss.html'];
const blocks=[
['presenter CSS','/* ============ 演讲者模式 ============ */','/* ============ /演讲者模式 ============ */'],
['presenter JavaScript','/* =============== 演讲者模式 / 观众屏同步 =============== */','/* =============== /演讲者模式 / 观众屏同步 =============== */'],
];
function extract(source,label,startMarker,endMarker,file){
const start=source.indexOf(startMarker),end=source.indexOf(endMarker,start+startMarker.length);
if(start<0||end<0)throw new Error(`${file}: missing ${label} boundary marker.`);
return source.slice(start,end+endMarker.length);
}
function firstDifferentLine(a,b){
const left=a.split('\n'),right=b.split('\n'),count=Math.max(left.length,right.length);
for(let i=0;i<count;i++)if(left[i]!==right[i])return i+1;
return 0;
}
const sources=files.map(file=>[file,readFileSync(resolve(root,file),'utf8')]);
let failed=false;
for(const [label,start,end] of blocks){
const extracted=sources.map(([file,source])=>[file,extract(source,label,start,end,file)]);
const [baseFile,base]=extracted[0];
let blockFailed=false;
for(const [file,value] of extracted.slice(1)){
if(value===base)continue;
failed=true;
blockFailed=true;
console.error(`ERROR ${label} drift: ${baseFile} and ${file} first differ at block line ${firstDifferentLine(base,value)}.`);
}
if(!blockFailed)console.log(`PASS ${label} is byte-identical across ${files.length} templates.`);
}
process.exit(failed?1:0);
@@ -39,6 +39,7 @@ function extractArray(name){
const slideTags=[...source.matchAll(/<section\b(?=[^>]*\bclass="[^"]*\bslide\b[^"]*")[^>]*>/g)].map(m=>m[0]);
const slideIds=slideTags.map((tag,i)=>tag.match(/\bdata-slide-id="([^"]+)"/)?.[1]||'');
if(!slideIds.length)warnings.push('No slides found; only the reusable presenter runtime can be validated.');
slideIds.forEach((id,i)=>{
if(!id)errors.push(`Slide ${i+1}: missing data-slide-id.`);
@@ -86,16 +87,15 @@ else{
const runtimeChecks=[
['right-bottom presenter entry','id="ppt-presenter-btn"'],
['presenter shell','id="ppt-presenter"'],
['vertical preview stack','grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px)'],
['16:9 preview viewport','class="ppt-frame-viewport"'],
['proportional preview fitter','fitPresenterFrames'],
['exact 16:9 fitting','availableHeight*(16/9)'],
['overview control','id="ppt-grid"'],
['embedded presenter overview','id="ppt-presenter-overview"'],
['first-page control','id="ppt-first"'],
['last/restart control','id="ppt-last"'],
['audience status','id="ppt-sync"'],
['audience recovery','id="ppt-reopen"'],
['audience shutdown handling','showAudienceEnded'],
['direct window sync','postMessage('],
['BroadcastChannel fallback','BroadcastChannel'],
['storage fallback','__guizangPptSync'],
@@ -110,9 +110,16 @@ const runtimeChecks=[
['layout presets','data-layout-choice'],
['capsule auto-advance switch','class="ppt-switch"'],
['styled interval stepper','class="ppt-stepper"'],
['reload-free preview navigation','preview-goto'],
['shortcut help','openShortcuts'],
];
for(const [label,needle]of runtimeChecks)if(!html.includes(needle))errors.push(`Presenter runtime missing ${label}.`);
if(!/\.ppt-preview-stack\s*\{[^}]*grid-template-rows\s*:/s.test(html))errors.push('Presenter preview stack must define vertical grid rows.');
if(!/\.ppt-frame\s*\{[^}]*aspect-ratio\s*:\s*16\s*\/\s*9/s.test(html))errors.push('Presenter preview frames must declare a 16:9 aspect ratio.');
if(!/fitPresenterFrames[\s\S]*?16\s*\/\s*9/.test(html))errors.push('Presenter preview fitter must preserve the 16:9 ratio.');
if(!/id="ppt-timer-toggle"[^>]*>开始计时<\/button>/.test(html))errors.push('Presenter timer must use the explicit label "开始计时".');
if(!/id="ppt-timer-reset"[^>]*>重置计时<\/button>/.test(html))errors.push('Presenter timer reset must use the explicit label "重置计时".');
if(!html.includes('继续计时'))errors.push('Presenter timer resume state must use the explicit label "继续计时".');
if(/\.ppt-preview-stack\{[^}]*grid-template-columns/.test(html))errors.push('Presenter previews must stack vertically; remove grid-template-columns from .ppt-preview-stack.');
const timedNotes=speakerNotes.filter(n=>Number.isFinite(Number(n?.minutes))&&Number(n.minutes)>0);
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作",
"version": "20260605",
"keySource": "none",
"syncedAt": "2026-08-06T16:02:10Z"
"syncedAt": "2026-08-08T16:02:04Z"
}
@@ -2,8 +2,8 @@
"sourceId": "next-skills",
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"commit": "0ae8c72462952df163f1b1e0726641bc5b40dc93",
"commit": "a677cf66af002fbdcf49a982ef435b03554817cc",
"adapter": "skill-collection",
"sourcePath": "skills",
"syncedAt": "2026-08-06T16:00:02Z"
"syncedAt": "2026-08-08T16:00:01Z"
}
@@ -2,8 +2,8 @@
"sourceId": "ppt-master",
"repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main",
"commit": "c0305342cf5c558e7242dba64cfc207874a5134a",
"commit": "eb095bfcc6b625fd8754e9f5d053ec4220865370",
"adapter": "claude-skill",
"sourcePath": "skills/ppt-master",
"syncedAt": "2026-08-06T16:00:02Z"
"syncedAt": "2026-08-08T16:00:01Z"
}
@@ -53,6 +53,7 @@ compete with it.
5. **No speculative execution** — Do not prepare later-phase artifacts before their owning step.
6. **Deterministic routing** — Do not add a route-choice question when [`routing.md`](workflows/routing.md) resolves the request. If a route prerequisite is missing, state it and stop that route.
7. **Owning-source recovery** — On failure, repair or regenerate the owning source artifact and resume from the route's declared pointer. Do not silently downgrade a required artifact.
8. **Stable paths** — Use absolute skill/project paths; never derive them from CWD.
## Global Communication Rules
@@ -39,7 +39,7 @@ history, or resumable planning state. Context loss restarts the Quick run.
| `images/image_prompts.json`, `image_queries.json`, `image_sources.json`, `formula_manifest.json` | Conditional resource contracts | AI/web/formula execution status and provenance | Create only for a triggered path, including Quick. They guide preparation/attribution, never page design. |
| `icons/` | Prepared project icon pool | Bundled icons copied by `icon_sync.py` plus user-provided, template, imported, or custom icon SVGs | SVG authoring may choose any icon in this project-local pool per page; `spec_lock.icons.inventory` indexes the default plan's curated synced bundled pool rather than assigning page usage or defining an exhaustive whitelist. Exporter global fallback is legacy compatibility only. |
| `${SKILL_DIR}/templates/{brands,styles,layouts,decks}/*_index.json` | Library discovery indexes | The complete registered option source for Default Stage-1 template selection and chat listing | The UI server or chat branch reads these indexes only to populate the Stage-1 choice, after the communication recommendation is authored. Never scan kind directories to add options or use index summaries as Stage-1 planning evidence. Derive a library root from kind + entry id. Exact unregistered roots remain explicit inputs. Quick does not read the catalog. |
| `templates/` | Project template reference | Stage-1-confirmed non-free selection or Quick direct-input installed/fused specs, optional Layout/Deck SVG prototypes, and non-image assets | Default template-aware Strategist work from Stage 2 onward, Quick's current agent before direct authoring, and every later role read this project-local state only, never the library/external installation root. The active planner reads every installed template Design Spec and an actual SVG roster only for Layout/Deck; Brand and Style are intentionally roster-free. Continuous Executor reuses that context; fresh Executor reads the Design Spec once and each selected complete SVG, when any, only before first use or after its SHA changes. |
| `templates/` | Project template reference | Stage-1-confirmed non-free selection or Quick direct-input installed specs, one file per selected workspace, optional Layout/Deck SVG prototypes, and non-image assets | Default template-aware Strategist work from Stage 2 onward, Quick's current agent before direct authoring, and every later role read this project-local state only, never the library/external installation root. The active planner reads every installed template Design Spec and an actual SVG roster only for Layout/Deck; Brand and Style are intentionally roster-free. Continuous Executor reuses that context; fresh Executor reads the Design Spec once and each selected complete SVG, when any, only before first use or after its SHA changes. |
| `templates/template_execution_manifest.json` (`v1`) + `templates/template_execution/*.text-slots.json` (`v2-min`) | Derived template index | Compact prototype/source-import summary plus per-prototype text-slot diagnostics; the sidecar integrity hash is tool-only | Materialization may publish these deterministic records, but page-context does not inject or require them and models do not read them during page authoring. The complete prototype SVG is the sole visual/template authority; never author from either JSON artifact. |
| `<import_workspace>/svg/` | Imported native-payload backing | Complete PPTX-derived metadata, hidden carriers, fallback evidence, and source structure | Keep immutable; create-template materialization may resolve a validated source ref against these files, but models do not edit or bulk-read them |
| `<import_workspace>/svg-flat/` | Optional complete-page verification backing | Self-contained visual composition generated only by explicit `--inheritance-mode both` | Keep immutable when requested; never use as authoring or materialization input |
@@ -72,7 +72,7 @@ history, or resumable planning state. Context loss restarts the Quick run.
| Sources read policy | In `sources/`, read content-type files (`.md` / `.markdown` / `.txt` / `.csv` / `.tsv` / `.json` / `.jsonl` / `.yaml` / `.yml`) and judge by content — a `.json` / `.csv` may be core content or just data. Exclude known sidecars: `*.conversion_profile.json` and `*_files/image_manifest.json`. `analysis/` facts (`source_profile.json`, `<stem>.slide_library.json`) are read per Step 4 / direct-PPTX workflow, not in the `sources/` content scan. |
| PPTX structure | `slide_library.json` owns native geometry, slot facts, and SmartArt layout/relationships for direct PPTX workflows. |
| Design contract | Final confirmation once → audited `design_spec.md` → optional same-file refinement/approval → context-authored lock. Never maintain a parallel draft/lock. Executor may apply `Template Application` prose but never replace identity. Repair divergence from the approved Design Spec/context unless it fails active-decision fidelity. |
| Flat packaging authority | Free-design, brand-only, Style-only, and every plan with `template_reuse_scope: style` declare `pptx_structure.mode: flat` and omit `pptx_masters`, `pptx_layouts`, `page_pptx_layouts`, and `page_layouts`. A Style fused with Layout/Deck changes only Direction / method and does not force the non-Style structure plan to flat. `svg_output/` owns the complete Slide-local visual design without root Master/Layout identity, fixed-layer ownership, or placeholder metadata. Export materializes one clean project-owned Master plus one Blank Layout, applies the locked theme defaults, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks. |
| Flat packaging authority | Free-design, brand-only, Style-only, and every plan with `template_reuse_scope: style` declare `pptx_structure.mode: flat` and omit `pptx_masters`, `pptx_layouts`, `page_pptx_layouts`, and `page_layouts`. A Style installed alongside Layout/Deck changes only Direction / method and does not force the non-Style structure plan to flat. `svg_output/` owns the complete Slide-local visual design without root Master/Layout identity, fixed-layer ownership, or placeholder metadata. Export materializes one clean project-owned Master plus one Blank Layout, applies the locked theme defaults, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks. |
| Template structure authority | `template_reuse_scope: mirror|layout` uses `page_layouts` for each page's authoring-input prototype. `pptx_masters` / `pptx_layouts` own the unique reusable output definitions, while `page_pptx_layouts` owns page assignment. Strict keeps the prototype contract; adaptive may use a current or new Layout already declared by Strategist. A construction-discovered structural change returns upstream for definition and assignment repair before authoring resumes. Mirror additionally preserves literal visuals/text topology; layout allows project-controlled reflow/re-skinning. Unused definitions may register without a published Slide. Templates validate provenance but never add missing visible page objects during export. |
| Fact classes | External facts resolve through `sources/*.facts.json`; invented demo KPIs/targets/internal ratios are labeled `scenario` in `design_spec.md §IX` and visibly in the page. Never promote scenario data into the external fact registry. |
| Imported-template authoring | Editable SVGs under `authoring-svg/` own create-template edits, `authoring_summary.json` owns model-facing orientation, and `authoring_manifest.json` owns tool-only source-object identity. Lossless `svg/` owns immutable native payload and fallback evidence; optional `svg-flat/` owns only complete-page verification. Materialized `templates/*.svg` own the validated deliverable contract and contain no IR-only source refs. |
@@ -7,17 +7,22 @@ Always-loaded Executor authority for flat SVG page authoring and behavior shared
| Trigger | Load |
|---|---|
| `pptx_structure.mode: structured` | [`executor-structured.md`](./executor-structured.md) |
| Any data chart, chart catalog selection, or text-grid table | [`executor-chart.md`](./executor-chart.md) |
| A page will use a preset pattern fill or evaluate native chart/table replacement | [`native-data-interface.md`](./native-data-interface.md) before deciding eligibility or emitting metadata |
| Any selected Chart/Table `family/key` reference, or a legacy `page_charts` row that resolves to a live Chart/Table SVG | [`executor-visualization.md`](./executor-visualization.md), then the resolver-returned Chart/Table branch |
| Any value-driven geometry, including a chart-family reference, mini chart, sparkline, inset, or small multiple | [`executor-chart.md`](./executor-chart.md) |
| The mandatory per-page Structure decision finds qualitative geometry | [`executor-structure.md`](./executor-structure.md) |
| Any semantic cell grid, including a table-family reference | [`executor-table.md`](./executor-table.md) |
| A page will use a preset pattern fill or an independent object is explicitly selected as native-ready | [`native-data-interface.md`](./native-data-interface.md) before emitting the pattern or replacement metadata |
| Any image/formula | [`executor-image.md`](./executor-image.md) + [`image-layout-spec.md`](./image-layout-spec.md) + [`image-layout-patterns.md`](./image-layout-patterns.md) + [`svg-image-embedding.md`](./svg-image-embedding.md) |
| Any `Status: Sourced` web image | [`executor-web-image.md`](./executor-web-image.md), after `executor-image.md` |
| Effective Speaker Notes outcome is enabled after all SVG pages pass | [`executor-notes.md`](./executor-notes.md) |
Evaluate branches from each object's actual information model, not only from a Chart/Table reference. A catalog family selects construction guidance but never native readiness; `Native-ready` is an independent object-level decision. Page-local qualitative geometry also never implies package-level `pptx_structure.mode: structured`.
> Narrative skeleton and visual aesthetic come from this deck's locked files under [`modes/`](./modes/_index.md) and [`visual-styles/`](./visual-styles/_index.md). [`shared-standards-core.md`](./shared-standards-core.md) supplies the technical boundary plus the fallback visual-quality and leading defaults when those authorities are silent.
**Hard rule — complete page SVG**: Every visible object intended for the exported slide MUST exist in the final page SVG or be explicitly referenced by it. Templates and `spec_lock.md` guide construction; they are not export-time overlays for missing visible content.
**Hard rule — Shape-first page authority**: Every visible object intended for the exported slide MUST exist in the final page SVG or be explicitly referenced by it. Templates and `spec_lock.md` guide construction; they are not export-time overlays for missing visible content. Optional native Chart/Table metadata belongs to an independently selected object and never replaces this visible fallback during authoring; [`native-data-interface.md`](./native-data-interface.md) alone defines that metadata and its export activation.
**Hard rule — flat PowerPoint structure**: Free-design, brand-only, Style-only, and every `template_reuse_scope: style` project use `pptx_structure.mode: flat`: write no root Master/Layout identity, `data-pptx-layer`, or `data-pptx-placeholder`; every visible object remains Slide-local, and the root declares exactly one canonical `data-pptx-page-role` (`cover` / `toc` / `section` / `content` / `ending`). A Style workspace supplies reusable communication/design direction, composition rhythm, and information-expression defaults without page prototypes. Its identity-adjacent color, typography, icon, and image defaults yield to the final Brand/Deck identity and confirmed project lock. When Style is fused with Layout/Deck, it changes only Direction / method and follows the resolved non-Style structure route. Export materializes one clean project-owned Master plus one Blank Layout from the current lock. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by specialized metadata; the marked element uses a stable unique `id`. See [`semantic-svg.md`](./semantic-svg.md).
**Hard rule — flat PowerPoint structure**: Free-design, brand-only, Style-only, and every `template_reuse_scope: style` project use `pptx_structure.mode: flat`: write no root Master/Layout identity, `data-pptx-layer`, or `data-pptx-placeholder`; every visible object remains Slide-local, and the root declares exactly one canonical `data-pptx-page-role` (`cover` / `toc` / `section` / `content` / `ending`). A Style workspace supplies reusable communication/design direction, composition rhythm, and information-expression defaults without page prototypes. Its identity-adjacent color, typography, icon, and image defaults yield to the final Brand/Deck identity and confirmed project lock. When a Style is installed alongside Layout/Deck, it changes only Direction / method and follows the resolved non-Style structure route. Export materializes one clean project-owned Master plus one Blank Layout from the current lock. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by specialized metadata; the marked element uses a stable unique `id`. See [`semantic-svg.md`](./semantic-svg.md).
**Hard rule — supported PPTX route**: The only supported generated-PPTX path is `svg_output/` through the project SVG-to-DrawingML converter. Step 7.2 still generates `svg_final/` as a mandatory self-contained visual preview that may be inserted as an SVG picture. Do not treat PowerPoint's manual Convert-to-Shape operation as an authoring target or compatibility requirement.
@@ -53,7 +58,7 @@ Before the first SVG page, output a confirmation listing: the compact communicat
- **Invalid**: fresh/resumed/restarted execution, compaction/summary-only recovery, or an external/unknown change requires one complete read of `design_spec.md`, then `spec_lock.md`, plus triggered references/template inputs. Mid-deck recovery also reads the latest completed SVG and, when images are used, current image metadata.
- **Uncertain**: consult the retained lock first, then only the owning Design Spec fragment; use sources only for facts. Design Spec remains upstream on conflict.
**On-demand page-context diagnostic**: only for explicit telemetry/debugging or an unresolved page/template/chart path-SHA question; never as a pre-page gate:
**On-demand page-context diagnostic**: only for explicit telemetry/debugging or an unresolved page/template/visualization path-SHA question; never as a pre-page gate:
```bash
python3 ${SKILL_DIR}/scripts/project_manager.py page-context <project_path> P<NN> [--record-usage]
@@ -65,7 +70,7 @@ Consume stdout directly; stop on non-zero exit. The projection is derived, not a
**Hard rule — exact page roster**: `design_spec.md §IX` is the ordered queue: one final slide per entry, with the same id/order. The UI range no longer applies. Never add, drop, merge, split, or reorder; repair/reconfirm the Design Spec first.
**Hard rule — binding selection vs realization**: use Strategist-selected semantic content, resources/paths, chart keys, template/layout routing keys, core fonts, palette anchors, icon-library/stroke anchors, and crop boundaries. Adapt realization—including which prepared project-local icon, if any, best serves each page—without changing those binding selections, except sparse local font/color garnish allowed below. A §VIII preferred image pattern is not a template/layout routing key; [`executor-image.md`](./executor-image.md) owns its realization freedom. Missing or unresolved material stops execution and returns to Strategist-owned acquisition/failure recovery; never search, generate, download, sync, invent, or substitute it. Binding selection changes require upstream repair.
**Hard rule — binding selection vs realization**: use Strategist-selected semantic content, resources/paths, Chart/Table `family/key` references, template/layout routing keys, core fonts, palette anchors, icon-library/stroke anchors, and crop boundaries. Adapt realization—including which prepared project-local icon, if any, best serves each page—without changing those binding selections, except sparse local font/color garnish allowed below. A §VIII preferred image pattern is not a template/layout routing key; [`executor-image.md`](./executor-image.md) owns its realization freedom. Missing or unresolved material stops execution and returns to Strategist-owned acquisition/failure recovery; never search, generate, download, sync, invent, or substitute it. Binding selection changes require upstream repair.
**Hard rule — content vs expression**: `design_spec.md §IX` owns each page's semantic content and supplies complete preferred wording and block texture; those expression choices are not verbatim requirements unless explicitly literal. Executor may paraphrase, condense repetition, regroup or reorder material within the same page, and switch among prose, bullets, keywords, labels, or visual annotation when fit or readability benefits. The result must remain information-equivalent: preserve the `Core message`, `Audience move`, and every substantive claim, fact, data value, proper name, qualifier or caveat, relationship, key argument or evidence, and literal requirement. Never add a claim, move content across pages, or drop information to make the layout fit; return an unfit or underspecified block for Design Spec repair.
@@ -75,6 +80,8 @@ Use named lock roles literally when that role applies, and use optional `Templat
**Per-page communication trace**: Read `communication.objective`, `communication.core_message`, and the current §IX `Core message` + `Audience move` before choosing composition. The page must advance the compact objective and move the audience as authored in §IX; the global core message remains the deck-wide north star. A page that cannot state this movement is an upstream outline defect — surface `warning: P<NN> has no communication move` instead of compensating with decorative layout. Do not invent a new purpose, ask, or outcome at execution time. Structural pages may advance the contract by establishing relevance / tension / decision frame or by completing the final commitment; they are not exempt from having a reason to exist.
**Mandatory — per-page Structure decision**: Before drawing, read the current §IX `Layout`, `Visualization`, and `Content` and decide whether geometry must carry any qualitative `order`, `link`, `parent`, `membership`, `contrast`, or `overlap` relationship. If none applies, continue on this shared base path. If any applies, load [`executor-structure.md`](./executor-structure.md) before constructing the page and retain the relationship statement in active page context; do not create a catalog reference, lock row, or new artifact. A Chart/Table reference never substitutes for this decision.
**Per-page reading-mode check**: Read `communication.consumption_mode` before choosing the page's composition. Apply it together with the authored §IX block texture and `page_rhythm`:
| `consumption_mode` | Page execution |
@@ -143,11 +150,11 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P<NN>`
- **Fact provenance**: when a §IX page lists `Fact IDs`, resolve each ID from `sources/*.facts.json` and keep the claim/value unchanged. Render a compact source footnote using the source name and a short URL/domain when space permits; when speaker notes are enabled, state the attribution naturally there too. When §IX says `Data class: scenario`, place a visible localized `Scenario data` / `情景数据` label adjacent to the affected KPI/chart and, when notes are enabled, state naturally there that the number is illustrative. Never attach an external fact ID to scenario data or let an unlabeled invented KPI look factual.
- **Default — stage each page with the style's composition geometry (may override when the content genuinely calls for a plain grid)**: an SVG page is a canvas, not a DOM. Before defaulting to stacked rounded-rect cards or uniform equal columns, pick one page-scale move from the locked visual style's §1 `Composition geometry` (a bleed shape, diagonal split, oversized numeral, orbit rings, …) to stage the page's primary zone. Card grids are one option among many, not the house layout.
- **Default — vary a planned deck motif instead of cloning it (may omit where it has no page job)**: when §III `Theme` names a cross-page motif, use the current §IX `Layout` to preserve its recognizable contour, direction, material, or relationship while varying scale, crop, density, position, and content interaction by page role. Apply it only where it supports hierarchy or continuity; do not paste identical ornament or invent a second recurring identity.
- **Inherited containers**: preserve meaningful template frames; restyle radius, fill, stroke, and depth from the active Design Spec and `spec_lock.md`. Chart-catalog adaptation is owned by [`executor-chart.md`](./executor-chart.md); preview effects never override project styling or structural roles.
- **Inherited containers**: preserve meaningful template frames; restyle radius, fill, stroke, and depth from the active Design Spec and `spec_lock.md`. Selected Chart/Table reference adaptation is owned by [`executor-visualization.md`](./executor-visualization.md); preview effects never override project styling or structural roles.
- **Reference — prefer semantic geometry over preset stacks**: for relationships such as ascending, converging, breaking through, or stacking, first seek a basic primitive, one exact preset, or a clear Boolean result. Only when none can faithfully express the relationship should one page-specific polygon/path replace a stack of generic arrows.
- **Reference — create depth with restraint**: use rhythm, spacing, typography, accent bars, and subtle tints before shadows. Reserve lift for a few genuinely floating elements; keep peer grids, dividers, and body containers flat.
- **Phased generation** (recommended):
1. **Visual Construction Phase**: generate all SVG pages sequentially for visual consistency. Use layout judgment for chart marks during the draft. **MUST embed plot-area markers** per [`executor-chart.md`](./executor-chart.md) §2.1 on every §IX-planned data-chart page — coordinate calibration is a post-generation step (see [`verify-charts`](../workflows/stages/verify-charts.md)) that depends on these markers — and **native object metadata** per [`executor-chart.md`](./executor-chart.md) §2.2 on every planned native-ready object. **Reach for native presets** per §3.0 as you draw each page: a block arrow, chevron, banner/ribbon, callout, standard flowchart node, or star is authored through `preset_shape_svg.py` at draw time — decided by the object's intent as you create it, never by scanning finished paths, and never committed to a bare `<path>`/`<polygon>` when a preset expresses it (a gradient fill/stroke or a pattern fill is the one paint exception — keep those ordinary SVG). **First-page gate (Mandatory)**: after completing the first page, run `python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage first-page --json` directly without output filtering. Review the whole P01 issue set, make one consolidated edit pass for every error and any selected warnings, then perform one verification rerun. If it still fails, treat that complete output as the next batch; never check between individual fixes. After it passes, draw P02 through the last page without checker calls.
1. **Visual Construction Phase**: generate all SVG pages sequentially for visual consistency. Apply every triggered information-model branch while drawing. **MUST embed one object-scoped plot-area marker** per §IX-named or Quick-promoted value-driven chart object under [`executor-chart.md`](./executor-chart.md) §2; coordinate calibration is a post-generation step (see [`verify-charts`](../workflows/stages/verify-charts.md)). Write every `<object-key>=yes` native marker plus JSON metadata atomically under [`native-data-interface.md`](./native-data-interface.md) §2. **Reach for native presets** per §3.0 as you draw each page: a block arrow, chevron, banner/ribbon, callout, standard flowchart node, or star is authored through `preset_shape_svg.py` at draw time — decided by the object's intent as you create it, never by scanning finished paths, and never committed to a bare `<path>`/`<polygon>` when a preset expresses it (a gradient fill/stroke or a pattern fill is the one paint exception — keep those ordinary SVG). **First-page gate (Mandatory)**: after completing the first page, run `python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage first-page --json` directly without output filtering. Review the whole P01 issue set, make one consolidated edit pass for every error and any selected warnings, then perform one verification rerun. If it still fails, treat that complete output as the next batch; never check between individual fixes. After it passes, draw P02 through the last page without checker calls.
2. **Quality Check Gate**: only after every planned SVG exists, run `python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage final --json` directly on `svg_output/` without `tail` / `head` / `grep` filtering. One run already reports all pages. Review its complete issue set, fix every `error` plus any selected advisory warnings in one consolidated edit pass, then perform one verification rerun. If it still fails, its complete output begins the next batch cycle; never use checker calls to discover or fix one next issue at a time. Every `warning` is advisory: it never sends the page back for required modification, never authorizes automatic rewriting of compatible user syntax, and needs no acknowledgement/disposition line. Recommendation warnings describe the generated-SVG default; fidelity/quality warnings may be surfaced when material, while the existing input remains releasable. Prototype-identical diagnostics are recorded as `inherited`, source conversion losses as `source-import`, changed/new advisories as `introduced`, and release failures as `blocking` in `validation/svg_quality_report.json`. If release truly depends on a condition, it belongs in `errors`. On success, use the exit status and terminal summary; do not open or `cat` the complete JSON into model context. If terminal output is truncated on failure, read only the relevant issue arrays from the report written by that same run. Do NOT defer error handling to after `finalize_svg.py` — finalize rewrites SVG and masks some violations.
3. **Logic Construction Phase (conditional)**: after SVGs pass the quality check, batch-generate speaker notes for narrative continuity only when the effective Speaker Notes outcome is enabled.
@@ -1,176 +1,120 @@
> Default Generate also loads [`executor-base.md`](./executor-base.md); Default and Quick load [`native-data-interface.md`](./native-data-interface.md) when native chart/table metadata is selected.
> Default Generate also loads [`executor-base.md`](./executor-base.md); a selected chart-family SVG is adapted through [`executor-visualization.md`](./executor-visualization.md), while independent native readiness and metadata remain exclusively in [`native-data-interface.md`](./native-data-interface.md).
# Executor Chart and Table Branch
# Executor Chart Branch
Conditional Executor authority for data charts, chart-catalog adaptations, chart verification markers, and eligible native chart/table replacement metadata.
Conditional Executor authority for value-driven SVG geometry, plot-area markers, and the [`verify-charts`](../workflows/stages/verify-charts.md) handoff.
**Trigger**: load when `design_spec.md §VII` contains a selected chart/table reference, `spec_lock.md page_charts` contains any row, the current §IX page block carries any data-encoded chart or text-grid table, or Quick Generate resolves such an object in active context. Mini charts, sparklines, inset charts, and small multiples count even when they are absent from `page_charts` or the chart catalog.
**Trigger**: load whenever source values determine visible geometry, including bar length/height, point position, arc angle, polygon vertex, connector or flow width/path, bubble center/radius, duration position, area, or another quantitative visual variable. Mini charts, sparklines, insets, and small multiples count even without a catalog reference.
**Profile authority**: Default follows the Design Spec/lock and returns missing or conflicting planning decisions upstream. Quick has no plan artifact: the current main agent resolves the data object, optional catalog key, fallback design, and native-readiness immediately in active context, writes the selected semantics into the SVG itself, and does not create a substitute roster or mapping file.
**Boundary**:
## 1. Reference Loading and Per-page Selection
For each selected `templates/charts/<key>.svg`, use its Skill-relative path and
read once before first use or after a known change, otherwise reuse it. Never
load the full catalog.
**Per-page chart reference**:
| Active profile | Selection authority |
| Information model | Route |
|---|---|
| Default Generate | Look up whether `spec_lock.md page_charts` supplies a page-local key, then read its §VII Usage and SVG for that page only |
| Quick Generate | Use bounded `chart_recall.py` only when a reusable visualization reference would help; keep the selected key and purpose in active context for that page only, or retain `no-template-match` and design from scratch |
Before drawing each page, apply the matching branch:
- Selected key present (e.g., `timeline_horizontal`) → read that SVG for the current page only and realize the active page intent without copying it or loading the catalog.
- No selected key → design the declared visualization/table from scratch; do not invent a catalog reference after drawing begins.
- Quick never persists its selection into §VII, `page_charts`, or another mapping artifact.
| Values, dates, or durations determine geometry or another visual variable | This branch |
| Qualitative order, grouping, containment, causality, or named zones determine topology | [`executor-structure.md`](./executor-structure.md) |
| A row header and column header jointly address each body fact | [`executor-table.md`](./executor-table.md) |
---
## 2. Chart and Native-data Authoring
## 1. Value-driven Geometry
### 2.1 Chart Plot-Area Marker (MANDATORY on active data-chart pages)
**Hard rule — data owns the marks**: derive every quantitative mark from the authoritative values and one explicit scale/encoding. Do not eyeball positions, preserve sample values from a catalog preview, or alter data to improve composition.
> The [`verify-charts`](../workflows/stages/verify-charts.md) stage enumerates Default data-driven chart pages from `design_spec.md §IX`; Quick runs it from the still-active page decisions and cross-checks those decisions against plot-area markers. A missing marker invokes that stage's declared fallback and adds avoidable derivation work.
Construct the chart in this order:
**Hard rule**: every Default page whose §IX `Visualization` declares
data-driven chart geometry, and every Quick page the current agent resolves as
data-driven, includes a plot-area marker inside `<g id="chartArea">`, placed
**after axis lines** and **before the first data element** (bar, line, area,
point). A legacy §VII data-chart row counts when its page block lacks that
declaration. An incidental microvisual needs no marker unless the current
profile promotes it to a coordinate-verified data object; Default repairs §IX,
while Quick resolves that decision immediately before drawing.
1. Resolve the data domain, categories, units, baseline, scale, and any radius/color/bin mapping.
2. Establish the plot frame, axes/grid or radial frame, and legend needed to decode those mappings.
3. Calculate marks from the values, including cumulative, derived, or hierarchical geometry where the chosen chart requires it.
4. Add data labels, axis/category labels, annotations, units, source notes, and visible exceptions from the active page contract.
5. Apply project typography, palette, effects, and container treatment without changing the encoding.
**Rectangular plot area** (bar / horizontal_bar / grouped_bar / stacked_bar / line / area / stacked_area / scatter / waterfall / pareto / butterfly):
**Per-object completeness**: preserve every authoritative series, category, point, label, unit, qualifier, source, and scale cue needed to read the chart. When the source cannot determine a required scale or derived value, return the ambiguity upstream in Default or resolve it from explicit source facts in Quick; never fabricate it at draw time.
**Hard rule — schedule geometry**: A schedule is a Gantt chart when dates or
durations determine each task bar's `x` and `width`, even if the source was a
PowerPoint table object. A qualitative stage × lane placement without that
mapping belongs to [`executor-structure.md`](./executor-structure.md).
**Selected reference**: when the page has a `chart/<key>` primary reference, [`executor-visualization.md`](./executor-visualization.md) owns its resolution and flexible adaptation. This branch still owns the actual value-to-geometry calculation. A chart authored from scratch follows the same geometry contract without loading a catalog SVG.
**Incidental microvisual**: draw a small value-driven trend or indicator accurately. It enters §2 and the verification handoff only when Default §IX or the Quick active-context decision promotes it to a coordinate-verified data object; do not infer that promotion from its appearance after drawing.
---
## 2. Plot-area Marker
### 2.1 Chart Plot-Area Marker (Mandatory per verified chart object)
> [`verify-charts`](../workflows/stages/verify-charts.md) enumerates Default pages from Design Spec §IX and Quick pages from the still-active authoring decisions. A missing marker invokes that stage's declared fallback and adds avoidable derivation work.
**Hard rule — object-scoped marker**: every Default chart object given a
semantic key in §IX `Visualization`, and every Quick chart object promoted for
coordinate verification, has one page-local `kebab-case` object key. Wrap that
object in `<g id="<object-key>">`; put exactly one marker inside its plot-area
group after the axes and before the first data mark. Use
`id="<object-key>-chartArea"` so several charts can coexist without duplicate
IDs. New pages prefix the marker payload with `object=<object-key> |`. A legacy
unscoped marker and `<g id="chartArea">` are accepted only when the page has
exactly one verified chart object.
**Rectangular plot area**:
```xml
<!-- chart-plot-area: x_min,y_min,x_max,y_max -->
<g id="revenue-trend">
<g id="revenue-trend-chartArea">
<!-- axes -->
<!-- chart-plot-area: object=revenue-trend | x_min,y_min,x_max,y_max -->
<!-- data marks -->
</g>
</g>
```
**Radial charts** (pie / donut / radar):
**Radial plot area**:
```xml
<!-- chart-plot-area: pie | center: cx,cy | radius: r -->
<!-- chart-plot-area: donut | center: cx,cy | outer-radius: r1 | inner-radius: r2 -->
<!-- chart-plot-area: radar | center: cx,cy | radius: r -->
<!-- chart-plot-area: object=share-pie | pie | center: cx,cy | radius: r -->
<!-- chart-plot-area: object=share-donut | donut | center: cx,cy | outer-radius: r1 | inner-radius: r2 -->
<!-- chart-plot-area: object=capability-radar | radar | center: cx,cy | radius: r -->
```
**How to determine coordinate values**:
| Value | Derivation |
|-------|------------|
| `x_min` | X coordinate of the Y-axis line (leftmost data boundary) |
| `y_min` | Y coordinate of the topmost grid line (highest data boundary) |
| `x_max` | X coordinate of the rightmost axis endpoint or grid line |
| `y_max` | Y coordinate of the X-axis baseline |
| `cx, cy` | Center point of pie/donut/radar (accounting for `transform="translate()"`) |
| `r` | Outer radius of the chart |
|---|---|
| `x_min` | X coordinate of the Y-axis line or leftmost data boundary |
| `y_min` | Y coordinate of the topmost grid line or data boundary |
| `x_max` | X coordinate of the rightmost axis endpoint or data boundary |
| `y_max` | Y coordinate of the X-axis baseline or bottom data boundary |
| `cx, cy` | Absolute center after accounting for containing translate transforms |
| `r`, `r1`, `r2` | Visible outer/inner radii used by the authored radial geometry |
**Per-page verification** — after writing each active data-chart SVG, confirm the marker exists:
Calculator-supported SVGs in `templates/charts/` carry the same comment, and
single-object previews may retain the legacy unscoped payload and
`id="chartArea"`. A qualitative structure or cell-grid table does not gain a
marker merely because it contains numbers.
### 2.2 Authoring-time Check
After writing each page containing verified charts, confirm marker count and
object ownership before continuing:
```bash
rg -n "chart-plot-area" <project_path>/svg_output/<current_page>.svg
```
> Calculator-supported data-chart templates in `templates/charts/` include this
> marker as a reference. If a data chart covered by §2.1 lacks it, that is a
> bug. Conceptual diagrams, frameworks, and other non-data visualizations in
> the same library do not use a plot-area marker.
The number of markers must equal the number of promoted chart objects, and each
marker must sit under its matching object wrapper. One marker somewhere on a
multi-chart page is insufficient.
Technical SVG/PPT constraints remain in [`shared-standards-core.md`](./shared-standards-core.md).
### 2.2 PowerPoint-Native Chart/Table Replacement Marker (MANDATORY on selected native-ready objects)
> `svg_to_pptx.py --native-charts-and-tables` replaces marked groups with PowerPoint-native Chart/Table objects (charts get an embedded Excel workbook). Markers stay dormant in the default export, whose SVG children become independently editable DrawingML shapes. Prepare this optional capability for selected independent data objects, not every numeric embellishment.
**Hard rule**: load [`native-data-interface.md`](./native-data-interface.md) for
each independent data chart or pure text-grid table selected as native-ready.
Default reads that decision from the current §IX page block (`yes`/`no`; legacy
§VII fallback only). Quick decides in active context before drawing based on
whether the PowerPoint Chart/Table object model is useful enough to accept its
normalization risk. A supported selected chart gets
`data-pptx-replace-with="chart"` plus one JSON `<metadata>` child; a selected
pure text-grid table gets the table form, transcribing all plotted data or
visible cells. An object not selected stays ordinary SVG even when a catalog
reference contains a marker. The parent marker selects the schema.
**MUST — atomic authoring**: For each native-ready object, treat the visible SVG fallback, the parent `data-pptx-replace-with` marker, and its JSON `<metadata>` child as one object. Write all three in the same SVG edit while the data is in context. Do not defer the marker or metadata to `verify-charts`, the final quality gate, or export.
**Hard rule — eligibility follows the active profile authority**: In Default, a
two-point line or small multiple gets metadata only when its §IX page block
plans it as an independent object with `Native-ready: yes`; changing eligibility
requires upstream repair. In Quick, the current agent makes that independent-
object decision before drawing and expresses it atomically through the marker,
metadata, and fallback. A sparkline, inset, KPI-card trend, or other incidental
microvisual stays ordinary SVG unless Quick deliberately promotes it before
authoring.
Generated authoring MUST omit `data-pptx-import-source` and
`data-pptx-fallback-sha256`: those attributes record imported-PPTX provenance
and its sealed fallback baseline. Never copy a static baseline from a chart
catalog or reusable template; normal content edits would make it stale.
`data-pptx-replace-with` is a **data-backed replacement claim**, not a generic label for a group that contains numbers and not a marker for ordinary PowerPoint shapes or connectors. Add it only when the matching JSON payload can be written in the same edit; if the object is meant to remain SVG geometry, do not add the marker.
- Chart types absent from that list and conceptual/diagrammatic graphics (process flows, cycles, quadrant cards, timelines, or a KPI card container) get **no marker**`svg_quality_checker.py` rejects unsupported marker types. A supported data chart nested inside one of those compositions gets its own marker only when Default §IX or the Quick active-context decision selects that object as native-ready.
- Canonical rectangular merged text cells may carry a table marker by putting anchor-only `row_span` / `col_span` in metadata and leaving covered cells blank. Nonrectangular/overlapping merges, nonblank covered cells, and graphical cells (icons, harvey balls, rating dots) get **no table marker** and stay on the SVG fallback route.
- Transcribe, don't restyle: `categories` / `series[].values` are the numbers just plotted; `style.colors` copies the series HEX values already rendered on the page, whether they use a recurring Default `spec_lock.colors` anchor or a Quick/contextual page-local color.
- Data-point color: when a single column/bar series uses data-point colors in the fallback, copy those fills into `series[].point_colors` in category order.
- Data labels: when visible point values are part of the fallback chart, write `data_labels` instead of companion text; use `data_labels.points` for selected labels, and use `number_format`, `font_size`, `font_family`, and per-point `colors` / `color` when the fallback labels carry suffixes or color-coded text.
- Line markers: when the fallback line chart draws visible point nodes, set `line_style: "lineMarker"`; leave the default `line` only for line charts without nodes.
- Area-under-line: when a combo plot is drawn as a filled area under a line, keep `type: "line"`, add `area_fill: true`, and copy the area transparency into `series[].fill_opacity`; copy visible line `stroke-width` into `series[].line_width` for line/area series.
- Native chrome: write `title`, `subtitle`, axis titles, or `show_legend: true` only when the fallback visibly renders the same chrome inside the native chart's replacement scope. `title` is the PowerPoint chart title, not an object name; use `name` for page-semantic object naming (e.g. `p03-revenue-chart`). Write explicit `x`/`y`/`width`/`height` read from the drawn plot area; omission is the fallback — the exporter then infers the frame from the drawn fallback geometry.
- Value-axis labels: when the fallback keeps category labels but intentionally omits numeric value-axis tick labels, set `show_value_axis_labels: false`.
- Freeform chart text: transcribe center labels, source notes, and other in-chart annotations as companion `caption` / `note` / `notes` entries with explicit slide-coordinate bounds; do not rely on fallback `<text>` children to survive native export.
- Native chart typography mirrors the SVG fallback. Copy the fallback's shared chart font into `style.font_family` and visible chart text sizes into the matching metadata fields (`title_font_size`, `subtitle_font_size`, `axis_font_size`, `note_font_size`, etc.) only when role sizes differ; otherwise let the exporter infer them from visible fallback text. When a visible chart title, subtitle, or axis title needs its own size/color/font, write that field as an object with `text`, `font_size`, `font_family`, and `color`. Use `axis_title_font_size`, `legend_font_size`, or companion per-entry `font_size` only when the fallback visibly uses a separate size.
- Native table typography mirrors the SVG fallback. Write `style.font_family` and `style.font_size` from the visible table text; use `header_font_size` or per-cell `font_size` only when the fallback visibly does so. If the fallback has no explicit table font, Default uses the deck body family and declared `spec_lock.md` body anchor; Quick uses the concrete body family/size already resolved in active context.
- The marker group's transform stays translate/scale only (no rotate / matrix / skew).
- Visual parity is not a goal: the SVG drawing remains the designed visual and exports as editable DrawingML shapes; the native object is the data-backed counterpart with PowerPoint's chart/table-specific model. Never simplify the SVG design to match what a native object could show.
**Per-page verification** — after writing a page with selected native-ready objects, enumerate those objects and confirm a one-to-one match: every object has one parent marker and exactly one JSON metadata child. Finding one marker somewhere on a page is insufficient when the page contains multiple selected objects.
```bash
rg -n 'data-pptx-replace-with="(chart|table)"|<metadata type="application/json">' <project_path>/svg_output/<current_page>.svg
```
---
## 3. Visualization Reference
## 3. Verification Handoff
§1 loads only a selected page-local SVG. Default takes Usage/semantics from
§VII/§IX; Quick takes them from the current active-context page decision.
Coordinate calibration is a conditional post-generation stage, not part of the page-authoring loop. After all SVG pages exist, run [`verify-charts`](../workflows/stages/verify-charts.md) whenever the active profile declares at least one page with value-driven chart geometry.
**Hard rule**: treat the loaded SVG as a page-local reference, not a required base. Default §IX or the Quick active-context page decision plus source data own the final information structure; never replicate the preview verbatim.
| Active profile | Verification page list |
|---|---|
| Default Generate | Design Spec §IX, with the stage's explicit legacy §VII fallback |
| Quick Generate | Still-active page decisions cross-checked one-for-one against plot-area markers |
**Adaptation rules**:
- **Preserve**: Default-planned or Quick-resolved information relationships, data encoding, and every active content obligation
- **Page-local only**: a reference row applies only to its mapped page; never spread it across the deck
- **Flexible realization**: borrow, recombine, or depart from the preview's type and geometry when the current page is better served another way
- **Carry forward**: every authoritative label, value, unit, status, source, and explanatory block; never shorten or drop content to imitate a lighter catalog preview
- **Adapt**: project data and labels, dimensions, axes, legend, and spacing as the authored content requires
- **Project-owned**: palette, typography, container treatment, effects, background, and page chrome; catalog preview values are fallbacks, never defaults
- **Bound final body modules**: add or revise root-coordinate `data-pptx-bounds` on every visible direct root `<g>` copied into the final page; nested groups need none, chart geometry and local references are not content-boundary inputs, and catalog reference warnings never waive the final-page contract
- **Adjust with fidelity**: composition, axis ranges, grouping, and grid may change when the actual content, relationships, hierarchy, and data encoding remain complete
- **Forbidden**: treating preview structure as the page specification; omitting authoritative data points, labels, relationships, or explanatory content to fit it
> Templates: `templates/charts/`. `page_charts` maps one optional reference to one page; execution opens only that SVG.
### 3.1 Chart Coordinate Calibration
Coordinate calibration runs as a **conditional post-generation stage**, not
inside the SVG authoring loop. After SVG generation completes, if the deck
contains data-driven charts, run
[`verify-charts`](../workflows/stages/verify-charts.md). Default follows its
declared gate order; Quick runs calibration before its one lockless final
checker.
The authoring obligation is upstream: embed the
`<!-- chart-plot-area ... -->` marker on every active data-chart page during the
initial draft (§2.1). Verify-charts enumerates Default pages from the Design
Spec; Quick enumerates them from the still-active page decisions and cross-checks
the markers before feeding `svg_position_calculator.py`.
> Do NOT run `svg_position_calculator.py` during the initial draft. The calculator calibrates already-generated SVGs against their declared plot areas; running it before the SVG exists has nothing to compare against.
Do not run `svg_position_calculator.py` during the initial draft. The stage calibrates completed SVG geometry against the declared plot area, handles direct/decomposable/formula/manual modes, repairs genuine coordinate mismatches, and then returns to the active profile's checker order.
@@ -30,21 +30,40 @@ Handle images by status; enum and lifecycle: [`svg-image-embedding.md`](svg-imag
**Default — active image integration (may override when plain placement is
stronger)**: Treat loaded [`image-layout-patterns.md`](./image-layout-patterns.md)
as vocabulary and [`image-layout-spec.md`](./image-layout-spec.md) as math, not
a quota or lock. Run the catalog's §7 playbook before drawing; its combinations
are recall aids, not coverage targets. A `#P...` suggestion completes only the
page skeleton; omitted `M`, effect, Boolean, or native overlay leaves
realization open and never means “keep plain.” For every image-bearing page,
derive the image/content or image/shape relationship from its communication job,
hierarchy, copy, asset ratio/focus, and deck rhythm. Before drawing, form one
relevant treatment candidate, compare it with `P`-only, implement the stronger
legal composition, and realize any adopted treatment through the already-loaded
[`svg-effects.md`](./svg-effects.md) and
[`native-shape-authoring.md`](./native-shape-authoring.md). This is an opportunity
pass, not an effect quota. Deepen, simplify, replace, or combine suggestions;
plain placement remains valid when it communicates better. Preserve role/source,
a quota or lock. A `#P...` suggestion is only a skeleton; deepen, simplify, or
combine it through the already-loaded [`svg-effects.md`](./svg-effects.md) and
[`native-shape-authoring.md`](./native-shape-authoring.md). Plain placement or a
regular grid remains valid when it communicates better. Preserve role/source,
must-use, crop/content, and explicit user/template constraints; expression-only
changes need no upstream rewrite.
**Mandatory — per-page image composition decision**: Using this already-loaded
branch, decide once immediately before geometry on every page containing a
non-formula image and keep the result only in active context:
`role → direction generator → parent contour → slots/rhythm → crop →
image-shape action → labels/overlays → depth/continuity`. Derive the relationship
from the communication job, hierarchy, copy, asset ratio/focus, and deck rhythm;
use `anchor`, `continue`, `bridge`, `overlap`, `reveal`, `echo`, or `register`
only when useful. Compare the resulting candidate with plain/`P`-only placement
and implement the stronger legal composition. Create no artifact or extra pass,
and do not reread the branch while its active context remains valid.
**Reference — direction generators, not templates**:
| Generator | Behavior |
|---|---|
| `vector` | Progress slots along one shared movement vector |
| `shared-baseline` | Share a baseline while size, offset, or overlap changes systematically |
| `curve-spine` | Derive slots and turns from one continuous curve or folded path |
| `panel` | Subdivide one coherent tilted, stepped, or polygonal parent panel |
| `none/grid` | Retain calm plain placement or a regular grid |
**Default — coherent multi-image direction (may override when deliberate disorder
serves the communication job)**: Within one image group, derive slot positions,
crop edges, overlap, and any rotation from one primary direction generator.
Varied angles need one declared rhythm or collision rule; they are not a shortcut
to variety.
**Reference — motion-ready image layering, not a constraint**: For adopted §IX or an explicit focus, comparison, evidence, reveal-order, or cross-page requirement, decide during SVG authoring whether the final composition needs separate visible units. Keep ordinary stable framing/background static and wrap each independently revealed or continuing Slide-local unit in a descriptive direct-root `<g id>`; structured atoms/slots retain their boundaries. Existing units or a page transition may suffice. The motion stage owns effects, pairing, order, and timing.
**Hard rule — visible-layer timing**: Any crop, lens, scrim, comparison, evidence, or annotation layer required by an adopted motion plan MUST already exist in the final SVG without violating structural contracts. The later stage may regroup ordinary Slide-local content visual-equivalently, but cannot invent or modify missing visible content. If no legal existing unit can serve a non-binding suggestion, simplify it to available units, a page transition, or `none`; an explicit requirement that cannot be represented follows failure recovery.
@@ -0,0 +1,97 @@
> See [`executor-base.md`](./executor-base.md) for shared authoring and [`executor-chart.md`](./executor-chart.md) / [`executor-table.md`](./executor-table.md) for other information models.
# Executor Shape Composition Grammar
Runtime grammar for Slide-local qualitative relationships built from editable shapes; it is not a diagram/template catalog.
**Load**: Quick reads this grammar once before all SVG authoring and reuses it for every per-page decision. Default loads it before realizing the first page whose Structure decision is yes, then reuses it in that execution context.
**Hard rule — no Structure catalog**: never recall or resolve `structure/<key>`. Compose from authoritative content, §IX relationships, the communication move, and the active visual system.
**Hard rule — not structured PPTX**: this branch owns Slide-local geometry. [`executor-structured.md`](./executor-structured.md) independently owns reusable Master/Layout/placeholders under `pptx_structure.mode: structured`; neither implies the other.
---
## 1. Relationship Atoms
| Atom | Meaning | Encode with |
|---|---|---|
| `order` | Sequence, progression, or rank | Position, numbering, direction, shared path |
| `link` | Dependency, exchange, influence, transition | Proximity/alignment when unmistakable; otherwise an edge |
| `parent` | One unit governs or decomposes into children | Branching, indentation, nesting, scale |
| `membership` | Units belong to a group, stage, lane, or region | Containment, shared field, band, repetition |
| `contrast` | Peers, states, options, or positions compare | Shared baseline, opposing regions, parallel framing |
| `overlap` | Units share a meaningful subset or duty | Intersecting regions plus a clear common area |
Combine atoms as needed; never force a named business model. Numbers used only as labels do not create a chart. Value-derived position/length/angle/area/radius/width/color routes to [`executor-chart.md`](./executor-chart.md); row-header × column-header facts route to [`executor-table.md`](./executor-table.md). Qualitative lanes use this grammar, but date/duration-driven task-bar `x`/`width` is Gantt Chart geometry.
---
## 2. Shape Roles and Operations
| Role | Job |
|---|---|
| `field` | Page/local region where a relationship operates |
| `node` | Semantic unit, state, actor, item, or group |
| `spine` | Explicit/implied scaffold establishing reading direction |
| `edge` | Necessary connection, branch, dependency, or transition |
| `label` | Text attached to a node, edge, region, or relationship |
| `garnish` | Non-semantic accent added after the relationship works |
| Operation | Job |
|---|---|
| `repeat` | Create peers from one role |
| `arrange` | Establish order, alignment, rhythm, rank, comparison |
| `transform` | Vary scale, rotation, crop, fill, or emphasis meaningfully |
| `connect` | Add an edge when layout/containment is insufficient |
| `region` | Partition, contain, intersect, band, or layer fields |
| `attach` | Bind labels, badges, annotations, or evidence to an owner |
**Hard rule — realization enters the construction gate**: Decide whether each
role is implicit/direct content or drawn geometry. Every drawn field, spine,
node carrier, or edge uses the first faithful tier under
[`native-shape-authoring.md`](./native-shape-authoring.md) §1: primitive → exact
preset → Boolean → necessary freeform. Text styling/rules cannot replace
required geometry; implicit/direct roles need no container. Decoration cannot
invent a relationship.
---
## 3. Construction Order
Choose the field and map required atoms, then follow:
**Mandatory — spine → nodes → connectors → labels → garnish**:
| Layer | Completion evidence |
|---|---|
| `spine` | Entry, direction, and organizing path are clear |
| `nodes` | Every required unit has one home and intentional weight; each is direct content or a §2-approved carrier |
| `connectors` (`edge`) | Only unresolved links become edges; route/source/target is clear; every drawn edge passes §2 |
| `labels` | Copy and caveats visibly attach to what they explain |
| `garnish` | Removing accents leaves all meaning intact |
**Hard rule — relationship before styling**: establish atoms, field, spine, nodes, and necessary edges before palette, type, effects, or containers. Prefer containment, alignment, baselines, and proximity; add lines/Connectors only for real edges, never to make a page look process-like.
**Default — visible structural move (may override for naked-text rhythm/style)**:
Make one relationship-bearing field, spine, node carrier, or directional shape
the page-scale move; never add geometry merely because Structure is `yes`.
> Note: spatial encoding may remove the need for a new object; any drawn node
> carrier, spine, or edge still passes §2.
---
## 4. Validation
| Check | Pass condition |
|---|---|
| Coverage | Every authoritative atom is visible; none was invented |
| Reading path | Entry, progression, hierarchy, and endpoint are unambiguous |
| Roles | Nodes/edges have one duty; garnish carries no meaning |
| Attachment | Labels/evidence belong to the correct node, edge, or region |
| Removal | Without color/effects/icons/garnish, placement still communicates |
| Fidelity | All required units, qualifiers, values, and caveats remain |
| Construction | Drawn fields/spines/node carriers/edges pass §2; implicit/direct roles need no carrier; freeform follows failed primitive/preset/Boolean tiers |
Load Chart/Table branches independently for embedded objects. Keep one dominant reading path while allowing secondary atoms whose ownership stays clear.
@@ -6,6 +6,8 @@ Conditional Executor authority for `template_reuse_scope: mirror|layout` with `p
**Trigger**: load only when the lock selects structured template reuse.
**Hard rule — package structure, not information structure**: this branch owns reusable PowerPoint Master/Layout atoms, placeholders, slots, and prototype topology. [`executor-structure.md`](./executor-structure.md) independently owns Slide-local qualitative shape composition. Neither one activates or implies the other.
## 1. Template Reuse Rules
### 1.0 Template Context Load
@@ -60,7 +62,7 @@ When `spec_lock.md` records the AI-derived `template_reuse_scope: mirror`, Execu
**Detecting mirror mode**: read `template_reuse_scope` from the retained lock. `replication_mode: mirror` in the installed template only determines whether that scope is legal; it must never force mirror behavior when the lock records `layout` or `style`.
**Mirror + chart pages**: chart structures inside a mirror SVG are already drawn (axis, series, labels). Treat them as visual references — replace the data labels and series text content to match the project's chart spec, but do not redraw the chart from a `templates/charts/<name>.svg` baseline. A mirror template's `page_charts` entries are normally absent for this reason.
**Mirror + visualization pages**: Chart, Table, and qualitative topology inside a mirror SVG are already drawn. Replace only permitted text while preserving prototype geometry; do not redraw from a catalog SVG or runtime grammar. A mirror template normally omits `page_visualizations`, and legacy `page_charts` never overrides fidelity.
**Legacy template boundary**: A template with missing root Master identity, direct atomic placeholders, `data-pptx-layout-kind`, unmapped `baseline`, `preserve`, or `layout_strategy: distill` is not a fallback input. Stop and create a new current workspace through [`create-template`](../workflows/create-template.md) before generation.
@@ -0,0 +1,65 @@
> See [`executor-base.md`](./executor-base.md) for page authoring and [`executor-visualization.md`](./executor-visualization.md) when a table-family SVG is selected.
# Executor Table Branch
Conditional Executor authority for semantic cell grids whose row/column intersections carry the information.
**Trigger**: load when the page contains an actual cell-grid table or its primary reference uses `table/<key>`.
---
## 1. Cell-grid Boundary
| Information model | Route |
|---|---|
| A row header and column header jointly address each body fact; summaries and optional rectangular spans preserve the same grid | This branch |
| Independent visual zones compare categories without a shared row/column grid | [`executor-structure.md`](./executor-structure.md) |
| Values determine marks, positions, lengths, areas, angles, radii, or color bins | [`executor-chart.md`](./executor-chart.md) |
Graphical indicators may appear inside cells without changing the table family, provided the row/column grid still carries their meaning. A row of metric cards or two prose columns is a qualitative structure, not a table.
**Hard rule — physical grid is insufficient**: A PowerPoint table object or a
rectangular drawing grid does not establish Table semantics. Exact dates or
durations that drive task-bar position and length route to
[`executor-chart.md`](./executor-chart.md); qualitative stage/lane placement
routes to [`executor-structure.md`](./executor-structure.md).
When a `table/<key>` primary reference exists, [`executor-visualization.md`](./executor-visualization.md) owns resolution and flexible adaptation. A custom cell grid follows this branch without loading a catalog SVG.
**Reference — not a constraint**: `record_table` covers heterogeneous record ×
field grids; `metric_table` covers entity × KPI scanning;
`comparison_matrix` covers heterogeneous criterion × alternative facts;
`feature_matrix` covers capability states; `rating_matrix` covers one repeated
ordinal scale; and `hierarchical_table` covers grouped/indented rows with detail
and totals. These keys separate recurring cell grammars without changing the
shared row/column information-model boundary.
---
## 2. Grid Construction
**Hard rule — grid before decoration**: establish the complete logical grid before drawing fills, borders, badges, or other cell treatment.
1. Resolve column count, row count, header rows, row labels, summaries, and any rectangular visual spans from the authoritative content.
2. Allocate column widths and row heights from semantic weight and real text/data fit; do not default to equal columns when labels or values differ materially.
3. Place every cell value, unit, qualifier, status, and source-bearing note in its correct intersection.
4. Apply alignment consistently by content role, including comparable numeric alignment and stable header/body hierarchy.
5. Add rules, fills, banding, highlights, and in-cell indicators only after the grid reads correctly in plain form.
**Per-cell completeness**: never drop a row, column, summary, footnote, unit, or qualifier to imitate a lighter catalog preview. Reflow text, widen the affected column, rebalance adjacent columns, or increase row height while preserving the active page's information contract and [`executor-base.md`](./executor-base.md) typography bounds.
**Visual span discipline**: merge only rectangular regions whose repeated boundaries would obscure an intended shared heading or group. Covered areas must not carry competing visible content. This branch owns the visible SVG geometry only; any native merge fields or payload topology belong exclusively to [`native-data-interface.md`](./native-data-interface.md).
**Table chrome**: make header/body/summary roles distinguishable with the lightest sufficient combination of weight, fill, rule, and whitespace. Keep comparison scanning stable across the grid; decorative card treatment must not break row or column continuity.
---
## 3. Object Boundary
Treat each semantic table as one independently bounded page object even when it contains nested cell groups. Captions, source notes, and explanatory callouts may sit outside the grid when their ownership is visually explicit.
Native readiness is decided per independent table object, not by the `table`
family or numeric cells. Reuse its §IX/Quick semantic object key in the
`Native-ready` map; only `<object-key>=yes` loads and follows
[`native-data-interface.md`](./native-data-interface.md). All others remain
ordinary Shape-first SVG geometry under [`executor-base.md`](./executor-base.md).
@@ -0,0 +1,57 @@
> See [`executor-base.md`](./executor-base.md) for page authoring and the Chart/Table branches for information-model construction.
# Executor Visualization Reference Branch
Conditional Executor authority for resolving one page-local Chart/Table `family/key` SVG reference and adapting it without turning the catalog preview into a page specification.
**Trigger**: load only when Default `spec_lock.md page_visualizations` maps the current page to a canonical Chart/Table reference, a legacy `page_charts` row resolves to a live Chart/Table SVG, or Quick already selected one canonical Chart/Table reference in active context.
---
## 1. Canonical Reference Resolution
| Family | Canonical reference | SVG root | Construction authority |
|---|---|---|---|
| `chart` | `chart/<key>` | `templates/charts/<key>.svg` | [`executor-chart.md`](./executor-chart.md) |
| `table` | `table/<key>` | `templates/tables/<key>.svg` | [`executor-table.md`](./executor-table.md) |
| Active profile | Resolve from |
|---|---|
| Default Generate | Prefer the current `P<NN>: family/key` row from retained `spec_lock.md page_visualizations`, then read that page's `Page | Family | Template | Usage` row in Design Spec §VII; use a legacy `page_charts` row and its legacy §VII Usage only when the canonical row is absent |
| Quick Generate | Use the canonical Chart/Table `family/key` and page-local purpose already selected in active context before SVG authoring |
**Hard rule — one primary reference per page**: one page resolves at most one catalog SVG. The reference guides one dominant reusable Chart/Table information structure; secondary objects are authored from their actual content through the applicable branch without loading another catalog SVG. Independent Chart/Table children retain their §IX or Quick semantic object keys for scoped native/verification contracts.
**Mandatory — shared resolution**: resolve the selected value through `visualization_recall.py validate`; consume its canonical `reference` and `path` instead of guessing a family or constructing a path from the input string. Add `--legacy-bare` only for a value read from legacy `page_charts`.
```bash
python3 ${SKILL_DIR}/scripts/visualization_recall.py validate <family/key>
python3 ${SKILL_DIR}/scripts/visualization_recall.py validate \
--legacy-bare <legacy-key>
```
New `page_visualizations` and Quick selections accept only canonical `chart/<key>` or `table/<key>`. A bare key is read-compatible only from legacy `page_charts`: it must resolve unambiguously to one live Chart/Table entry, otherwise stop for upstream correction. If canonical and legacy rows both exist for one page, stop on the duplicate contract even when both resolve to the same SVG.
**Legacy Structure boundary**: a retired Structure bare key is semantic intent, not a live visualization reference. Do not resolve it to an SVG or load this branch; recover the qualitative relationship from §IX and apply [`executor-structure.md`](./executor-structure.md) when the mandatory per-page Structure decision is yes. If §IX lacks enough meaning, return upstream for Design Spec repair.
Read the resolver-returned SVG once before its first use in the valid active context and reuse that reading until a known file change or context invalidation. Do not manually open indexes or scan family directories during Executor realization; the shared resolver owns live-catalog reads. Selection and bounded recall belong before this branch.
---
## 2. Flexible Page-local Adaptation
**Hard rule — reference, not lock**: the selected SVG is a page-local construction reference. The current §IX page block or Quick page decision plus authoritative source content owns the final information structure; the preview does not lock visualization type, geometry, styling, or native replacement.
| Preserve | Adapt freely |
|---|---|
| Authoritative labels, values, units, statuses, sources, relationships, hierarchy, and explanatory content | Dimensions, spacing, axes, grouping, orientation, density, and exact primitive/preset composition |
| Selected Usage and valid information encoding | Borrow, recombine, simplify, extend, or depart when another realization preserves the information more faithfully |
| Complete page content obligations | Palette, typography, container treatment, effects, background, and page chrome from project authorities |
**Forbidden — preview substitution**:
- Do not copy sample labels/data as content.
- Do not omit authoritative content to fit lighter preview density.
- Do not spread one page's reference to another page without its own mapping.
The namespace selects a reference registry and construction authority only; it does not assert native readiness or mirror a source PowerPoint object type. An imported table used to place duration-driven bars remains Chart semantics. Native eligibility is an independent per-object decision owned by [`native-data-interface.md`](./native-data-interface.md).
@@ -149,7 +149,56 @@ Use weighted tracks when one item is primary. A spanning item receives the sum o
### 4.3 Free multi-item composition
For montage, arc, overlap, or another non-grid arrangement, assign one explicit region to every item and verify the union against `(W,H)`. Reuse one gap/rhythm system where separation is intended; overlap is explicit geometry, not a negative-gap accident.
**Mandatory**: For montage, arc, overlap, or another non-grid arrangement, give
every carrier a finite center and positive size, give every intended overlap an
unambiguous front item, and verify the visible union against `(W,H)`.
**Default — shared direction (may override when deliberate disorder serves the
communication job)**: Select one direction generator and derive related carriers
from shared geometry. An override still declares a bounded placement/angle rule
so disorder is authored rather than accidental. Use the following state:
| State | Definition |
|---|---|
| `p[i] = (cx[i], cy[i])` | Explicit center of item `i` |
| `(w[i], h[i])` | Explicit positive carrier size |
| `s[i] > 0` | Optional shared size rhythm: `(w[i], h[i]) = s[i] × (w0, h0)` |
| `P` (optional) | Parent contour controlling the outer silhouette, shared seam, reveal, or attachment path |
| `V[i]` | Visible carrier after applying its clip and, when `P` is a silhouette, intersecting it with `P` |
| `z[i]` | Stacking rank required for carriers that overlap |
For a straight shared direction `θ`, calculate centers in its local frame:
```text
d = (cos(θ), sin(θ))
n = (-sin(θ), cos(θ))
p[i] = p0 + t[i] × d + e[i] × n
```
Choose `t[i]` and transverse offset `e[i]` as explicit sequences; add `s[i]`
when scale carries the rhythm. Reuse a progression when rhythm is intended; do
not substitute unrelated per-item values.
| Generator | Executable rule |
|---|---|
| `vector` | Use the straight-frame equation with ordered `t[i]`; set `t[i+1] = t[i] + advance[i]`, where each `advance[i] > 0`. Control overlap through `advance[i]`, not an accidental negative gap. |
| `shared-baseline` | Choose baseline `B(t) = b + t × d`. If `r[i]` is the carrier's half-extent along `n`, place `p[i] = B(t[i]) + r[i] × n`; this keeps one carrier edge on the same baseline while sizes vary. |
| `curve-spine` | Choose ordered parameters `u[i]` on `C(u)` with `||C'(u[i])|| > 0`. Set `d[i] = normalize(C'(u[i]))`, `n[i] = (-d[i].y, d[i].x)`, and `p[i] = C(u[i]) + e[i] × n[i]`; at a zero derivative, use the secant between nearest distinct samples or another generator. |
| `panel` | Define one convex 2D quadrilateral `A,B,C,D` in consistent winding and `F(u,v) = (1-u)(1-v)A + u(1-v)B + uvC + (1-u)vD`. Split monotone `u`/`v` intervals; each cell uses its four `F` corners. |
For each intended overlap, calculate `area(V[i] ∩ V[j]) > 0` and assign the
front item through `z[i]` and `z[j]`. `P` must visibly control at least one
listed structural role; otherwise use the selected region boundary and omit it.
**Reference — not a constraint**: Use one of these angle mechanisms according to the selected composition:
| Mechanism | Geometry |
|---|---|
| Clip-shape angle | Keep the bitmap upright and angle only the carrier contour or clip path. |
| Parent-group rotation | Build the complete arrangement first, then rotate its carriers, frames, and attached labels together around one pivot. |
| Tangent rotation | On `curve-spine`, rotate item `i` around `p[i]` by `atan2(d[i].y, d[i].x)` when the carriers should follow the path. |
**Forbidden — unsupported image deformation**: Do not use shear, skew, or true perspective mapping. A `panel` is a set of 2D quadrilateral clips/crops; it does not warp the image plane.
---
@@ -178,6 +227,11 @@ Contain each formula independently in its lane. When formulas are visual peers,
| Adjacent text/content region cannot carry its material | Reweight or change the selected relationship |
| Equal cells imply equality that the content does not have | Use weighted tracks or a free composition |
| Peer images use inconsistent visual scale without meaning | Normalize their regions or make the hierarchy explicit |
| A free carrier lacks an explicit center or positive size, or an intended overlap lacks stacking order | Supply the missing geometry or z-order before drawing |
| Items intended as one system lack both a shared direction and a deliberate-disorder rule | Derive them from one vector, baseline, curve, panel, or bounded override |
| Per-item angles vary without a shared direction or deliberate-disorder rule | Use one parent-group angle, a shared clip-shape direction, curve tangents, or a bounded angle rhythm |
| The parent contour does not affect silhouette, seam, reveal, or attachment | Remove it or reconstruct the carriers from that contour |
| A panel depends on shear, skew, or perspective warping | Replace it with 2D quadrilateral carriers and focal-safe crops |
| Formula symbols become unreadable at the intended viewing size | Enlarge its region or restructure the page |
| Gaps, alignments, or overlaps drift without purpose | Recalculate from the shared region and gap values |
@@ -24,11 +24,11 @@ Each type has its own file with: composition skeleton (LAYOUT / ELEMENTS / NEGAT
|---|---|---|
| [`infographic`](./infographic.md) | 2-5 parallel ordered zones with icons + minimal labels | Data summary / step list / KPI rundown |
| [`flowchart`](./flowchart.md) | Sequential blocks connected by directional arrows | Process / workflow / pipeline |
| [`framework`](./framework.md) | Central node + radiating satellites (hub-spoke) | Methodology / model / system architecture |
| [`matrix`](./matrix.md) | 2×2 quadrant grid with two perpendicular axes | SWOT / BCG / Eisenhower / Ansoff / Porter |
| [`cycle`](./cycle.md) | Closed loop, 3-6 steps with arrows returning to start | PDCA / flywheel / design thinking / continuous improvement |
| [`framework`](./framework.md) | Central node + radiating satellites (hub-spoke) | Relational system / architecture |
| [`matrix`](./matrix.md) | 2×2 quadrant grid with two perpendicular axes | Priority / risk / effort-impact regions |
| [`cycle`](./cycle.md) | Closed loop, 3-6 steps with arrows returning to start | Iteration / lifecycle / continuous improvement |
| [`funnel`](./funnel.md) | Top-wide bottom-narrow conversion stack | Marketing funnel / sales pipeline / hiring funnel |
| [`pyramid`](./pyramid.md) | Bottom-wide top-narrow hierarchical tiers | Maslow / capability stack / value hierarchy |
| [`pyramid`](./pyramid.md) | Bottom-wide top-narrow hierarchical tiers | Capability stack / value hierarchy |
| [`comparison`](./comparison.md) | Symmetric split (left vs right, before vs after) | A/B / pros-cons / Before-After |
| [`timeline`](./timeline.md) | Linear axis with milestone markers | History / roadmap / evolution |
| [`map`](./map.md) | Stylized geographic outline with annotated markers | Offices / market presence / regional data / supply chain |
@@ -44,11 +44,11 @@ Each type has its own file with: composition skeleton (LAYOUT / ELEMENTS / NEGAT
|---|---|
| Data summary / metrics rundown / step list | `infographic` |
| Process / workflow / pipeline / steps with arrows | `flowchart` |
| Methodology / model / framework / architecture diagram | `framework` |
| 2×2 quadrant / SWOT / BCG / Eisenhower / Ansoff | `matrix` |
| Closed-loop process / PDCA / flywheel / continuous improvement | `cycle` |
| Relational system / framework / architecture diagram | `framework` |
| 2×2 quadrant / priority / risk / effort-impact | `matrix` |
| Closed-loop process / iteration / lifecycle / continuous improvement | `cycle` |
| Conversion funnel / sales pipeline / hiring funnel | `funnel` |
| Hierarchy / Maslow / value stack / capability layer | `pyramid` |
| Hierarchy / value stack / capability layer | `pyramid` |
| Comparison / Before-After / A/B / VS | `comparison` |
| History / evolution / roadmap / timeline | `timeline` |
| Offices / market presence / regions / supply chain / geography | `map` |
@@ -1,6 +1,6 @@
# Type: cycle
A **closed-loop process** — 3-6 steps arranged in a circle (or rounded loop), connected by directional arrows that return the flow to its starting point. The structural backbone for PDCA, growth flywheel, design-thinking cycle, continuous-improvement loops, lifecycle diagrams.
A **closed-loop process** — 3-6 steps arranged in a circle (or rounded loop), connected by directional arrows that return the flow to its starting point. Use it for iterative processes, continuous-improvement loops, or lifecycle diagrams.
> **What cycle means inside a PPT block**: a **closed circular flow** — every step leads to the next, and the last step leads back to the first. Unlike `flowchart` (linear, one-way), cycle has no terminal end. Unlike `framework` (centered hub with passive satellites), cycle has directional motion around the perimeter.
@@ -45,6 +45,6 @@ Self-contained cycle diagram with step names typeset into the artwork. Keep each
## 3. Fewshot prompt snippets
**Snippet A — vector-illustration + cool-corporate PDCA cycle, text_policy: none, 700×700**
**Snippet A — vector-illustration + cool-corporate four-stage cycle, text_policy: none, 700×700**
> Clean flat vector illustration of a closed-loop process cycle. Four step nodes arranged in a perfect circle on the canvas perimeter — at 12 o'clock, 3 o'clock, 6 o'clock, and 9 o'clock positions. Each node is a rounded square (about 130×130px equivalent) filled with primary deep navy `#1E3A5F`, with one simple white iconic symbol centered inside — a clipboard (plan), a hand (do), a magnifying glass (check), a gear (act). Curved directional arrows in accent gold `#D4AF37` connect each node to the next going clockwise, closing the loop. The center of the circle is calm — secondary light gray `#F8F9FA` field with one small accent gold dot at the exact center as the cycle's anchor. Crisp 2px outlines, soft 8% drop shadow under each node. Composed as a 700×700 half-page cycle block with 15% padding. NO text, letters, or step labels anywhere — SVG will overlay all step names. Color values are rendering guidance only.
> Clean flat vector illustration of a closed-loop process cycle. Four step nodes arranged in a perfect circle on the canvas perimeter — at 12 o'clock, 3 o'clock, 6 o'clock, and 9 o'clock positions. Each node is a rounded square (about 130×130px equivalent) filled with primary deep navy `#1E3A5F`, with one simple white iconic symbol centered inside — a compass, a hand, a magnifying glass, and a gear. Curved directional arrows in accent gold `#D4AF37` connect each node to the next going clockwise, closing the loop. The center of the circle is calm — secondary light gray `#F8F9FA` field with one small accent gold dot at the exact center as the cycle's anchor. Crisp 2px outlines, soft 8% drop shadow under each node. Composed as a 700×700 half-page cycle block with 15% padding. NO text, letters, or step labels anywhere — SVG will overlay all step names. Color values are rendering guidance only.
@@ -1,6 +1,6 @@
# Type: framework
A central concept surrounded by related sub-concepts, or a multi-component system shown as a relational structure. Used when the image's job is to make a methodology, model, or architecture **legible at a glance**.
A central concept surrounded by related sub-concepts, or a multi-component system shown as a relational structure. Use it when the image's job is to make a relational system or architecture **legible at a glance**.
> **What framework means inside a PPT block**: the image *itself* is a small relational diagram — central hub + radiating satellites, or a labeled matrix, or a layered stack. The PPT page's slide layout (where this image sits, what text accompanies it) is a separate decision. This file only governs what's *inside* the image rectangle.
@@ -34,7 +34,7 @@ Three valid sub-structures. Pick one per image; do not mix.
```
| LAYOUT | Equal-sized cells in a grid; each cell carries one concept |
| ELEMENTS | One icon or symbolic shape per cell; cells are visually equal (no cell dominates unless that's the point — e.g. SWOT, BCG matrices) |
| ELEMENTS | One icon or symbolic shape per cell; cells are visually equal unless one region is intentionally emphasized |
| NEGATIVE SPACE | Generous cell padding — internal content occupies the inner 70% of each cell |
### Sub-structure 3 — Layered stack
@@ -1,6 +1,6 @@
# Type: matrix
A **2×2 quadrant grid** (occasionally 3×3) where each cell carries its own label, color, or icon. The structural backbone of consulting frameworks — SWOT, BCG, Eisenhower, Ansoff, Porter's, Risk/Reward. Used wherever two orthogonal axes divide the page into discrete zones.
A **2×2 quadrant grid** (occasionally 3×3) where each cell carries its own label, color, or icon. Use it wherever two orthogonal axes divide the image into discrete qualitative zones.
> **What matrix means inside a PPT block**: the canvas is **rigorously divided into equal quadrants** by perpendicular axes. Unlike `framework` (central hub + radiating satellites), matrix has no center — meaning lives in the four cells. Unlike `comparison` (two side-by-side zones), matrix splits **both** horizontally and vertically.
@@ -41,12 +41,12 @@ Sample fragment:
### 3.2 `text_policy: embedded`
When the matrix's identity comes from its in-image lettering — a stylized SWOT poster with the four letters as visuals, or a designer matrix whose axis labels are part of the artwork. Keep labels concise and stable in the required script. Specify a font family that echoes the deck's body typography; move exact/editable labels to SVG instead.
When the matrix's identity comes from its in-image lettering or its axis labels are part of the artwork, keep labels concise and stable in the required script. Specify a font family that echoes the deck's body typography; move exact/editable labels to SVG instead.
---
## 3. Fewshot prompt snippets
**Snippet A — vector-illustration + cool-corporate SWOT matrix, text_policy: none, 800×800**
**Snippet A — vector-illustration + cool-corporate quadrant matrix, text_policy: none, 800×800**
> Clean flat vector illustration of a 2×2 strategic matrix. Two perpendicular thin lines in primary deep navy `#1E3A5F` cross at the exact canvas center, dividing the canvas into four equal quadrants. Each quadrant has a subtle background tint: upper-left in pale primary navy at 10% opacity, upper-right in pale accent gold `#D4AF37` at 15% opacity, lower-left in pale gray `#F8F9FA`, lower-right in slightly deeper pale navy at 18% opacity. Each quadrant contains one simple iconic symbol in primary navy, centered within its quadrant — a shield (strength), a lightning bolt (opportunity), a target (weakness), an alert triangle (threat). Each icon occupies about 45% of its quadrant. Small accent gold dots sit at the four outer corners. Composed as an 800×800 reference matrix block with 10% padding. NO text, letters, axis labels, or quadrant names anywhere — SVG will overlay all labels. Color values are rendering guidance only.
> Clean flat vector illustration of a 2×2 qualitative matrix. Two perpendicular thin lines in primary deep navy `#1E3A5F` cross at the exact canvas center, dividing the canvas into four equal quadrants. Each quadrant has a subtle background tint: upper-left in pale primary navy at 10% opacity, upper-right in pale accent gold `#D4AF37` at 15% opacity, lower-left in pale gray `#F8F9FA`, lower-right in slightly deeper pale navy at 18% opacity. Each quadrant contains one simple iconic symbol in primary navy, centered within its quadrant — a compass, a lightning bolt, a target, and an alert triangle. Each icon occupies about 45% of its quadrant. Small accent gold dots sit at the four outer corners. Composed as an 800×800 reference matrix block with 10% padding. NO text, letters, axis labels, or quadrant names anywhere — SVG will overlay all labels. Color values are rendering guidance only.
@@ -1,6 +1,6 @@
# Type: pyramid
A **tiered triangular stack** — bottom-wide, top-narrow — where each layer carries its own label/color/concept. The structural backbone for Maslow's hierarchy, food pyramids, capability stacks, value hierarchies, software-architecture layer diagrams.
A **tiered triangular stack** — bottom-wide, top-narrow — where each layer carries its own label/color/concept. Use it for capability stacks, value hierarchies, or software-architecture layers.
> **What pyramid means inside a PPT block**: a **converging-upward** layered structure embodying "foundation supports value" — wide stable base, narrow apex of meaning. Unlike `funnel` (narrows downward, about filtering), pyramid narrows upward, about **stacking value or hierarchy**. Unlike `framework` (centered hub), pyramid is **strictly vertical layered**.
@@ -45,6 +45,6 @@ Self-contained pyramid with tier names typeset into the artwork. Keep tier names
## 3. Fewshot prompt snippets
**Snippet A — vector-illustration + cool-corporate Maslow pyramid, text_policy: none, 600×800**
**Snippet A — vector-illustration + cool-corporate capability pyramid, text_policy: none, 600×800**
> Clean flat vector illustration of a hierarchy pyramid. Five horizontal stepped tiers stacked vertically, each tier centered on the canvas vertical axis and ~18% narrower than the tier below. From bottom to top: tier 1 (foundation, widest) in deeper primary `#1E3A5F`; tier 2 in primary `#3B5478`; tier 3 in lighter primary `#5A7099`; tier 4 in secondary blue tint `#A8BDDD`; tier 5 (apex, narrowest) in accent gold `#D4AF37`. Each tier has one simple white iconic symbol centered — a brick (physiological), a shield (safety), a heart (belonging), a trophy (esteem), a star (self-actualization). Thin secondary cream `#F8F9FA` dividers separate the tiers. Background secondary cream. Composed as a 600×800 portrait pyramid block with 12% padding. NO text or labels — SVG will overlay tier names. Color values are rendering guidance only.
> Clean flat vector illustration of a hierarchy pyramid. Five horizontal stepped tiers stacked vertically, each tier centered on the canvas vertical axis and ~18% narrower than the tier below. From bottom to top: tier 1 (foundation, widest) in deeper primary `#1E3A5F`; tier 2 in primary `#3B5478`; tier 3 in lighter primary `#5A7099`; tier 4 in secondary blue tint `#A8BDDD`; tier 5 (apex, narrowest) in accent gold `#D4AF37`. Each tier has one simple white iconic symbol centered — a brick, a shield, a gear, an upward arrow, and a star. Thin secondary cream `#F8F9FA` dividers separate the tiers. Background secondary cream. Composed as a 600×800 portrait pyramid block with 12% padding. NO text or labels — SVG will overlay tier names. Color values are rendering guidance only.
@@ -26,7 +26,7 @@ Neutral information delivery. Lay the facts out plainly and completely, organize
- Even hierarchy within a section; consistent layout across sibling pages so the eye always knows where to look.
- Where one figure genuinely matters (a total, a status flag, an exception), surface it — but don't invent a punchline the content doesn't have.
> Table / list / dashboard / status-card geometry lives in [`templates/charts/`](../../templates/charts/); this mode decides *that the page informs completely and neutrally*, not pixel positions.
> Cell-grid tables and value-driven marks may use [`templates/tables/`](../../templates/tables/) and [`templates/charts/`](../../templates/charts/); qualitative topology is composed at runtime through [`executor-structure.md`](../executor-structure.md). A dashboard may combine them. This mode decides *that the page informs completely and neutrally*, not pixel positions.
## 3. Speaker-notes register
@@ -26,7 +26,7 @@ Titles state what the page teaches ("How attention weights are computed") — cl
- Diagrams that build incrementally; annotate the part currently being explained.
- Concrete examples anchor abstract points when they improve transfer or comprehension.
> Step / flow / diagram geometry lives in [`templates/charts/`](../../templates/charts/); this mode decides *the learning order and granularity*.
> Compose step, flow, and diagram topology at runtime through [`executor-structure.md`](../executor-structure.md); value-driven teaching charts remain in [`templates/charts/`](../../templates/charts/). This mode decides *the learning order and granularity*.
---
@@ -24,7 +24,7 @@ Titles read as beats that advance the arc ("Then the numbers stopped adding up")
- Visual weight guides the eye through each beat (hero image, one focal number, a pull quote).
- Continuity within a chapter, variation between chapters.
> Structure serves the arc, not a grid. Layout / chart geometry lives in [`templates/charts/`](../../templates/charts/) and [`executor-base.md`](../executor-base.md); this mode decides *the emotional beat of each page*.
> Structure serves the arc, not a grid. Compose qualitative topology at runtime through [`executor-structure.md`](../executor-structure.md); Chart/Table references remain in their catalogs, and general composition in [`executor-base.md`](../executor-base.md). This mode decides *the emotional beat of each page*.
---
@@ -38,7 +38,7 @@ SCQA opening, pyramid body:
- Decomposition pages (driver tree / MECE breakdown / 2×2 matrix) carry the analytical load.
- Source attribution on every data page.
> Page structure is a tendency, not a coordinate template. Card / tree / chart / KPI geometry lives in [`templates/charts/`](../../templates/charts/) — adapt those skeletons, do not reinvent. This mode decides *what argument each page makes*, not pixel positions.
> Page structure is a tendency, not a coordinate template. Compose qualitative card/tree/KPI/2×2 topology at runtime through [`executor-structure.md`](../executor-structure.md); value-driven charts and cell grids may use their catalogs. This mode decides *what argument each page makes*, not pixel positions.
---
@@ -1,8 +1,8 @@
> See [`shared-standards-core.md`](./shared-standards-core.md) for the mandatory SVG foundation.
> See [`executor-base.md`](./executor-base.md) for the Shape-first page authority and [`shared-standards-core.md`](./shared-standards-core.md) for the mandatory SVG foundation.
# Native Data Interface
Conditional interface for preset pattern fills and PowerPoint-native chart/table replacement metadata. Load when either feature appears in the authored SVG.
Sole conditional interface for preset pattern fills and PowerPoint-native chart/table replacement eligibility, markers, metadata schemas, and export activation. Load only when either feature is selected for the authored SVG.
## 1. Pattern Fill — `<pattern>` with PPTX preset annotation
@@ -40,18 +40,30 @@ this enum.
## 2. PowerPoint-Native Chart / Table Replacement Markers (Opt-in)
Native PowerPoint tables and Excel-backed charts activate at export time only. Generated pages prepare dormant replacement metadata for independently selected native-ready objects while keeping hand-authored SVG geometry pixel-stable across PowerPoint / Keynote / LibreOffice / WPS.
[`executor-base.md`](./executor-base.md) remains the single Shape-first authoring authority: the complete visible SVG fallback is required regardless of native eligibility. This section only adds dormant replacement metadata to independently selected objects and defines how export may activate it.
**Hard rule — selected-object authoring**: write the marker and JSON metadata in
the same edit only for a supported chart or pure text-grid table selected as
native-ready under [`executor-chart.md`](./executor-chart.md) §2.2. Default reads
`Native-ready: yes` from §IX (legacy §VII fallback only); Quick makes the
decision in active context before drawing. Default `no`, a Quick decision not to
use the native object model, and incidental microvisuals stay on the SVG
fallback route. Canonical rectangular merged text cells may use the narrow
the same edit only for a supported chart or pure text-grid table independently
selected as native-ready. Default reads the exact semantic object key from §IX
`Native-ready: <object-key>=yes|no; ...`; Quick assigns the same page-local
`kebab-case` key in active context before drawing. Use that key as the marker
group `id` and metadata `name`. A catalog `family/key`, family name, §VII row,
numeric content, or another ready object on the page never implies eligibility.
Default `<object-key>=no`, Quick `no`, and unlisted incidental microvisuals stay
on the SVG fallback route. Canonical rectangular merged text cells may use the narrow
`row_span` / `col_span` contract below; graphical cells stay unmarked. The
marker group supplies visible SVG fallback children for browser rendering and
JSON metadata for `svg_to_pptx` native export.
marker group supplies visible fallback children for browser rendering and JSON
metadata for `svg_to_pptx` native export.
A legacy bare `Native-ready: yes|no` maps only to the page's sole eligible
object. Zero or multiple eligible objects make it ambiguous and require
upstream repair.
**MUST — atomic authoring**: treat one object's visible SVG fallback, parent
`data-pptx-replace-with` marker, and single JSON `<metadata>` child as one
authoring unit. Write all three while that object's data is in context. Do not
defer the marker or metadata to `verify-charts`, the final quality gate, or
export.
**Hard rule — activation is the opt-in, dormant unless exported with `--native-charts-and-tables`**: A marker only declares that a group is eligible for PowerPoint-native Chart/Table replacement. Normal `svg_to_pptx.py` runs keep the fallback SVG children and convert them into independently editable DrawingML shapes. Pass `--native-charts-and-tables` only when the data source and chart/table-specific object model matter more than cross-renderer layout fidelity: it emits the PowerPoint Chart/Table object and skips the fallback children to avoid duplicates. Native styling preserves the core palette, text, axis, grid, and background colors where possible, but it is still a PowerPoint Chart/Table object rather than a pixel-identical SVG drawing.
@@ -94,6 +106,7 @@ free of those attributes; use the exact behavior and field index in
<metadata type="application/json">
{
"x": 120, "y": 150, "width": 520, "height": 320,
"name": "p03-revenue-chart",
"type": "column",
"title": "Revenue by Segment",
"categories": ["Q1", "Q2", "Q3"],
@@ -107,6 +120,31 @@ free of those attributes; use the exact behavior and field index in
</g>
```
**Hard rule — transcribe the authored object**: metadata is the native object's
source of truth and must describe the same data and visible chart/table chrome
as the fallback drawn in that marker group.
| Object | Required projection from the visible fallback |
|---|---|
| Chart | Every category/point and series value, the actual x/y/size data where applicable, visible point colors and labels, line/area treatment needed by the schema, visible title/axis/legend chrome, companion text, bounds, and fallback typography/style values that native export cannot infer unambiguously |
| Table | Every resolved row/column cell, header/summary line, rectangular span, required cell style, alignment, bounds, and visible typography |
Do not simplify the SVG artwork to match the native object model. When the
closed payload cannot represent an object without losing required data or cell
topology, Default returns the native-ready decision upstream and Quick revises
that per-object decision before drawing; only the resolved non-native object
stays unmarked on its complete SVG fallback. Never silently ignore an explicit
`<object-key>=yes` declaration.
**Per-page verification**: enumerate every `<object-key>=yes` on the current
page and confirm a one-to-one key match. Each key identifies one parent marker
and exactly one JSON metadata child; `<object-key>=no` and unlisted incidental
objects have no marker. Finding one marker somewhere on a page is insufficient.
```bash
rg -n 'data-pptx-replace-with="(chart|table)"|<metadata type="application/json">' <project_path>/svg_output/<current_page>.svg
```
**Table schema**: Native tables are rectangular DrawingML grids. Use `columns`
for the optional header row and `rows` for body rows; shorter rows are padded
with blank cells unless `strict_grid: true` is set. Tables may contain at most
@@ -10,7 +10,7 @@ Conditional extension for applying an installed Brand/Style/Layout/Deck workspac
## 1. AI-Authored Template Application Plan
**Template vs preset**: A style mention and a Style workspace are different inputs. Bare names and style words remain interpretive input and never resolve to a local path; only a selected and installed workspace activates the rules below. Its fused `<project_path>/templates/design_spec.md` is the template-design source. Treat `kind: style` or a fused provenance entry for `style` as an active Direction / method segment. Whether a source root was labelled `library` or `explicit` is installation provenance only and never affects Stage-2 precedence.
**Template vs preset**: A style mention and a Style workspace are different inputs. Bare names and style words remain interpretive input and never resolve to a local path; only a selected and installed workspace activates the rules below. Every installed `<project_path>/templates/design_spec.<kind>.<id>.md` is a template-design source; read all of them. The presence of a `design_spec.style.*.md` file is what marks an active Direction / method segment. Whether a source root was labelled `library` or `explicit` is installation provenance only and never affects Stage-2 precedence.
**Legacy template boundary**: A Layout/Deck template containing `native_structure.json`, `source_template.pptx`, missing root Master identity, direct atomic placeholders, or old `baseline` / `preserve` / distillation metadata is not a Generate Step 3 input. Create a current workspace through [`create-template`](../workflows/create-template.md), preferably from the original PPTX when native topology matters. Brand and Style are intentionally roster-free; never reject either for omitting SVG or Master identity. Do not mutate the input in place.
@@ -21,8 +21,8 @@ Conditional extension for applying an installed Brand/Style/Layout/Deck workspac
Immediately before authoring the Stage-2 solution, load each relevant template
resource once per path + SHA and inspect:
- every installed `design_spec.md`; for Layout/Deck only, also inspect the actual Page Roster and relevant SVG prototypes;
- the resolved Identity, Structure, Reusable Application Context, and Direction / method segment owners from the post-Stage-1 fusion receipt;
- every installed `design_spec.<kind>.<id>.md`; for Layout/Deck only, also inspect the actual Page Roster and relevant SVG prototypes;
- the Identity, Structure, Reusable Application Context, and Direction / method segment owners, resolved here from the installed set under [`apply-template-workspace`](../workflows/stages/apply-template-workspace.md) §5;
- the confirmed current communication contract, source obligations, planned page count, and content shape of every planned page;
- the user's natural-language instructions, including any page names/numbers or elements they explicitly require.
@@ -74,7 +74,7 @@ When the communication contract conflicts with the workspace, choose and state t
## 3. Structured Lock Planning
For Style-only or Style + Brand, write `pptx_structure.mode: flat` plus `template_reuse_scope: style`; omit `template_adherence` and every structured mapping section. When Style is fused with Layout or Deck, it changes only Direction / method and does not change the non-Style capability label or force flat/structured routing. Derive reuse scope from the selected Layout/Deck application plan; a literal `mirror` plan is compatible only when the Style segment requires no visual or topology change.
For Style-only or Style + Brand, write `pptx_structure.mode: flat` plus `template_reuse_scope: style`; omit `template_adherence` and every structured mapping section. When a Style is installed alongside Layout or Deck, it changes only Direction / method and does not force flat/structured routing. Derive reuse scope from the selected Layout/Deck application plan; a literal `mirror` plan is compatible only when the Style segment requires no visual or topology change.
For `mirror` / `layout`, write `pptx_structure.mode: structured` plus `template_adherence: strict|adaptive`; mirror always writes `strict`. Do not write legacy `baseline`, `template`, `preserve`, `layout_strategy`, or Layout-kind rows.
@@ -85,4 +85,12 @@ For `mirror` / `layout`, write `pptx_structure.mode: structured` plus `template_
- **Adaptive refinement**: Initial definitions are complete. If construction shows that reusable framing or slot topology/bounds must change, return to Strategist to add a definition sourced from that page and update its assignment before execution resumes. Executor never mutates or extends the contract; export only compiles declared structure and never discovers or clusters Layouts.
- **Input prototypes**: Add one `page_layouts` row per page. Strict preserves that SVG's contract; adaptive keeps its Master and may declare a new output Layout; mirror also preserves literal visuals and text-node topology.
**Chart compatibility**: Use `page_layouts` together with `page_charts` only when the selected prototype shell is compatible. For a chart page without an exact roster match, adaptive mode starts from the closest neutral prototype and declares an output Layout; strict mode selects an existing compatible Layout or revises the outline. Never omit `page_layouts` on a structured route.
**Visualization compatibility**: Use `page_layouts` with optional Chart/Table
`page_visualizations` only when the prototype shell can carry the actual §IX
information model. The reference changes neither Layout identity, slot
topology, nor final visualization type. Qualitative relationships stay in §IX
and are composed Slide-locally; they never supply Master/Layout/placeholder
ownership. Without an exact prototype match, adaptive mode starts from the
closest neutral prototype and declares an output Layout; strict mode selects an
existing compatible Layout or revises the outline. Never omit `page_layouts`
on a structured route or write legacy `page_charts` in a new lock.
@@ -56,7 +56,7 @@ Do not force communication intent into one catalog label; Stage 1 records compos
| Reference | Preserve the selected direction or role; adapt its realization to context. |
| Permission / default | An allowed candidate/source boundary or preference; Strategist may leave it unused, with no quota. |
**Authority chain — materials → Strategist preparation → realization.** User inputs set materials/acquisition bounds. Strategist owns sufficiency, gap-filling, and selection: roster/content, resources, chart/layout keys, fonts, palette anchors, the icon library/stroke plus curated project pool, and crop bans. Fact research may precede confirmation; AI/web/slice follows final confirmation plus completed §VIII/lock; icons are synced/validated during authoring without page assignment. Before Executor, each resource has a path and terminal/`Needs-Manual` state. Executor owns geometry, composition, hierarchy, spacing, treatment, and per-page choice among prepared icons; it never searches, generates, syncs, invents, or substitutes resources. Missing material/reselection returns upstream. Specificity defines freedom; References flex realization, never selection.
**Authority chain — materials → Strategist preparation → realization.** User inputs set materials/acquisition bounds. Strategist owns sufficiency, gap-filling, and selection: roster/content, resources, page-local visualization/Layout references, fonts, palette anchors, the icon library/stroke plus curated project pool, and crop bans. Fact research may precede confirmation; AI/web/slice follows final confirmation plus completed §VIII/lock; icons are synced/validated during authoring without page assignment. Before Executor, each resource has a path and terminal/`Needs-Manual` state. Executor owns geometry, composition, hierarchy, spacing, treatment, and per-page choice among prepared icons; it never searches, generates, syncs, invents, or substitutes resources. Missing material/reselection returns upstream. Specificity defines freedom; References flex realization, never selection.
Explicit *must*, *only*, *exactly*, *verbatim*, *do not*, or `no-crop` wording may strengthen only the named property into the appropriate Literal or Semantic requirement. Accepting an AI recommendation keeps the field's default type; it does not promote a Reference or Permission into a Literal requirement.
@@ -309,17 +309,20 @@ the effective outcome. Suggestions remain non-binding and never activate the
custom stage; only an explicit motion requirement or an enabled outcome may
require visible lifecycle-state preparation.
Review planned pages through two lenses:
Classify by information model, never source PowerPoint object type:
| Lens | Content shapes |
| Model | Planning action |
|---|---|
| Numeric / data | comparisons, trends, proportions, KPIs, financials, rankings, distributions, funnels |
| Structural information | rosters, agendas, principles, phases, journeys, capability maps, OKR cascades, roadmaps, strategic frameworks |
| Qualitative `order`, `link`, `parent`, `membership`, `contrast`, or `overlap` | Preserve units, relationship, and reading path as free §IX prose; no catalog key |
| Values/dates/durations determine geometry | Chart; recall is optional |
| Row header × column header addresses each fact | Table; recall is optional |
**Reference — not a constraint**: use catalog recall when it would help. Its CLI accepts 38 distinct English content-shape tags; a page already planned as a custom visualization or table needs no recall. Run:
**Mandatory — relationship handoff**: keep every qualitative relationship in §IX free prose; never serialize grammar atoms, coordinates, or named models. Executor makes the per-page Structure decision at runtime.
**Reference — not a constraint**: recall Chart/Table with 38 English tags when useful; add `--family chart|table` only when certain. Skip custom objects and qualitative composition.
```bash
python3 skills/ppt-master/scripts/chart_recall.py recall \
python3 skills/ppt-master/scripts/visualization_recall.py recall \
--page P03 \
--tag "time series" \
--tag "three metrics" \
@@ -327,28 +330,29 @@ python3 skills/ppt-master/scripts/chart_recall.py recall \
--limit 6
```
The command returns a bounded shortlist plus `no-template-match`. Read it unfiltered; `tail` / `head` / `grep` can hide ranked candidates. `confidence` is lexical only. At `high` / `medium`, keep no-match after candidate review. At `low` / `none`, use a fitting candidate directly; otherwise rerun once with `--semantic-fallback` before no-match. Do not open a second index.
The command returns a bounded shortlist plus `no-template-match`. Read it unfiltered; `tail` / `head` / `grep` can hide ranked candidates. `confidence` is lexical only. At `high` / `medium`, keep no-match after candidate review. At `low` / `none`, use a fitting candidate directly; otherwise rerun once with `--semantic-fallback` before no-match. Do not open any family index separately.
**Selection**:
1. Choose the most relevant candidate as a reference for that page. It does not lock the final visualization type or geometry and never applies to another page without its own row.
2. Retain `no-template-match` when no reference fits. Choose a custom visualization, table, diagram/layout, or permitted image from the content semantics, communication job, and editability need. Record the choice only in §IX; never serialize `no-template-match` into §VII.
3. Validate all selected keys before writing the lock:
1. Choose at most one flexible Chart/Table `family/key` per page; keep children and qualitative relationships in §IX.
2. If none fits, keep `no-template-match` and plan the fallback only in §IX; never serialize no-match.
3. Validate every selected canonical reference before the lock:
```bash
python3 skills/ppt-master/scripts/chart_recall.py validate <key> [<key> ...]
python3 skills/ppt-master/scripts/visualization_recall.py validate \
<family>/<key> [<family>/<key> ...]
```
A failed validation must be corrected with a recalled key. `no-template-match` is not a key and never appears in `page_charts`.
Correct failed selections by recall; `no-template-match` never enters `page_visualizations`.
**Section VII selection list**: when a reference is selected, write `Page | Template | Usage`; Usage is one short page-local purpose, not geometry. Omit §VII when none is selected, and never add path, summary, runners-up, `no-template-match`, or `n/a`. §IX remains authoritative.
**Section VII selection list**: write `Page | Family | Template | Usage` for each `chart|table` reference; Usage is semantic purpose. Omit empty/no-match detail. Qualitative composition stays in §IX; only Layout/Deck owns reusable PowerPoint structure.
**Native-ready boundary**: For every independent data chart or pure text-grid table, add `Native-ready: yes|no` to its §IX page block. Choose `yes` only when the confirmed requirement or artifact afterlife benefits from an editable native data object; otherwise keep the designed SVG with `no`. Conceptual rows and incidental sparklines, KPI trends, or insets omit the field; Executor never promotes them.
**Native-ready boundary**: Give every independent data chart and pure text-grid table in §IX `Visualization` a unique page-local semantic `kebab-case` key, then write one `Native-ready` map: `<key>=yes|no; ...`. Use `yes` only when an editable native object benefits the confirmed requirement/afterlife. Qualitative shape compositions and incidental microvisuals stay unlisted.
```markdown
| Page | Template | Usage |
| --- | --- | --- |
| P03 | line_chart | Compare the source metrics over time |
| Page | Family | Template | Usage |
| --- | --- | --- | --- |
| P03 | chart | line_chart | Compare the source metrics over time |
```
**Native-geometry candidate detail**: Add `Native shape suggestion` to the
@@ -469,7 +473,7 @@ final Stage 2 `false`, explicit objects-off, or explicit all-motion-off; only th
includes transitions.
1. With Generate Step 4's retained complete final-confirmation state, read `${SKILL_DIR}/templates/design_spec_reference.md`.
2. Compose the whole Design Spec in active context before touching the target path. Create `design_spec.md` once from the schema marker through §X; do not copy a scaffold into the project or patch placeholder fields. Record production mechanics in §I, including one effective outcome plus provenance for Speaker Notes, Custom Animations, and Narration Audio. Resolve them from latest explicit user instruction → matching final Stage 2 proactive value → workflow default `enabled` / `disabled` / `disabled`; Narration Audio enabled requires Speaker Notes enabled without rewriting the raw proactive evidence, and a dependency-driven notes outcome records that provenance. In §IX, create the complete ordered roster; each entry carries layout, title, core message, **Audience move**, complete preferred wording, applicable capability recommendations, visualization/image references, sourced `Fact IDs`, and `Data class: scenario` for invented demo data. After Gate 1 plus conditional refine approval, roster ids/count/order and semantic content are authoritative; non-literal wording, block texture, layout, cover/closing composition, capability recommendations, and image/chart patterns remain References unless promoted.
2. Compose the whole Design Spec in active context before touching the target path. Create `design_spec.md` once from the schema marker through §X; do not copy a scaffold into the project or patch placeholder fields. Record production mechanics in §I, including one effective outcome plus provenance for Speaker Notes, Custom Animations, and Narration Audio. Resolve them from latest explicit user instruction → matching final Stage 2 proactive value → workflow default `enabled` / `disabled` / `disabled`; Narration Audio enabled requires Speaker Notes enabled without rewriting the raw proactive evidence, and a dependency-driven notes outcome records that provenance. In §IX, create the complete ordered roster; each entry carries layout, title, core message, **Audience move**, complete preferred wording, applicable capability recommendations, visualization/image references, sourced `Fact IDs`, and `Data class: scenario` for invented demo data. After Gate 1 plus conditional refine approval, roster ids/count/order and semantic content are authoritative; non-literal wording, block texture, layout, cover/closing composition, capability recommendations, and image/visualization patterns remain References unless promoted.
3. Compare `design_spec.md` against the final confirmation field by field. Repair every omission or deviation before entering an enabled refine-spec review or authoring `spec_lock.md`.
4. If enabled, run [`refine-spec`](../workflows/stages/refine-spec.md) after Gate 1; edit only that Design Spec and create no lock before explicit approval.
5. Read `${SKILL_DIR}/templates/spec_lock_reference.md`; create the lock once or resynchronize stale derived state from the approved Design Spec and context. Retain identity/refinements and stable roles/routing; omit unnamed page-local values, do not reopen evidence, and make no new recommendation.
@@ -492,7 +496,7 @@ includes transitions.
**GATE 2 — lock context fidelity.** After Gate 1 closes, author machine-relevant anchors/routing into `spec_lock.md`. The lock may normalize syntax and add justified recurring roles, but must not change identity, discard a refinement, introduce a direction, or become a field copy/allowlist. On contradiction, return to Gate 1 using retained confirmation by default or the approved revised Design Spec after refinement; fresh recovery reads persisted final evidence once only when active state is absent.
**Execution lock content**: `spec_lock.md` compactly carries communication, stable color/type anchors, icons, images, page rhythm, chart choices, and route-specific PowerPoint structure. Name every recurring typography role; a planned short non-structural Hero/Display size may stay omitted only while the same value appears at most twice, and its third occurrence requires a named role. Never re-derive a confirmed anchor. New locks keep `font_family` as the body/default compatibility stack and also write explicit `title_family` + `body_family`; every additional recurring Design Spec role projects to `<role>_family`. Collapsing distinct Design Spec stacks into `font_family`, or dropping an extra role, fails Gate 2. Keep core fonts/palette roles stable; page authoring varies treatment and may add sparse local garnish. Project every placed §VIII image's source, layout suggestion, and crop policy; omit unplaced sheets and planning provenance. Free-design, brand-only, and `template_reuse_scope: style` use `pptx_structure.mode: flat`; the template module owns structured mappings. Executor context policy lives in [executor-base.md](executor-base.md) §2.1. Repair from Gate 2's active decision authority, then re-author affected lock rows.
**Execution lock content**: `spec_lock.md` compactly carries communication, stable color/type anchors, icons, images, page rhythm, Chart/Table references, and route-specific PowerPoint structure; qualitative relationships stay only in §IX. Name every recurring typography role; a planned short non-structural Hero/Display size may stay omitted only while the same value appears at most twice, and its third occurrence requires a named role. Never re-derive a confirmed anchor. New locks keep `font_family` as the body/default compatibility stack and also write explicit `title_family` + `body_family`; every additional recurring Design Spec role projects to `<role>_family`. Collapsing distinct Design Spec stacks into `font_family`, or dropping an extra role, fails Gate 2. Keep core fonts/palette roles stable; page authoring varies treatment and may add sparse local garnish. Project every placed §VIII image's source, layout suggestion, and crop policy; omit unplaced sheets and planning provenance. Free-design, brand-only, and `template_reuse_scope: style` use `pptx_structure.mode: flat`; the template module owns structured mappings. Executor context policy lives in [executor-base.md](executor-base.md) §2.1. Repair from Gate 2's active decision authority, then re-author affected lock rows.
**Contextual extension**: derived paint or sparse local font/color garnish may stay in one SVG while non-structural and non-recurring. New base/semantic colors, structural/recurring fonts, resources, or recurring cross-page identity patterns require upstream repair; a page-local §VIII preferred image pattern follows [`executor-image.md`](./executor-image.md) and may change during realization. Executor never reverse-projects a local choice as planning fact. Promote recurring garnish upstream before reuse, read back and validate the affected planning fragments, and never add values to silence a comparison.
@@ -507,7 +511,7 @@ includes transitions.
- **pptx_structure is mandatory**: Free-design, brand-only, and `template_reuse_scope: style` routes write `mode: flat`; a style-reference route may also record `template_reuse_scope: style` but omits every structure mapping and `template_adherence`. `template_reuse_scope: mirror|layout` writes `mode: structured` plus `template_adherence: strict|adaptive`. Do not write legacy `baseline`, `template`, `preserve`, `layout_strategy`, or Layout-kind rows into a new project.
- **Flat-route boundary**: With `mode: flat`, omit `pptx_masters`, `pptx_layouts`, `page_pptx_layouts`, and `page_layouts`. Do not plan native Master/Layout families or reusable placeholder slots. Every generated SVG object remains Slide-local: omit root Master/Layout identity, `data-pptx-layer`, and `data-pptx-placeholder*` metadata. Export materializes one clean project-owned Master plus one Blank Layout from the current color/typography lock, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks.
- **Structured template route**: When [`strategist-template.md`](./strategist-template.md) is active and reuse is `mirror|layout`, follow its complete Master/Layout/slot/prototype mapping rules.
- **page_charts**: project each §VII row's `Page` and `Template` as `P<NN>: <template_key>`; Usage stays in the Design Spec. This is a page-local reference, not a type/geometry lock. Omit no-match pages and the empty section.
- **page_visualizations**: project at most one §VII `P<NN>: <chart|table>/<key>` per page. Usage/children/qualitative relationships stay in the Design Spec; omit empty/no-match. It locks no geometry/native output. New locks never write legacy `page_charts`.
---
@@ -309,8 +309,8 @@ rules; final font families, weights, colors, and absolute sizes do not.
Do not prescribe communication objectives, audience outcomes, required
narrative order, fixed boilerplate, or example-content retention. The
frontmatter `summary` carries concise structural selection context; the
deck-only Template Overview remains the application segment used during
fusion.
deck-only Template Overview remains the application segment read during
template application.
Sections to **omit** from template `design_spec.md` (sourced elsewhere — listing them here is noise):
@@ -25,7 +25,7 @@ Each review subagent processes a **batch** of pages (see §6.1 for batch sizing)
2. **Path to this rubric file**
3. **`<project>/design_spec.md`** (read-only) — §IX outline is the source of truth for "what should this page deliver"
4. **`<project>/spec_lock.md`** (read-only) — brand-locked values
5. **Style Review Focus excerpt** (conditional, read-only) — supplied by the orchestrator only when `<project>/templates/design_spec.md` contains an active Style segment; it retains the source path and exact §VII wording
5. **Style Review Focus excerpt** (conditional, read-only) — supplied by the orchestrator only when an installed `<project>/templates/design_spec.style.<id>.md` exists; it retains the source path and exact §VII wording
6. **`<project>/.review/`** (writable) — where backups and findings JSON go
The subagent reads inputs 25 **once** at the start of its turn (input 5 may be absent), then iterates over the page batch sequentially (one page at a time): apply the rubric → apply any Style supplement → write `<project>/.review/<page>.json` → move on. This is the core token-saving move — fixed context is read N/K times instead of N times.
@@ -55,7 +55,7 @@ python3 scripts/update_repo.py
| SVG pipeline | `preset_shape_svg.py`, `shape_boolean_svg.py`, `svg_authoring_view.py`, `compact_svg_coordinates.py`, `mirror_template_materialize.py`, `finalize_svg.py`, `svg_to_pptx.py`, `template_preview_pptx.py`, `total_md_split.py`, `svg_quality_checker.py`, `extract_svg_assets.py`, `extract_svg_pictures.py`, `animation_config.py`, `notes_to_audio.py`, `narration_sync.py` | [docs/svg-pipeline.md](./docs/svg-pipeline.md); [native shape authoring](../references/native-shape-authoring.md) |
| PPTX transitions | `pptx_transitions.py` | [docs/pptx-transitions.md](./docs/pptx-transitions.md) |
| PPTX animations | `pptx_animations.py`, `animation_config.py` | [docs/pptx-animations.md](./docs/pptx-animations.md) |
| Spec maintenance | `update_spec.py`, `chart_recall.py` | [docs/update_spec.md](./docs/update_spec.md); [docs/chart-recall.md](./docs/chart-recall.md) |
| Spec maintenance | `update_spec.py`, `visualization_recall.py`; legacy `chart_recall.py` | [docs/update_spec.md](./docs/update_spec.md); [docs/visualization-recall.md](./docs/visualization-recall.md) |
| Image tools | `image_gen.py`, `latex_render.py`, `analyze_images.py`, `gemini_watermark_remover.py` | [docs/image.md](./docs/image.md) |
| Maintenance smokes | Inline temporary-project commands | [advanced image and motion](./docs/advanced-image-motion-smoke.md); [mask and gradient](./docs/mask-gradient-smoke.md); [multilingual text](./docs/multilingual-text-smoke.md) |
| Repo maintenance | `update_repo.py` | README install/update section |
@@ -91,17 +91,17 @@ python3 scripts/project_manager.py page-context-report <project_path>
routing checks, or context measurement; normal generation retains the complete
Design Spec and lock once per valid execution context. Each invocation includes
the global lock projection as a continuity anchor set, not a color/font allowlist; large Design Specs,
prototype, and `templates/charts/` references are emitted only as scoped
prototype, and selected family visualization references are emitted only as scoped
path/SHA fingerprints and are read once per execution context. `--bundle` is a
deprecated compatibility no-op. `--record-usage` writes one derived snapshot
under `analysis/page-context/`; exact `o200k_base` token counts are optional and
degrade to `tokens: null` when `tiktoken` is absent. Telemetry may be partial.
Chart candidate recall:
Visualization candidate recall:
```bash
python3 scripts/chart_recall.py recall --page P03 --tag "time series" --tag "three metrics" --tag "direction over time"
python3 scripts/chart_recall.py validate line_chart
python3 scripts/visualization_recall.py recall --page P03 --tag "time series" --tag "three metrics" --tag "direction over time"
python3 scripts/visualization_recall.py validate chart/line_chart
```
Template source import:
@@ -1,346 +1,27 @@
#!/usr/bin/env python3
"""
PPT Master - Chart Candidate Recall
PPT Master - Legacy Chart Recall Compatibility
Recalls a deterministic chart-template shortlist from page-shape semantic tags,
or validates selected chart keys against the live chart catalog.
Preserve the historical broad chart-recall CLI while delegating to the unified
visualization recall implementation. Published Structure bare keys validate as
intent-only compatibility values; they do not recall or load SVG assets. The
legacy ``valid`` list remains intact while ``resolved`` exposes each kind.
Usage:
python3 scripts/chart_recall.py recall --page P03 --tag "time series" --tag "three metrics" --tag "trend"
python3 scripts/chart_recall.py validate line_chart column_chart
Examples:
python3 scripts/chart_recall.py recall --page P07 --tag "named quadrants" \
--tag "bullet lists" --tag "SWOT" --limit 6
python3 scripts/chart_recall.py validate quadrant_text_bullets
python3 scripts/chart_recall.py validate process_flow
Dependencies:
None (only uses standard library)
See scripts/docs/chart-recall.md for the Strategist workflow and output contract.
None (only uses the standard library)
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import unicodedata
from pathlib import Path
from typing import Optional
from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
_SCRIPTS_DIR = Path(__file__).resolve().parent
_INDEX_PATH = _SCRIPTS_DIR.parent / "templates" / "charts" / "charts_index.json"
_TOKEN_RE = re.compile(r"[a-z0-9]+")
_PAGE_RE = re.compile(r"^P\d{2,}$")
_KEY_RE = re.compile(r"^[a-z0-9]+(?:_[a-z0-9]+)*$")
_SKIP_RE = re.compile(r"\bskip\s+(?:if|for)\b", re.IGNORECASE)
_STOP_WORDS = {
"a",
"an",
"and",
"as",
"at",
"by",
"for",
"from",
"if",
"in",
"into",
"of",
"on",
"or",
"per",
"the",
"to",
"use",
"with",
}
def _normalize(text: str) -> str:
normalized = unicodedata.normalize("NFKC", text).casefold().replace("_", " ")
return " ".join(_TOKEN_RE.findall(normalized))
def _stem(token: str) -> str:
if len(token) > 5 and token.endswith("ies"):
return token[:-3] + "y"
if len(token) > 5 and token.endswith("ing"):
return token[:-3]
if len(token) > 4 and token.endswith("ed"):
return token[:-2]
if len(token) > 4 and token.endswith("es"):
return token[:-2]
if len(token) > 3 and token.endswith("s"):
return token[:-1]
return token
def _tokens(text: str) -> set[str]:
return {
_stem(token)
for token in _TOKEN_RE.findall(_normalize(text))
if token not in _STOP_WORDS
}
def load_catalog() -> dict[str, str]:
"""Load and validate the live chart catalog."""
try:
payload = json.loads(_INDEX_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(f"Cannot read chart catalog {_INDEX_PATH}: {exc}") from exc
if not isinstance(payload, dict):
raise RuntimeError(f"Chart catalog {_INDEX_PATH} root must be an object")
raw_charts = payload.get("charts")
if not isinstance(raw_charts, dict) or not raw_charts:
raise RuntimeError(f"Chart catalog {_INDEX_PATH} has no non-empty 'charts' object")
charts: dict[str, str] = {}
for key, item in raw_charts.items():
if (
not isinstance(key, str)
or _KEY_RE.fullmatch(key) is None
or not isinstance(item, dict)
):
raise RuntimeError(f"Chart catalog entry {key!r} is malformed")
summary = item.get("summary")
if not isinstance(summary, str) or not summary.strip():
raise RuntimeError(f"Chart catalog entry {key!r} has no non-empty summary")
charts[key] = summary.strip()
return charts
def _score_candidate(key: str, summary: str, tags: list[str]) -> tuple[int, list[str]]:
skip_match = _SKIP_RE.search(summary)
if skip_match is None:
pick_clause, skip_clause = summary, ""
else:
pick_clause = summary[:skip_match.start()]
skip_clause = summary[skip_match.start():]
key_text = _normalize(key)
pick_text = _normalize(pick_clause)
skip_text = _normalize(skip_clause)
key_tokens = _tokens(key)
pick_tokens = _tokens(pick_clause)
skip_tokens = _tokens(skip_clause)
score = 0
matched_tags: list[str] = []
for tag in tags:
tag_text = _normalize(tag)
tag_tokens = _tokens(tag)
positive_score = 0
negative_score = 0
if tag_text and tag_text in key_text:
positive_score += 20
elif tag_text and tag_text in pick_text:
positive_score += 14
if tag_text and tag_text in skip_text:
negative_score += 12
for token in tag_tokens:
if token in key_tokens:
positive_score += 9
elif token in pick_tokens:
positive_score += 5
if token in skip_tokens:
negative_score += 6
if positive_score:
matched_tags.append(tag)
score += positive_score - negative_score
return score, matched_tags
def recall_candidates(
page: str,
tags: list[str],
limit: int,
*,
force_semantic_fallback: bool = False,
) -> dict[str, object]:
"""Recall a deterministic shortlist for one page."""
charts = load_catalog()
scored: list[tuple[int, str, str, list[str]]] = []
for key, summary in charts.items():
score, matched_tags = _score_candidate(key, summary, tags)
if score > 0 and matched_tags:
scored.append((score, key, summary, matched_tags))
scored.sort(key=lambda item: (-item[0], item[1]))
candidates = []
for score, key, summary, matched_tags in scored[:limit]:
candidates.append(
{
"key": key,
"path": f"templates/charts/{key}.svg",
"summary": summary,
"score": score,
"matched_tags": matched_tags,
}
)
top_score = candidates[0]["score"] if candidates else 0
if top_score >= 35:
confidence = "high"
elif top_score >= 15:
confidence = "medium"
elif top_score > 0:
confidence = "low"
else:
confidence = "none"
fallback_required_before_no_match = (
confidence in {"low", "none"} and not force_semantic_fallback
)
if fallback_required_before_no_match:
no_match_instruction = (
f"Lexical confidence is {confidence}. Select a bounded candidate when one "
"fits; otherwise rerun the same recall with --semantic-fallback before "
"keeping no-template-match. Keep the final negative result out of Design "
"Spec Section VII and describe the chosen fallback in the page's Section "
"IX block."
)
else:
no_match_instruction = (
"Use when none of the reviewed candidates fits the page structure. Keep "
"this result out of Design Spec Section VII and describe the chosen fallback "
"in the page's Section IX block."
)
result: dict[str, object] = {
"page": page,
"semantic_tags": tags,
"confidence": confidence,
"candidates": candidates,
"no_template_match": {
"allowed": not fallback_required_before_no_match,
"key": "no-template-match",
"instruction": no_match_instruction,
},
}
if force_semantic_fallback:
result["semantic_fallback"] = {
"reason": "requested-after-bounded-review",
"instruction": (
"Semantically compare the page tags with every returned selection rule. "
"Choose one exact catalog key or keep no-template-match; lexical overlap "
"is not required in this review."
),
"path_pattern": "templates/charts/{key}.svg",
"catalog": charts,
}
return result
def _dedupe(values: list[str]) -> list[str]:
result: list[str] = []
seen: set[str] = set()
for value in values:
stripped = value.strip()
normalized = _normalize(stripped)
if not stripped or not normalized or normalized in seen:
continue
seen.add(normalized)
result.append(stripped)
return result
def _run_recall(args: argparse.Namespace) -> int:
page = args.page.upper()
if not _PAGE_RE.fullmatch(page):
print("Error: --page must match P<NN>, for example P03.", file=sys.stderr)
return 2
tags = _dedupe(args.tag)
if not 3 <= len(tags) <= 8:
print("Error: recall requires 3-8 distinct non-empty --tag values.", file=sys.stderr)
return 2
result = recall_candidates(
page,
tags,
args.limit,
force_semantic_fallback=args.semantic_fallback,
)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
def _run_validate(args: argparse.Namespace) -> int:
charts = load_catalog()
selected = _dedupe(args.keys)
invalid = sorted(key for key in selected if key not in charts)
result = {
"invalid": invalid,
"valid": sorted(key for key in selected if key in charts),
}
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if invalid:
print(
"Error: replace each invalid key with a key returned by the recall command, "
"or keep no-template-match out of Section VII and page_charts while "
"recording the custom fallback in the page's Section IX block.",
file=sys.stderr,
)
return 1
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Recall chart-template candidates or validate selected keys.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
recall = subparsers.add_parser("recall", help="Recall candidates for one planned page.")
recall.add_argument("--page", required=True, help="Planned page key, for example P03.")
recall.add_argument(
"--tag",
action="append",
required=True,
help="English semantic content-shape tag; repeat 3-8 times.",
)
recall.add_argument(
"--limit",
type=int,
choices=range(3, 9),
default=6,
metavar="3..8",
help="Candidate count (default: 6).",
)
recall.add_argument(
"--semantic-fallback",
action="store_true",
help="Include the full catalog when bounded recall may have missed a semantic match.",
)
recall.set_defaults(handler=_run_recall)
validate = subparsers.add_parser("validate", help="Validate selected catalog keys.")
validate.add_argument("keys", nargs="+", help="One or more selected chart keys.")
validate.set_defaults(handler=_run_validate)
return parser
def main(argv: Optional[list[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
return args.handler(args)
except RuntimeError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
from visualization_recall import main
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main(legacy_output=True))
@@ -665,15 +665,30 @@ def _read_template_handoff(project_path: Path, handoff_file: Path) -> dict:
f'{TEMPLATE_HANDOFF_NAME} selection_sha256 does not match selection'
)
if data['mode'] == 'templates':
installed_spec = project_path / 'templates' / 'design_spec.md'
if not installed_spec.is_file():
if not _installed_template_specs(project_path):
raise ValueError(
f'{TEMPLATE_HANDOFF_NAME} requires installed template spec: '
f'{installed_spec}'
f'{TEMPLATE_HANDOFF_NAME} requires at least one installed '
f'template spec: {project_path / "templates"}/'
'design_spec.<kind>.<id>.md'
)
return data
def _installed_template_specs(project_path: Path) -> list[Path]:
"""Return every template spec installed into the project by the apply stage.
The apply stage installs one file per selected workspace, named
``design_spec.<kind>.<id>.md``. A bare ``design_spec.md`` under
``templates/`` means the project is itself a Create Template workspace and
is deliberately excluded here.
"""
return sorted(
path
for path in (project_path / 'templates').glob('design_spec.*.md')
if path.is_file()
)
def _complete_template_selection(project_path: Path) -> int:
"""Write the agent-owned handoff after Stage 1 and template application."""
confirm_dir = project_path / CONFIRM_DIR_NAME
@@ -695,11 +710,11 @@ def _complete_template_selection(project_path: Path) -> int:
)
return 1
if selection['mode'] == 'templates':
installed_spec = project_path / 'templates' / 'design_spec.md'
if not installed_spec.is_file():
if not _installed_template_specs(project_path):
logger.error(
'cannot complete template selection before template apply writes %s',
installed_spec,
'cannot complete template selection before template apply '
'writes %s/design_spec.<kind>.<id>.md',
project_path / 'templates',
)
return 1
@@ -2786,7 +2801,8 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument(
'--complete-template-selection', action='store_true',
help='Agent-only: after Stage 1, bind its template selection to a ready '
'handoff. Template mode requires <project>/templates/design_spec.md.',
'handoff. Template mode requires at least one '
'<project>/templates/design_spec.<kind>.<id>.md.',
)
parser.add_argument(
'--reset-template-selection', action='store_true',
@@ -1,51 +1,26 @@
# Chart Candidate Recall
# Legacy Chart Recall Compatibility
`chart_recall.py` gives Default Strategist or the Quick Generate main agent a bounded deterministic shortlist. It exposes the full live catalog only when the caller explicitly requests semantic review. It reads `templates/charts/charts_index.json` on every invocation, so the catalog remains the only template registry.
## Recall candidates
Describe one page's information shape with 3-8 concise English semantic tags. Translate source-language or industry terms into structural meaning before invoking the script.
`chart_recall.py` preserves the historical broad visualization-recall CLI for
existing projects and external callers. It delegates to
`visualization_recall.py`, reads the same live Chart/Table registries, and emits
the legacy bare-key JSON shape.
```bash
python3 skills/ppt-master/scripts/chart_recall.py recall \
--page P03 \
--tag "time series" \
--tag "three metrics" \
--tag "direction over time" \
--limit 6
--tag "direction over time"
python3 skills/ppt-master/scripts/chart_recall.py validate process_flow
```
`--limit` accepts 3-8 and defaults to 6. The JSON is already bounded and must be read unfiltered: `tail`, `head`, `grep`, or another truncator can discard higher-ranked candidates. `confidence` reports lexical strength only; it never decides whether a candidate fits. At `low` / `none`, a fitting bounded candidate needs no expansion, but `no_template_match.allowed` remains false until one explicit semantic fallback review.
The validation example intentionally uses the legacy bare Structure intent `process_flow`;
the shared resolver recognizes it as a Structure intent without an SVG path.
The wrapper retains the original string in its historical `valid` list and adds
a `resolved` item with `family`, `key`, and `kind`; a Structure intent omits
`reference` and `path`. New planning describes qualitative relationships in §IX
or Quick active context; it does not write a Structure catalog key.
Semantically review the bounded candidates. At `high` / `medium`, retain `no-template-match` when none fits. At `low` / `none`, select a fitting bounded candidate directly; otherwise rerun the same command once with `--semantic-fallback`, compare the returned rules semantically, and only then retain `no-template-match`. The full-catalog review is therefore a narrow low-confidence no-match gate, not a routine recall step. Do not open or maintain a second keyword/category index. `no-template-match` is an internal recall result, not a Design Spec §VII row.
| Field | Contract |
|---|---|
| `page` | Input `P<NN>` page key |
| `semantic_tags` | Deduplicated input tags |
| `confidence` | Lexical recall strength; never a selection decision |
| `candidates` | Ranked keys, SVG paths, verbatim catalog summaries, scores, and matched tags |
| `semantic_fallback` | Full live catalog, present only with `--semantic-fallback`; requires semantic comparison |
| `no_template_match` | Explicit fallback; `allowed` stays false for `low` / `none` until `--semantic-fallback` is used |
The scorer treats the key and the summary's Pick clause as positive evidence and the Skip clause as negative evidence. A term found only in Skip cannot make a candidate eligible, and Skip matches explicitly reduce a candidate's score. Unicode input is NFKC-normalized before matching. The active profile owner still applies semantic judgment: inspect the returned candidates, reject candidates whose Skip clause matches, and prefer the most specific valid structure. An empty or low-confidence shortlist requires one `--semantic-fallback` review only when the owner is about to keep `no-template-match`.
## Validate selected keys
Validate every selected template key before Default writes `design_spec.md §VII` / `spec_lock.md page_charts` or Quick opens that key for immediate use:
```bash
python3 skills/ppt-master/scripts/chart_recall.py validate line_chart quadrant_text_bullets
```
The command is read-only. It exits `0` when every key exists and `1` when any key is absent. A Default `no-template-match` page appears in neither §VII nor `page_charts`; record its chosen fallback in the page's §IX `Visualization` / `Layout`. Quick keeps the fallback only in active context and writes no mapping artifact.
## Selection boundary
- Preserve the two-lens review: numeric/data pages and structural-information pages.
- Default keeps §VII as a positive selection list: record `Page | Template | Usage` for each selected key, and omit the whole section when no candidate is selected.
- Default makes `Usage` one concise page-local purpose, not geometry or execution instructions; derive `templates/charts/<key>.svg` from the key and keep detailed adaptation in §IX.
- Quick retains the selected key and immediate page purpose only in active context; do not create §VII, `page_charts`, or a substitute mapping file.
- Never serialize `no-template-match`, an empty table, or a no-reference explanation into §VII.
- Do not serialize returned summaries, paths, or runners-up into new §VII tables. Legacy wider tables remain readable.
- Open the selected `<key>.svg` only as a reference for its mapped page; it does not lock type or geometry. Do not load unrelated catalog SVGs.
Do not use this wrapper in new planning prompts. See
[`visualization-recall.md`](./visualization-recall.md) for the canonical
`family/key` workflow and `page_visualizations` contract.
@@ -271,9 +271,9 @@ The Stage-1 submission writes this receipt and the Stage-1 `result.json`
together. Generate reads both exactly once after `--wait-only --wait-stage
stage1` returns. Free design skips installation. Template mode runs
`apply-template-workspace` against all selected roots and waits for complete
project-local installation/fusion. Only then does the agent complete the
handoff below. Installation resolves template-to-template ownership; Stage 2
later evaluates current-project fit from the installed copy. Strategist never
project-local installation. Only then does the agent complete the
handoff below. Installation copies each selected spec separately; Stage 2
resolves segment ownership and current-project fit from the installed set. Strategist never
reads the source roots.
### Agent handoff — `template_handoff.json`
@@ -395,7 +395,7 @@ Stage 2: its recommendations must include `template_application.value`.
### Input — `recommendations.stage1.json` (created beside template options)
Author this file before reading candidate index summaries in chat and without
reading any template spec, prototype, asset, fused segment owner, or template
reading any template spec, prototype, asset, segment owner, or template
canvas. `template_options.json` supplies display state only and never changes
the communication recommendation.
@@ -550,14 +550,43 @@ Markdown output.
when the converter derives a title-based filename.
## `rotate_images.py`
## Image Orientation Review
Fix image EXIF orientation in downloaded or imported assets.
Run this review when the user requests orientation correction, converted text
asks the reader to rotate the device, or a downloaded asset is visibly
sideways. EXIF and dimensions may trigger review, but they cannot determine the
semantic direction of pixels that are already stored sideways.
Generate a labeled static contact sheet. This command previews the first frame
after EXIF normalization and does not modify source images:
```bash
python3 scripts/rotate_images.py auto projects/xxx_files
python3 scripts/rotate_images.py gen projects/xxx_files
python3 scripts/rotate_images.py fix fixes.json
python3 ${SKILL_DIR}/scripts/rotate_images.py sheet <images_directory>
```
Use this when extracted photos appear sideways after conversion or import.
The default output is
`<images_directory>/../analysis/<directory>_orientation_contact_sheet.jpg`.
Inspect it with the current multimodal agent, identify only visually confirmed
rotations, and write a temporary JSON list. `rotation` is clockwise degrees and
must be `90`, `180`, or `270`:
```json
[
{"path": "/absolute/path/to/sideways.jpg", "rotation": 270}
]
```
Apply the confirmed fixes and regenerate image facts:
```bash
python3 ${SKILL_DIR}/scripts/rotate_images.py fix /tmp/orientation_fixes.json
python3 ${SKILL_DIR}/scripts/analyze_images.py <images_directory>
```
GIF files are excluded: `sheet` does not list them, and `fix` rejects a batch
that references one so all GIF files remain unchanged.
Do not infer a rotation from prose, EXIF, or aspect ratio alone, and do not
launch the HTML `gen` command in source intake. `auto` remains an in-place EXIF
normalizer. `gen` is a compatibility UI that runs the same normalization before
writing HTML; neither belongs to source intake.
@@ -0,0 +1,107 @@
# Visualization Candidate Recall
`visualization_recall.py` gives Default Strategist or the Quick Generate main
agent one bounded deterministic shortlist across the live Chart and Table
family registries. It exposes the selected full catalog only when the
caller explicitly requests semantic review. The tool reads these indexes on
every invocation and maintains no second category or keyword index:
- `templates/charts/charts_index.json`
- `templates/tables/tables_index.json`
Qualitative Structure does not enter recall. Default records its relationship
model in §IX; Quick keeps the same decision in active context. Both load
`executor-structure.md` and compose the shapes for the current page.
## Recall candidates
Describe one page's information shape with 3-8 concise English semantic tags.
Translate source-language or industry terms into structural meaning first.
```bash
python3 skills/ppt-master/scripts/visualization_recall.py recall \
--page P03 \
--tag "time series" \
--tag "three metrics" \
--tag "direction over time" \
--limit 6
```
Use `--family chart|table` only when the page semantics already make that
boundary certain; the default `all` preserves unified Chart/Table recall.
`--limit` accepts 3-8 and defaults to 6. Read the returned JSON unfiltered:
`tail`, `head`, `grep`, or another truncator can discard higher-ranked
candidates. `confidence` reports lexical strength only and never decides fit.
At `high` / `medium`, retain `no-template-match` when none fits. At `low` /
`none`, select a fitting bounded candidate directly; otherwise rerun the same
command once with `--semantic-fallback`, compare the returned selection rules
semantically, and only then retain `no-template-match`. The full-catalog review
is a narrow low-confidence no-match gate, not a routine recall step.
| Field | Contract |
|---|---|
| `page` | Input `P<NN>` page key |
| `family_filter` | Requested family or `all` |
| `semantic_tags` | Deduplicated input tags |
| `confidence` | Lexical recall strength; never a selection decision |
| `candidates` | Ranked family/key references, SVG paths, summaries, scores, and matched tags |
| `semantic_fallback` | Selected live catalogs, present only with `--semantic-fallback` |
| `no_template_match` | Explicit fallback; blocked at low/none until semantic fallback review |
The scorer treats the key and summary Pick clause as positive evidence and the
Skip clause as negative evidence. A term found only in Skip cannot make a
candidate eligible. Unicode input is NFKC-normalized before matching. The
active profile owner still applies semantic judgment and prefers the most
specific valid information structure.
## Validate selected references
Validate every selected reference before Default writes Design Spec §VII and
`spec_lock.md page_visualizations`, or before Quick opens it for immediate use:
```bash
python3 skills/ppt-master/scripts/visualization_recall.py validate \
chart/line_chart table/record_table
```
The command is read-only. It exits `0` when every supplied reference resolves
to a registered SVG and `1` otherwise. New planning supplies canonical
`family/key`. When validating an existing legacy mapping, opt into bare-key
resolution explicitly; every key must resolve uniquely:
```bash
python3 skills/ppt-master/scripts/visualization_recall.py validate \
--legacy-bare pros_cons_chart
```
A Default `no-template-match` page appears in neither §VII nor
`page_visualizations`; record its custom fallback in §IX. A qualitative
Structure is not a no-match case: describe its relationships in §IX and build
it without catalog lookup.
## Selection boundary
- Default records `Page | Family | Template | Usage` for each positive
selection and projects `family/key` into `page_visualizations`.
- Usage is one concise page-local purpose; detailed adaptation remains in §IX.
- Quick keeps the selected reference and purpose only in active context.
- Structure is a separate runtime information model, never a catalog candidate
or `page_visualizations` entry.
- Never serialize `no-template-match`, empty tables, summaries, paths, or
runners-up into planning artifacts.
- Open only the selected SVG for its mapped page. It is a flexible reference,
not a type, geometry, style, or native-replacement lock.
## Legacy compatibility
`chart_recall.py` remains a compatibility wrapper for existing callers. It
uses the same scorer and live Chart/Table registries, preserves bare-key
validation and the historical JSON shape, and resolves live candidates to their
current family paths. New prompts and automation use `visualization_recall.py`.
When `validate --legacy-bare` receives one of the 36 retired canonical Structure
bare keys, the shared resolver returns `kind=legacy-structure-intent`,
`family=structure`, and the original key without `path` or `reference`. This
compatibility result is a semantic hint for old `page_charts`, not a live catalog
entry, and never participates in recall.
@@ -204,7 +204,7 @@ def generate_examples_index(examples_dir: str = 'examples') -> str:
content.append(
f"- [Role Definitions]({skill_link(skill_dir / 'references')})")
content.append(
f"- [Chart Templates]({skill_link(skill_dir / 'templates' / 'charts' / 'README.md')})\n")
f"- [Visualization Templates]({skill_link(skill_dir / 'templates' / 'README.md')})\n")
# Footer
content.append("---\n")
@@ -1714,7 +1714,7 @@ def build_parser() -> argparse.ArgumentParser:
fingerprint.add_argument(
"--subtitle-dir",
default=None,
help="Page-local SRT directory; default: <project>/notes/subtitles",
help="Page-local SRT directory; default: <project>/audio",
)
animations = subparsers.add_parser(
@@ -1735,7 +1735,7 @@ def build_parser() -> argparse.ArgumentParser:
animations.add_argument(
"--subtitle-dir",
default=None,
help="Page-local SRT directory; default: <project>/notes/subtitles",
help="Page-local SRT directory; default: <project>/audio",
)
animations.add_argument(
"--audio-dir",
@@ -1779,7 +1779,7 @@ def build_parser() -> argparse.ArgumentParser:
subtitles.add_argument(
"--subtitle-dir",
default=None,
help="Page-local SRT directory; default: <project>/notes/subtitles",
help="Page-local SRT directory; default: <project>/audio",
)
subtitles.add_argument(
"--video",
@@ -1801,7 +1801,7 @@ def build_parser() -> argparse.ArgumentParser:
"-o",
"--output",
default=None,
help="Merged SRT output; default: <project>/notes/subtitles/total.srt",
help="Merged SRT output; default: <project>/audio/total.srt",
)
subtitles.add_argument(
"--force",
@@ -1823,7 +1823,7 @@ def main(argv: list[str] | None = None) -> int:
subtitle_dir = _project_path(
project_path,
args.subtitle_dir,
Path("notes/subtitles"),
Path("audio"),
)
slide_names = _page_subtitle_names(subtitle_dir)
for slide_name in slide_names:
@@ -1845,7 +1845,7 @@ def main(argv: list[str] | None = None) -> int:
subtitle_dir = _project_path(
project_path,
args.subtitle_dir,
Path("notes/subtitles"),
Path("audio"),
)
audio_dir = _project_path(
project_path,
@@ -1881,7 +1881,7 @@ def main(argv: list[str] | None = None) -> int:
subtitle_dir = _project_path(
project_path,
args.subtitle_dir,
Path("notes/subtitles"),
Path("audio"),
)
audio_dir = _project_path(
project_path,
@@ -1896,7 +1896,7 @@ def main(argv: list[str] | None = None) -> int:
output_path = _project_path(
project_path,
args.output,
Path("notes/subtitles/total.srt"),
Path("audio/total.srt"),
)
result = _merge_subtitles_result(
project_path,
@@ -4,6 +4,9 @@
This script uses provider backends for the same per-slide output contract on
macOS, Linux, and Windows. `edge-tts` remains the default no-key backend and
also writes one compact, word-timed SRT file per slide from the same TTS stream.
MiniMax and CosyVoice request word timings; ElevenLabs requests character
alignment. All four apply the same compact, text-faithful cue regrouping.
Qwen remains audio-only because its current TTS API exposes no timestamps.
Usage:
python3 skills/ppt-master/scripts/notes_to_audio.py <project_path> --voice zh-CN-XiaoxiaoNeural
@@ -26,11 +29,13 @@ from __future__ import annotations
import argparse
import asyncio
import hashlib
import json
import os
import re
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from console_encoding import configure_utf8_stdio
@@ -43,6 +48,7 @@ from tts_backends import (
backend_minimax,
backend_qwen,
)
from tts_backends.backend_common import temporary_path
configure_utf8_stdio()
@@ -220,6 +226,108 @@ def _remove_stale_audio_variants(output_path: Path) -> None:
candidate.unlink()
def _sha256_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _provider_manifest_details(
args: argparse.Namespace,
backend: AudioBackend,
) -> tuple[str, dict[str, object]]:
if backend.provider == "edge":
return "edge-tts", {
"rate": args.rate,
}
if backend.provider == "elevenlabs":
return args.elevenlabs_model, {
"stability": args.elevenlabs_stability,
"similarity_boost": args.elevenlabs_similarity_boost,
"style": args.elevenlabs_style,
"speed": args.elevenlabs_speed,
"speaker_boost": args.elevenlabs_speaker_boost,
}
if backend.provider == "minimax":
return args.minimax_model, {
"speed": args.minimax_speed,
"volume": args.minimax_volume,
"pitch": args.minimax_pitch,
"language_boost": args.minimax_language_boost,
}
if backend.provider == "qwen":
return args.qwen_model, {
"language_type": args.qwen_language_type,
"optimize_instructions": args.qwen_optimize_instructions,
"custom_instructions": True if args.qwen_instructions else None,
}
return args.cosyvoice_model, {
"volume": args.cosyvoice_volume,
"rate": args.cosyvoice_rate,
"pitch": args.cosyvoice_pitch,
"language_hint": args.cosyvoice_language_hint,
"custom_instruction": True if args.cosyvoice_instruction else None,
}
def _narration_manifest(
args: argparse.Namespace,
backend: AudioBackend,
*,
writes_subtitles: bool,
) -> dict[str, object]:
model, raw_settings = _provider_manifest_details(args, backend)
settings = {
key: value
for key, value in raw_settings.items()
if value is not None
}
voice_ref = backend.voice_id
if backend.provider != "edge":
voice_ref = f"sha256:{_sha256_text(voice_ref)}"
manifest: dict[str, object] = {
"schema": "ppt-master.narration.v1",
"generated_at": datetime.now(timezone.utc).isoformat(
timespec="seconds"
).replace("+00:00", "Z"),
"provider": backend.provider,
"model": model,
"voice_ref": voice_ref,
"audio_format": backend.extension.lstrip("."),
}
if settings:
manifest["settings"] = settings
if writes_subtitles:
manifest["subtitles"] = {
"format": "srt",
"timing": (
"character" if backend.provider == "elevenlabs" else "word"
),
"max_visible_chars": args.subtitle_max_chars,
}
return manifest
def _publish_manifest(path: Path, manifest: dict[str, object]) -> None:
descriptor, staged_path = temporary_path(path, ".tmp")
try:
with os.fdopen(
descriptor,
"w",
encoding="utf-8",
newline="\n",
) as stream:
descriptor = -1
json.dump(manifest, stream, ensure_ascii=False, indent=2)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(staged_path, path)
finally:
if descriptor >= 0:
os.close(descriptor)
staged_path.unlink(missing_ok=True)
async def _generate_edge_jobs(
jobs: list[AudioJob],
subtitle_dir: Path,
@@ -293,7 +401,7 @@ def main() -> int:
"--subtitle-max-chars",
type=int,
default=backend_edge.DEFAULT_SUBTITLE_MAX_CHARS,
help="maximum visible characters per Edge subtitle cue (default: 20)",
help="maximum visible characters per provider-timed subtitle cue (default: 20)",
)
parser.add_argument(
"--elevenlabs-api-key-env",
@@ -328,6 +436,12 @@ def main() -> int:
default=None,
help="optional ElevenLabs style exaggeration override, 0.0-1.0",
)
parser.add_argument(
"--elevenlabs-speed",
type=float,
default=None,
help="optional ElevenLabs speaking speed override, 0.7-1.2",
)
parser.add_argument(
"--elevenlabs-speaker-boost",
action=argparse.BooleanOptionalAction,
@@ -377,6 +491,7 @@ def main() -> int:
parser.add_argument("--cosyvoice-output-format", default="mp3", choices=["mp3", "wav"],
help="CosyVoice audio format for PPT narration (default: mp3)")
parser.add_argument("--cosyvoice-sample-rate", type=int, default=24000,
choices=[8000, 16000, 22050, 24000, 44100, 48000],
help="CosyVoice sample rate (default: 24000)")
parser.add_argument("--cosyvoice-volume", type=int, default=None,
help="optional CosyVoice volume, 0-100")
@@ -388,6 +503,14 @@ def main() -> int:
help="optional CosyVoice instruction text for supported voices/models")
parser.add_argument("--cosyvoice-language-hint", default=None,
help="optional CosyVoice language hint, e.g. zh, en, ja")
parser.add_argument(
"--cosyvoice-audio-only",
action="store_true",
help=(
"skip CosyVoice timestamps/SRT for a model or voice that does not "
"support them"
),
)
parser.add_argument("--list-common-voices", action="store_true", help="print a curated voice list and exit")
parser.add_argument("--list-voices", action="store_true", help="query provider voices and exit")
parser.add_argument("--locale", default=None, help='filter --list-voices by locale, e.g. "zh-CN"')
@@ -441,6 +564,40 @@ def main() -> int:
parser.error("--concurrency must be at least 1")
raise AssertionError("unreachable")
for option, value, minimum, maximum in (
("--elevenlabs-stability", args.elevenlabs_stability, 0.0, 1.0),
(
"--elevenlabs-similarity-boost",
args.elevenlabs_similarity_boost,
0.0,
1.0,
),
("--elevenlabs-style", args.elevenlabs_style, 0.0, 1.0),
("--elevenlabs-speed", args.elevenlabs_speed, 0.7, 1.2),
("--cosyvoice-volume", args.cosyvoice_volume, 0, 100),
("--cosyvoice-rate", args.cosyvoice_rate, 0.5, 2.0),
("--cosyvoice-pitch", args.cosyvoice_pitch, 0.5, 2.0),
):
if value is not None and not minimum <= value <= maximum:
parser.error(f"{option} must be between {minimum} and {maximum}")
raise AssertionError("unreachable")
if args.cosyvoice_audio_only and args.provider != "cosyvoice":
parser.error("--cosyvoice-audio-only requires --provider cosyvoice")
raise AssertionError("unreachable")
if args.provider == "qwen" and args.qwen_instructions:
if "instruct" not in args.qwen_model:
parser.error(
"--qwen-instructions requires a Qwen3 TTS Instruct model"
)
raise AssertionError("unreachable")
if args.qwen_optimize_instructions and not args.qwen_instructions:
parser.error(
"--qwen-optimize-instructions requires --qwen-instructions"
)
raise AssertionError("unreachable")
if args.provider == "elevenlabs":
if not voice_id:
parser.error("--voice-id is required for --provider elevenlabs")
@@ -479,14 +636,31 @@ def main() -> int:
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
backend = AudioBackend(provider=args.provider, extension=extension, api_key=api_key, voice_id=voice_id)
backend = AudioBackend(
provider=args.provider,
extension=extension,
api_key=api_key,
voice_id=voice_id,
)
else:
backend = AudioBackend(provider=args.provider, extension=backend_edge.edge_output_extension(), voice_id=args.voice)
backend = AudioBackend(
provider=args.provider,
extension=backend_edge.edge_output_extension(),
voice_id=args.voice,
)
project = args.project_path
notes_dir = project / "notes"
output_dir = args.output or (project / "audio")
subtitle_dir = notes_dir / "subtitles"
subtitle_dir = output_dir
writes_subtitles = backend.provider in {
"edge",
"elevenlabs",
"minimax",
} or (
backend.provider == "cosyvoice"
and not args.cosyvoice_audio_only
)
try:
note_roster = _expected_note_roster(project)
@@ -502,8 +676,13 @@ def main() -> int:
return 2
output_dir.mkdir(parents=True, exist_ok=True)
if backend.provider == "edge":
subtitle_dir.mkdir(parents=True, exist_ok=True)
manifest_path = output_dir / "manifest.json"
try:
manifest_path.unlink(missing_ok=True)
(output_dir / "total.srt").unlink(missing_ok=True)
except OSError as exc:
print(f"error: failed to clear stale narration metadata: {exc}", file=sys.stderr)
return 1
generated = 0
if backend.provider == "edge":
@@ -553,8 +732,11 @@ def main() -> int:
for job in jobs:
output_path = job.output_path
text = job.text
subtitle_path: Path | None = None
try:
if backend.provider == "elevenlabs":
if writes_subtitles:
subtitle_path = subtitle_dir / f"{output_path.stem}.srt"
backend_elevenlabs.generate(
text,
output_path,
@@ -565,9 +747,14 @@ def main() -> int:
stability=args.elevenlabs_stability,
similarity_boost=args.elevenlabs_similarity_boost,
style=args.elevenlabs_style,
speed=args.elevenlabs_speed,
speaker_boost=args.elevenlabs_speaker_boost,
subtitle_path=subtitle_path,
subtitle_max_chars=args.subtitle_max_chars,
)
elif backend.provider == "minimax":
if writes_subtitles:
subtitle_path = subtitle_dir / f"{output_path.stem}.srt"
backend_minimax.generate(
text,
output_path,
@@ -583,6 +770,8 @@ def main() -> int:
pitch=args.minimax_pitch,
language_boost=args.minimax_language_boost,
base_url=args.minimax_base_url,
subtitle_path=subtitle_path,
subtitle_max_chars=args.subtitle_max_chars,
)
elif backend.provider == "qwen":
backend_qwen.generate(
@@ -597,6 +786,8 @@ def main() -> int:
base_url=args.qwen_base_url,
)
elif backend.provider == "cosyvoice":
if writes_subtitles:
subtitle_path = subtitle_dir / f"{output_path.stem}.srt"
backend_cosyvoice.generate(
text,
output_path,
@@ -611,21 +802,41 @@ def main() -> int:
instruction=args.cosyvoice_instruction,
language_hint=args.cosyvoice_language_hint,
base_url=args.cosyvoice_base_url,
subtitle_path=subtitle_path,
subtitle_max_chars=args.subtitle_max_chars,
)
_remove_stale_audio_variants(output_path)
if not writes_subtitles:
output_path.with_suffix(".srt").unlink(missing_ok=True)
except Exception as exc:
print(f"error: failed to generate {output_path}: {exc}", file=sys.stderr)
return 1
generated += 1
print(f"[OK] {output_path}")
if subtitle_path is not None:
print(f" {subtitle_path}")
if backend.provider == "edge":
try:
_publish_manifest(
manifest_path,
_narration_manifest(
args,
backend,
writes_subtitles=writes_subtitles,
),
)
except (OSError, RuntimeError) as exc:
print(f"error: failed to publish narration manifest: {exc}", file=sys.stderr)
return 1
if writes_subtitles:
print(
f"[Done] Generated {generated}/{len(note_roster)} audio/SRT pair(s): "
f"{output_dir} + {subtitle_dir}"
f"{output_dir}"
)
else:
print(f"[Done] Generated {generated}/{len(note_roster)} audio file(s): {output_dir}")
print(f"[REPORT] Narration manifest: {manifest_path}")
return 0
@@ -26,7 +26,6 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Iterable
from .paths import CHARTS_DIR as _CHARTS_DIR
from .paths import SKILL_DIR as _SKILL_DIR
from .project_specs import (
default_spec_lock_forbidden,
@@ -39,6 +38,13 @@ from svg_to_pptx.pptx_package.template_structure import (
TemplateStructureError,
load_pptx_structure_lock,
)
from visualization_catalog import (
LEGACY_STRUCTURE_INTENT_KIND,
VISUALIZATION_SVG_KIND,
VisualizationCatalogError,
VisualizationEntry,
resolve_visualization_reference,
)
PAGE_CONTEXT_SCHEMA = "ppt-master.page-context.v2"
@@ -413,24 +419,55 @@ def _reference_payload(
return payload
def _chart_reference(chart_key: str) -> tuple[dict[str, str], Path]:
"""Resolve one locked chart key to the shared Skill catalog."""
if Path(chart_key).name != chart_key or not chart_key:
raise PageContextError(f"invalid page_charts key: {chart_key!r}")
chart_path = (_CHARTS_DIR / f"{chart_key}.svg").resolve()
if not chart_path.is_file():
raise PageContextError(
f"page_charts key {chart_key!r} has no shared SVG reference"
def _visualization_reference(
value: str,
*,
allow_legacy_bare: bool,
) -> tuple[dict[str, str] | None, Path | None, VisualizationEntry]:
"""Resolve one live asset or one legacy intent-only Structure key."""
source_section = "page_charts" if allow_legacy_bare else "page_visualizations"
try:
entry = resolve_visualization_reference(
value,
allow_legacy_bare=allow_legacy_bare,
)
return (
_reference_payload(
"chart-svg",
chart_path,
scope="skill",
display_path=f"templates/charts/{chart_path.name}",
),
chart_path,
except VisualizationCatalogError as exc:
raise PageContextError(
f"{source_section} value {value!r} cannot resolve a visualization: {exc}"
) from exc
if entry.kind == LEGACY_STRUCTURE_INTENT_KIND:
if not allow_legacy_bare or entry.path is not None:
raise PageContextError(
f"{source_section} value {value!r} has an invalid legacy "
"Structure intent resolution"
)
return None, None, entry
if entry.kind != VISUALIZATION_SVG_KIND or entry.path is None:
raise PageContextError(
f"{source_section} value {value!r} resolves to unsupported kind "
f"{entry.kind!r}"
)
path = Path(entry.path).resolve()
try:
display_path = path.relative_to(_SKILL_DIR.resolve()).as_posix()
except ValueError as exc:
raise PageContextError(
f"{source_section} value {value!r} resolves outside the Skill: {path}"
) from exc
payload = _reference_payload(
entry.kind,
path,
scope="skill",
display_path=display_path,
)
payload.update(
{
"reference": entry.reference,
"family": entry.family,
"key": entry.key,
}
)
return payload, path, entry
def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
@@ -475,7 +512,16 @@ def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
if rhythm is None:
rhythm = "dense"
warnings.append(f"page_rhythm has no {page}; using compatibility default dense")
chart_key = _section_fields(lock_sections, "page_charts").get(page)
visualization_value = _section_fields(
lock_sections,
"page_visualizations",
).get(page)
legacy_chart_key = _section_fields(lock_sections, "page_charts").get(page)
if visualization_value is not None and legacy_chart_key is not None:
raise PageContextError(
f"{page} is declared in both page_visualizations and legacy "
"page_charts; keep only page_visualizations"
)
try:
structure_lock = load_pptx_structure_lock(project_path)
except TemplateStructureError as exc:
@@ -545,10 +591,22 @@ def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
display_path=_relative_project_path(project_path, prototype_path),
)
)
if chart_key is not None:
chart_reference, chart_path = _chart_reference(chart_key)
inputs.append(chart_path)
reference_set.append(chart_reference)
visualization_entry: VisualizationEntry | None = None
selected_visualization = (
visualization_value
if visualization_value is not None
else legacy_chart_key
)
if selected_visualization is not None:
visualization_reference, visualization_path, visualization_entry = (
_visualization_reference(
selected_visualization,
allow_legacy_bare=visualization_value is None,
)
)
if visualization_path is not None and visualization_reference is not None:
inputs.append(visualization_path)
reference_set.append(visualization_reference)
mode_fields = _section_fields(lock_sections, "mode")
visual_style_fields = _section_fields(lock_sections, "visual_style")
# Each on-demand projection includes bounded lock anchors; large reference
@@ -579,8 +637,18 @@ def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
"rhythm": rhythm,
"image_selection": image_selection,
}
if chart_key is not None:
current_page["chart"] = chart_key
if (
visualization_entry is not None
and visualization_entry.kind == VISUALIZATION_SVG_KIND
):
current_page["visualization"] = visualization_entry.reference
elif (
visualization_entry is not None
and visualization_entry.kind == LEGACY_STRUCTURE_INTENT_KIND
):
current_page["structure_intent"] = visualization_entry.key
if legacy_chart_key is not None:
current_page["chart"] = legacy_chart_key
if selected_images:
current_page["images"] = selected_images
if template is not None:
@@ -49,6 +49,25 @@ except ImportError:
validate_communication_trace,
)
try:
from visualization_catalog import (
LEGACY_STRUCTURE_INTENT_KIND,
VISUALIZATION_SVG_KIND,
VisualizationCatalogError,
resolve_visualization_reference,
)
except ImportError:
import sys
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from visualization_catalog import ( # type: ignore
LEGACY_STRUCTURE_INTENT_KIND,
VISUALIZATION_SVG_KIND,
VisualizationCatalogError,
resolve_visualization_reference,
)
TOOLS_DIR = SCRIPTS_DIR
@@ -1017,6 +1036,8 @@ def _validate_spec_lock_relations(
layouts = fields("pptx_layouts")
page_pptx_layouts = fields("page_pptx_layouts")
page_layouts = fields("page_layouts")
page_visualizations = fields("page_visualizations")
legacy_page_charts = fields("page_charts")
structure = fields("pptx_structure")
for layout_key, raw_value in layouts.items():
@@ -1059,14 +1080,71 @@ def _validate_spec_lock_relations(
f"pages {', '.join(extra)}"
)
chart_pages = set(fields("page_charts"))
unknown_chart_pages = sorted(chart_pages - set(rhythm))
if unknown_chart_pages:
overlapping_visualization_pages = sorted(
set(page_visualizations) & set(legacy_page_charts)
)
if overlapping_visualization_pages:
errors.append(
f"{markdown_name} schema: page_charts has unknown pages "
f"{', '.join(unknown_chart_pages)}"
f"{markdown_name} schema: pages "
f"{', '.join(overlapping_visualization_pages)} are declared in both "
"page_visualizations and legacy page_charts; keep only "
"page_visualizations"
)
for section_id, mapping, allow_legacy_bare in (
("page_visualizations", page_visualizations, False),
("page_charts", legacy_page_charts, True),
):
unknown_pages = sorted(set(mapping) - set(rhythm))
if unknown_pages:
errors.append(
f"{markdown_name} schema: {section_id} has unknown pages "
f"{', '.join(unknown_pages)}"
)
for page_key, raw_reference in mapping.items():
reference = _normalize_schema_value(raw_reference)
try:
entry = resolve_visualization_reference(
reference,
allow_legacy_bare=allow_legacy_bare,
)
except VisualizationCatalogError as exc:
errors.append(
f"{markdown_name} schema: {section_id}.{page_key} "
f"cannot resolve visualization {reference!r}: {exc}"
)
continue
if entry.kind == LEGACY_STRUCTURE_INTENT_KIND:
if section_id != "page_charts" or not allow_legacy_bare:
errors.append(
f"{markdown_name} schema: {section_id}.{page_key} resolves "
"to a legacy Structure intent outside page_charts"
)
if entry.path is not None:
errors.append(
f"{markdown_name} schema: {section_id}.{page_key} legacy "
"Structure intent unexpectedly has an asset path"
)
continue
if entry.kind != VISUALIZATION_SVG_KIND:
errors.append(
f"{markdown_name} schema: {section_id}.{page_key} resolves "
f"to unsupported kind {entry.kind!r}"
)
continue
if entry.path is None:
errors.append(
f"{markdown_name} schema: {section_id}.{page_key} does not "
"resolve to an SVG asset path"
)
continue
asset_path = Path(entry.path)
if asset_path.suffix.casefold() != ".svg" or not asset_path.is_file():
errors.append(
f"{markdown_name} schema: {section_id}.{page_key} does not "
f"resolve to an SVG asset at {asset_path}"
)
info = get_project_info_common(str(markdown_path.parent))
format_key = str(info.get("format", "unknown"))
canvas = CANVAS_FORMATS.get(format_key)
@@ -9,10 +9,11 @@
"AGENTS.md",
"skills/ppt-master/**/*.md",
"skills/ppt-master/templates/charts/charts_index.json",
"skills/ppt-master/templates/tables/tables_index.json",
"skills/ppt-master/templates/schemas/*.json"
],
"exclude": [],
"max_tokens": 450000
"max_tokens": 500000
},
"file_budgets": {
"AGENTS.md": 2750,
@@ -20,6 +21,9 @@
"skills/ppt-master/references/executor-base.md": 9250,
"skills/ppt-master/references/executor-structured.md": 5500,
"skills/ppt-master/references/executor-chart.md": 3500,
"skills/ppt-master/references/executor-visualization.md": 1250,
"skills/ppt-master/references/executor-structure.md": 1250,
"skills/ppt-master/references/executor-table.md": 1000,
"skills/ppt-master/references/executor-image.md": 2000,
"skills/ppt-master/references/executor-web-image.md": 750,
"skills/ppt-master/references/executor-notes.md": 1000,
@@ -199,6 +203,7 @@
"skills/ppt-master/references/svg-effects.md",
"skills/ppt-master/references/native-shape-authoring.md",
"skills/ppt-master/references/semantic-svg.md",
"skills/ppt-master/references/executor-structure.md",
"skills/ppt-master/references/modes/_index.md",
"skills/ppt-master/references/visual-styles/_index.md",
{
@@ -307,18 +312,19 @@
"files": [],
"max_tokens": 92000
},
"route.generate.quick-generate.chart": {
"description": "Quick Generate with bounded chart recall, chart/table authoring, optional native-data metadata, and in-context coordinate verification before the final checker.",
"route.generate.quick-generate.visualization": {
"description": "Quick Generate with bounded Chart/Table recall, their data-object branches, optional native-data metadata, and chart-coordinate verification before the final checker; the fixed Quick context already carries Shape Composition Grammar.",
"scope": "cumulative",
"include": [
"route.generate.quick-generate",
"stage.generate.executor.visualization",
"stage.generate.executor.chart",
"stage.generate.executor.table",
"stage.generate.executor.native-data",
"stage.generate.chart-authoring",
"stage.generate.verify-charts"
],
"files": [
"skills/ppt-master/scripts/docs/chart-recall.md"
"skills/ppt-master/scripts/docs/visualization-recall.md"
],
"max_tokens": 105000
},
@@ -369,7 +375,7 @@
"skills/ppt-master/workflows/stages/apply-template-workspace.md",
"skills/ppt-master/templates/README.md"
],
"max_tokens": 5000
"max_tokens": 6000
},
"stage.generate.template.brand": {
"description": "Conditional Brand workspace application with one selected Brand spec.",
@@ -483,7 +489,7 @@
"load_event": "template-application"
}
],
"max_tokens": 16000
"max_tokens": 19000
},
"route.generate.flat-no-image": {
"description": "Default fixed Generate-PPTX context with no image-acquisition role.",
@@ -554,14 +560,16 @@
"files": [],
"max_tokens": 130000
},
"route.generate.flat-no-image-chart": {
"description": "Default flat Generate-PPTX path with chart authoring, native-data replacement, and verification.",
"route.generate.flat-no-image-visualization": {
"description": "Default flat Generate-PPTX path with visualization-reference consumption, all three information-model branches, optional native-data replacement, and chart verification.",
"scope": "cumulative",
"include": [
"route.generate.flat-no-image",
"stage.generate.executor.visualization",
"stage.generate.executor.chart",
"stage.generate.executor.structure",
"stage.generate.executor.table",
"stage.generate.executor.native-data",
"stage.generate.chart-authoring",
"stage.generate.verify-charts"
],
"files": [],
@@ -769,12 +777,13 @@
],
"max_tokens": 7000
},
"stage.generate.chart-authoring": {
"description": "Conditional visualization-library authoring guidance.",
"maintenance.visualization-authoring": {
"description": "Shared chart and table catalog-template authoring guidance; not a Generate runtime load.",
"scope": "incremental",
"files": [
"skills/ppt-master/templates/VISUALIZATION_TEMPLATE_AUTHORING.md",
"skills/ppt-master/templates/charts/README.md",
"skills/ppt-master/templates/charts/CHART_STYLE_GUIDE.md"
"skills/ppt-master/templates/tables/README.md"
],
"max_tokens": 10000
},
@@ -884,13 +893,37 @@
"max_tokens": 88000
},
"stage.generate.executor.chart": {
"description": "Conditional chart/table page execution rules.",
"description": "Conditional value-driven chart execution rules.",
"scope": "incremental",
"files": [
"skills/ppt-master/references/executor-chart.md"
],
"max_tokens": 3500
},
"stage.generate.executor.visualization": {
"description": "Conditional selected visualization-reference resolution and flexible page-local adaptation rules.",
"scope": "incremental",
"files": [
"skills/ppt-master/references/executor-visualization.md"
],
"max_tokens": 1250
},
"stage.generate.executor.structure": {
"description": "Conditional page-local qualitative-topology construction rules.",
"scope": "incremental",
"files": [
"skills/ppt-master/references/executor-structure.md"
],
"max_tokens": 1250
},
"stage.generate.executor.table": {
"description": "Conditional semantic cell-grid construction rules.",
"scope": "incremental",
"files": [
"skills/ppt-master/references/executor-table.md"
],
"max_tokens": 1000
},
"stage.generate.strategist.image": {
"description": "Conditional Strategist image/formula policy with the always-read layout math and compact composition vocabulary.",
"scope": "incremental",
@@ -1090,6 +1123,51 @@
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/shared-standards-core.md"
},
{
"concern": "visualization-reference-routing",
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/executor-visualization.md"
},
{
"concern": "structured-executor-routing",
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/executor-structured.md"
},
{
"concern": "chart-executor-routing",
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/executor-chart.md"
},
{
"concern": "structure-executor-routing",
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/executor-structure.md"
},
{
"concern": "table-executor-routing",
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/executor-table.md"
},
{
"concern": "native-data-routing",
"from": "skills/ppt-master/references/executor-base.md",
"to": "skills/ppt-master/references/native-data-interface.md"
},
{
"concern": "visualization-chart-routing",
"from": "skills/ppt-master/references/executor-visualization.md",
"to": "skills/ppt-master/references/executor-chart.md"
},
{
"concern": "visualization-structure-routing",
"from": "skills/ppt-master/references/executor-visualization.md",
"to": "skills/ppt-master/references/executor-structure.md"
},
{
"concern": "visualization-table-routing",
"from": "skills/ppt-master/references/executor-visualization.md",
"to": "skills/ppt-master/references/executor-table.md"
},
{
"concern": "structured-execution-contract",
"from": "skills/ppt-master/references/executor-structured.md",
@@ -1100,6 +1178,16 @@
"from": "skills/ppt-master/references/executor-chart.md",
"to": "skills/ppt-master/references/native-data-interface.md"
},
{
"concern": "chart-verification-handoff",
"from": "skills/ppt-master/references/executor-chart.md",
"to": "skills/ppt-master/workflows/stages/verify-charts.md"
},
{
"concern": "native-table-contract",
"from": "skills/ppt-master/references/executor-table.md",
"to": "skills/ppt-master/references/native-data-interface.md"
},
{
"concern": "strategist-image-routing",
"from": "skills/ppt-master/references/strategist.md",
@@ -1249,6 +1337,26 @@
"type",
"types"
]
},
{
"name": "table-templates",
"kind": "json_collection",
"source": "skills/ppt-master/templates/tables/tables_index.json",
"key": "tables",
"reference_terms": [
"tables_index.json",
"table catalog"
],
"claim_nouns": [
"table",
"tables",
"summary",
"summaries",
"template",
"templates",
"type",
"types"
]
}
],
"schema_grammars": [
@@ -1257,6 +1365,7 @@
"fields": [
"font_family",
"image_rendering",
"page_visualizations",
"page_charts",
"page_layouts",
"page_pptx_layouts",
@@ -1339,6 +1448,10 @@
"glob": "skills/ppt-master/scripts/docs/update_spec.md",
"reason": "Maintainer command reference for an optional helper script."
},
{
"glob": "skills/ppt-master/scripts/docs/chart-recall.md",
"reason": "Legacy compatibility note; current generation loads visualization-recall.md."
},
{
"glob": "skills/ppt-master/scripts/pptx_shapes/data/NOTICE.md",
"reason": "License notice; never loaded as prompt context."
@@ -1353,7 +1466,11 @@
},
{
"glob": "skills/ppt-master/templates/charts/charts_index.json",
"reason": "Machine-consumed chart-recall registry; Strategist receives only the ranked candidate summaries."
"reason": "Machine-consumed chart-family registry; Strategist receives only bounded visualization-recall candidates."
},
{
"glob": "skills/ppt-master/templates/tables/tables_index.json",
"reason": "Machine-consumed table-family registry; Strategist receives only bounded visualization-recall candidates."
},
{
"glob": "skills/ppt-master/templates/schemas/*.json",
@@ -6,6 +6,7 @@ Provides visual image orientation filtering, fix code generation,
and batch image rotation functionality.
Usage:
python3 scripts/rotate_images.py sheet <images_directory>
python3 scripts/rotate_images.py gen <images_directory>
python3 scripts/rotate_images.py fix <fixes.json>
python3 scripts/rotate_images.py auto <images_directory>
@@ -23,7 +24,13 @@ from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
from PIL import Image, ExifTags
from PIL import (
ExifTags,
Image,
ImageDraw,
ImageFont,
ImageOps,
)
ORIENTATION_TAG_ID = 274 # 0x0112
@@ -123,6 +130,153 @@ class ImageRotator:
return fixed_count
def generate_contact_sheet(
self,
target_dir: Union[str, Path],
output_path: Optional[Union[str, Path]] = None,
) -> str:
"""Generate a labeled, read-only contact sheet for visual review."""
target_path = Path(target_dir).resolve()
if not target_path.is_dir():
raise FileNotFoundError(f"Directory not found: {target_path}")
valid_exts = {
'.bmp', '.jpeg', '.jpg', '.png', '.tif', '.tiff', '.webp'
}
files = sorted(
(
path for path in target_path.iterdir()
if path.is_file() and path.suffix.lower() in valid_exts
),
key=lambda path: self._natural_sort_key(path.name),
)
if not files:
raise ValueError("No image files found")
columns = 6
thumbnail_width = 180
thumbnail_height = 180
label_height = 34
padding = 12
cell_width = thumbnail_width + padding * 2
cell_height = thumbnail_height + label_height + padding * 2
rows = (len(files) + columns - 1) // columns
sheet = Image.new(
'RGB',
(columns * cell_width, rows * cell_height),
'white',
)
draw = ImageDraw.Draw(sheet)
try:
font = ImageFont.truetype('DejaVuSans.ttf', 13)
except OSError:
font = ImageFont.load_default()
resampling = getattr(Image, 'Resampling', Image)
for index, file_path in enumerate(files):
column = index % columns
row = index // columns
cell_x = column * cell_width
cell_y = row * cell_height
draw.rectangle(
(
cell_x,
cell_y,
cell_x + cell_width - 1,
cell_y + cell_height - 1,
),
outline='#D0D7DE',
)
details = 'unreadable'
try:
with Image.open(file_path) as img:
if getattr(img, 'is_animated', False):
img.seek(0)
prepared = ImageOps.exif_transpose(img)
try:
source_width, source_height = prepared.size
preview = prepared.convert('RGBA')
finally:
if prepared is not img:
prepared.close()
preview.thumbnail(
(thumbnail_width, thumbnail_height),
resampling.LANCZOS,
)
preview_x = cell_x + padding + (thumbnail_width - preview.width) // 2
preview_y = cell_y + padding + (thumbnail_height - preview.height) // 2
sheet.paste(preview, (preview_x, preview_y), preview)
preview.close()
details = f'{source_width}x{source_height}'
except (
EOFError,
OSError,
SyntaxError,
ValueError,
Image.DecompressionBombError,
):
draw.line(
(
cell_x + padding,
cell_y + padding,
cell_x + cell_width - padding,
cell_y + thumbnail_height,
),
fill='#CF222E',
width=2,
)
draw.line(
(
cell_x + cell_width - padding,
cell_y + padding,
cell_x + padding,
cell_y + thumbnail_height,
),
fill='#CF222E',
width=2,
)
label = file_path.name
if len(label) > 30:
label = f'{label[:13]}...{label[-14:]}'
label_y = cell_y + padding + thumbnail_height + 2
for line_index, text in enumerate((label, details)):
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
draw.text(
(
cell_x + (cell_width - text_width) / 2,
label_y + line_index * 15,
),
text,
fill='#24292F',
font=font,
)
if output_path is None:
resolved_output = (
target_path.parent
/ 'analysis'
/ f'{target_path.name}_orientation_contact_sheet.jpg'
)
else:
resolved_output = Path(output_path).expanduser().resolve()
suffix = resolved_output.suffix.lower()
if suffix not in {'.jpg', '.jpeg', '.png'}:
sheet.close()
raise ValueError("Contact sheet output must use .jpg, .jpeg, or .png")
resolved_output.parent.mkdir(parents=True, exist_ok=True)
if suffix == '.png':
sheet.save(resolved_output, format='PNG', optimize=True)
else:
sheet.save(resolved_output, format='JPEG', quality=90, optimize=True)
sheet.close()
return str(resolved_output)
def generate_html_tool(self, target_dir: str, output_filename: str = "image_orientation_tool.html") -> str:
"""Generate the image filtering HTML tool
@@ -203,6 +357,19 @@ class ImageRotator:
elif isinstance(json_source, list):
tasks = json_source
gif_paths = []
for task in tasks:
if not isinstance(task, dict):
continue
task_path = self._normalize_task_path(task.get('path', ''))
if Path(task_path).suffix.lower() == '.gif':
gif_paths.append(task_path)
if gif_paths:
raise ValueError(
"GIF rotation is unsupported; preserve GIF files unchanged: "
+ ", ".join(gif_paths)
)
print(f"[WORK] Starting {len(tasks)} manual rotation task(s)...")
print("=" * 60)
@@ -532,6 +699,7 @@ def build_parser() -> argparse.ArgumentParser:
description="Manage image orientation and manual rotation fixes.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Examples:
python3 scripts/rotate_images.py sheet projects/demo/images
python3 scripts/rotate_images.py gen projects/demo/images
python3 scripts/rotate_images.py fix fixes.json
python3 scripts/rotate_images.py auto projects/demo/images
@@ -539,6 +707,17 @@ def build_parser() -> argparse.ArgumentParser:
)
subparsers = parser.add_subparsers(dest="command", required=True)
sheet = subparsers.add_parser(
"sheet",
help="Generate a static contact sheet without modifying images",
)
sheet.add_argument("images_directory", help="Images directory")
sheet.add_argument(
"-o",
"--output",
help="Output .jpg or .png path (default: sibling analysis directory)",
)
gen = subparsers.add_parser("gen", help="Generate the visual rotation HTML tool")
gen.add_argument("images_directory", help="Images directory")
@@ -556,6 +735,18 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
rotator = ImageRotator()
if args.command == 'sheet':
try:
output_path = rotator.generate_contact_sheet(
args.images_directory,
args.output,
)
print(f"[REPORT] Image orientation contact sheet: {output_path}")
except (OSError, ValueError) as e:
print(f"[ERROR] Contact sheet generation failed: {e}")
return 1
return 0
if args.command == 'gen':
target_dir = args.images_directory
try:
@@ -61,18 +61,66 @@ def download_audio(url: str, output_path: Path) -> None:
publish_audio_bytes(get_bytes(url), output_path)
def temporary_path(target: Path, suffix: str) -> tuple[int, Path]:
"""Create a temporary file beside its eventual target."""
target.parent.mkdir(parents=True, exist_ok=True)
descriptor, raw_path = tempfile.mkstemp(
prefix=f".{target.name}.",
suffix=suffix,
dir=target.parent,
)
return descriptor, Path(raw_path)
def publish_staged_pair(
staged_first: Path,
first_target: Path,
staged_second: Path,
second_target: Path,
) -> None:
"""Publish two staged files together, restoring prior targets on failure."""
targets = (first_target, second_target)
if first_target.resolve() == second_target.resolve():
raise ValueError("paired outputs must use different paths")
backups: dict[Path, Path] = {}
published: set[Path] = set()
try:
for target in targets:
if not target.exists():
continue
descriptor, backup = temporary_path(target, ".bak")
os.close(descriptor)
backup.unlink()
os.replace(target, backup)
backups[target] = backup
for staged, target in (
(staged_first, first_target),
(staged_second, second_target),
):
os.replace(staged, target)
published.add(target)
except Exception:
for target in published:
target.unlink(missing_ok=True)
for target, backup in backups.items():
if backup.exists():
os.replace(backup, target)
raise
finally:
staged_first.unlink(missing_ok=True)
staged_second.unlink(missing_ok=True)
for backup in backups.values():
backup.unlink(missing_ok=True)
def publish_audio_bytes(audio: bytes, output_path: Path) -> None:
"""Publish non-empty provider audio without exposing a partial target."""
if not audio:
raise RuntimeError("TTS provider returned empty audio data")
output_path.parent.mkdir(parents=True, exist_ok=True)
descriptor, raw_path = tempfile.mkstemp(
prefix=f".{output_path.name}.",
suffix=".tmp",
dir=output_path.parent,
)
staged_path = Path(raw_path)
descriptor, staged_path = temporary_path(output_path, ".tmp")
try:
with os.fdopen(descriptor, "wb") as stream:
descriptor = -1
@@ -88,6 +136,62 @@ def publish_audio_bytes(audio: bytes, output_path: Path) -> None:
staged_path.unlink(missing_ok=True)
def publish_audio_subtitle(
audio: bytes,
output_path: Path,
subtitle: str,
subtitle_path: Path,
) -> None:
"""Publish one audio/SRT pair without exposing mismatched targets."""
if not audio:
raise RuntimeError("TTS provider returned empty audio data")
if not subtitle.strip():
raise RuntimeError("TTS provider returned empty subtitle data")
audio_descriptor = -1
subtitle_descriptor = -1
staged_audio: Path | None = None
staged_subtitle: Path | None = None
try:
audio_descriptor, staged_audio = temporary_path(output_path, ".tmp")
subtitle_descriptor, staged_subtitle = temporary_path(subtitle_path, ".tmp")
audio_stream = os.fdopen(audio_descriptor, "wb")
audio_descriptor = -1
with audio_stream:
audio_stream.write(audio)
audio_stream.flush()
os.fsync(audio_stream.fileno())
subtitle_stream = os.fdopen(
subtitle_descriptor,
"w",
encoding="utf-8",
newline="\n",
)
subtitle_descriptor = -1
with subtitle_stream:
subtitle_stream.write(subtitle)
subtitle_stream.flush()
os.fsync(subtitle_stream.fileno())
publish_staged_pair(
staged_audio,
output_path,
staged_subtitle,
subtitle_path,
)
finally:
if audio_descriptor >= 0:
os.close(audio_descriptor)
if subtitle_descriptor >= 0:
os.close(subtitle_descriptor)
if staged_audio is not None:
staged_audio.unlink(missing_ok=True)
if staged_subtitle is not None:
staged_subtitle.unlink(missing_ok=True)
def extension_from_format(audio_format: str) -> str:
normalized = audio_format.strip().lower()
if normalized in {"mp3", "wav"}:
@@ -2,14 +2,30 @@
from __future__ import annotations
import json
import math
import os
from pathlib import Path
from urllib import error, request
from tts_backends.backend_common import download_audio, extension_from_format, post_json, read_api_key
from tts_backends.backend_common import (
download_audio,
extension_from_format,
get_bytes,
post_json,
publish_audio_subtitle,
read_api_key,
read_http_error,
)
from tts_backends.backend_edge import (
DEFAULT_SUBTITLE_MAX_CHARS,
format_word_timed_srt,
)
DEFAULT_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer"
DEFAULT_MODEL = "cosyvoice-v3-flash"
_TICKS_PER_MILLISECOND = 10_000
def output_extension(audio_format: str) -> str:
@@ -28,6 +44,167 @@ def resolve_url(base_url: str | None = None) -> str:
return base + "/api/v1/services/audio/tts/SpeechSynthesizer"
def _parse_sse_event(data_lines: list[str]) -> dict | None:
payload = "\n".join(data_lines).strip()
if not payload or payload == "[DONE]":
return None
try:
event = json.loads(payload)
except json.JSONDecodeError as exc:
raise RuntimeError("CosyVoice streaming response contains invalid JSON") from exc
if not isinstance(event, dict):
raise RuntimeError("CosyVoice streaming event is not an object")
return event
def _parse_sse(raw: bytes) -> list[dict]:
try:
body = raw.decode("utf-8-sig")
except UnicodeError as exc:
raise RuntimeError("CosyVoice streaming response is not valid UTF-8") from exc
if not any(line.startswith("data:") for line in body.splitlines()):
event = _parse_sse_event([body])
return [event] if event is not None else []
events: list[dict] = []
data_lines: list[str] = []
for line in body.splitlines():
if not line:
event = _parse_sse_event(data_lines)
if event is not None:
events.append(event)
data_lines = []
continue
if line.startswith("data:"):
data_lines.append(line[5:].lstrip())
event = _parse_sse_event(data_lines)
if event is not None:
events.append(event)
if not events:
raise RuntimeError("CosyVoice streaming response contains no events")
return events
def _post_streaming(
url: str,
*,
api_key: str,
payload: dict,
) -> list[dict]:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = request.Request(
url,
data=body,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"X-DashScope-SSE": "enable",
},
method="POST",
)
try:
with request.urlopen(req, timeout=180) as response:
return _parse_sse(response.read())
except error.HTTPError as exc:
raise RuntimeError(read_http_error(exc)) from exc
except error.URLError as exc:
raise RuntimeError(
f"CosyVoice streaming request failed: {exc.reason}"
) from exc
def _ticks(value: object, *, field: str) -> int:
if isinstance(value, bool):
raise RuntimeError(f"CosyVoice word {field} is not numeric")
try:
numeric = float(value)
except (TypeError, ValueError) as exc:
raise RuntimeError(f"CosyVoice word {field} is not numeric") from exc
if not math.isfinite(numeric):
raise RuntimeError(f"CosyVoice word {field} is not finite")
return int(round(numeric * _TICKS_PER_MILLISECOND))
def _word_boundaries(events: list[dict]) -> tuple[list[dict], str]:
sentence_text: dict[int, str] = {}
sentence_words: dict[int, list] = {}
audio_url = ""
for event in events:
code = event.get("code")
message = event.get("message")
if code or message:
details = ": ".join(str(value) for value in (code, message) if value)
raise RuntimeError(f"CosyVoice TTS failed: {details}")
output = event.get("output") or {}
if not isinstance(output, dict):
continue
audio = output.get("audio") or {}
if isinstance(audio, dict) and isinstance(audio.get("url"), str):
audio_url = audio["url"]
sentence = output.get("sentence") or {}
if not isinstance(sentence, dict):
continue
index = sentence.get("index")
if isinstance(index, bool) or not isinstance(index, int) or index < 0:
continue
original_text = output.get("original_text")
if isinstance(original_text, str) and original_text:
sentence_text[index] = original_text
words = sentence.get("words")
if isinstance(words, list) and words:
sentence_words[index] = words
if not audio_url:
raise RuntimeError("CosyVoice streaming response contains no audio URL")
if not sentence_words:
raise RuntimeError(
"CosyVoice returned no word timestamps. Use a cloned voice from "
"cosyvoice-v3.5-plus/flash, cosyvoice-v3-plus/flash, or cosyvoice-v2, "
"or a system voice marked as timestamp-supported; otherwise pass "
"--cosyvoice-audio-only."
)
boundaries: list[dict] = []
for sentence_index in sorted(sentence_words):
original_text = sentence_text.get(sentence_index, "")
for item in sentence_words[sentence_index]:
if not isinstance(item, dict):
raise RuntimeError("CosyVoice word timing is not an object")
word = item.get("text")
begin_index = item.get("begin_index")
end_index = item.get("end_index")
if (
original_text
and isinstance(begin_index, int)
and not isinstance(begin_index, bool)
and isinstance(end_index, int)
and not isinstance(end_index, bool)
and 0 <= begin_index < end_index <= len(original_text)
):
word = original_text[begin_index:end_index]
if not isinstance(word, str) or not word:
raise RuntimeError("CosyVoice word timing contains no text")
start = _ticks(item.get("begin_time"), field="start time")
end = _ticks(item.get("end_time"), field="end time")
if start < 0 or end <= start:
raise RuntimeError(
"CosyVoice response contains an invalid word interval"
)
if boundaries and start < boundaries[-1]["offset"]:
raise RuntimeError(
"CosyVoice word timestamps are not in chronological order"
)
boundaries.append({
"text": word,
"offset": start,
"duration": end - start,
})
return boundaries, audio_url
def generate(
text: str,
output_path: Path,
@@ -43,6 +220,8 @@ def generate(
instruction: str | None,
language_hint: str | None,
base_url: str | None,
subtitle_path: Path | None = None,
subtitle_max_chars: int = DEFAULT_SUBTITLE_MAX_CHARS,
) -> None:
input_payload: dict[str, object] = {
"text": text,
@@ -61,13 +240,38 @@ def generate(
if language_hint:
input_payload["language_hints"] = [language_hint]
payload = {
"model": model,
"input": input_payload,
}
url = resolve_url(base_url)
if subtitle_path is not None:
input_payload["word_timestamp_enabled"] = True
events = _post_streaming(
url,
api_key=api_key,
payload=payload,
)
boundaries, audio_url = _word_boundaries(events)
subtitle = format_word_timed_srt(
text,
boundaries,
subtitle_max_chars,
provider_label="CosyVoice",
)
publish_audio_subtitle(
get_bytes(audio_url),
output_path,
subtitle,
subtitle_path,
)
return
data = post_json(
resolve_url(base_url),
url,
headers={"Authorization": f"Bearer {api_key}"},
payload={
"model": model,
"input": input_payload,
},
payload=payload,
timeout=180,
)
audio = (data.get("output") or {}).get("audio") or {}
@@ -80,4 +284,8 @@ def generate(
def print_voices() -> None:
print("CosyVoice voices are selected by voice.")
print("Use a system voice name or a cloned/designed voice_id from CosyVoice.")
print("Example system voice from Alibaba Cloud docs: longanyang")
print("Timestamp-supported system voice example: longanyang")
print(
"For SRT, use a supported CosyVoice cloned voice or a system voice "
"marked timestamp-supported in the provider catalog."
)
@@ -4,10 +4,11 @@ from __future__ import annotations
import os
import re
import tempfile
from dataclasses import dataclass
from pathlib import Path
from tts_backends.backend_common import publish_staged_pair, temporary_path
DEFAULT_SUBTITLE_MAX_CHARS = 20
DEFAULT_BOUNDARY_OVERLAP_TOLERANCE_MS = 100
@@ -102,58 +103,6 @@ async def generate(
await communicate.save(str(output_path))
def _temporary_path(target: Path, suffix: str) -> tuple[int, Path]:
target.parent.mkdir(parents=True, exist_ok=True)
descriptor, raw_path = tempfile.mkstemp(
prefix=f".{target.name}.",
suffix=suffix,
dir=target.parent,
)
return descriptor, Path(raw_path)
def _publish_pair(
staged_audio: Path,
output_path: Path,
staged_subtitle: Path,
subtitle_path: Path,
) -> None:
targets = (output_path, subtitle_path)
if output_path.resolve() == subtitle_path.resolve():
raise ValueError("audio and subtitle outputs must use different paths")
backups: dict[Path, Path] = {}
published: set[Path] = set()
try:
for target in targets:
if not target.exists():
continue
descriptor, backup = _temporary_path(target, ".bak")
os.close(descriptor)
backup.unlink()
os.replace(target, backup)
backups[target] = backup
for staged, target in (
(staged_audio, output_path),
(staged_subtitle, subtitle_path),
):
os.replace(staged, target)
published.add(target)
except Exception:
for target in published:
target.unlink(missing_ok=True)
for target, backup in backups.items():
if backup.exists():
os.replace(backup, target)
raise
finally:
staged_audio.unlink(missing_ok=True)
staged_subtitle.unlink(missing_ok=True)
for backup in backups.values():
backup.unlink(missing_ok=True)
def _text_key(text: str) -> str:
return "".join(character.casefold() for character in text if character.isalnum())
@@ -170,13 +119,17 @@ def _source_key_positions(text: str) -> tuple[str, list[int]]:
return "".join(key), positions
def _map_word_boundaries(text: str, boundaries: list[dict]) -> list[_MappedWord]:
def _map_word_boundaries(
text: str,
boundaries: list[dict],
provider_label: str,
) -> list[_MappedWord]:
source_key, source_positions = _source_key_positions(text)
boundary_keys = [_text_key(boundary["text"]) for boundary in boundaries]
boundary_key = "".join(boundary_keys)
if not source_key or source_key != boundary_key:
raise RuntimeError(
"Edge TTS word boundaries could not be aligned with the narration text; "
f"{provider_label} word boundaries could not be aligned with the narration text; "
"subtitle timing was not generated"
)
@@ -335,6 +288,7 @@ def _clamp_small_overlaps(
cues: list[_SubtitleCue],
*,
tolerance_ms: int = DEFAULT_BOUNDARY_OVERLAP_TOLERANCE_MS,
provider_label: str,
) -> list[_SubtitleCue]:
tolerance = tolerance_ms * _TICKS_PER_MILLISECOND
normalized: list[_SubtitleCue] = []
@@ -344,7 +298,7 @@ def _clamp_small_overlaps(
if overlap > tolerance:
overlap_ms = overlap / _TICKS_PER_MILLISECOND
raise RuntimeError(
f"Edge TTS returned {overlap_ms:g} ms of overlapping "
f"{provider_label} returned {overlap_ms:g} ms of overlapping "
"word-boundary timing; audio and subtitles were not published"
)
cue = _SubtitleCue(
@@ -354,7 +308,7 @@ def _clamp_small_overlaps(
)
if cue.end <= cue.start:
raise RuntimeError(
"Edge TTS returned an invalid subtitle timing interval; "
f"{provider_label} returned an invalid subtitle timing interval; "
"audio and subtitles were not published"
)
normalized.append(cue)
@@ -365,10 +319,11 @@ def _subtitle_cues(
text: str,
boundaries: list[dict],
max_chars: int,
provider_label: str,
) -> list[_SubtitleCue]:
if max_chars < 1:
raise ValueError("subtitle_max_chars must be at least 1")
words = _map_word_boundaries(text, boundaries)
words = _map_word_boundaries(text, boundaries, provider_label)
spans = [
span
for sentence in _sentence_spans(text)
@@ -391,29 +346,29 @@ def _subtitle_cues(
if assigned_word_indexes != list(range(len(words))):
raise RuntimeError(
"Edge TTS word boundaries crossed subtitle split points; "
f"{provider_label} word boundaries crossed subtitle split points; "
"subtitle timing was not generated"
)
if not pending:
raise RuntimeError("Edge TTS produced no timed subtitle cues")
raise RuntimeError(f"{provider_label} produced no timed subtitle cues")
cues: list[_SubtitleCue] = []
for index, (start, word_end, cue_text) in enumerate(pending):
next_start = pending[index + 1][0] if index + 1 < len(pending) else None
end = next_start if next_start is not None and next_start > word_end else word_end
cues.append(_SubtitleCue(start=start, end=end, text=cue_text))
cues = _clamp_small_overlaps(cues)
cues = _clamp_small_overlaps(cues, provider_label=provider_label)
source_text = re.sub(r"\s+", "", text)
subtitle_text = re.sub(r"\s+", "", "".join(cue.text for cue in cues))
if subtitle_text != source_text:
raise RuntimeError(
"Generated subtitle text does not match the narration text; "
f"{provider_label} subtitle text does not match the narration text; "
"audio and subtitles were not published"
)
if any(_display_length(cue.text, 0, len(cue.text)) > max_chars for cue in cues):
raise RuntimeError(
"A single Edge TTS word boundary exceeds the subtitle character limit; "
f"A single {provider_label} word boundary exceeds the subtitle character limit; "
"audio and subtitles were not published"
)
return cues
@@ -439,6 +394,24 @@ def _format_srt(cues: list[_SubtitleCue]) -> str:
return "\n\n".join(blocks) + "\n"
def format_word_timed_srt(
text: str,
boundaries: list[dict],
max_chars: int = DEFAULT_SUBTITLE_MAX_CHARS,
*,
provider_label: str = "Edge TTS",
) -> str:
"""Regroup provider word timings into compact, text-faithful SRT cues."""
return _format_srt(
_subtitle_cues(
text,
boundaries,
max_chars,
provider_label,
)
)
async def _generate_with_subtitles(
text: str,
output_path: Path,
@@ -470,8 +443,8 @@ async def _generate_with_subtitles(
boundaries: list[dict] = []
received_audio = False
try:
audio_descriptor, staged_audio = _temporary_path(output_path, ".tmp")
subtitle_descriptor, staged_subtitle = _temporary_path(subtitle_path, ".tmp")
audio_descriptor, staged_audio = temporary_path(output_path, ".tmp")
subtitle_descriptor, staged_subtitle = temporary_path(subtitle_path, ".tmp")
audio_stream = os.fdopen(audio_descriptor, "wb")
audio_descriptor = -1
@@ -489,7 +462,7 @@ async def _generate_with_subtitles(
raise RuntimeError("Edge TTS returned no audio data")
if not boundaries:
raise RuntimeError("Edge TTS returned no word-boundary timing")
subtitle_text = _format_srt(_subtitle_cues(text, boundaries, max_chars))
subtitle_text = format_word_timed_srt(text, boundaries, max_chars)
subtitle_stream = os.fdopen(
subtitle_descriptor,
@@ -504,7 +477,7 @@ async def _generate_with_subtitles(
os.fsync(subtitle_stream.fileno())
assert staged_audio is not None
assert staged_subtitle is not None
_publish_pair(
publish_staged_pair(
staged_audio,
output_path,
staged_subtitle,
@@ -2,14 +2,27 @@
from __future__ import annotations
import base64
import binascii
import json
import math
from pathlib import Path
from urllib import error, request
from urllib.parse import quote, urlencode
from tts_backends.backend_common import publish_audio_bytes, read_api_key
from tts_backends.backend_common import (
publish_audio_bytes,
publish_audio_subtitle,
read_api_key,
)
from tts_backends.backend_edge import (
DEFAULT_SUBTITLE_MAX_CHARS,
format_word_timed_srt,
)
API_BASE = "https://api.elevenlabs.io/v1"
_TICKS_PER_SECOND = 10_000_000
def read_elevenlabs_api_key(env_name: str) -> str:
@@ -34,6 +47,118 @@ def _read_http_error(exc: error.HTTPError) -> str:
return f"HTTP {exc.code}: {body or exc.reason}"
def _seconds_to_ticks(value: object, *, field: str) -> int:
if isinstance(value, bool):
raise RuntimeError(f"ElevenLabs alignment {field} is not numeric")
try:
numeric = float(value)
except (TypeError, ValueError) as exc:
raise RuntimeError(
f"ElevenLabs alignment {field} is not numeric"
) from exc
if not math.isfinite(numeric):
raise RuntimeError(f"ElevenLabs alignment {field} is not finite")
return int(round(numeric * _TICKS_PER_SECOND))
def _is_unspaced_character(character: str) -> bool:
"""Return whether a script is safer to time one character at a time."""
codepoint = ord(character)
return (
0x3040 <= codepoint <= 0x30FF
or 0x3400 <= codepoint <= 0x4DBF
or 0x4E00 <= codepoint <= 0x9FFF
or 0xAC00 <= codepoint <= 0xD7AF
or 0xF900 <= codepoint <= 0xFAFF
or 0x20000 <= codepoint <= 0x2FA1F
)
def _alignment_characters(
text: str,
alignment: object,
) -> tuple[list[str], list[object], list[object]]:
if not isinstance(alignment, dict):
raise RuntimeError("ElevenLabs response contains no original-text alignment")
raw_characters = alignment.get("characters")
raw_starts = alignment.get("character_start_times_seconds")
raw_ends = alignment.get("character_end_times_seconds")
if not all(isinstance(values, list) for values in (
raw_characters,
raw_starts,
raw_ends,
)):
raise RuntimeError("ElevenLabs alignment arrays are missing")
if not raw_characters or not (
len(raw_characters) == len(raw_starts) == len(raw_ends)
):
raise RuntimeError("ElevenLabs alignment arrays have inconsistent lengths")
if any(not isinstance(item, str) or not item for item in raw_characters):
raise RuntimeError("ElevenLabs alignment contains an invalid character")
if "".join(raw_characters) != text:
raise RuntimeError(
"ElevenLabs original-text alignment differs from the narration text; "
"audio and subtitles were not published"
)
characters: list[str] = []
starts: list[object] = []
ends: list[object] = []
for raw_character, start, end in zip(
raw_characters,
raw_starts,
raw_ends,
):
for character in raw_character:
characters.append(character)
starts.append(start)
ends.append(end)
return characters, starts, ends
def _word_boundaries(text: str, alignment: object) -> list[dict]:
characters, starts, ends = _alignment_characters(text, alignment)
boundaries: list[dict] = []
index = 0
while index < len(characters):
character = characters[index]
if not character.isalnum():
index += 1
continue
token_end = index + 1
if not _is_unspaced_character(character):
while token_end < len(characters):
following = characters[token_end]
if (
not following.isalnum()
or _is_unspaced_character(following)
):
break
token_end += 1
start = _seconds_to_ticks(starts[index], field="start time")
end = _seconds_to_ticks(ends[token_end - 1], field="end time")
if start < 0 or end <= start:
raise RuntimeError(
"ElevenLabs alignment contains an invalid character interval"
)
if boundaries and start < boundaries[-1]["offset"]:
raise RuntimeError(
"ElevenLabs alignment is not in chronological order"
)
boundaries.append({
"text": "".join(characters[index:token_end]),
"offset": start,
"duration": end - start,
})
index = token_end
if not boundaries:
raise RuntimeError("ElevenLabs alignment contains no timed words")
return boundaries
def generate(
text: str,
output_path: Path,
@@ -45,7 +170,10 @@ def generate(
stability: float | None,
similarity_boost: float | None,
style: float | None,
speed: float | None,
speaker_boost: bool | None,
subtitle_path: Path | None = None,
subtitle_max_chars: int = DEFAULT_SUBTITLE_MAX_CHARS,
) -> None:
payload: dict[str, object] = {
"text": text,
@@ -59,13 +187,19 @@ def generate(
voice_settings["similarity_boost"] = similarity_boost
if style is not None:
voice_settings["style"] = style
if speed is not None:
voice_settings["speed"] = speed
if speaker_boost is not None:
voice_settings["use_speaker_boost"] = speaker_boost
if voice_settings:
payload["voice_settings"] = voice_settings
query = urlencode({"output_format": output_format})
suffix = "/with-timestamps" if subtitle_path is not None else ""
url = (
f"{API_BASE}/text-to-speech/{quote(voice_id, safe='')}{suffix}?{query}"
)
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
url = f"{API_BASE}/text-to-speech/{voice_id}?output_format={output_format}"
req = request.Request(
url,
data=body,
@@ -77,12 +211,39 @@ def generate(
)
try:
with request.urlopen(req, timeout=120) as response:
publish_audio_bytes(response.read(), output_path)
response_data = response.read()
except error.HTTPError as exc:
raise RuntimeError(_read_http_error(exc)) from exc
except error.URLError as exc:
raise RuntimeError(f"ElevenLabs request failed: {exc.reason}") from exc
if subtitle_path is None:
publish_audio_bytes(response_data, output_path)
return
try:
data = json.loads(response_data.decode("utf-8"))
except (UnicodeError, json.JSONDecodeError) as exc:
raise RuntimeError(
"ElevenLabs timing response is not valid UTF-8 JSON"
) from exc
encoded_audio = data.get("audio_base64")
if not isinstance(encoded_audio, str) or not encoded_audio:
raise RuntimeError("ElevenLabs timing response contains no audio")
try:
audio = base64.b64decode(encoded_audio, validate=True)
except (binascii.Error, ValueError) as exc:
raise RuntimeError(
"ElevenLabs timing response audio is not valid base64"
) from exc
subtitle = format_word_timed_srt(
text,
_word_boundaries(text, data.get("alignment")),
subtitle_max_chars,
provider_label="ElevenLabs",
)
publish_audio_subtitle(audio, output_path, subtitle, subtitle_path)
def print_voices(api_key: str) -> None:
req = request.Request(
@@ -3,19 +3,28 @@
from __future__ import annotations
import binascii
import json
import math
import os
from pathlib import Path
from tts_backends.backend_common import (
extension_from_format,
get_bytes,
post_json,
publish_audio_bytes,
publish_audio_subtitle,
read_api_key,
)
from tts_backends.backend_edge import (
DEFAULT_SUBTITLE_MAX_CHARS,
format_word_timed_srt,
)
DEFAULT_ENDPOINT = "https://api.minimaxi.com/v1/t2a_v2"
DEFAULT_MODEL = "speech-2.8-hd"
_TICKS_PER_MILLISECOND = 10_000
# International fallback: set MINIMAX_TTS_BASE_URL=https://api.minimax.io if needed.
@@ -37,6 +46,70 @@ def resolve_url(base_url: str | None = None) -> str:
return base + "/v1/t2a_v2"
def _ticks(value: object, *, field: str) -> int:
if isinstance(value, bool):
raise RuntimeError(f"MiniMax subtitle {field} is not numeric")
try:
numeric = float(value)
except (TypeError, ValueError) as exc:
raise RuntimeError(f"MiniMax subtitle {field} is not numeric") from exc
if not math.isfinite(numeric):
raise RuntimeError(f"MiniMax subtitle {field} is not finite")
return int(round(numeric * _TICKS_PER_MILLISECOND))
def _word_boundaries(raw: object) -> list[dict]:
if not isinstance(raw, list) or not raw:
raise RuntimeError("MiniMax subtitle response contains no sentence blocks")
boundaries: list[dict] = []
for sentence in raw:
if not isinstance(sentence, dict):
raise RuntimeError("MiniMax subtitle sentence block is not an object")
words = sentence.get("timestamped_words")
if not isinstance(words, list) or not words:
raise RuntimeError("MiniMax subtitle sentence block contains no word timings")
for item in words:
if not isinstance(item, dict):
raise RuntimeError("MiniMax subtitle word timing is not an object")
word = item.get("word")
if not isinstance(word, str) or not word:
raise RuntimeError("MiniMax subtitle word timing contains no text")
start = _ticks(item.get("time_begin"), field="start time")
end = _ticks(item.get("time_end"), field="end time")
if start < 0 or end <= start:
raise RuntimeError(
"MiniMax subtitle response contains an invalid word interval"
)
if boundaries and start < boundaries[-1]["offset"]:
raise RuntimeError("MiniMax subtitle words are not in chronological order")
boundaries.append(
{
"text": word,
"offset": start,
"duration": end - start,
}
)
return boundaries
def _download_subtitle(
url: str,
text: str,
max_chars: int,
) -> str:
try:
raw = json.loads(get_bytes(url).decode("utf-8-sig"))
except (UnicodeError, json.JSONDecodeError) as exc:
raise RuntimeError("MiniMax subtitle response is not valid UTF-8 JSON") from exc
return format_word_timed_srt(
text,
_word_boundaries(raw),
max_chars,
provider_label="MiniMax",
)
def generate(
text: str,
output_path: Path,
@@ -53,6 +126,8 @@ def generate(
pitch: int,
language_boost: str,
base_url: str | None,
subtitle_path: Path | None = None,
subtitle_max_chars: int = DEFAULT_SUBTITLE_MAX_CHARS,
) -> None:
payload = {
"model": model,
@@ -73,6 +148,10 @@ def generate(
"channel": channel,
},
}
if subtitle_path is not None:
payload["subtitle_enable"] = True
payload["subtitle_type"] = "word"
data = post_json(
resolve_url(base_url),
headers={"Authorization": f"Bearer {api_key}"},
@@ -83,14 +162,31 @@ def generate(
if base_resp.get("status_code") not in (None, 0, "0"):
raise RuntimeError(f"MiniMax TTS failed: {data}")
audio_hex = (data.get("data") or {}).get("audio")
response_data = data.get("data") or {}
audio_hex = response_data.get("audio")
if not audio_hex:
raise RuntimeError(f"MiniMax response missing audio data: {data}")
try:
audio = binascii.unhexlify(audio_hex)
except (binascii.Error, ValueError) as exc:
raise RuntimeError("MiniMax response audio is not valid hex data") from exc
publish_audio_bytes(audio, output_path)
if subtitle_path is None:
publish_audio_bytes(audio, output_path)
return
subtitle_url = response_data.get("subtitle_file")
if not isinstance(subtitle_url, str) or not subtitle_url.strip():
trace_id = data.get("trace_id") or "unknown"
raise RuntimeError(
f"MiniMax response missing subtitle file (trace_id={trace_id})"
)
subtitle = _download_subtitle(
subtitle_url,
text,
subtitle_max_chars,
)
publish_audio_subtitle(audio, output_path, subtitle, subtitle_path)
def print_voices() -> None:
@@ -72,4 +72,4 @@ def print_voices() -> None:
print("Qwen TTS voices are selected by voice.")
print("Use a system voice name or a cloned voice from Qwen voice cloning.")
print("Example system voice from Alibaba Cloud docs: Cherry")
print("Current Qwen TTS responses do not include word/character timestamps.")
@@ -2,7 +2,7 @@
"""
PPT Master - Final Video Subtitles
Align the exact narration text frozen in page-local Edge SRT files against the
Align the exact narration text frozen in page-local narration SRT files against the
audio track of a finished PowerPoint-exported video. This produces a delivery
SRT from the actual video timeline without rewriting speaker notes or relying
on theoretical slide offsets.
@@ -189,7 +189,7 @@ def _page_subtitle_paths(subtitle_dir: Path) -> list[Path]:
]
if not paths:
raise FileNotFoundError(
f"No page-local Edge SRT files found under {subtitle_dir}"
f"No page-local narration SRT files found under {subtitle_dir}"
)
return paths
@@ -321,7 +321,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument(
"--subtitle-dir",
default=None,
help="Page-local Edge SRT directory; default: <project>/notes/subtitles",
help="Page-local narration SRT directory; default: <project>/audio",
)
parser.add_argument(
"-o",
@@ -367,7 +367,7 @@ def main(argv: list[str] | None = None) -> int:
subtitle_dir = (
Path(args.subtitle_dir)
if args.subtitle_dir
else Path("notes/subtitles")
else Path("audio")
)
if not subtitle_dir.is_absolute():
subtitle_dir = project_path / subtitle_dir
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
PPT Master - Visualization Catalog Resolver
Resolve chart and table references from their live family indexes.
Historical ``page_charts`` structure keys remain readable as intent-only
compatibility values. They do not resolve to SVG assets and are never part of
the live recall catalog.
Usage:
Import load_visualization_entries() or resolve_visualization_reference().
Examples:
from visualization_catalog import resolve_visualization_reference
Dependencies:
None (only uses the standard library)
"""
from __future__ import annotations
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
_SCRIPTS_DIR = Path(__file__).resolve().parent
_TEMPLATES_DIR = _SCRIPTS_DIR.parent / "templates"
_KEY_RE = re.compile(r"^[a-z0-9]+(?:_[a-z0-9]+)*$")
_FAMILY_SPECS = {
"chart": ("charts", "charts_index.json", "charts"),
"table": ("tables", "tables_index.json", "tables"),
}
VISUALIZATION_SVG_KIND = "visualization-svg"
LEGACY_STRUCTURE_INTENT_KIND = "legacy-structure-intent"
# Frozen from the 36 Structure entries published in origin/main's historical
# broad charts catalog. Keep this exact allowlist: later split-only aliases and
# generated Structure keys were never part of that public compatibility input.
_LEGACY_STRUCTURE_INTENT_KEYS = frozenset(
{
"agenda_list",
"arc_anchored_list",
"chevron_chain_with_tail",
"chevron_process",
"circular_stages",
"client_server_flow",
"comparison_columns",
"concentric_circles",
"fishbone_diagram",
"hub_inward_arrows",
"hub_spoke",
"icon_grid",
"isometric_stairs",
"journey_map",
"kpi_cards",
"labeled_card",
"layered_architecture",
"mind_map",
"module_composition",
"numbered_steps",
"pipeline_with_stages",
"process_flow",
"pros_cons_chart",
"pyramid_chart",
"pyramid_isometric",
"quadrant_bubble_scatter",
"quadrant_text_bullets",
"roadmap_vertical",
"segmented_wheel",
"snake_flow",
"team_roster",
"timeline",
"top_down_tree",
"venn_diagram",
"vertical_list",
"vertical_pillars",
}
)
class VisualizationCatalogError(RuntimeError):
"""Reject an unreadable, malformed, missing, or ambiguous reference."""
@dataclass(frozen=True)
class VisualizationEntry:
"""One live SVG entry or one frozen legacy Structure intent."""
family: str
key: str
summary: str
path: Path | None
kind: str = VISUALIZATION_SVG_KIND
@property
def reference(self) -> str:
"""Return the canonical live ``family/key`` reference."""
if self.kind != VISUALIZATION_SVG_KIND:
raise VisualizationCatalogError(
f"legacy Structure intent {self.key!r} has no canonical reference"
)
return f"{self.family}/{self.key}"
@property
def display_path(self) -> str:
"""Return the Skill-relative SVG path."""
if self.path is None:
raise VisualizationCatalogError(
f"{self.key!r} is a legacy Structure intent without an SVG path"
)
return self.path.relative_to(_SCRIPTS_DIR.parent).as_posix()
@dataclass(frozen=True)
class VisualizationCatalog:
"""Canonical entries plus family-local compatibility aliases."""
entries: dict[str, VisualizationEntry]
aliases: dict[str, str]
def visualization_families() -> tuple[str, ...]:
"""Return the stable live visualization family order."""
return tuple(_FAMILY_SPECS)
def legacy_structure_intent_keys() -> tuple[str, ...]:
"""Return the frozen legacy ``page_charts`` Structure bare keys."""
return tuple(sorted(_LEGACY_STRUCTURE_INTENT_KEYS))
def _normalize_families(families: Iterable[str] | None) -> tuple[str, ...]:
requested = visualization_families() if families is None else tuple(families)
normalized: list[str] = []
for raw_family in requested:
family = str(raw_family).strip().casefold()
if family not in _FAMILY_SPECS:
allowed = ", ".join(visualization_families())
raise VisualizationCatalogError(
f"unknown visualization family {raw_family!r}; expected one of {allowed}"
)
if family not in normalized:
normalized.append(family)
if not normalized:
raise VisualizationCatalogError("at least one visualization family is required")
return tuple(normalized)
def _load_family(family: str) -> tuple[dict[str, VisualizationEntry], dict[str, str]]:
directory_name, index_name, object_key = _FAMILY_SPECS[family]
family_dir = _TEMPLATES_DIR / directory_name
index_path = family_dir / index_name
try:
payload = json.loads(index_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise VisualizationCatalogError(
f"cannot read {family} catalog {index_path}: {exc}"
) from exc
if not isinstance(payload, dict):
raise VisualizationCatalogError(f"{family} catalog root must be an object")
raw_entries = payload.get(object_key)
if not isinstance(raw_entries, dict):
raise VisualizationCatalogError(
f"{family} catalog {index_path} has no '{object_key}' object"
)
entries: dict[str, VisualizationEntry] = {}
for raw_key, raw_item in raw_entries.items():
if (
not isinstance(raw_key, str)
or _KEY_RE.fullmatch(raw_key) is None
or not isinstance(raw_item, dict)
):
raise VisualizationCatalogError(
f"{family} catalog entry {raw_key!r} is malformed"
)
summary = raw_item.get("summary")
if not isinstance(summary, str) or not summary.strip():
raise VisualizationCatalogError(
f"{family} catalog entry {raw_key!r} has no non-empty summary"
)
entry = VisualizationEntry(
family=family,
key=raw_key,
summary=summary.strip(),
path=(family_dir / f"{raw_key}.svg").resolve(),
)
entries[entry.reference] = entry
raw_aliases = payload.get("aliases", {})
if not isinstance(raw_aliases, dict):
raise VisualizationCatalogError(f"{family} catalog aliases must be an object")
aliases: dict[str, str] = {}
for raw_alias, raw_target in raw_aliases.items():
if (
not isinstance(raw_alias, str)
or _KEY_RE.fullmatch(raw_alias) is None
or not isinstance(raw_target, str)
or _KEY_RE.fullmatch(raw_target) is None
):
raise VisualizationCatalogError(
f"{family} catalog alias {raw_alias!r} is malformed"
)
target_reference = f"{family}/{raw_target}"
if raw_alias in raw_entries:
raise VisualizationCatalogError(
f"{family} alias {raw_alias!r} collides with a canonical key"
)
if target_reference not in entries:
raise VisualizationCatalogError(
f"{family} alias {raw_alias!r} targets missing key {raw_target!r}"
)
aliases[f"{family}/{raw_alias}"] = target_reference
return entries, aliases
def load_visualization_catalog(
families: Iterable[str] | None = None,
) -> VisualizationCatalog:
"""Load selected live family registries."""
selected = _normalize_families(families)
entries: dict[str, VisualizationEntry] = {}
aliases: dict[str, str] = {}
for family in selected:
family_entries, family_aliases = _load_family(family)
entries.update(family_entries)
aliases.update(family_aliases)
if not entries:
shown = ", ".join(selected)
raise VisualizationCatalogError(
f"no visualization entries are available for family selection {shown}"
)
return VisualizationCatalog(entries=entries, aliases=aliases)
def load_visualization_entries(
families: Iterable[str] | None = None,
) -> dict[str, VisualizationEntry]:
"""Return canonical entries keyed by ``family/key``."""
return load_visualization_catalog(families).entries
def _require_svg(entry: VisualizationEntry) -> VisualizationEntry:
if entry.kind != VISUALIZATION_SVG_KIND:
raise VisualizationCatalogError(
f"{entry.family}/{entry.key} has unsupported kind {entry.kind!r}"
)
if entry.path is None:
raise VisualizationCatalogError(f"{entry.reference!r} has no SVG asset path")
if entry.path.suffix.casefold() != ".svg" or not entry.path.is_file():
raise VisualizationCatalogError(
f"{entry.reference!r} has no SVG asset at {entry.path}"
)
return entry
def resolve_visualization_reference(
value: str,
*,
allow_legacy_bare: bool = False,
) -> VisualizationEntry:
"""Resolve one live ``family/key`` or supported legacy bare key.
Legacy Structure intents are accepted only when ``allow_legacy_bare`` is
true. A qualified ``structure/<key>`` is never a live reference.
"""
normalized = str(value).strip().casefold()
if not normalized:
raise VisualizationCatalogError("visualization reference must not be empty")
catalog = load_visualization_catalog()
if "/" in normalized:
family, separator, key = normalized.partition("/")
if (
not separator
or family not in _FAMILY_SPECS
or _KEY_RE.fullmatch(key) is None
or "/" in key
):
raise VisualizationCatalogError(
f"invalid canonical visualization reference {value!r}"
)
reference = f"{family}/{key}"
entry = catalog.entries.get(reference)
if entry is None:
if reference in catalog.aliases:
raise VisualizationCatalogError(
f"{value!r} is a legacy alias; use {catalog.aliases[reference]!r}"
)
raise VisualizationCatalogError(
f"canonical visualization reference {value!r} is not registered"
)
return _require_svg(entry)
if not allow_legacy_bare:
raise VisualizationCatalogError(
f"{value!r} must use canonical family/key grammar"
)
if _KEY_RE.fullmatch(normalized) is None:
raise VisualizationCatalogError(f"invalid legacy visualization key {value!r}")
matches = [
entry
for entry in catalog.entries.values()
if entry.key == normalized
]
for alias_reference, target_reference in catalog.aliases.items():
_, alias = alias_reference.split("/", 1)
if alias == normalized:
matches.append(catalog.entries[target_reference])
if normalized in _LEGACY_STRUCTURE_INTENT_KEYS:
matches.append(
VisualizationEntry(
family="structure",
key=normalized,
summary=(
"Frozen legacy page_charts Structure intent; author the "
"page structure from its semantic relationships."
),
path=None,
kind=LEGACY_STRUCTURE_INTENT_KIND,
)
)
unique_matches = {
(entry.kind, entry.family, entry.key): entry
for entry in matches
}
if not unique_matches:
raise VisualizationCatalogError(
f"legacy visualization key {value!r} is not registered"
)
if len(unique_matches) > 1:
candidates = ", ".join(
f"{family}/{key} ({kind})"
for kind, family, key in sorted(unique_matches)
)
raise VisualizationCatalogError(
f"legacy visualization key {value!r} is ambiguous across {candidates}"
)
resolved = next(iter(unique_matches.values()))
if resolved.kind == LEGACY_STRUCTURE_INTENT_KIND:
if resolved.path is not None:
raise VisualizationCatalogError(
f"legacy Structure intent {resolved.key!r} must not have an asset path"
)
return resolved
return _require_svg(resolved)
if __name__ == "__main__":
from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
print(
"Use visualization_catalog via visualization_recall.py or project_manager.py.",
file=sys.stderr,
)
raise SystemExit(2)
@@ -0,0 +1,486 @@
#!/usr/bin/env python3
"""
PPT Master - Visualization Candidate Recall
Recall a deterministic chart or table shortlist from semantic tags, or validate
selected references against the live family catalogs.
Usage:
python3 scripts/visualization_recall.py recall --page P03 --tag "time series" --tag "three metrics" --tag "trend"
python3 scripts/visualization_recall.py validate chart/line_chart table/record_table
Examples:
python3 scripts/visualization_recall.py recall --page P07 --family table \
--tag "option comparison" --tag "shared criteria" \
--tag "cell values" --limit 6
python3 scripts/visualization_recall.py validate table/record_table
python3 scripts/visualization_recall.py validate --legacy-bare process_flow
Dependencies:
None (only uses the standard library)
See scripts/docs/visualization-recall.md for the planning workflow and output contract.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import unicodedata
from typing import Optional
from console_encoding import configure_utf8_stdio
from visualization_catalog import (
LEGACY_STRUCTURE_INTENT_KIND,
VISUALIZATION_SVG_KIND,
VisualizationCatalogError,
VisualizationEntry,
load_visualization_entries,
resolve_visualization_reference,
visualization_families,
)
configure_utf8_stdio()
_TOKEN_RE = re.compile(r"[a-z0-9]+")
_PAGE_RE = re.compile(r"^P\d{2,}$")
_SKIP_RE = re.compile(r"\bskip\s+(?:if|for)\b", re.IGNORECASE)
_STOP_WORDS = {
"a",
"an",
"and",
"as",
"at",
"by",
"for",
"from",
"if",
"in",
"into",
"of",
"on",
"or",
"per",
"the",
"to",
"use",
"with",
}
def _normalize(text: str) -> str:
normalized = unicodedata.normalize("NFKC", text).casefold().replace("_", " ")
return " ".join(_TOKEN_RE.findall(normalized))
def _stem(token: str) -> str:
if len(token) > 5 and token.endswith("ies"):
return token[:-3] + "y"
if len(token) > 5 and token.endswith("ing"):
return token[:-3]
if len(token) > 4 and token.endswith("ed"):
return token[:-2]
if len(token) > 4 and token.endswith("es"):
return token[:-2]
if len(token) > 3 and token.endswith("s"):
return token[:-1]
return token
def _tokens(text: str) -> set[str]:
return {
_stem(token)
for token in _TOKEN_RE.findall(_normalize(text))
if token not in _STOP_WORDS
}
def _selected_families(family: str) -> tuple[str, ...]:
return visualization_families() if family == "all" else (family,)
def load_catalog(family: str = "all") -> dict[str, VisualizationEntry]:
"""Load the selected live family catalogs."""
return load_visualization_entries(
_selected_families(family),
)
def _score_candidate(
key: str,
summary: str,
tags: list[str],
) -> tuple[int, list[str]]:
skip_match = _SKIP_RE.search(summary)
if skip_match is None:
pick_clause, skip_clause = summary, ""
else:
pick_clause = summary[:skip_match.start()]
skip_clause = summary[skip_match.start():]
key_text = _normalize(key)
pick_text = _normalize(pick_clause)
skip_text = _normalize(skip_clause)
key_tokens = _tokens(key)
pick_tokens = _tokens(pick_clause)
skip_tokens = _tokens(skip_clause)
score = 0
matched_tags: list[str] = []
for tag in tags:
tag_text = _normalize(tag)
tag_tokens = _tokens(tag)
positive_score = 0
negative_score = 0
if tag_text and tag_text in key_text:
positive_score += 20
elif tag_text and tag_text in pick_text:
positive_score += 14
if tag_text and tag_text in skip_text:
negative_score += 12
for token in tag_tokens:
if token in key_tokens:
positive_score += 9
elif token in pick_tokens:
positive_score += 5
if token in skip_tokens:
negative_score += 6
if positive_score:
matched_tags.append(tag)
score += positive_score - negative_score
return score, matched_tags
def _candidate_payload(
entry: VisualizationEntry,
score: int,
matched_tags: list[str],
*,
legacy_output: bool,
) -> dict[str, object]:
payload: dict[str, object] = {
"key": entry.key,
"path": entry.display_path,
"summary": entry.summary,
"score": score,
"matched_tags": matched_tags,
}
if not legacy_output:
payload.update(
{
"family": entry.family,
"reference": entry.reference,
}
)
return payload
def _validation_payload(entry: VisualizationEntry) -> dict[str, object]:
"""Describe one resolution without inventing an asset for intent-only keys."""
payload: dict[str, object] = {
"family": entry.family,
"key": entry.key,
"kind": entry.kind,
}
if entry.kind == VISUALIZATION_SVG_KIND:
payload.update(
{
"path": entry.display_path,
"reference": entry.reference,
}
)
elif entry.kind != LEGACY_STRUCTURE_INTENT_KIND:
raise VisualizationCatalogError(
f"{entry.key!r} resolves to unsupported kind {entry.kind!r}"
)
return payload
def recall_candidates(
page: str,
tags: list[str],
limit: int,
*,
family: str = "all",
force_semantic_fallback: bool = False,
legacy_output: bool = False,
) -> dict[str, object]:
"""Recall a deterministic shortlist for one page."""
entries = load_catalog(family)
scored: list[tuple[int, VisualizationEntry, list[str]]] = []
for entry in entries.values():
score, matched_tags = _score_candidate(entry.key, entry.summary, tags)
if score > 0 and matched_tags:
scored.append((score, entry, matched_tags))
scored.sort(key=lambda item: (-item[0], item[1].family, item[1].key))
candidates = [
_candidate_payload(
entry,
score,
matched_tags,
legacy_output=legacy_output,
)
for score, entry, matched_tags in scored[:limit]
]
top_score = candidates[0]["score"] if candidates else 0
if top_score >= 35:
confidence = "high"
elif top_score >= 15:
confidence = "medium"
elif top_score > 0:
confidence = "low"
else:
confidence = "none"
fallback_required_before_no_match = (
confidence in {"low", "none"} and not force_semantic_fallback
)
if fallback_required_before_no_match:
no_match_instruction = (
f"Lexical confidence is {confidence}. Select a bounded candidate when one "
"fits; otherwise rerun the same recall with --semantic-fallback before "
"keeping no-template-match. Keep the final negative result out of Design "
"Spec Section VII and describe the chosen fallback in the page's Section "
"IX block."
)
else:
no_match_instruction = (
"Use when none of the reviewed candidates fits the page structure. Keep "
"this result out of Design Spec Section VII and describe the chosen fallback "
"in the page's Section IX block."
)
result: dict[str, object] = {
"page": page,
"semantic_tags": tags,
"confidence": confidence,
"candidates": candidates,
"no_template_match": {
"allowed": not fallback_required_before_no_match,
"key": "no-template-match",
"instruction": no_match_instruction,
},
}
if not legacy_output:
result["family_filter"] = family
if force_semantic_fallback:
if legacy_output:
catalog: object = {
entry.key: entry.summary
for entry in sorted(entries.values(), key=lambda item: item.key)
}
else:
catalog = {
entry.reference: {
"family": entry.family,
"key": entry.key,
"path": entry.display_path,
"summary": entry.summary,
}
for entry in sorted(
entries.values(),
key=lambda item: (item.family, item.key),
)
}
result["semantic_fallback"] = {
"reason": "requested-after-bounded-review",
"instruction": (
"Semantically compare the page tags with every returned selection rule. "
"Choose one exact catalog reference or keep no-template-match; lexical "
"overlap is not required in this review."
),
"reference_pattern": "family/key",
"catalog": catalog,
}
return result
def _dedupe(values: list[str]) -> list[str]:
result: list[str] = []
seen: set[str] = set()
for value in values:
stripped = value.strip()
normalized = _normalize(stripped)
if not stripped or not normalized or normalized in seen:
continue
seen.add(normalized)
result.append(stripped)
return result
def _run_recall(args: argparse.Namespace) -> int:
page = args.page.upper()
if not _PAGE_RE.fullmatch(page):
print("Error: --page must match P<NN>, for example P03.", file=sys.stderr)
return 2
tags = _dedupe(args.tag)
if not 3 <= len(tags) <= 8:
print("Error: recall requires 3-8 distinct non-empty --tag values.", file=sys.stderr)
return 2
result = recall_candidates(
page,
tags,
args.limit,
family=args.family,
force_semantic_fallback=args.semantic_fallback,
legacy_output=args.legacy_output,
)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
def _resolve_validation_value(
raw: str,
family: str,
*,
allow_legacy_bare: bool,
) -> VisualizationEntry:
if "/" in raw:
return resolve_visualization_reference(raw)
if family != "all":
return resolve_visualization_reference(f"{family}/{raw}")
return resolve_visualization_reference(
raw,
allow_legacy_bare=allow_legacy_bare,
)
def _run_validate(args: argparse.Namespace) -> int:
selected = _dedupe(args.keys)
invalid: list[str] = []
valid_entries: list[tuple[str, VisualizationEntry]] = []
for value in selected:
try:
valid_entries.append(
(
value,
_resolve_validation_value(
value,
args.family,
allow_legacy_bare=args.legacy_bare,
),
)
)
except VisualizationCatalogError:
invalid.append(value)
if args.legacy_output:
valid: object = sorted(raw for raw, _entry in valid_entries)
else:
valid = [
_validation_payload(entry)
for _raw, entry in sorted(
valid_entries,
key=lambda item: (item[1].kind, item[1].family, item[1].key),
)
]
result: dict[str, object] = {"invalid": sorted(invalid), "valid": valid}
if args.legacy_output:
result["resolved"] = [
{"input": raw, **_validation_payload(entry)}
for raw, entry in sorted(valid_entries, key=lambda item: item[0])
]
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if invalid:
mapping_name = (
"page_charts"
if args.legacy_output or args.legacy_bare
else "page_visualizations"
)
print(
"Error: replace each invalid reference with one returned by recall, or "
f"keep no-template-match out of Section VII and {mapping_name} while "
"recording the custom fallback in the page's Section IX block.",
file=sys.stderr,
)
return 1
return 0
def _add_family_argument(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--family",
choices=("all",) + visualization_families(),
default="all",
help="Limit candidates to one family (default: all).",
)
def build_parser(*, legacy_output: bool = False) -> argparse.ArgumentParser:
description = (
"Recall legacy chart-catalog candidates or validate selected keys."
if legacy_output
else "Recall visualization candidates or validate selected references."
)
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.set_defaults(legacy_output=legacy_output)
subparsers = parser.add_subparsers(dest="command", required=True)
recall = subparsers.add_parser("recall", help="Recall candidates for one page.")
recall.add_argument("--page", required=True, help="Planned page key, for example P03.")
recall.add_argument(
"--tag",
action="append",
required=True,
help="English semantic content-shape tag; repeat 3-8 times.",
)
recall.add_argument(
"--limit",
type=int,
choices=range(3, 9),
default=6,
metavar="3..8",
help="Candidate count (default: 6).",
)
recall.add_argument(
"--semantic-fallback",
action="store_true",
help="Include the selected live catalogs after a low-confidence review.",
)
_add_family_argument(recall)
recall.set_defaults(handler=_run_recall)
validate = subparsers.add_parser("validate", help="Validate selected references.")
validate.add_argument("keys", nargs="+", help="One or more keys or family/key references.")
validate.add_argument(
"--legacy-bare",
action="store_true",
default=legacy_output,
help=(
argparse.SUPPRESS
if legacy_output
else "Allow unqualified keys from an existing legacy page_charts mapping."
),
)
_add_family_argument(validate)
validate.set_defaults(handler=_run_validate)
return parser
def main(
argv: Optional[list[str]] = None,
*,
legacy_output: bool = False,
) -> int:
parser = build_parser(legacy_output=legacy_output)
args = parser.parse_args(argv)
try:
return args.handler(args)
except VisualizationCatalogError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -52,8 +52,8 @@ can always switch modes. The page accepts one registered choice per kind plus
one supplied-root choice. A registered exact root is `library`; any other exact
root is `explicit`. After that combined confirmation,
[`apply-template-workspace`](../workflows/stages/apply-template-workspace.md)
validates, fuses, and installs every selected workspace into the current
project before Stage 2 starts. Template-aware reading begins in final Stage 2 from
validates and installs every selected workspace into the current
project as its own `design_spec.<kind>.<id>.md` before Stage 2 starts. Template-aware reading begins in final Stage 2 from
that project-local copy. Quick skips the page, applies supplied exact roots, and
otherwise uses free design.
@@ -65,7 +65,7 @@ otherwise uses free design.
| Selection source | `library` / `explicit` | Step-3 discovery provenance only: exact index-derived root or exact unregistered root; it does not change template semantics |
| Internal creation strategy | `standard` / `fidelity` / `mirror` | AI-derived Create Layout/Create Deck implementation: newly author a compact or broad roster, or materialize validated source-package facts into a new workspace; persisted for tools, never presented as a required user choice |
| Internal application plan | `template_reuse_scope` plus optional `template_adherence` | Strategist derives literal, structural, or style-only use and any strict/adaptive exporter behavior after inspecting the installed template and current content |
| PPTX structure | `flat` / `structured` | Derived application plans that use template structure compile declared Masters and Layouts; Style-only, style-scope, brand-only, and free design remain Slide-local. Style fused with Layout/Deck does not change the non-Style structure plan. |
| PPTX structure | `flat` / `structured` | Derived application plans that use template structure compile declared Masters and Layouts; Style-only, style-scope, brand-only, and free design remain Slide-local. A Style installed alongside Layout/Deck does not change the non-Style structure plan. |
These axes must not be used as synonyms or exposed as a user mode matrix. In
particular, a mirror-created deck is still an ordinary reusable `deck` package
@@ -92,7 +92,7 @@ Style narrows this shared routing shape to `templates/design_spec.md` only and
does not carry asset or review payloads; initialized-project sibling scaffolding
may exist but is not Style input.
The conditional [`apply-template-workspace`](../workflows/stages/apply-template-workspace.md)
stage owns installation and fusion after a non-free Default Stage-1 selection or
stage owns installation after a non-free Default Stage-1 selection or
on the Quick exact-root branch. Brand/Layout/Deck consume package-owned
`templates/`, `images/`, and `icons/`; Style consumes only
`templates/design_spec.md` and ignores sibling project scaffolding. Every kind
@@ -116,23 +116,22 @@ effects and PowerPoint interfaces loaded only when triggered.
## Visualization Templates
The `charts/` directory contains the registered visualization templates. For backward compatibility, the directory name remains `charts/`, but its scope includes charts, infographics, process diagrams, relationship diagrams, strategic frameworks, and system architecture diagrams:
Page-local Shape-first references are catalog families, not reusable template
kinds:
- KPI Cards
- Bar Chart / Stacked Bar Chart
- Line Chart / Dual-Axis Line Chart
- Donut Chart
- Radar Chart
- Funnel Chart
- Matrix (2x2)
- Timeline
- Gantt Chart
- Process Flow
- Org Chart
- Layered Architecture / Module Composition / Hub with Described Spokes / Pipeline with Stages / Client-Server Flow
| Family | Owns | Directory | Index |
|---|---|---|---|
| Chart | Value-driven geometry (33) | [`charts/`](./charts/) | [`charts_index.json`](./charts/charts_index.json) |
| Table | Row × column fact grid (6) | [`tables/`](./tables/) | [`tables_index.json`](./tables/tables_index.json) |
- **Library index (single source of truth)**: [charts/charts_index.json](./charts/charts_index.json)
- **Directory overview**: [charts/README.md](./charts/README.md)
[`VISUALIZATION_TEMPLATE_AUTHORING.md`](./VISUALIZATION_TEMPLATE_AUTHORING.md)
is the shared authoring contract. Each index owns family membership and
selection rules.
Qualitative Structure is a Slide-local Executor method rather than a catalog:
Default and Quick both derive its relationship model and compose shapes for the
current page. Only [`layouts/`](./layouts/) owns reusable Master/Layout, page
types, slots, and placeholders.
## Icon Library
@@ -1,17 +1,37 @@
# Chart Template Authoring Guide
# Visualization Template Authoring Guide
`templates/charts/` 的模板负责可视化结构、数据编码和信息关系,不负责最终项目风格。模板必须保持源码可读、独立可渲染,并允许 Executor 根据项目 Design Spec 与 `spec_lock.md` 重做字体、配色和装饰。
这是供维护者审阅的中文指南,因此放在两个 Visualization catalog 的共同父目录,
而不是归某一个 family 所有。`templates/charts/``templates/tables/` 共同组成
page-local Visualization 模板库;模板负责数据编码或单元格关系,不负责最终项目
风格。模板必须保持源码可读、独立可渲染,并允许 Executor 根据项目 Design Spec
`spec_lock.md` 重做字体、配色和装饰。
| Family | 定义 | 判定边界 |
|---|---|---|
| `chart` | Value-driven visualization | 数值、类别、时间、权重或持续时间决定 mark 的位置、长度、面积、角度、字号或连接宽度 |
| `table` | Row × column fact grid | 行头与列头共同寻址一个单元格事实;合并、对齐和边界保持该交点关系 |
**Hard rule — Structure is a method, not a catalog**: 定性顺序、层级、角色、
分区和关系由 [`executor-structure.md`](../references/executor-structure.md) 在当前页面
现场构形,不登记固定 `structure/<key>`、SVG roster 或 index。Default 和 Quick 都先
判断页面真实信息模型;需要定性拓扑时都必须采用这套 Structure 方法。删除 catalog
只删除固定示例,不删除 Quick 或 Default 的 Structure 能力。
**Hard rule — Visualization is not Layout**: Chart/Table 模板和运行时 Structure
都不拥有 `data-pptx-master``data-pptx-layout``data-pptx-layer`
`data-pptx-placeholder`。跨页 Master/Layout、page type、slot geometry 和
placeholder 合同仍只属于 [`layouts/`](./layouts/) workspace。
## 0. 上游规范
**Hard rule**: 本指南只定义 Chart 模板库的结构与中性预览合同。通用 SVG 语法、效果、原生数据接口和 PowerPoint 结构分别由以下权威文件定义:
**Hard rule**: 本指南只定义 Chart/Table 两个 catalog family 的结构与中性预览合同。通用 SVG 语法、效果、原生数据接口和 PowerPoint 结构分别由以下权威文件定义:
| 合同 | 权威文件 |
|---|---|
| 通用 SVG | [`shared-standards.md`](../../references/shared-standards.md) |
| 效果与兼容输入 | [`svg-effects.md`](../../references/svg-effects.md) |
| Native Chart/Table | [`native-data-interface.md`](../../references/native-data-interface.md) |
| 画布格式 | [`canvas-formats.md`](../../references/canvas-formats.md) |
| 通用 SVG | [`shared-standards.md`](../references/shared-standards.md) |
| 效果与兼容输入 | [`svg-effects.md`](../references/svg-effects.md) |
| Native Chart/Table | [`native-data-interface.md`](../references/native-data-interface.md) |
| 画布格式 | [`canvas-formats.md`](../references/canvas-formats.md) |
**Forbidden — second SVG specification**: 不在本指南复述或放宽上游语法。发生冲突时以上游权威文件为准。
@@ -19,19 +39,29 @@
## 1. 所有权边界
### 1.1 模板与项目
### 1.1 作者合同与项目消费
| Chart 模板拥有 | 项目拥有 |
以下所有权只约束库内单个模板工件的维护,不把该工件变成项目页面的布局锁:
| 单个 Visualization 模板的作者合同 | 项目页面决策拥有 |
|---|---|
| 可视化类型与数据到图形的映射 | 项目字体与字号体系 |
| 节点、连接、轴、系列和标签关系 | 项目调色板与品牌色 |
| 可视化类型、构图骨架和阅读顺序 | 实际分组、框架数量、项目数量与容量适配 |
| 该工件的 family 与相应信息模型 | 最终采用的页面级结构、类型与几何 |
| 该工件内的数据映射或单元格关系 | 项目字体、字号、调色板与品牌色 |
| 该工件的示例骨架和阅读顺序 | 实际分组、框架数量、项目数量、组合方式与容量适配 |
| 必要的状态与语义区分 | 页面背景、页头、页脚和品牌 chrome |
| 独立预览所需的中性样式 | 最终强调策略与页面级视觉层级 |
**Hard rule**: Executor 适配模板时保留可视化类型、信息关系和数据准确性;最终视觉必须来自当前项目,而不是继承模板的示例审美。
**Hard rule — authoring integrity**: 维护某个库模板时必须保持该模板自身的
family 与信息模型:Chart 的 value mapping、Table 的 row × column fact grid。
若修改结果属于定性关系拓扑,不新增第三类模板,而由 runtime Structure 方法构建;
若仍属于另一 catalog family,应登记到正确 family,不能保留错误标签。最终视觉样式
仍来自使用它的项目,而不是模板的示例审美。
**Reference — not a constraint**: 模板的分组、框架数、项目数和示例容量用于展示结构,不是项目上限。Executor 可按实际内容调整,但不能改变已选可视化类型、关系或数据语义。
**Hard rule — flexible project reference**: Executor 选中的 `family/key` 仍只是
flexible page-local reference。Default 的 §IX 或 Quick 的页面决策拥有最终结构;
选中引用不会锁死页面的可视化类型、几何、分组数、项目数或组合方式,Executor
可按已确认的页面意图适配、重组或替换。项目若实际采用某一信息模型,必须保持
其真实数据与关系准确;仅有引用本身不构成页面结构权威。
### 1.2 保留判断
@@ -44,12 +74,17 @@
| 只让示例显得更精致、立体、品牌化或“高级” | 作为简化候选;通过文本与前后渲染核对后再删除 |
| 只对某个项目风格成立 | 交给 Executor 重建 |
**Default — structure first (may override when semantics require it)**: 优先使用清楚的线、面、标签和留白。装饰不能成为理解结构的前提。
**Default — information first (may override when semantics require it)**: 优先使用清楚的线、面、标签和留白。装饰不能成为理解信息的前提。
### 1.3 保真优先
**Hard rule — fidelity before slimming**: 模板瘦身不得改写或删除原有可见标题、标签、说明、数值、单位、状态、来源、顺序、容量和关系。占位内容保持原文;只有明确重复的信息可以删除,并记录理由。
**Explicit migration exception**: 经明确批准的 family/key 合并、重组或退役可以用
中性占位内容重建 canonical SVG,或移除不再存活的工件;这不是普通模板瘦身。
迁移必须在 §8 记录 catalog 边界和仍可读取的 legacy 语义。未列入批准迁移的模板
仍受上一条逐字保真约束。
**Hard rule — structural frames survive**: 框线、底色、分隔、标签页或面板只要表达真实的信息单元、父子层级、阶段范围、绘图区或输出区,就属于结构。可以减少叠加效果,但不得为了 token 数字把有效层级压平。
**Forbidden — compression by rewriting**: 不用缩写、概括、换词或删句降低 token。体积优化来自属性继承、重复样式合并和非语义效果简化,不来自内容编辑。
@@ -165,7 +200,7 @@
---
## 5. 结构与边界
## 5. SVG 分组与边界
### 5.1 语义分组
@@ -174,11 +209,14 @@
| 顶层组 | 典型内容 |
|---|---|
| `header` | 标题与副标题 |
| `chart-area` / replacement carrier | 轴、数据系列、标签、必要 metadata |
| `chart-area` / replacement carrier | Chart 的轴、数据系列、标签、必要 metadata |
| `legend` | 系列或状态说明 |
| `card-1` / `feature-card-1` | 一个完整信息单元 |
| `timeline-track` | 时间轴与阶段标签 |
| `milestone-cards` | 同一结构的一组里程碑卡片 |
| `table-area` / replacement carrier | Table 的行、列、单元格和必要 metadata |
| `table-notes` | 来源、口径或脚注 |
**Forbidden — workspace metadata**: 两个 catalog family 都不得写入
Master/Layout/placeholder 所有权标记。运行时 Structure 即使占满整个
`1280×720` 画布也仍是 Slide-local 构形,不会因此成为 Layout workspace。
### 5.2 `data-pptx-bounds`
@@ -211,7 +249,6 @@
| 基础节点/容器 | `<rect>``<circle>``<ellipse>` |
| 直线关系/分隔/引线 | `<line>` |
| 预设可精确表达的弯折/曲线关系 | 完整 compact authored `bentConnector*` / `curvedConnector*` `<g>`;端点不附着 |
| 标准块箭头/流程节点 | 仅在 preset 精确匹配时使用完整 compact authored-preset `<g>` |
| 单一预设不能表达、但封闭形状可组合的对象 | 优先用 `shape_boolean_svg.py` 物化 Merge Shapes 结果 |
| 图元、预设、Boolean 都不能表达的数据/语义/锁定风格几何 | `<path>``<polygon>``<polyline>` |
| 数据图表 | 默认 Shape fallback;符合条件时附带 native replacement marker |
@@ -220,7 +257,7 @@
---
## 6. 数据图表合同
## 6. Chart 与 Native Data 合同
### 6.1 绘图区标记
@@ -232,9 +269,14 @@
Pie、Donut、Radar 使用对应中心和半径格式。该注释是工具输入,不得作为“清理注释”删除。
Catalog 模板本身只有一个主 Chart,可保留上述 unscoped marker 与
`id="chartArea"`。Executor 在项目页面组合多个已验证 Chart 对象时,改用
[`executor-chart.md`](../references/executor-chart.md) 定义的 semantic object key
和 scoped marker;不能在同一 SVG 中复制多个 `chartArea` id。
### 6.2 Native Chart/Table
**Hard rule**: 只有 [`native-data-interface.md`](../../references/native-data-interface.md) 支持的真实数据图表或纯文本表格使用 replacement marker。JSON metadata 与可见 fallback 必须表达同一份数据。
**Hard rule**: 只有 [`native-data-interface.md`](../references/native-data-interface.md) 支持的真实 Chart 或纯文本 Table 使用 replacement marker。JSON metadata 与可见 fallback 必须表达同一份数据。运行时构建的 Structure 永远不使用 Chart/Table replacement marker。
```xml
<g id="line-chart"
@@ -275,9 +317,16 @@ Pie、Donut、Radar 使用对应中心和半径格式。该注释是工具输入
**Forbidden — placeholder storytelling**: 不写长篇营销文案、部门归属、真实品牌或无法复用的项目背景。
### 7.2 `charts_index.json`
### 7.2 Family indexes
新增模板必须登记 `<key>.summary`
新增模板只登记到其 owning family index
| Family | Directory | Index object |
|---|---|---|
| `chart` | `charts/` | `charts_index.json``charts` |
| `table` | `tables/` | `tables_index.json``tables` |
每个 `<key>.summary` 使用相同的选择句合同:
```json
"line_chart": {
@@ -285,96 +334,62 @@ Pie、Donut、Radar 使用对应中心和半径格式。该注释是工具输入
}
```
**Hard rule**: `summary` 是选型句,使用 `Pick for ... Skip if ...`,不是视觉描述;`key` 与文件名一致,`meta.total` 与 catalog 数量一致。
**Hard rule**: `summary` 是选型句,使用 `Pick for ... Skip if ...`,不是视觉描述;`key`同 family 文件名一致,`meta.total`该 family catalog 数量一致。不要建立跨 family 的第二份成员清单。
---
## 8. 迁移边界
本指南是新建和修改模板的目标合同。当前目录中的 76 个 SVG 均已纳入该合同;后续不得以历史文件为由恢复无语义装饰,也不得把中性化误解为删除结构边界。
本指南是新建和修改 Chart/Table 模板的目标合同。当前两个 catalog family 共
39 个 SVGChart 33 个、Table 6 个。各 live index 是成员清单的唯一权威;
Structure 不再有固定 SVG roster、index 或 canonical key。
**Current reference set**:
**Hard rule — topology is generated, not recalled**: 从来源 deck 吸收的是 Shape
grammar 和构形思维,而不是页面示例清单。先还原顺序、层级、分组、分区、连接、
交叠等真实关系,再由 `executor-structure.md` 使用区域、节点、主轴、连接、标签与
装饰等角色现场组合。Default 在 §IX 描述该关系,Quick 在当前上下文作同一判断;
两条路径都不依赖 `structure/<key>` 或固定 SVG,不能因 Quick 跳过 §VII/lock 就跳过
Structure 载体判断。
| 模板 | 覆盖结构 |
**Canonical Table set**:
| Canonical key | 核心信息关系 |
|---|---|
| `timeline.svg` | 时间、状态和里程碑卡片 |
| `kpi_cards.svg` | KPI 值、单位与趋势 |
| `labeled_card.svg` | 2×2 标签卡片结构 |
| `icon_grid.svg` | 2×3 图标槽与能力卡片 |
| `line_chart.svg` | 双系列折线与 native chart metadata |
| `pipeline_with_stages.svg` | 分阶段管线、贯通流程和输出链 |
| `layered_architecture.svg` | 分层架构、能力输出和底座 |
| `stacked_area_chart.svg` | 累计面积、图例和统计卡片 |
| `heatmap_chart.svg` | 时间×日期矩阵、连续色阶和统计侧栏 |
| `bubble_chart.svg` | 三变量气泡、象限、系列清单和尺寸图例 |
| `quadrant_text_bullets.svg` | 二轴四象限、分区说明和行动标签 |
| `financial_statement_table.svg` | 财务层级、数值列和强调合计行 |
| `box_plot_chart.svg` | 五数分布、异常值、图例和统计摘要 |
| `dual_axis_line_chart.svg` | 双轴序列、阶段带和数据标注 |
| `stacked_bar_chart.svg` | 堆叠分类、总量标签和洞察侧栏 |
| `segmented_wheel.svg` | 中心主题、等权扇区和配对说明卡 |
| `sankey_chart.svg` | 零损耗流向、节点层级和流量编码 |
| `roadmap_vertical.svg` | 纵向里程碑、状态轨道和目标侧栏 |
| `snake_flow.svg` | 多行蛇形长流程、顺序节点和配对里程碑卡 |
| `concentric_circles.svg` | 同心优先级、资源占比和分层说明卡 |
| `scatter_chart.svg` | 双系列散点、回归趋势、置信区间和统计洞察 |
| `area_chart.svg` | 双系列累计面积、月度趋势和摘要指标 |
| `chevron_process.svg` | 连续阶段箭头、周期带和阶段交付物 |
| `radar_chart.svg` | 多维能力对比、系列面积和基准数据表 |
| `module_composition.svg` | 父模块边界、三级处理链和端到端数据流 |
| `fishbone_diagram.svg` | 核心问题、六类原因分支和具体成因标签 |
| `numbered_steps.svg` | 编号步骤、连接顺序、任务卡和阶段时长 |
| `pareto_chart.svg` | 80/20 分界、降序柱体、累计曲线和行动洞察 |
| `top_down_tree.svg` | 父子层级、汇报连线和末级节点摘要 |
| `butterfly_chart.svg` | 共用中轴、双侧镜像系列和对称刻度 |
| `chevron_chain_with_tail.svg` | 连续箭头阶段、支撑标签和结果汇总尾块 |
| `gauge_chart.svg` | 单项指标、目标区间、当前值和状态说明 |
| `gantt_chart.svg` | 任务行、时间跨度、依赖关系和当前时间标记 |
| `waterfall_chart.svg` | 起始值、增减贡献、连接基线和最终合计 |
| `hub_inward_arrows.svg` | 外围输入、向心关系和中心结论 |
| `treemap_chart.svg` | 层级面积编码、分类色块、标签和解释注记 |
| `hub_spoke.svg` | 中心枢纽、径向连接、能力节点和参考环 |
| `matrix_2x2.svg` | 双轴象限、点位分布、象限标签和优先级说明 |
| `process_flow.svg` | 顺序节点、连接关系、阶段时长和状态图例 |
| `donut_chart.svg` | 环形占比、中心总值、系列图例和摘要指标 |
| `grouped_bar_chart.svg` | 多系列并列柱、共用分类轴和系列图例 |
| `pyramid_isometric.svg` | 分层金字塔、等距深度面和层级说明 |
| `progress_bar_chart.svg` | 多项进度、目标标记、当前值和状态分组 |
| `basic_table.svg` | 表头、数据行、对齐列和状态单元格 |
| `mind_map.svg` | 中心主题、放射分支、二级节点和分支骨架 |
| `comparison_columns.svg` | 并列方案列、价格层级、功能清单和推荐状态 |
| `bullet_chart.svg` | 定性区间、实际值、目标线和多指标对照 |
| `comparison_table.svg` | 多方案表头、横向属性行和结果强调 |
| `client_server_flow.svg` | 客户端与服务端分区、请求响应和交互方向 |
| `dumbbell_chart.svg` | 双状态端点、变化连线、差值和项目排序 |
| `pyramid_chart.svg` | 递进层级、分层容量和层级说明 |
| `vertical_pillars.svg` | 并列支柱、分类标题、要点列表和底部结论 |
| `journey_map.svg` | 阶段轨道、用户行动、情绪曲线和痛点卡片 |
| `consulting_table.svg` | 分层行列、指标数值、数据条和重点结论 |
| `pros_cons_chart.svg` | 正反双栏、判断轴、论据列表和建议结论 |
| `project_schedule_table.svg` | 任务表格、负责人、状态和横向排期 |
| `horizontal_bar_chart.svg` | 长标签排名、横向数值条和洞察侧栏 |
| `funnel_chart.svg` | 递减阶段、转化率、流失关系和摘要指标 |
| `isometric_stairs.svg` | 递进台阶、阶段标签、空间顺序和接地基线 |
| `pie_chart.svg` | 单层占比、扇区标签、图例和总量摘要 |
| `circular_stages.svg` | 环形阶段、循环方向、阶段说明和中心主题 |
| `team_roster.svg` | 成员卡片、头像槽、姓名职务和简介容量 |
| `harvey_balls_table.svg` | 评价行列、分级圆点、评分图例和汇总状态 |
| `column_chart.svg` | 单系列分类柱、数值标签、坐标轴和基准线 |
| `arc_anchored_list.svg` | 弧线主轴、锚点节点、顺序条目和配对说明 |
| `vertical_list.svg` | 纵向轨道、顺序节点、内容卡片和阶段状态 |
| `sunburst_chart.svg` | 多层环形层级、父子占比、叶子图例和解释面板 |
| `agenda_list.svg` | 编号议程、条目说明、时长信息和纵向导轨 |
| `stock_chart.svg` | OHLC 蜡烛、日期轴、价格区间和指标摘要 |
| `feature_matrix_table.svg` | 功能行、产品列、二元状态和方案对照 |
| `histogram_chart.svg` | 连续分箱、频数柱、统计标记和分布解释 |
| `venn_diagram.svg` | 集合边界、交集区域、关系标签和结论说明 |
| `quadrant_bubble_scatter.svg` | 双轴象限、气泡位置、尺寸编码和项目标签 |
| `bar_of_pie_chart.svg` | 主饼占比、长尾聚合、堆叠条明细和连接关系 |
| `pie_of_pie_chart.svg` | 主饼占比、长尾聚合、次级饼明细和连接关系 |
| `word_cloud.svg` | 词项权重、字号编码、主题分布和关键词层级 |
| `record_table` | 每行一条记录、每列一个稳定字段 |
| `metric_table` | 实体与 KPI 的交点承载度量、变化、状态或 cell 内微图形 |
| `comparison_matrix` | 行维度与列方案的交点承载文本、精确值或异构事实 |
| `feature_matrix` | 能力与方案的交点承载支持、不支持、部分支持或例外状态 |
| `rating_matrix` | 评价维度与方案的交点使用同一套序数等级 |
| `hierarchical_table` | 分组或缩进行、明细与小计/总计形成层级网格 |
**Hard rule**: 修改任一模板时先冻结可见文本、数据和结构层级,再简化确认无语义的效果、补齐直属根 bounds,并完成文本差异、独立渲染与双路线验证。未经明确说明的文本删除、改写或结构边界丢失都会阻断变更。不要仅为追求 catalog 一次性整齐而批量重写。
**Hard rule — semantic family before physical object**: PowerPoint 中的物理
Table 对象不自动属于 `table` family。只有行头与列头的交点可寻址为一个事实时才是
Table;日期或持续时间决定 `x`/`width` 的排期是 `chart/gantt_chart`;阶段与泳道只
表达定性活动位置时由 runtime Structure 构建。
**Canonical catalog aliases**:
| Canonical reference | Legacy bare keys |
|---|---|
| `table/record_table` | `basic_table` |
| `table/metric_table` | `consulting_table` |
| `table/comparison_matrix` | `comparison_table` |
| `table/feature_matrix` | `feature_matrix_table` |
| `table/rating_matrix` | `harvey_balls_table` |
| `table/hierarchical_table` | `financial_statement_table` |
| `chart/gantt_chart` | `project_schedule_table` |
新文档、§VII 和 `page_visualizations` 只写 canonical `chart|table/<key>`。表中 Alias
只服务旧 `page_charts` 读取,不是新模板命名候选;旧的 family-qualified key 不获得
兼容特权。此前 36 个 canonical Structure bare key 只在旧 `page_charts` 读取时保留为
`legacy-structure-intent`:它没有 canonical SVG、path 或 reference,不参与 recall
也不能写入新 §VII/lock。消费方把该意图投影为定性关系提示,再交给 runtime Structure
构形;不得把它伪装成仍存活的模板。
**Hard rule**: 修改一个仍属 canonical 的模板时先冻结可见文本、数据和结构层级,
再简化确认无语义的效果、补齐直属根 bounds,并完成文本差异、独立渲染与双路线
验证。经本节明确登记的 catalog 合并/退役不要求保留旧示例文案;除此之外,未经
说明的文本删除、改写或结构边界丢失都会阻断变更。
---
@@ -397,9 +412,11 @@ Pie、Donut、Radar 使用对应中心和半径格式。该注释是工具输入
- [ ] 颜色差异确实表达 series、state、positive/negative 等语义。
- [ ] 标题、副标题和来源只用于展示必要结构或容量。
### 9.3 数据与 PowerPoint
### 9.3 Family 与 PowerPoint
- [ ] 数据图表保留准确 `chart-plot-area` 标记
- [ ] Family 判定正确:Chart 为 value-driven、Table 为 row × column fact grid;定性拓扑退出 catalog 并由 runtime Structure 构建
- [ ] Chart/Table 模板不含 Master/Layout/layer/placeholder ownership metadata。
- [ ] Calculator-supported Chart 保留准确 `chart-plot-area` 标记。
- [ ] Eligible Chart/Table 的 metadata 与可见 fallback 数据一致。
- [ ] 默认 Shape-first 导出通过。
- [ ] 存在 replacement marker 时,显式 native Chart/Table 导出通过。
@@ -407,8 +424,8 @@ Pie、Donut、Radar 使用对应中心和半径格式。该注释是工具输入
### 9.4 Catalog
- [ ] 新模板登记 `charts_index.json`
- [ ] 修改 key/summary 后通过 `chart_recall.py validate` 和 recall 烟测。
- [ ] 新模板登记到 owning family indexindex object、`meta.total` 与 SVG roster 一致
- [ ] 修改 key/summary 后通过 `visualization_recall.py validate` 和 recall 烟测。
- [ ] 前后可见文本差异已审阅,非重复内容没有意外丢失或改写。
- [ ] 前后渲染对比确认结构仍可读。
- [ ] 记录 bytes/tokens 变化,但不以牺牲源码可读性换取数字。
@@ -420,55 +437,15 @@ Pie、Donut、Radar 使用对应中心和半径格式。该注释是工具输入
```bash
# 单文件 SVG 合同
python3 skills/ppt-master/scripts/svg_quality_checker.py \
skills/ppt-master/templates/charts/<key>.svg
skills/ppt-master/templates/<family-directory>/<key>.svg
# Catalog key
python3 skills/ppt-master/scripts/chart_recall.py validate <key>
# Canonical family/key
python3 skills/ppt-master/scripts/visualization_recall.py validate \
<family>/<key>
# 可安全压缩的页面坐标(默认 dry-run)
python3 skills/ppt-master/scripts/compact_svg_coordinates.py \
skills/ppt-master/templates/charts/<key>.svg
skills/ppt-master/templates/<family-directory>/<key>.svg
```
**Validation**: 修改后至少完成 XML 解析、独立 SVG 渲染、Checker、默认 Shape-first 导出,以及 marker 模板的 native Chart/Table 导出。
---
## 11. 结构图式兼容索引
本节保留旧引用锚点,但所有图式都受 §1 所有权边界约束。
### 11.1 Attached Section Tab
**Reference — not a constraint**: 半圆标签只在“标签从属于当前信息块”是结构信息时使用。颜色、圆角和高度由项目适配;它不是卡片的默认装饰。
**Forbidden — cover hack**: 不把“全圆角矩形 + 同色覆盖矩形”作为两个未合并对象叠放来伪造单侧圆角。需要时优先以封闭的圆角矩形和矩形为 operands,通过 `shape_boolean_svg.py` 物化 Union 结果;只有 Boolean 仍不能忠实表达时才手写可编辑 path。
### 11.2 Nested Card Border
**Default — single boundary (may override when hierarchy requires two levels)**: 中性模板优先一层描边或留白。浅色外框 + 内层白卡属于旧视觉配方,不再作为共享模板默认;只有外层与内层表达两个真实层级时才保留。
### 11.3 Card Grid
卡片网格表达并列关系和容量,不决定最终卡片风格:
| 结构 | 典型容量 | 参考画布分配 |
|---|---|---|
| 2×2 | 4 个平行方面/KPI | `560×255`,横向间距约 40 |
| 2×3 | 6 个能力/服务 | `370×260`,横向间距约 25 |
| 1×3 | 3 个平行支柱 | 每列约 `400×540` |
| 1×4 | 4 个紧凑指标 | 每列约 `280×250` |
**Hard rule**: `page_rhythm: breathing` 不因 catalog 示例自动变成卡片网格;最终结构仍服从页面内容和项目节奏。
### 11.5 Diagonal Relationship Arrow
**Hard rule**: 倾斜虚线箭头只表达跨象限迁移、影响或建议方向,并配一条简短关系标签。颜色与标签外观由项目决定。
### 11.6 Ground Anchor
**Default — omit (may override when depth is semantic)**: 接地椭圆是深度装饰,不属于中性模板默认。只有物体与地面/层级的空间关系本身有意义时保留;不得为了“漂浮感”普遍添加。
### 11.7 Bidirectional Interaction Arrows
**Hard rule**: 双向关系使用两条方向明确的线,每条线都有动作标签。请求/响应的颜色只需可区分,最终映射由项目调色板决定。
@@ -18,7 +18,7 @@ the same selector, defaults Stage 1 to template mode, and preselects that
specific candidate only when it is the sole supplied root.
Registered exact roots are `library`; other exact roots remain `explicit`. The conditional
[`apply-template-workspace`](../../workflows/stages/apply-template-workspace.md)
stage owns path normalization, portable-root installation, multi-kind fusion,
stage owns path normalization, portable-root installation, per-workspace spec naming,
same-kind conflict resolution, and provenance after Stage 1 and before Stage 2. Template-aware
reading begins in final Stage 2 from the installed project-local copy. This file owns
only the Brand schema. Quick applies a supplied exact Brand root directly and
@@ -0,0 +1,63 @@
---
brand_id: accenture
kind: brand
summary: Accenture-style consulting and technology-services identity — violet accent on black for transformation, technology, and delivery decks
primary_color: "#A100FF"
---
# Accenture Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Accenture |
| Use Cases | Technology and business transformation, systems integration and delivery proposals, industry point-of-view papers, and client advisory decks |
| Tone | Formal, forward-looking, delivery-focused, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07; the violet is widely documented as PMS 2592 C. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#A100FF` | approx | Violet consistently documented as the firm's signature accent; commonly mapped to PMS 2592 C |
| accent | `#000000` | approx | Black, documented as the wordmark color and the identity's typographic weight |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F5F3F7` | approx | Cool off-white — card and module surfaces |
| border | `#DCD8E0` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5B5560` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented to a client as the firm's specification. The violet is a highly saturated accent, not a field color: keep it off large fills and body text, where it vibrates against white and fails contrast at small sizes. Black carries the typographic weight and the violet marks the single decisive element per page. Choose semantic chart and status colors per deck; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The firm uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The firm's marks are protected trademarks, and a redrawn or approximated wordmark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains a single-color `simple-icons/accenture` symbol for genuine brand-recognition contexts such as a client or partner listing; it is a symbol only, not the official lockup, and must never be combined with typed text to fabricate one. Never imply affiliation, sponsorship, endorsement, or authorship by the firm.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Tie capability claims to delivery evidence, keep commitments specific, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: alibaba
kind: brand
summary: Alibaba-style commerce and cloud identity — signature orange on black for platform, merchant, and cloud solution decks
primary_color: "#FF6A00"
---
# Alibaba Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Alibaba Group |
| Use Cases | Commerce platform and merchant solution decks, cloud and digital-infrastructure proposals, ecosystem and partner presentations, and industry briefings |
| Tone | Professional, commercially direct, outcome-focused |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, giving the orange as PMS 1585 C. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#FF6A00` | approx | Signature orange consistently documented across sources; commonly mapped to PMS 1585 C |
| accent | `#000000` | approx | Black documented alongside the orange as the wordmark and typographic weight |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F7F5F3` | approx | Warm off-white — card and module surfaces |
| border | `#E3DEDA` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5F5A55` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented as the company's specification. This orange is highly saturated and fails contrast against white at text sizes: keep it off body text and large fills, and let black carry the typographic weight while the orange marks the decisive element. It also sits in the conventional warning range, so where a chart or diagram encodes status, declare that mapping explicitly and keep it distinguishable from identity use. Note that several Chinese technology brands share nearly this exact orange — when a deck shows more than one of them together, distinguish them by mark and label rather than by color. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The group's marks are protected trademarks, and its subsidiaries and platforms carry materially different identities that must not be conflated with the group mark. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains single-color `simple-icons/alibabacloud` and `simple-icons/alibabadotcom` symbols for genuine brand-recognition contexts; they are symbols for those specific properties, not a group lockup. Never imply affiliation, sponsorship, endorsement, or authorization.
## V. Voice & Tone
- Formality: neutral
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Name the specific platform or business unit rather than the group where the claim belongs to one of them, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: aws
kind: brand
summary: AWS-style cloud identity — signature orange on deep navy for cloud architecture, migration, and well-architected review decks
primary_color: "#FF9900"
---
# Amazon Web Services Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Amazon Web Services (AWS) |
| Use Cases | Cloud architecture and migration decks, well-architected and cost reviews, partner and ISV solution presentations, and technical enablement |
| Tone | Technical, pragmatic, architecture-focused, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, which give the orange as PMS 1375 C and the navy as PMS 7546 C. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#FF9900` | approx | Signature orange consistently documented across sources; commonly mapped to PMS 1375 C |
| accent | `#252F3E` | approx | Deep navy documented as the other half of the pairing; commonly mapped to PMS 7546 C |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F2F3F3` | approx | Cool gray — card and module surfaces |
| border | `#D5DBDB` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#545B64` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented as the company's specification. The navy carries the page and the orange marks what matters — an architecture diagram tinted entirely orange loses all hierarchy. The orange also sits squarely in the conventional warning range, so where a diagram encodes state or a chart encodes status, declare that mapping explicitly and keep it distinguishable from identity use. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The company's marks are protected trademarks whose use by third parties is governed by published trademark guidelines this preset cannot grant. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains single-color `simple-icons/amazonwebservices` and per-service symbols for genuine brand-recognition and architecture-diagram contexts; they are symbols only, not the official lockup. Never imply affiliation, sponsorship, endorsement, partner tier, or certification.
## V. Voice & Tone
- Formality: neutral
- Person: we / you (English), 我们 / 你 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Name services exactly and in full on first use, state the region and pricing basis behind any cost figure, and mark preview services as such.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline` for generic concepts, and reserve the bundled service symbols for actual named services in an architecture diagram. Keep one generic family across the deck.
@@ -0,0 +1,63 @@
---
brand_id: bain
kind: brand
summary: Bain-style consulting identity — scarlet red on restrained neutrals for results-oriented strategy and performance decks
primary_color: "#EE3224"
---
# Bain & Company Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Bain & Company |
| Use Cases | Results-oriented strategy, private equity and diligence support, performance improvement, and executive advisory decks |
| Tone | Formal, direct, results-focused, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, which state explicitly that these values are not published in the firm's own guidelines. Every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#EE3224` | approx | Scarlet red consistently documented as the firm's dominant identity color |
| accent | `#8C1B12` | approx | Deeper red derived from the primary for chapter grounds and depth |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F5F5F5` | approx | Neutral off-white — card and module surfaces |
| border | `#DCDCDC` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5C5C5C` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`, and the compiled sources say so themselves — the firm does not publish these values. Nothing here may be treated as brand truth or represented to a client as the firm's specification. A saturated red carries alarm meaning by default, so use the primary for identity and decisive emphasis only, keep it off large fills behind body text, and never let it read as a risk or failure signal where the page means neither. Choose semantic chart and status colors per deck with that collision in mind; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The firm uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The firm's marks are protected trademarks, and a redrawn or approximated wordmark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. Never imply affiliation, sponsorship, endorsement, or authorship by the firm.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Lead with the result, tie every recommendation to its expected effect, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: bcg
kind: brand
summary: BCG-style consulting identity — forest green and warm neutrals for growth strategy, transformation, and advantage decks
primary_color: "#147B58"
---
# Boston Consulting Group Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Boston Consulting Group (BCG) |
| Use Cases | Growth and corporate strategy, transformation programs, competitive advantage studies, and executive advisory decks |
| Tone | Formal, analytical, forward-looking, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, which state explicitly that these values are not published in the firm's own guidelines. Every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#147B58` | approx | Forest green consistently documented as the firm's dominant identity color; commonly mapped to PMS 341 C |
| accent | `#0A3D2C` | approx | Deeper green derived from the primary for chapter grounds and depth |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F4F6F4` | approx | Warm off-white — card and module surfaces |
| border | `#D8DED9` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5B6660` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`, and the compiled sources say so themselves — the firm does not publish these values. Nothing here may be treated as brand truth or represented to a client as the firm's specification. The green is a grounding color rather than a highlighter: use it for structure, hierarchy, and the decisive mark, not to tint every element on the page. Choose semantic chart and status colors per deck, and take care that a green primary does not collide with a green "positive" status meaning; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The firm uses a proprietary licensed typeface that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The firm's marks are protected trademarks, and a redrawn or approximated mark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. Never imply affiliation, sponsorship, endorsement, or authorship by the firm.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. State the implication rather than only the observation, keep qualification precise, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -1,12 +1,72 @@
{
"accenture": {
"summary": "Accenture-style consulting and technology-services identity — violet accent on black for transformation, technology, and delivery decks",
"primary_color": "#A100FF"
},
"alibaba": {
"summary": "Alibaba-style commerce and cloud identity — signature orange on black for platform, merchant, and cloud solution decks",
"primary_color": "#FF6A00"
},
"anthropic": {
"summary": "Anthropic / Claude brand family — AI research, product talks, developer conferences, technical training, and launches",
"primary_color": "#D97757"
},
"aws": {
"summary": "AWS-style cloud identity — signature orange on deep navy for cloud architecture, migration, and well-architected review decks",
"primary_color": "#FF9900"
},
"bain": {
"summary": "Bain-style consulting identity — scarlet red on restrained neutrals for results-oriented strategy and performance decks",
"primary_color": "#EE3224"
},
"bcg": {
"summary": "BCG-style consulting identity — forest green and warm neutrals for growth strategy, transformation, and advantage decks",
"primary_color": "#147B58"
},
"deloitte": {
"summary": "Deloitte-style professional-services identity — signature green on black and neutrals for audit, tax, risk, and advisory decks",
"primary_color": "#86BC25"
},
"google": {
"summary": "Google brand identity — multi-product corporate decks, developer events (Google I/O style), education and training in the Google ecosystem",
"primary_color": "#4285F4"
},
"huawei": {
"summary": "Huawei-style enterprise identity — restrained red accent on near-black and gray for ICT solution, product, and partner decks",
"primary_color": "#CF0A2C"
},
"ibm": {
"summary": "IBM-style enterprise identity — Carbon blue on a disciplined gray scale for hybrid cloud, AI, and enterprise consulting decks",
"primary_color": "#0F62FE"
},
"jpmorgan": {
"summary": "JPMorgan Chase-style financial-institution identity — corporate blue on navy and neutrals for research, banking, and internal governance decks",
"primary_color": "#117ACA"
},
"mckinsey": {
"summary": "McKinsey-style consulting identity — deep navy and electric blue for strategy, diagnostic, and executive decision decks",
"primary_color": "#051C2C"
},
"microsoft": {
"summary": "Microsoft-style enterprise technology identity — four-square product spectrum on neutral grays for Microsoft 365, Azure, and IT program decks",
"primary_color": "#00A4EF"
},
"nvidia": {
"summary": "NVIDIA-style accelerated-computing identity — signature green on near-black for AI, GPU, and data-center technical decks",
"primary_color": "#76B900"
},
"pwc": {
"summary": "PwC-style professional-services identity — warm orange-to-rose spectrum on black for assurance, tax, and advisory decks",
"primary_color": "#DB4E18"
},
"tencent": {
"summary": "Tencent-style internet and cloud identity — measured blue on light neutrals for platform, cloud, and ecosystem decks",
"primary_color": "#3458B0"
},
"xiaomi": {
"summary": "Xiaomi-style consumer-technology identity — signature orange on clean white for product launches, ecosystem, and retail decks",
"primary_color": "#FF6900"
},
"中国电信": {
"summary": "China Telecom brand identity for digital infrastructure, government-enterprise solutions, transformation planning, and corporate reporting",
"primary_color": "#004EA2"
@@ -0,0 +1,63 @@
---
brand_id: deloitte
kind: brand
summary: Deloitte-style professional-services identity — signature green on black and neutrals for audit, tax, risk, and advisory decks
primary_color: "#86BC25"
---
# Deloitte Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Deloitte |
| Use Cases | Audit, tax, risk, and consulting deliverables, regulatory and compliance readouts, industry point-of-view papers, and client advisory decks |
| Tone | Formal, precise, measured, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07; the green is widely documented as PMS 368. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#86BC25` | approx | Signature yellow-green consistently documented as the firm's identity color; commonly mapped to PMS 368 |
| accent | `#000000` | approx | Black, documented alongside the green as the other half of the core pairing |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F5F6F5` | approx | Off-white — card and module surfaces |
| border | `#DBDEDB` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#63666A` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented to a client as the firm's specification. This green is bright and low-contrast against white: never set body text in it, and reserve it for marks, rules, and the single decisive element rather than large fills. Black carries the typographic weight. Choose semantic chart and status colors per deck, taking care that a green primary does not collide with a green "positive" status meaning; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The firm uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The firm's marks — including the green dot device — are protected trademarks, and a redrawn or approximated mark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. Never imply affiliation, sponsorship, endorsement, or authorship by the firm, and never place a green dot beside a wordmark as a substitute for the real mark.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Keep scope, basis, and period explicit, separate observation from opinion, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: huawei
kind: brand
summary: Huawei-style enterprise identity — restrained red accent on near-black and gray for ICT solution, product, and partner decks
primary_color: "#CF0A2C"
---
# Huawei Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Huawei |
| Use Cases | ICT and enterprise solution decks, product and technology launches, carrier and government-enterprise proposals, and partner enablement |
| Tone | Formal, technical, capability-focused, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, giving the red as PMS 186 C and the black as PMS 426 C; public descriptions of the company's visual identity manual for the restrained-accent principle. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#CF0A2C` | approx | Signature red consistently documented across sources; commonly mapped to PMS 186 C |
| accent | `#232527` | approx | Near-black documented alongside the red; commonly mapped to PMS 426 C |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F5F5F6` | approx | Neutral gray — card and module surfaces |
| border | `#DEDEE0` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5A5C5E` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented as the company's specification. Public accounts of the company's visual identity describe a deliberate restrained-accent principle — the red appears as a controlled, small-area highlight rather than a field color — and that discipline is the most important thing to carry over: let near-black and gray build the page and spend the red on one focal element. A saturated red also carries alarm meaning by default, so keep it off large fills behind body text and never let it read as a risk signal where the page means neither. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The company's marks are protected trademarks, and the petal device in particular cannot be redrawn faithfully. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains a single-color `simple-icons/huawei` symbol for genuine brand-recognition contexts such as a partner or ecosystem listing; it is a symbol only, not the official lockup. Never imply affiliation, sponsorship, endorsement, or authorization.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Name products and versions exactly, tie capability claims to deployed evidence, and keep commitments specific.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: ibm
kind: brand
summary: IBM-style enterprise identity — Carbon blue on a disciplined gray scale for hybrid cloud, AI, and enterprise consulting decks
primary_color: "#0F62FE"
---
# IBM Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | IBM |
| Use Cases | Hybrid cloud and AI solution decks, enterprise consulting deliverables, technical architecture reviews, and industry point-of-view papers |
| Tone | Formal, engineered, systematic, evidence-led |
| Sources | IBM's Carbon Design System — the company's own open design system — reviewed 2026-08-07 for Blue 60. Neutral and surface rows are presentation conventions, not published brand tokens |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#0F62FE` | fact | Blue 60, the primary interactive color published in the company's own Carbon Design System |
| accent | `#002D9C` | approx | Deeper blue from the same family for chapter grounds and depth |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F4F4F4` | approx | Neutral gray — card and module surfaces |
| border | `#E0E0E0` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#525252` | approx | Secondary text, annotations, and chart labels |
The primary is `fact` — it is the published token in the company's own open design system. Every other row is `approx` and represents presentation convention, not an official token. Blue 60 is defined there as an *interactive* color for buttons, links, and focus states rather than a decorative brand fill; on a slide, keep that spirit by using it for the decisive mark and structural emphasis instead of tinting large areas. Because it reads as a link color, avoid setting non-interactive body text in it. Choose semantic chart and status colors per deck; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company maintains its own open typeface family, which this preset neither bundles nor assumes is installed. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The company's marks are protected trademarks, and the striped wordmark in particular degrades badly when redrawn or rescaled without official artwork. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains a single-color `simple-icons/ibm` symbol for genuine brand-recognition contexts such as a partner or ecosystem listing; it is a symbol only, not the official lockup. Never imply affiliation, sponsorship, endorsement, or certification.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Keep architecture claims tied to named components and versions, and separate available capability from roadmap.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: jpmorgan
kind: brand
summary: JPMorgan Chase-style financial-institution identity — corporate blue on navy and neutrals for research, banking, and internal governance decks
primary_color: "#117ACA"
---
# JPMorgan Chase Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | JPMorgan Chase |
| Use Cases | Market and sector research, banking and treasury client materials, investment committee and governance readouts, and internal performance reviews |
| Tone | Formal, conservative, precise, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, giving the blue as approximately PMS 285 C. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#117ACA` | approx | Corporate blue consistently documented across sources; commonly mapped to approximately PMS 285 C |
| accent | `#004B87` | approx | Navy documented alongside the blue for depth and chapter grounds |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F4F6F8` | approx | Cool off-white — card and module surfaces |
| border | `#DDE2E7` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#54606C` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented as the institution's specification. Financial decks carry dense numeric tables, so keep the blue for structure, headers, and the decisive series rather than tinting data regions — a blue-filled table defeats the row scanning it exists for. Chart series in a research deck need their own declared, colorblind-safe scale that does not collide with identity blue. Distinguish the corporate parent from its consumer-facing brand: their identities differ materially and must not be mixed on one page. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The institution uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face. Prefer a face with tabular figures so numeric columns align.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The institution's wordmarks and its octagon device are registered trademarks, and financial materials carry regulatory disclosure obligations that a preset cannot satisfy. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules and any required disclosures here. Never imply affiliation, sponsorship, endorsement, or that the material constitutes research or advice from the institution.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. State the basis, period, and currency of every figure, separate historical fact from forecast, and keep any forward-looking statement explicitly labeled.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: mckinsey
kind: brand
summary: McKinsey-style consulting identity — deep navy and electric blue for strategy, diagnostic, and executive decision decks
primary_color: "#051C2C"
---
# McKinsey & Company Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | McKinsey & Company |
| Use Cases | Strategy and diagnostic decks, executive committee readouts, transformation programs, and management-consulting deliverables |
| Tone | Formal, restrained, answer-first, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07. McKinsey publishes no open brand manual; every value here is an approximation, not an official token |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#051C2C` | approx | Deep navy consistently documented as the firm's dominant identity color |
| accent | `#2251FF` | approx | Electric blue documented as the identity's single high-energy accent |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F2F4F6` | approx | Off-white — card and module surfaces |
| border | `#D9DEE3` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5A6872` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual — the firm does not publish one — so nothing here may be treated as brand truth or represented to a client as the firm's specification. Keep the accent genuinely rare: it marks the answer, the decisive figure, or the recommendation, never ordinary emphasis. Choose semantic chart and status colors per deck; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The firm uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The firm's marks are protected trademarks, and a redrawn or approximated wordmark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. Never imply affiliation, sponsorship, endorsement, or authorship by the firm.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Lead with the answer, keep qualification precise rather than hedged, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,65 @@
---
brand_id: microsoft
kind: brand
summary: Microsoft-style enterprise technology identity — four-square product spectrum on neutral grays for Microsoft 365, Azure, and IT program decks
primary_color: "#00A4EF"
---
# Microsoft Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Microsoft |
| Use Cases | Microsoft 365 and Azure solution decks, enterprise IT programs and migrations, partner and ISV presentations, and technical enablement |
| Tone | Professional, accessible, capability-focused, plainly worded |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, consistent across several independent sources. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#00A4EF` | approx | The blue quadrant, documented as the Windows and Azure tone; chosen as primary because enterprise decks sit in that product space |
| accent-warm | `#F25022` | approx | The orange-red quadrant, documented as the Office tone |
| accent-green | `#7FBA00` | approx | The green quadrant, documented as the Xbox tone |
| accent-yellow | `#FFB900` | approx | The yellow quadrant, documented as the Bing and devices tone |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F3F2F1` | approx | Neutral gray — card and module surfaces |
| border | `#E1DFDD` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#605E5C` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented as the company's specification. The four quadrant colors are documented as carrying product-line meaning, so do not spread all four across one page as a decorative rainbow: pick the one matching the product being discussed and let neutrals carry the rest. The green and yellow quadrants collide with conventional success and warning semantics — where a page also needs status meaning, declare that mapping explicitly and keep it distinguishable from identity use. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The company's marks are protected trademarks, and a redrawn or approximated mark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains a single-color `simple-icons/microsoft` symbol for genuine brand-recognition contexts such as a partner or ecosystem listing; it is a symbol only, not the official lockup, and must never be combined with typed text to fabricate one. Never imply affiliation, sponsorship, endorsement, or certification.
## V. Voice & Tone
- Formality: neutral
- Person: we / you (English), 我们 / 你 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Name products exactly and in full on first use, keep capability claims tied to what is generally available, and mark preview or roadmap features as such.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: nvidia
kind: brand
summary: NVIDIA-style accelerated-computing identity — signature green on near-black for AI, GPU, and data-center technical decks
primary_color: "#76B900"
---
# NVIDIA Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | NVIDIA |
| Use Cases | AI and accelerated-computing talks, GPU and data-center architecture decks, developer sessions, and technical partner enablement |
| Tone | Technical, precise, performance-focused, evidence-led |
| Sources | NVIDIA's published trademark and logo usage guidelines, reviewed 2026-08-07, which state the green as RGB 118/185/0 and Pantone 376 C. Neutral and surface rows are presentation conventions, not published tokens |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#76B900` | fact | Signature green; the published guidelines specify RGB 118/185/0, which is this hex, referenced to Pantone 376 C |
| accent | `#1A1A1A` | approx | Near-black field the green is normally presented against |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F4F6F1` | approx | Off-white with a green cast — card and module surfaces |
| border | `#DCE0D6` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5A5F55` | approx | Secondary text, annotations, and chart labels |
The primary is `fact` — it is the value published in the company's own usage guidelines. Every other row is `approx` and represents presentation convention, not an official token. This green is bright and fails contrast against white at text sizes: never set body text in it, and prefer it on the near-black field or as a mark, rule, and single decisive accent. It also collides directly with conventional "pass" and "success" semantics — where a page needs status meaning, declare that mapping explicitly and keep it distinguishable from identity use. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses a proprietary licensed typeface that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The published guidelines require written approval before any use of the logo or branded elements, set a minimum size of 60 pixels on screen, mandate clear space, and prohibit unapproved color modification — conditions this preset cannot grant. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially approved asset into this workspace's `images/` directory and record the approval and its usage rules here. The bundled icon library contains a single-color `simple-icons/nvidia` symbol for genuine brand-recognition contexts such as a partner or ecosystem listing; it is a symbol only, not the official lockup. Never imply affiliation, sponsorship, endorsement, or certification.
## V. Voice & Tone
- Formality: neutral
- Person: we / you (English), 我们 / 你 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Give benchmark numbers their workload, configuration, and software version; name architectures and products exactly.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,66 @@
---
brand_id: pwc
kind: brand
summary: PwC-style professional-services identity — warm orange-to-rose spectrum on black for assurance, tax, and advisory decks
primary_color: "#DB4E18"
---
# PwC Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | PwC (PricewaterhouseCoopers) |
| Use Cases | Assurance, tax, and advisory deliverables, regulatory readouts, industry surveys and point-of-view papers, and client advisory decks |
| Tone | Formal, measured, precise, evidence-led |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#DB4E18` | approx | Dark orange, the weight-bearing tone of the documented warm spectrum |
| accent | `#E88D14` | approx | Light orange from the same documented spectrum |
| accent-warm | `#F3BE26` | approx | Yellow from the same documented spectrum |
| accent-rose | `#E669A2` | approx | Rose from the same documented spectrum |
| accent-deep | `#AD1B02` | approx | Deep red anchoring the warm end of the spectrum |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F6F5F4` | approx | Warm off-white — card and module surfaces |
| border | `#DEDBD8` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#5C5754` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented to a client as the firm's specification. The identity is a warm spectrum rather than one flat brand color: pick a single tone per deck to carry emphasis and let the rest support it, and never spread all five across one page as a decorative rainbow. This spectrum overlaps the conventional warning and alert range, so where a page also needs status meaning, declare that mapping explicitly and keep it distinguishable from identity use. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The firm uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The firm's marks are protected trademarks, and a redrawn or approximated mark would misrepresent them. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. Never imply affiliation, sponsorship, endorsement, or authorship by the firm, and never assemble colored geometric shapes beside a wordmark as a substitute for the real mark.
## V. Voice & Tone
- Formality: formal
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Keep scope, basis, and period explicit, separate observation from opinion, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a restrained stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: tencent
kind: brand
summary: Tencent-style internet and cloud identity — measured blue on light neutrals for platform, cloud, and ecosystem decks
primary_color: "#3458B0"
---
# Tencent Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Tencent |
| Use Cases | Internet platform and cloud solution decks, ecosystem and developer presentations, industry research briefings, and partner enablement |
| Tone | Professional, measured, platform-oriented |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, which state explicitly that these values are not published in the company's own guidelines. Every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#3458B0` | approx | Medium-dark blue consistently documented as the dominant identity color |
| accent | `#1F3A78` | approx | Deeper blue derived from the primary for chapter grounds and depth |
| bg | `#FFFFFF` | approx | Default light presentation background |
| surface | `#F4F6FA` | approx | Cool off-white — card and module surfaces |
| border | `#DCE1EA` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#59606E` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`, and the compiled sources say so themselves — the company does not publish these values. Nothing here may be treated as brand truth or represented as the company's specification. This blue sits close to the conventional hyperlink color, so keep non-interactive text out of it and reserve it for structure, marks, and the decisive element. The group's individual products carry materially different and much stronger identities than the corporate blue; when a deck is about one of them, that product's identity governs, not this preset. Choose semantic chart and status colors per deck; do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled, and the bundled icon library has no symbol for this brand. The company's marks are protected trademarks, and its product marks must not be substituted for the corporate one. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. Never imply affiliation, sponsorship, endorsement, or authorization.
## V. Voice & Tone
- Formality: neutral
- Person: we / you (English), 我们 / 您 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. Name the specific product or business unit rather than the group where the claim belongs to one of them, and attribute every figure.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -0,0 +1,63 @@
---
brand_id: xiaomi
kind: brand
summary: Xiaomi-style consumer-technology identity — signature orange on clean white for product launches, ecosystem, and retail decks
primary_color: "#FF6900"
---
# Xiaomi Brand Specification
> Identity-only preset. No SVG page roster — pages are composed freely under these constraints.
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | Xiaomi |
| Use Cases | Consumer product launches, smart-device ecosystem presentations, retail and channel decks, and technology briefings |
| Tone | Accessible, product-forward, plainly worded, specification-honest |
| Sources | Public third-party brand-color compilations reviewed 2026-08-07, giving the orange as PMS 1585 C. No value was read from an official manual, so every value here is an approximation |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#FF6900` | approx | Signature orange consistently documented across sources; commonly mapped to PMS 1585 C |
| accent | `#1F1F1F` | approx | Near-black for typographic weight against a predominantly white field |
| bg | `#FFFFFF` | approx | Default light presentation background; the identity is normally presented on clean white |
| surface | `#F7F7F7` | approx | Light gray — card and module surfaces |
| border | `#E5E5E5` | approx | Hairline rules, dividers, and table borders |
| muted-text | `#666666` | approx | Secondary text, annotations, and chart labels |
Every row is `approx`. No value was read from an official asset or manual, so nothing here may be treated as brand truth or represented as the company's specification. The identity lives on generous white with product imagery carrying the page, so resist filling space with orange: use it for the mark, one accent, and the decisive figure. The orange fails contrast against white at text sizes and sits in the conventional warning range — keep it off body text, and where a page encodes status, declare that mapping separately. This orange is also nearly identical to another major Chinese technology brand's; when both appear together, distinguish them by mark and label rather than by color. Do not lock an invented success/warning/error trio as identity.
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `Arial, "Microsoft YaHei", sans-serif` | 600700 |
| body | `Arial, "Microsoft YaHei", sans-serif` | 400 |
> The company uses proprietary licensed typefaces that this preset neither bundles nor names as a claim. PPT Master does not auto-embed fonts or follow CSS tails in PowerPoint. The rows above are the default Windows/Office export; replace them only with a user-confirmed target-installed face.
## IV. Logo
- File: `none`
- Usage: never
No logo asset is bundled. The company's marks are protected trademarks whose current rounded-square form cannot be redrawn faithfully. Where a presenting entity must appear, set it as editable text in the deck's own typography, or install an officially supplied asset into this workspace's `images/` directory and add its usage rules here. The bundled icon library contains a single-color `simple-icons/xiaomi` symbol for genuine brand-recognition contexts such as an ecosystem listing; it is a symbol only, not the official lockup. Never imply affiliation, sponsorship, endorsement, or authorization.
## V. Voice & Tone
- Formality: neutral
- Person: we / you (English), 我们 / 你 (Chinese)
- Emoji: forbidden
- Abbreviations: spell-out-first-use
> Presentation convention derived from the register these decks are normally written in, not an official brand token. State specifications exactly with their measurement conditions, and keep comparative claims tied to a named basis.
## VI. Icon Style
- Preference: stroke
> Presentation convention, not an official brand token. Prefer a consistent stroke family such as `tabler-outline`, keep one family across the deck, and use an icon only where it clarifies a role, state, or relationship.
@@ -1,74 +1,54 @@
# SVG Visualization Template Library
# Chart Visualization Templates
This directory contains the standardized SVG visualization templates used by PPT Master — charts, infographics, process diagrams, relationship diagrams, and strategic frameworks. The directory name `charts/` is kept for backward compatibility; the library scope is broader than charts.
This directory contains 33 canonical value-driven references. A chart belongs here
when source values, categories, time, weights, or durations determine visual
mark position, length, area, angle, font size, or connection width.
Qualitative page topology is built as a page-specific Structure by Executor.
Cell-grid semantics belong in [`tables/`](../tables/). Reusable PowerPoint
Master/Layout, page-type, slot, and placeholder contracts belong in
[`layouts/`](../layouts/).
## Source of truth
[`charts_index.json`](./charts_index.json) is the single source of truth for the library: total count + one selection-rule `summary` per template (format: `"Pick for X. Skip if Y (use other_key)."`). [`chart_recall.py`](../../scripts/chart_recall.py) can recall a bounded candidate set from this live registry without maintaining a second category or keyword index.
[`charts_index.json`](./charts_index.json) is the sole chart registry. Its
`charts` object maps each canonical key to one selection-rule `summary` in the
form `Pick for ... Skip if ...`. The key matches `<key>.svg`; `meta.total`
matches the canonical SVG roster.
For one page, provide 3-8 English semantic content-shape tags, then inspect the positive-scoring summaries plus the explicit `no-template-match` option. The requested limit is a cap, not a padding target, and lexical confidence never expands the output automatically. At `high` / `medium`, retain `no-template-match` when no bounded candidate fits. At `low` / `none`, a fitting candidate needs no expansion, but rerun the same query once with `--semantic-fallback` before keeping `no-template-match`. In Default Generate, a selected result creates one `Page | Template | Usage` row in §VII and one matching `page_charts` entry; its path is derived from the key, while Usage is concise page-local intent and detailed adaptation remains in §IX. Quick Generate keeps the selected key/purpose only in active context and writes no mapping artifact. A negative result creates no row in either profile. See [`chart-recall.md`](../../scripts/docs/chart-recall.md). The Generate workflow remains the authority for when the active profile uses this helper; maintainers may still open the registry directly when editing or auditing the catalog.
Use [`visualization_recall.py`](../../scripts/visualization_recall.py) for
bounded Chart/Table or chart-only recall. New Default planning writes
`chart/<key>` to `page_visualizations`; Quick keeps the selected reference in
active context. [`chart_recall.py`](../../scripts/chart_recall.py) and bare keys
remain legacy compatibility only.
## Authoring contract
[`CHART_STYLE_GUIDE.md`](./CHART_STYLE_GUIDE.md) owns readable standalone SVG, semantic and structural fidelity, root-group bounds, data encoding, and neutral previews. Default Design Spec §IX or the Quick active-context page decision plus source data own the generated page's semantics; Default `spec_lock.md` owns stable project anchors. A selected SVG is a page-local structural reference, while its type, styling, frames, grouping, item count, capacity, and geometry remain adaptable.
[`VISUALIZATION_TEMPLATE_AUTHORING.md`](../VISUALIZATION_TEMPLATE_AUTHORING.md)
owns the shared standalone-SVG, neutral-preview, root-boundary, Shape-first,
family, and catalog rules. Chart-specific requirements are:
## Visualization output model
- Preserve the exact value-to-mark mapping, labels, units, categories, series,
ordering, and source notes required by the information.
- Keep calculator-supported `chart-plot-area` markers accurate.
- Default output remains independently editable DrawingML shapes.
- Add native Chart replacement metadata only for a supported independent data
object. The visible fallback and metadata describe the same data.
- Do not classify a named quadrant, process, hierarchy, or relationship diagram
as a chart unless values actually determine its marks.
This library is **Shape-first**. Infographics, process diagrams, architecture
diagrams, frameworks, and the default rendering of data charts export as
independently editable PowerPoint shapes:
`matrix_2x2` is a chart: each item's x/y coordinates encode two values and its
radius encodes a third metric. A fixed 2×2 set of titled text regions is a
page-specific Structure. A schedule whose dates or durations determine task-bar
position and length is `chart/gantt_chart`; a qualitative stage/lane plan is a
Structure built from those relationships.
- Use ordinary SVG primitives for basic nodes, containers, and straight
relationships; they export as editable PowerPoint shapes. A straight
directional relationship uses `<line>` with a registered arrow marker.
- When a bent or curved relationship exactly matches a stock Connector contour,
prefer a compact authored `bentConnector*` / `curvedConnector*` preset. It is
an unconnected native Connector shape and does not create node attachment or
automatic routing.
- Use a stock PowerPoint preset for a solid block arrow, chevron, or standard
flowchart node when it exactly expresses the intended object. A template may
retain the complete compact atomic `<g>` generated by `preset_shape_svg.py`,
so direct export preserves the native preset identity. The group owns the
metadata and base paint once; its direct visible paths are registry-derived
layers with only necessary per-layer paint overrides. It has no hidden
carrier, preview wrapper, or stored fingerprint. If preset, frame,
adjustments, or paint changes, regenerate the whole group instead of editing
metadata or paths. PPTX import and `mirror` retain their separate expanded
lossless representation; do not copy that transport form into new templates.
- When no single preset suffices but closed operands can express the object,
materialize the Union / Combine / Fragment / Intersect / Subtract result with
`shape_boolean_svg.py` before considering a hand-authored contour.
- Keep a hand-authored `<path>` / `<polygon>` only for branded, data-defined,
locked organic / hand-drawn, or other contours that primitives, an exact
preset, and Boolean materialization cannot faithfully express. They still
export as editable DrawingML shapes.
- Authored Connector metadata comes only from `preset_shape_svg.py`; never
hand-write endpoint attachment metadata. Existing attached Connector topology
imported from a source PPTX belongs to the preserve/mirror round-trip
contract, not chart-template authoring.
## Runtime boundary
Data-backed charts retain the separate opt-in replacement route described
below. Conceptual diagrams and frameworks are never labeled as native charts.
## Native Chart/Table replacement markers
Supported data chart templates include a `<g data-pptx-replace-with="chart">` marker by default, and pure text-grid table templates include the table form. These are capability examples, not project decisions: Default keeps and rewrites the marker only when its Design Spec §IX page block says `Native-ready: yes` (legacy §VII fallback only); Quick makes the same independent-object decision in active context before drawing. Otherwise omit it. The default SVG export path remains shape-based DrawingML. With `--native-charts-and-tables`, prepared groups become native PowerPoint Chart/Table objects from their JSON metadata. The legacy `--native-objects` spelling remains a compatibility alias.
`--native-charts-and-tables` is an explicit native-object opt-in and may be lossy or visually normalized. Replacement-local value labels, center KPIs, callouts, quadrant notes, fixed axis ranges, and custom binning/splits may not survive native replacement unless represented by the payload. ChartEx palette entries do survive when supplied through valid payload colors, but this does not preserve every ChartEx style detail. Detectable information-loss risks should be reported as warnings, not handled by disabling an otherwise supported replacement marker. Review those warnings and compare the native-object export with the default shape-based export before delivery.
Native replacement authoring remains active for all 23 currently supported data-chart templates:
| Family | Active replacement templates | Native output |
|---|---|---|
| Category comparison | `column_chart`, `horizontal_bar_chart`, `grouped_bar_chart`, `stacked_bar_chart` | Classic category charts |
| Time trend | `line_chart`, `area_chart`, `stacked_area_chart`, `dual_axis_line_chart` | Classic line/area/combo charts |
| Part-to-whole | `pie_chart`, `donut_chart`, `pie_of_pie_chart`, `bar_of_pie_chart`, `treemap_chart`, `sunburst_chart` | Classic pie-family or ChartEx hierarchy charts |
| Distribution and relationship | `scatter_chart`, `bubble_chart`, `histogram_chart`, `pareto_chart`, `box_plot_chart` | Classic XY or ChartEx distribution charts |
| Specialty business charts | `waterfall_chart`, `funnel_chart`, `stock_chart`, `radar_chart` | ChartEx or classic specialty charts |
| Text-grid tables | `basic_table`, `financial_statement_table` | Native DrawingML tables |
Replacement payloads must include explicit `name`, `x`, `y`, `width`, and `height` fields so the native frame aligns with the fallback drawing. Keep legends, explanatory cards, source notes, center KPIs, and custom callouts outside the marked group when they must remain separate shapes; otherwise accept and review the native-object export warning. Canonical rectangular merged text cells may use anchor-only `row_span` / `col_span` metadata with blank covered cells; nonrectangular merges and graphical cells (harvey balls, rating dots, avatars) stay unmarked on the SVG fallback route. Per-side borders, plain multi-paragraph cells, and the closed run-rich paragraph schema use the contracts in [`native-data-interface.md`](../../references/native-data-interface.md#2-powerpoint-native-chart--table-replacement-markers-opt-in). Relationship-bearing text, structural line breaks, fields, tabs, bullets, and arbitrary rich-text OOXML stay on the SVG fallback route.
## Usage
Execution loads only selected keys under [`executor-chart.md`](../../references/executor-chart.md) §1 and reuses an unchanged file within the active context. Files are named after the `key` field in `charts_index.json` (e.g. `column_chart.svg`, `quadrant_bubble_scatter.svg`). Templates are named by visual structure, not by business-model name — terms like SWOT, BCG, PEST, OKR, Porter's Five Forces, and Value Chain are translated into semantic content-shape tags before candidate recall.
One selected SVG is a flexible reference for one mapped page. Design Spec §IX
or the Quick active-context decision plus source data owns final semantics.
Project palette, typography, chrome, grouping, capacity, and geometry remain
adaptable. Selecting a chart reference does not itself select native output;
§IX/Quick names independent objects separately and decides
`<object-key>=yes|no`, while explicit `--native-charts-and-tables` export is a
second opt-in.
@@ -1,142 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Agenda List Template
Structure: Numbered timeline of 3-6 agenda items, each with title, brief, duration pill
Use cases: Table of contents, meeting agendas, section dividers, presentation roadmaps
Design Principles: Vertical timeline anchoring numbered circles; large hero duration in header; sparse content, generous whitespace
-->
<defs>
<linearGradient id="rail" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#3B82F6"/>
<stop offset="100%" stop-color="#6366F1"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<rect x="80" y="56" width="4" height="68" rx="2" fill="#3B82F6"/>
<text x="100" y="86" font-size="34" font-weight="800" fill="#0F172A">
<tspan>Meeting Agenda</tspan>
</text>
<text x="100" y="114" font-size="13" font-weight="700" fill="#64748B" letter-spacing="2">
<tspan>Q3 STRATEGIC ALIGNMENT &amp; REVIEW · MAY 13, 2026</tspan>
</text>
<!-- Hero duration badge (right) -->
<g transform="translate(1010, 50)">
<rect x="0" y="0" width="180" height="80" rx="14" fill="#EFF6FF"/>
<text x="90" y="40" font-size="32" font-weight="800" fill="#3B82F6" text-anchor="middle">
<tspan>75</tspan><tspan font-size="20" dx="2">min</tspan>
</text>
<text x="90" y="64" font-weight="800" fill="#1E3A8A" text-anchor="middle" letter-spacing="2">
<tspan>TOTAL DURATION</tspan>
</text>
</g>
</g>
<g id="agenda-track" data-pptx-bounds="0 126 1280 528">
<line x1="80" y1="160" x2="1200" y2="160" stroke="#E2E8F0" stroke-width="2"/>
<rect x="128.5" y="218.5" width="3" height="403" rx="1.5" fill="url(#rail)"/>
</g>
<!-- ==================== Agenda Items ==================== -->
<!-- Item 01 -->
<g id="item-01" data-pptx-bounds="22 203 1240 91" transform="translate(0, 200)">
<!-- Number circle on rail -->
<circle cx="130" cy="36" r="26" fill="#FFFFFF" stroke="#3B82F6" stroke-width="3"/>
<text x="130" y="44" font-size="16" font-weight="800" fill="#3B82F6" text-anchor="middle">
<tspan>01</tspan>
</text>
<!-- Title + brief -->
<text x="190" y="30" font-size="22" font-weight="800" fill="#0F172A">
<tspan>Executive Summary &amp; Q3 Performance</tspan>
</text>
<text x="190" y="58" font-size="14" font-weight="500" fill="#64748B">
<tspan>High-level overview of revenue targets, key metrics, and major wins from the last quarter.</tspan>
</text>
<!-- Duration pill (right) -->
<rect x="1060" y="14" width="120" height="36" rx="18" fill="#EFF6FF"/>
<text x="1120" y="38" font-size="14" font-weight="800" fill="#3B82F6" text-anchor="middle" letter-spacing="1">
<tspan>10 MIN</tspan>
</text>
<!-- Owner -->
<text x="190" y="84" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>OWNER · CEO</tspan>
</text>
</g>
<!-- Item 02 -->
<g id="item-02" data-pptx-bounds="22 323 1240 91" transform="translate(0, 320)">
<circle cx="130" cy="36" r="26" fill="#FFFFFF" stroke="#3B82F6" stroke-width="3"/>
<text x="130" y="44" font-size="16" font-weight="800" fill="#3B82F6" text-anchor="middle">
<tspan>02</tspan>
</text>
<text x="190" y="30" font-size="22" font-weight="800" fill="#0F172A">
<tspan>Product Roadmap Updates</tspan>
</text>
<text x="190" y="58" font-size="14" font-weight="500" fill="#64748B">
<tspan>Review of upcoming features, delayed items, and engineering resource allocation.</tspan>
</text>
<rect x="1060" y="14" width="120" height="36" rx="18" fill="#EFF6FF"/>
<text x="1120" y="38" font-size="14" font-weight="800" fill="#3B82F6" text-anchor="middle" letter-spacing="1">
<tspan>20 MIN</tspan>
</text>
<text x="190" y="84" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>OWNER · VP PRODUCT</tspan>
</text>
</g>
<!-- Item 03 -->
<g id="item-03" data-pptx-bounds="22 443 1240 91" transform="translate(0, 440)">
<circle cx="130" cy="36" r="26" fill="#FFFFFF" stroke="#5575EC" stroke-width="3"/>
<text x="130" y="44" font-size="16" font-weight="800" fill="#5575EC" text-anchor="middle">
<tspan>03</tspan>
</text>
<text x="190" y="30" font-size="22" font-weight="800" fill="#0F172A">
<tspan>Competitive Market Analysis</tspan>
</text>
<text x="190" y="58" font-size="14" font-weight="500" fill="#64748B">
<tspan>Deep dive into recent moves by Competitor A and our strategic response plan.</tspan>
</text>
<rect x="1060" y="14" width="120" height="36" rx="18" fill="#EEF2FF"/>
<text x="1120" y="38" font-size="14" font-weight="800" fill="#5575EC" text-anchor="middle" letter-spacing="1">
<tspan>15 MIN</tspan>
</text>
<text x="190" y="84" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>OWNER · HEAD OF STRATEGY</tspan>
</text>
</g>
<!-- Item 04 — highlighted as focal -->
<g id="item-04" data-pptx-bounds="20 561 1242 93" transform="translate(0, 560)">
<circle cx="130" cy="36" r="28" fill="#6366F1"/>
<text x="130" y="44" font-size="16" font-weight="800" fill="#FFFFFF" text-anchor="middle">
<tspan>04</tspan>
</text>
<text x="190" y="30" font-size="22" font-weight="800" fill="#0F172A">
<tspan>Q4 Goals &amp; OKR Planning</tspan><tspan dx="10" font-size="12" font-weight="800" fill="#6366F1" letter-spacing="2">· DECISION</tspan>
</text>
<text x="190" y="58" font-size="14" font-weight="500" fill="#64748B">
<tspan>Collaborative session to finalize departmental objectives and assign ownership.</tspan>
</text>
<rect x="1060" y="14" width="120" height="36" rx="18" fill="#6366F1"/>
<text x="1120" y="38" font-size="14" font-weight="800" fill="#FFFFFF" text-anchor="middle" letter-spacing="1">
<tspan>30 MIN</tspan>
</text>
<text x="190" y="84" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>OWNER · LEADERSHIP TEAM</tspan>
</text>
</g>
<!-- ==================== Footer ==================== -->
<g id="source" data-pptx-bounds="40 672 1200 38">
<text x="80" y="695" font-weight="600" fill="#94A3B8">
<tspan>Presenter: Management Team · Conference Room A · Recorded</tspan>
</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,94 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif"
font-size="12">
<!--
Arc-Anchored List Template
Purpose: 3-5 parallel tracks/aspects unified by a left-edge radial fan acting as visual anchor
Use cases: Multi-track growth plans, parallel improvement areas, four-pillar overview with a unifying core
Design Principles:
- Shared center sits on the slide's LEFT EDGE (x=0): every wedge and the hub are TRUE half-shapes, not clipped full circles
- Hub drawn as an explicit half-disc path (flat edge on slide left, curved edge bulging right) so it stays a half-shape after SVG->PPTX round-trip
- Four 43 deg wedges spanning -89..-46 / -44..-1 / 1..44 / 46..89, with 2 deg angular gaps; R_in=145, R_out=280
- Numbered badge per wedge sits at mid-radius midpoint; row content (title + body + right-end color square) aligned to wedge mid-y
- Single hue progression top->bottom; wedge color drives row title color and end-cap color
-->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="640" y="58" text-anchor="middle" font-size="32" font-weight="800" fill="#0F172A"><tspan>Four-Track Improvement Plan</tspan></text>
<text x="640" y="86" text-anchor="middle" font-size="14" font-weight="600" fill="#64748B" letter-spacing="1.5"><tspan>RADIAL ANCHOR · FOUR PARALLEL TRACKS</tspan></text>
</g>
<!-- ==================== Left Radial Half-Fan ==================== -->
<!-- Shared center at (0, 400) on slide left edge ; R_hub=115, R_in=145, R_out=280 -->
<g id="fan" data-pptx-bounds="0 88 316 624">
<!-- Wedge 1: -89 to -46 deg (top), mid -67.5 -->
<g id="wedge-01">
<path d="M 2.53 255.02 L 4.89 120.04 A 280 280 0 0 1 194.50 198.58 L 100.73 295.70 A 145 145 0 0 0 2.53 255.02 Z" fill="#1E3A8A"/>
<text x="81" y="218" text-anchor="middle" font-size="42" font-weight="800" fill="#FFFFFF"><tspan>01</tspan></text>
</g>
<!-- Wedge 2: -44 to -1 deg (upper-mid), mid -22.5 -->
<g id="wedge-02">
<path d="M 104.30 299.27 L 201.42 205.50 A 280 280 0 0 1 279.96 395.11 L 144.98 397.47 A 145 145 0 0 0 104.30 299.27 Z" fill="#2563EB"/>
<text x="196" y="333" text-anchor="middle" font-size="42" font-weight="800" fill="#FFFFFF"><tspan>02</tspan></text>
</g>
<!-- Wedge 3: 1 to 44 deg (lower-mid), mid 22.5 -->
<g id="wedge-03">
<path d="M 144.98 402.53 L 279.96 404.89 A 280 280 0 0 1 201.42 594.50 L 104.30 500.73 A 145 145 0 0 0 144.98 402.53 Z" fill="#3B82F6"/>
<text x="196" y="496" text-anchor="middle" font-size="42" font-weight="800" fill="#FFFFFF"><tspan>03</tspan></text>
</g>
<!-- Wedge 4: 46 to 89 deg (bottom), mid 67.5 -->
<g id="wedge-04">
<path d="M 100.73 504.30 L 194.50 601.42 A 280 280 0 0 1 4.89 679.96 L 2.53 544.98 A 145 145 0 0 0 100.73 504.30 Z" fill="#60A5FA"/>
<text x="81" y="611" text-anchor="middle" font-size="42" font-weight="800" fill="#FFFFFF"><tspan>04</tspan></text>
</g>
<!-- Central hub: explicit half-disc (flat edge at x=0, curved edge bulging right with r=115) -->
<g id="hub">
<path d="M 0 285 A 115 115 0 0 1 0 515 Z" fill="#0F172A"/>
<text x="58" y="392" text-anchor="middle" font-size="22" font-weight="800" fill="#FFFFFF"><tspan>Growth</tspan></text>
<text x="58" y="420" text-anchor="middle" font-size="22" font-weight="800" fill="#FFFFFF"><tspan>Plan</tspan></text>
</g>
</g>
<!-- ==================== Row 1 (paired with Wedge 01, mid-y 204) ==================== -->
<g id="row-01" data-pptx-bounds="238 151 1042 106">
<rect x="320" y="159" width="780" height="90" fill="#F1F5F9"/>
<text x="340" y="193" font-size="20" font-weight="700" fill="#1E3A8A"><tspan>Track 01 — Heading Goes Here</tspan></text>
<text x="340" y="219" font-size="13" fill="#475569"><tspan>Concise description line one explaining the goal and scope of this track.</tspan></text>
<text x="340" y="239" font-size="13" fill="#475569"><tspan>Key actions, expected outcome, and the resources or partners needed.</tspan></text>
<rect x="1120" y="159" width="100" height="90" fill="#1E3A8A"/>
<circle cx="1170" cy="204" r="18" fill="none" stroke="#FFFFFF" stroke-width="2"/>
</g>
<!-- ==================== Row 2 (paired with Wedge 02, mid-y 319) ==================== -->
<g id="row-02" data-pptx-bounds="238 266 1042 106">
<rect x="320" y="274" width="780" height="90" fill="#F1F5F9"/>
<text x="340" y="308" font-size="20" font-weight="700" fill="#2563EB"><tspan>Track 02 — Heading Goes Here</tspan></text>
<text x="340" y="334" font-size="13" fill="#475569"><tspan>Concise description line one explaining the goal and scope of this track.</tspan></text>
<text x="340" y="354" font-size="13" fill="#475569"><tspan>Key actions, expected outcome, and the resources or partners needed.</tspan></text>
<rect x="1120" y="274" width="100" height="90" fill="#2563EB"/>
<circle cx="1170" cy="319" r="18" fill="none" stroke="#FFFFFF" stroke-width="2"/>
</g>
<!-- ==================== Row 3 (paired with Wedge 03, mid-y 481) ==================== -->
<g id="row-03" data-pptx-bounds="238 428 1042 106">
<rect x="320" y="436" width="780" height="90" fill="#F1F5F9"/>
<text x="340" y="470" font-size="20" font-weight="700" fill="#3B82F6"><tspan>Track 03 — Heading Goes Here</tspan></text>
<text x="340" y="496" font-size="13" fill="#475569"><tspan>Concise description line one explaining the goal and scope of this track.</tspan></text>
<text x="340" y="516" font-size="13" fill="#475569"><tspan>Key actions, expected outcome, and the resources or partners needed.</tspan></text>
<rect x="1120" y="436" width="100" height="90" fill="#3B82F6"/>
<circle cx="1170" cy="481" r="18" fill="none" stroke="#FFFFFF" stroke-width="2"/>
</g>
<!-- ==================== Row 4 (paired with Wedge 04, mid-y 596) ==================== -->
<g id="row-04" data-pptx-bounds="238 543 1042 106">
<rect x="320" y="551" width="780" height="90" fill="#F1F5F9"/>
<text x="340" y="585" font-size="20" font-weight="700" fill="#60A5FA"><tspan>Track 04 — Heading Goes Here</tspan></text>
<text x="340" y="611" font-size="13" fill="#475569"><tspan>Concise description line one explaining the goal and scope of this track.</tspan></text>
<text x="340" y="631" font-size="13" fill="#475569"><tspan>Key actions, expected outcome, and the resources or partners needed.</tspan></text>
<rect x="1120" y="551" width="100" height="90" fill="#60A5FA"/>
<circle cx="1170" cy="596" r="18" fill="none" stroke="#FFFFFF" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,163 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="Roboto, -apple-system, BlinkMacSystemFont, sans-serif"
font-size="12">
<!--
Basic Grid Table Template
Purpose: The most universally recognizable standard data table (Full Grid)
Use cases: Traditional data reporting, detailed lists, Excel-like presentations
Design Principles: Full horizontal and vertical borders, strict text alignment rules
-->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="80" y="70" font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="32" font-weight="800" fill="#0F172A">Quarterly Resource Allocation</text>
<text x="80" y="100" font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="14" font-weight="700" fill="#64748B" letter-spacing="1">BASIC TABLE FORMAT · TRADITIONAL FULL GRID WITH ALIGNMENT RULES</text>
</g>
<!-- ==================== Table Structure ==================== -->
<g id="basic_table" data-pptx-bounds="0 127 1280 386" data-pptx-replace-with="table">
<metadata type="application/json">
{
"name": "basic_table",
"x": 80,
"y": 150,
"width": 1120,
"height": 340,
"strict_grid": true,
"column_widths": [160, 400, 160, 280, 120],
"row_heights": [50, 48, 48, 48, 48, 48, 50],
"style": {
"font_family": "Roboto",
"font_size": 15,
"header_font_size": 13,
"header_fill": "#F8FAFC",
"header_text": "#475569",
"body_fill": "#FFFFFF",
"body_text": "#0F172A",
"band_fill": "#FFFFFF",
"border_color": "#E2E8F0",
"border_width": 1,
"padding": {"left": 10, "right": 10, "top": 4, "bottom": 4},
"valign": "middle"
},
"columns": [
{"text": "PROJECT ID", "bold": true},
{"text": "PROJECT DESCRIPTION", "bold": true},
{"text": "DEPARTMENT", "align": "ctr", "bold": true},
{"text": "HEADCOUNT", "align": "ctr", "bold": true},
{"text": "BUDGET (USD)", "align": "r", "bold": true}
],
"rows": [
[
{"text": "PRJ-2025-01A", "color": "#334155"},
{"text": "Cloud Infrastructure Migration", "bold": true},
{"text": "Engineering", "align": "ctr", "color": "#475569"},
{"text": "12", "align": "ctr", "color": "#334155", "bold": true},
{"text": "450,000", "align": "r", "bold": true}
],
[
{"text": "PRJ-2025-02B", "color": "#334155"},
{"text": "Global Marketing Campaign Q2", "bold": true},
{"text": "Marketing", "align": "ctr", "color": "#475569"},
{"text": "8", "align": "ctr", "color": "#334155", "bold": true},
{"text": "820,500", "align": "r", "bold": true}
],
[
{"text": "PRJ-2025-03C", "color": "#334155"},
{"text": "Sales Enablement Training", "bold": true},
{"text": "Sales", "align": "ctr", "color": "#475569"},
{"text": "4", "align": "ctr", "color": "#334155", "bold": true},
{"text": "95,000", "align": "r", "bold": true}
],
[
{"text": "PRJ-2025-04A", "color": "#334155"},
{"text": "Mobile App Redesign V3.0", "bold": true},
{"text": "Product", "align": "ctr", "color": "#475569"},
{"text": "15", "align": "ctr", "color": "#334155", "bold": true},
{"text": "315,200", "align": "r", "bold": true}
],
[
{"text": "PRJ-2025-05B", "color": "#334155"},
{"text": "Cybersecurity Audit &amp; Fixes", "bold": true},
{"text": "IT Security", "align": "ctr", "color": "#475569"},
{"text": "5", "align": "ctr", "color": "#334155", "bold": true},
{"text": "120,000", "align": "r", "bold": true}
],
[
{"text": "TOTAL ALLOCATION", "color": "#1D4ED8", "bold": true, "fill": "#EFF6FF"},
{"text": "", "fill": "#EFF6FF"},
{"text": "", "fill": "#EFF6FF"},
{"text": "44", "align": "ctr", "color": "#1D4ED8", "bold": true, "fill": "#EFF6FF"},
{"text": "1,800,700", "align": "r", "color": "#1D4ED8", "bold": true, "fill": "#EFF6FF"}
]
]
}
</metadata>
<!-- Table bounding box -->
<rect x="80" y="150" width="1120" height="340" fill="none" stroke="#CBD5E1" stroke-width="2"/>
<g id="tableArea" transform="translate(80, 150)">
<!-- ==================== Table Header ==================== -->
<rect x="0" y="0" width="1120" height="50" fill="#F8FAFC"/>
<line x1="0" y1="50" x2="1120" y2="50" stroke="#CBD5E1" stroke-width="2"/>
<!-- Column 1: ID / Code -->
<text x="20" y="30" font-size="13" font-weight="800" fill="#475569" letter-spacing="1">PROJECT ID</text>
<!-- Column 2: Name / Description -->
<text x="180" y="30" font-size="13" font-weight="800" fill="#475569" letter-spacing="1">PROJECT DESCRIPTION</text>
<!-- Column 3: Category -->
<text x="640" y="30" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="middle">DEPARTMENT</text>
<!-- Column 4: Small Number -->
<text x="860" y="30" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="middle">HEADCOUNT</text>
<!-- Column 5: Large Number -->
<text x="1100" y="30" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="end">BUDGET (USD)</text>
<!-- Vertical Grid Lines -->
<line x1="160" y1="0" x2="160" y2="340" stroke="#E2E8F0" stroke-width="1.5"/>
<line x1="560" y1="0" x2="560" y2="340" stroke="#E2E8F0" stroke-width="1.5"/>
<line x1="720" y1="0" x2="720" y2="340" stroke="#E2E8F0" stroke-width="1.5"/>
<line x1="1000" y1="0" x2="1000" y2="340" stroke="#E2E8F0" stroke-width="1.5"/>
<!-- ==================== Data Rows ==================== -->
<!-- Row 1 -->
<line x1="0" y1="98" x2="1120" y2="98" stroke="#E2E8F0" stroke-width="1"/>
<text x="20" y="80" font-size="14" font-weight="500" fill="#334155">PRJ-2025-01A</text>
<text x="180" y="80" font-size="15" font-weight="600" fill="#0F172A">Cloud Infrastructure Migration</text>
<text x="640" y="80" font-size="14" font-weight="500" fill="#475569" text-anchor="middle">Engineering</text>
<text x="860" y="80" font-size="15" font-weight="600" fill="#334155" text-anchor="middle">12</text>
<text x="1100" y="80" font-size="15" font-weight="600" fill="#0F172A" text-anchor="end">450,000</text>
<!-- Row 2 -->
<line x1="0" y1="146" x2="1120" y2="146" stroke="#E2E8F0" stroke-width="1"/>
<text x="20" y="128" font-size="14" font-weight="500" fill="#334155">PRJ-2025-02B</text>
<text x="180" y="128" font-size="15" font-weight="600" fill="#0F172A">Global Marketing Campaign Q2</text>
<text x="640" y="128" font-size="14" font-weight="500" fill="#475569" text-anchor="middle">Marketing</text>
<text x="860" y="128" font-size="15" font-weight="600" fill="#334155" text-anchor="middle">8</text>
<text x="1100" y="128" font-size="15" font-weight="600" fill="#0F172A" text-anchor="end">820,500</text>
<!-- Row 3 -->
<line x1="0" y1="194" x2="1120" y2="194" stroke="#E2E8F0" stroke-width="1"/>
<text x="20" y="176" font-size="14" font-weight="500" fill="#334155">PRJ-2025-03C</text>
<text x="180" y="176" font-size="15" font-weight="600" fill="#0F172A">Sales Enablement Training</text>
<text x="640" y="176" font-size="14" font-weight="500" fill="#475569" text-anchor="middle">Sales</text>
<text x="860" y="176" font-size="15" font-weight="600" fill="#334155" text-anchor="middle">4</text>
<text x="1100" y="176" font-size="15" font-weight="600" fill="#0F172A" text-anchor="end">95,000</text>
<!-- Row 4 -->
<line x1="0" y1="242" x2="1120" y2="242" stroke="#E2E8F0" stroke-width="1"/>
<text x="20" y="224" font-size="14" font-weight="500" fill="#334155">PRJ-2025-04A</text>
<text x="180" y="224" font-size="15" font-weight="600" fill="#0F172A">Mobile App Redesign V3.0</text>
<text x="640" y="224" font-size="14" font-weight="500" fill="#475569" text-anchor="middle">Product</text>
<text x="860" y="224" font-size="15" font-weight="600" fill="#334155" text-anchor="middle">15</text>
<text x="1100" y="224" font-size="15" font-weight="600" fill="#0F172A" text-anchor="end">315,200</text>
<!-- Row 5 -->
<line x1="0" y1="290" x2="1120" y2="290" stroke="#CBD5E1" stroke-width="2"/>
<text x="20" y="272" font-size="14" font-weight="500" fill="#334155">PRJ-2025-05B</text>
<text x="180" y="272" font-size="15" font-weight="600" fill="#0F172A">Cybersecurity Audit &amp; Fixes</text>
<text x="640" y="272" font-size="14" font-weight="500" fill="#475569" text-anchor="middle">IT Security</text>
<text x="860" y="272" font-size="15" font-weight="600" fill="#334155" text-anchor="middle">5</text>
<text x="1100" y="272" font-size="15" font-weight="600" fill="#0F172A" text-anchor="end">120,000</text>
<!-- ==================== Total Row ==================== -->
<rect x="0" y="290" width="1120" height="50" fill="#EFF6FF"/>
<text x="20" y="322" font-size="15" font-weight="800" fill="#1D4ED8" letter-spacing="1">TOTAL ALLOCATION</text>
<text x="860" y="322" font-size="15" font-weight="800" fill="#1D4ED8" text-anchor="middle">44</text>
<text x="1100" y="322" font-size="16" font-weight="800" fill="#1D4ED8" text-anchor="end">1,800,700</text>
</g>
</g>
<!-- ==================== Footer ==================== -->
<g id="source" data-pptx-bounds="40 507 1200 38">
<text x="1200" y="530" text-anchor="end" font-weight="600" fill="#94A3B8">Data Source: PMO Budget Allocation Draft V1.0</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

@@ -1,12 +1,17 @@
{
"meta": {
"total": 76,
"total": 33,
"defaultViewBox": "0 0 1280 720",
"formats": ["ppt169"],
"formats": [
"ppt169"
],
"filePattern": "{key}.svg",
"libraryPositioning": "Visualization template library covering charts, infographics, diagrams, and strategic frameworks. The `charts/` path is retained for backward compatibility. Templates are named by visual structure, not by business-model name — model-specific frameworks (SWOT, BCG, PEST, OKR, Porter's Five Forces, Value Chain, etc.) are matched via keywords in `summary`.",
"summaryGrammar": "Each chart's `summary` is a selection rule, not a description. Format: 'Pick for <content shape + scale>. Skip if <reason → alternative>'. Strategist supplies 38 semantic content-shape tags per candidate page; chart_recall.py returns a bounded lexical shortlist and exposes the full catalog only through an explicitly requested semantic fallback.",
"updated": "2026-07-21"
"libraryPositioning": "Data-driven visualization templates whose source values determine geometry or visual encoding.",
"summaryGrammar": "Each chart summary is a selection rule, not a description. Format: 'Pick for <content shape + scale>. Skip if <reason → alternative>'.",
"updated": "2026-08-08"
},
"aliases": {
"project_schedule_table": "gantt_chart"
},
"charts": {
"area_chart": {
@@ -15,9 +20,6 @@
"bar_of_pie_chart": {
"summary": "Pick for pie composition where small long-tail slices should be expanded into a stacked bar detail. Skip if secondary detail should stay circular (use pie_of_pie_chart) or flat shares suffice (use pie_chart)."
},
"column_chart": {
"summary": "Pick for single-series category value comparison, 3-8 categories. Skip for >12 long-label items (use horizontal_bar_chart) or multi-series (use grouped_bar_chart)."
},
"box_plot_chart": {
"summary": "Pick for distribution shape per group (median, quartiles, outliers). Skip if only the average per group matters (use column_chart)."
},
@@ -30,20 +32,23 @@
"butterfly_chart": {
"summary": "Pick for two mirrored datasets sharing a common axis (age pyramid, A/B, income vs expense). Skip for >2 sides (use grouped_bar_chart)."
},
"column_chart": {
"summary": "Pick for single-series category value comparison, 3-8 categories. Skip for >12 long-label items (use horizontal_bar_chart) or multi-series (use grouped_bar_chart)."
},
"donut_chart": {
"summary": "Pick for 3-6 part proportions where a center KPI/total deserves emphasis. Skip if no center value to feature (use pie_chart)."
},
"dual_axis_line_chart": {
"summary": "Pick when 2 metrics with different units/scales must be compared over time. Skip if both metrics share the same unit (use line_chart)."
"summary": "Pick for comparing 2 metrics with different units/scales over time. Skip if both metrics share the same unit (use line_chart)."
},
"dumbbell_chart": {
"summary": "Pick for before-vs-after or two-state difference across 5-10 items. Skip for single snapshot (use column_chart) or 3+ states (use grouped_bar_chart)."
},
"funnel_chart": {
"summary": "Pick for 3-5 sequential conversion stages with monotonic drop-off. Skip if flow branches/merges (use sankey_chart) or steps don't entail loss (use process_flow)."
"summary": "Pick for 3-5 sequential conversion stages whose values drive a monotonic drop-off. Skip if the stages do not encode loss or qualitative branches and returns carry the message (build a Structure from the page relationships)."
},
"gantt_chart": {
"summary": "Pick for project schedule with 6-12 tasks, durations, and dependencies. Skip for simple milestones without duration (use timeline) or vertical roadmap (use roadmap_vertical)."
"summary": "Pick for project schedule with 6-12 tasks whose dates or durations determine bar position and length, with optional dependencies. Skip for milestones without duration or qualitative lane-stage handoffs (build a Structure from the page relationships)."
},
"gauge_chart": {
"summary": "Pick for one hero metric or single KPI's goal achievement rate. Skip for multiple metrics (use bullet_chart for target+actual or progress_bar_chart for completion %)."
@@ -60,23 +65,14 @@
"horizontal_bar_chart": {
"summary": "Pick for ranking 5-12 items, especially with long labels. Skip if <=8 short-label items (use column_chart)."
},
"kpi_cards": {
"summary": "Pick for 4-8 standalone numeric metrics shown as overview cards (2x2 or 1x4) — exec summary opener, dashboard headline, quarterly recap, results-at-a-glance. Skip if metrics have target baselines (use bullet_chart) or single hero number (use gauge_chart)."
},
"line_chart": {
"summary": "Pick for 1-3 time-series on a continuous axis showing direction. Skip if cumulative volume matters (use area_chart) or different units (use dual_axis_line_chart)."
},
"matrix_2x2": {
"summary": "Pick for items plotted on Impact x Effort or similar generic 2-axis prioritization. Skip for named-quadrant text frameworks (use quadrant_text_bullets) or bubble-sized portfolios (use quadrant_bubble_scatter)."
},
"quadrant_text_bullets": {
"summary": "Pick for any 2×2 framework where each quadrant holds a titled bullet list — SWOT (Strengths/Weaknesses/Opportunities/Threats, internal-external × helpful-harmful), Ansoff (Existing/New Markets × Existing/New Products), or any named two-axis matrix with text content. Skip for items plotted as points (use matrix_2x2) or bubble-sized portfolios (use quadrant_bubble_scatter)."
},
"quadrant_bubble_scatter": {
"summary": "Pick for 2×2 portfolio where items are plotted as bubbles sized by a third metric — BCG Growth × Share with Stars/Cash Cows/Question Marks/Dogs, revenue-weighted portfolio matrix. Skip for textual named quadrants (use quadrant_text_bullets) or generic prioritization without bubble size (use matrix_2x2)."
"summary": "Pick for 4-10 items whose x and y positions encode two dimensions and whose bubble radius encodes a third magnitude inside a 2×2 quadrant frame. Skip for fixed qualitative regions with text assigned to zones (build a Structure from the page relationships)."
},
"pareto_chart": {
"summary": "Pick to show 80/20 contribution: descending bars + cumulative line. Skip if cumulative line is not the message (use column_chart)."
"summary": "Pick for showing 80/20 contribution with descending bars and a cumulative line. Skip if the cumulative line is not the message (use column_chart)."
},
"pie_chart": {
"summary": "Pick for simple 3-6 part proportions of one whole. Skip for >=7 parts (use donut_chart or treemap_chart) or hierarchical composition (use treemap_chart)."
@@ -84,12 +80,6 @@
"pie_of_pie_chart": {
"summary": "Pick for pie composition where small slices need a secondary pie detail. Skip if the secondary detail is easier to compare as bars (use bar_of_pie_chart) or hierarchy matters (use sunburst_chart)."
},
"hub_inward_arrows": {
"summary": "Pick for 1 center subject + 4-5 surrounding forces with arrows pointing inward — Porter's Five Forces (central rivalry, threat of new entrants/substitutes, bargaining power of buyers/suppliers), threat-model with central asset, pressure-on-core diagrams. Skip for outward radiation from center capability (use hub_spoke) or non-directional cluster (use mind_map)."
},
"process_flow": {
"summary": "Pick for 3-8 sequential steps connected by simple arrows — approval workflows, customer onboarding, request handling, lifecycle stages. Skip if cyclical (use circular_stages) or stages produce named outputs (use pipeline_with_stages)."
},
"progress_bar_chart": {
"summary": "Pick for 3-8 items each with a completion %. Skip if items have explicit target+actual values (use bullet_chart)."
},
@@ -106,7 +96,7 @@
"summary": "Pick for 2-4 series showing both total and composition over time. Skip if individual series matter more than total (use line_chart)."
},
"stacked_bar_chart": {
"summary": "Pick when each category splits into 2-4 internal parts and total still matters. Skip if only comparing totals (use column_chart)."
"summary": "Pick for categories split into 2-4 internal parts when each total still matters. Skip if only comparing totals (use column_chart)."
},
"stock_chart": {
"summary": "Pick for open-high-low-close price movement over dates. Skip if showing only close-price direction (use line_chart) or category volume comparison (use column_chart)."
@@ -114,128 +104,14 @@
"sunburst_chart": {
"summary": "Pick for hierarchical part-to-whole composition across 2-3 levels where each ring should stay visible. Skip if rectangular area comparison is clearer (use treemap_chart) or shares are flat (use donut_chart)."
},
"timeline": {
"summary": "Pick for 3-8 milestone events on a horizontal time axis (no duration). Skip for tasks with start/end ranges (use gantt_chart) or vertical layout (use roadmap_vertical)."
},
"treemap_chart": {
"summary": "Pick for hierarchical/grouped area proportion with many parts of varying size. Skip for flat 3-6 parts (use pie_chart or donut_chart)."
},
"waterfall_chart": {
"summary": "Pick for stepwise additive/subtractive breakdown bridging a starting value to an ending value. Skip if no running total (use column_chart)."
},
"pyramid_chart": {
"summary": "Pick for 3-6 stratified hierarchy layers in flat 2D side-view — Maslow's hierarchy, maturity models, value hierarchy, capability tiers, market segments, audience pyramid. Skip for dramatic tone (use pyramid_isometric), flat priority list (use vertical_list), or org reporting (use top_down_tree)."
},
"pyramid_isometric": {
"summary": "Pick for 4-tier maturity/achievement model with presentation drama (review reports, capability tiers). Tier count fixed at 4 (hand-tuned vertices). Skip for documentary tone (use pyramid_chart) or tier count != 4."
},
"venn_diagram": {
"summary": "Pick for 2-3 overlapping sets where the intersection is the message. Skip if no overlap exists (use icon_grid)."
},
"pros_cons_chart": {
"summary": "Pick for bilateral pros/cons list, 2-5 items per side. Skip for full feature comparison (use comparison_table) or numeric A/B mirror data (use butterfly_chart)."
},
"circular_stages": {
"summary": "Pick for 4-6 stage closed loop where stages compose a cycle — PDCA, flywheel compounding loops (Attract → Engage → Delight), lifecycle, continuous improvement. Skip for linear flow (use process_flow), one-shot sequence (use numbered_steps), or wedge-based central topic (use segmented_wheel)."
},
"numbered_steps": {
"summary": "Pick for 3-6 horizontal sequential steps with numeric emphasis — how-it-works section, getting-started guide, methodology overview, implementation phases. Skip if steps need connector arrows (use process_flow) or named output artifacts (use pipeline_with_stages)."
},
"icon_grid": {
"summary": "Pick for 4-9 parallel features/capabilities/services as icon cards — feature grid, service lineup, benefits matrix, brand values, product highlights. Skip for sequential ordering (use numbered_steps) or hierarchical layers (use pyramid_chart)."
},
"isometric_stairs": {
"summary": "Pick for 4-7 ascending stages emphasizing growth/maturity progression visually. Skip for flat sequential steps (use numbered_steps) or formal hierarchy (use pyramid_chart)."
},
"mind_map": {
"summary": "Pick for one core topic radiating into 3-6 brainstorm branches. Skip for capability hub-and-spoke (use hub_spoke) or strict hierarchy (use top_down_tree)."
},
"comparison_table": {
"summary": "Pick for 2-4 plans/products compared across many feature rows (dense matrix). Skip for pricing-tier marketing layout (use comparison_columns)."
},
"snake_flow": {
"summary": "Pick for 6-10 winding sequential steps fitting a long journey/lifecycle on one slide. Skip for <=5 steps (use numbered_steps)."
},
"roadmap_vertical": {
"summary": "Pick for 4-8 milestones on a vertical timeline with status indicators. Skip for horizontal time emphasis (use timeline) or tasks with durations (use gantt_chart)."
},
"word_cloud": {
"summary": "Pick for 20-30 keywords sized by frequency/weight. Skip if individual keyword position carries meaning (use icon_grid)."
},
"concentric_circles": {
"summary": "Pick for 3-5 priority rings from core to periphery — bullseye/onion model, sphere of influence, stakeholder rings, core-to-ecosystem priority layers, target-vs-extended audience. Skip for non-prioritized layers (use pyramid_chart)."
},
"segmented_wheel": {
"summary": "Pick for one central topic split into 4-8 equally-weighted parallel dimensions (5W1H, 4P, 5S, 6-aspect review), each wedge paired with a description card. Skip for sequential cycles (use circular_stages), proportional shares (use pie_chart), or hub with non-wedge spokes (use hub_spoke)."
},
"arc_anchored_list": {
"summary": "Pick for 3-5 parallel tracks/pillars unified by a left-edge radial fan as visual anchor (each lane = numbered wedge + title + body + right-end color square). Skip for plain numbered enumeration without unifying core (use vertical_list or numbered_steps), or sequential phases (use process_flow)."
},
"chevron_process": {
"summary": "Pick for 3-6 phase methodology with chunky arrow-chain progression and deliverables per phase. Skip for <=2 phases or non-linear flow (use process_flow), or chain ending in an aggregate outcome wedge (use chevron_chain_with_tail)."
},
"chevron_chain_with_tail": {
"summary": "Pick for 4-6 sequential chevron blocks plus a final wedge representing aggregate outcome — Porter's value chain (primary + support activities mapped to margin), process leading to a summary deliverable, contribution chain to a result. Skip for plain chevron flow without summary tail (use chevron_process) or non-chevron pipeline (use pipeline_with_stages)."
},
"comparison_columns": {
"summary": "Pick for 2-4 pricing/service tier cards in side-by-side columns (marketing layout). Skip for dense feature comparison (use comparison_table)."
},
"fishbone_diagram": {
"summary": "Pick for cause-and-effect root cause analysis with 4-6 cause branches (Ishikawa, 6M). Skip for sequential flow (use process_flow)."
},
"hub_spoke": {
"summary": "Pick for 1 core capability + 4-8 surrounding capabilities (platform/ecosystem); each spoke = title or title + 1-2 line description. Skip if center is a system containing parts with their own descriptions (use module_composition), or surroundings exert inward pressure on the center (use hub_inward_arrows)."
},
"vertical_list": {
"summary": "Pick for 3-6 numbered key points each with a short description — design principles, core tenets, action items, key takeaways, recommendations, executive summary points. Skip for icon-style cards (use icon_grid) or sequential steps (use numbered_steps)."
},
"vertical_pillars": {
"summary": "Pick for 1×3 / 1×4 / 1×5 vertical column layout where each pillar = one independent category with title + bullets — PEST (Political/Economic/Social/Technological), four-pillar strategy overview, side-by-side independent categories. Skip for 2×2 quadrant (use quadrant_text_bullets), pricing tiers (use comparison_columns), or 2×2 parallel aspects (use labeled_card)."
},
"layered_architecture": {
"summary": "Pick for 3-4 horizontal architecture layers (presentation/service/data), 2-4 module cards per layer, each card = title + 1-line description (description required, even if source brief). Skip if no per-module descriptions (use icon_grid) or no horizontal layering (use module_composition)."
},
"module_composition": {
"summary": "Pick for one parent container wrapping 3-N child module cards, each = title + 2-3 bullets — fits 'Feature X contains 3 parts, each with its own description'. Skip if source has only labels without descriptions (use numbered_steps or icon_grid)."
},
"pipeline_with_stages": {
"summary": "Pick for 3-5 horizontal pipeline stages, each = title + 1-line description + output artifact, connected by arrows (data pipelines, ETL, build pipelines). Skip if any stage lacks an artifact (use process_flow or numbered_steps)."
},
"client_server_flow": {
"summary": "Pick for left-side clients + right-side servers with labeled bidirectional arrows for key interactions (request/response/push). Each module = name + 1-line description; each arrow must have an action label. Skip for non-distributed flows (use process_flow)."
},
"basic_table": {
"summary": "Pick for plain tabular text/number grid, 3-8 columns. Skip if cells need visual bars (use consulting_table) or qualitative scores (use harvey_balls_table)."
},
"consulting_table": {
"summary": "Pick for high-density tables with embedded micro bar visuals (consulting/financial reports). Skip for plain text data (use basic_table)."
},
"project_schedule_table": {
"summary": "Pick for table-style task tracker (task / owner / status / timeline). Skip for true Gantt with dependencies (use gantt_chart) or schedule without ownership (use roadmap_vertical)."
},
"financial_statement_table": {
"summary": "Pick for income statement / balance sheet style with right-aligned numbers and strong total rows. Skip for non-financial tabular data (use basic_table)."
},
"feature_matrix_table": {
"summary": "Pick for competitive feature checklist with checkmarks across products. Skip for qualitative scores (use harvey_balls_table) or pricing tier marketing (use comparison_columns)."
},
"harvey_balls_table": {
"summary": "Pick for qualitative scoring grid using 0-100% Harvey balls (vendor/skill assessment). Skip for binary checkmarks (use feature_matrix_table) or numeric values (use basic_table)."
},
"team_roster": {
"summary": "Pick for 3-12 leadership/team profile cards (photo + name + title + short bio). Skip for reporting hierarchy (use top_down_tree)."
},
"top_down_tree": {
"summary": "Pick for hierarchical top-down tree 2-4 levels deep with parent→children reporting/decomposition lines — org charts (CEO → VPs → Directors), OKR cascades (Objective → Key Results → Initiatives), WBS decomposition. Skip for non-hierarchical brainstorm (use mind_map) or flat team showcase (use team_roster)."
},
"journey_map": {
"summary": "Pick for multi-phase customer experience matrix with actions, emotion curve, and pain points per phase. Skip for simple linear funnel (use funnel_chart)."
},
"agenda_list": {
"summary": "Pick for table of contents, meeting agendas, or presentation roadmap — numbered items + brief description + duration / owner per row. Skip for substantive content lists (use vertical_list) or single-page section dividers (use a cover layout)."
},
"labeled_card": {
"summary": "Pick for 3-4 parallel aspects of one subject with per-aspect titles + short body (self-introduction, four-pillar overview, capability quadrant). Skip for plain feature lists (use icon_grid), sequential steps (use numbered_steps), or strategic quadrants (use quadrant_text_bullets / matrix_2x2)."
"summary": "Pick for 20-30 keywords whose font size encodes frequency or weight. Skip if items are equal peers without value-driven sizing (build a Structure from the page relationships)."
}
}
}
@@ -1,221 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Chevron Chain with Tail Template
Structure: 4-6 sequential chevron blocks + a final wedge representing the aggregate outcome
Use cases: Porter's value chain (primary + support activities mapped to margin), process leading to a summary deliverable, contribution chain ending in a result
Design Principles: Interlocking chevron blocks with right-facing outcome wedge in accent color. Differs from chevron_process by the appended summary tail.
-->
<!-- Background -->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<rect x="60" y="48" width="4" height="56" rx="2" fill="#6366F1"/>
<text x="78" y="72" font-size="30" font-weight="800" fill="#0F172A">
<tspan>Value Chain Analysis</tspan>
</text>
<text x="78" y="98" font-size="13" font-weight="700" fill="#64748B" letter-spacing="2">
<tspan>INTERNAL OPERATIONS · COMPETITIVE ADVANTAGE EVALUATION</tspan>
</text>
<!-- Headline metric chip -->
<g transform="translate(1010, 56)">
<rect x="0" y="0" width="200" height="48" rx="14" fill="#ECFDF5"/>
<text x="22" y="32" font-size="22" font-weight="800" fill="#10B981">
<tspan>+18%</tspan>
</text>
<text x="92" y="22" font-weight="800" fill="#065F46" letter-spacing="1.5">
<tspan>MARGIN VS PEERS</tspan>
</text>
<text x="92" y="38" font-weight="600" fill="#10B981">
<tspan>FY24 weighted avg</tspan>
</text>
</g>
</g>
<!-- ==================== Axis Labels (Support / Primary) ==================== -->
<g id="section-labels" data-pptx-bounds="0 140 140 390">
<text x="38" y="240" font-weight="800" fill="#475569" text-anchor="middle" transform="rotate(-90, 38, 240)" letter-spacing="2">
<tspan>SUPPORT ACTIVITIES</tspan>
</text>
<text x="38" y="432" font-weight="800" fill="#475569" text-anchor="middle" transform="rotate(-90, 38, 432)" letter-spacing="2">
<tspan>PRIMARY ACTIVITIES</tspan>
</text>
</g>
<!-- ==================== Value Chain ==================== -->
<g id="chain" data-pptx-bounds="0 117 1280 390">
<!-- ===== Support Activities: 4 horizontal bars, aligned right edge to last chevron point at x=980 ===== -->
<g id="support">
<rect x="60" y="140" width="920" height="44" rx="6" fill="#DBEAFE"/>
<rect x="60" y="190" width="920" height="44" rx="6" fill="#DBEAFE"/>
<rect x="60" y="240" width="920" height="44" rx="6" fill="#DBEAFE"/>
<rect x="60" y="290" width="920" height="44" rx="6" fill="#DBEAFE"/>
</g>
<!-- ===== Primary Activities: 5 chevrons, stride 180, rect width 175, point overhang 25 ===== -->
<g id="primary">
<!-- Chev 1: rect 60-235, flat left, point at 260 -->
<g id="primary-chevron-01" data-pptx-authoring="preset" data-pptx-object="shape" data-pptx-prst="homePlate" data-pptx-frame="60 380 200 104" data-pptx-av-adj="val 24038" fill="#E0E7FF" stroke="none">
<path d="M 60 380 L 235 380 L 260 432 L 235 484 L 60 484 Z"/>
</g>
<!-- Chev 2: rect 240-415, notch 265, point at 440 -->
<g id="primary-chevron-02" data-pptx-authoring="preset" data-pptx-object="shape" data-pptx-prst="chevron" data-pptx-frame="240 380 200 104" data-pptx-av-adj="val 24038" fill="#C7D2FE" stroke="none">
<path d="M 240 380 L 415 380 L 440 432 L 415 484 L 240 484 L 265 432 Z"/>
</g>
<!-- Chev 3: rect 420-595, notch 445, point at 620 -->
<g id="primary-chevron-03" data-pptx-authoring="preset" data-pptx-object="shape" data-pptx-prst="chevron" data-pptx-frame="420 380 200 104" data-pptx-av-adj="val 24038" fill="#E0E7FF" stroke="none">
<path d="M 420 380 L 595 380 L 620 432 L 595 484 L 420 484 L 445 432 Z"/>
</g>
<!-- Chev 4: rect 600-775, notch 625, point at 800 -->
<g id="primary-chevron-04" data-pptx-authoring="preset" data-pptx-object="shape" data-pptx-prst="chevron" data-pptx-frame="600 380 200 104" data-pptx-av-adj="val 24038" fill="#C7D2FE" stroke="none">
<path d="M 600 380 L 775 380 L 800 432 L 775 484 L 600 484 L 625 432 Z"/>
</g>
<!-- Chev 5: rect 780-955, notch 805, point at 980 -->
<g id="primary-chevron-05" data-pptx-authoring="preset" data-pptx-object="shape" data-pptx-prst="chevron" data-pptx-frame="780 380 200 104" data-pptx-av-adj="val 24038" fill="#E0E7FF" stroke="none">
<path d="M 780 380 L 955 380 L 980 432 L 955 484 L 780 484 L 805 432 Z"/>
</g>
</g>
<!-- ===== Margin Tail: full-height wedge merging both halves into one outcome point ===== -->
<!-- Left edge at x=975 (slightly overlapping chev5 point at x=980 to convey "feeding into"), right tip at (1200, 312) -->
<path id="marginTail" d="M 975 140 L 1200 312 L 975 484 Z" fill="#059669"/>
</g>
<!-- ==================== Support Activity Labels ==================== -->
<g id="support-labels" data-pptx-bounds="14 142 655 192">
<text x="80" y="168" font-size="14" font-weight="800" fill="#1E3A8A">
<tspan>Firm Infrastructure</tspan>
</text>
<text x="300" y="168" font-weight="500" fill="#2563EB">
<tspan>General mgmt · Planning · Finance · Legal · Quality</tspan>
</text>
<text x="80" y="218" font-size="14" font-weight="800" fill="#1E3A8A">
<tspan>Human Resources</tspan>
</text>
<text x="300" y="218" font-weight="500" fill="#2563EB">
<tspan>Recruitment · Training · Development · Compensation</tspan>
</text>
<text x="80" y="268" font-size="14" font-weight="800" fill="#1E3A8A">
<tspan>Tech Development</tspan>
</text>
<text x="300" y="268" font-weight="500" fill="#2563EB">
<tspan>R&amp;D · Product design · Process automation · IT systems</tspan>
</text>
<text x="80" y="318" font-size="14" font-weight="800" fill="#1E3A8A">
<tspan>Procurement</tspan>
</text>
<text x="300" y="318" font-weight="500" fill="#2563EB">
<tspan>Supplier sourcing · Raw materials · Vendor management</tspan>
</text>
</g>
<!-- ==================== Primary Activity Labels (centered on each chevron rect) ==================== -->
<g id="primary-labels" data-pptx-bounds="0 399 1014 71">
<!-- Chev 1 rect center: x=(60+235)/2=147 -->
<text x="147" y="418" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Inbound</tspan>
</text>
<text x="147" y="436" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Logistics</tspan>
</text>
<text x="147" y="460" font-weight="600" fill="#4F46E5" text-anchor="middle">
<tspan>Inventory · Warehousing</tspan>
</text>
<!-- Chev 2 rect center: x=(240+415)/2=327 -->
<text x="327" y="428" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Operations</tspan>
</text>
<text x="327" y="452" font-weight="600" fill="#4F46E5" text-anchor="middle">
<tspan>Machining · Assembly</tspan>
</text>
<!-- Chev 3 rect center: x=(420+595)/2=507 -->
<text x="507" y="418" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Outbound</tspan>
</text>
<text x="507" y="436" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Logistics</tspan>
</text>
<text x="507" y="460" font-weight="600" fill="#4F46E5" text-anchor="middle">
<tspan>Distribution · Shipping</tspan>
</text>
<!-- Chev 4 rect center: x=(600+775)/2=687 -->
<text x="687" y="418" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Marketing</tspan>
</text>
<text x="687" y="436" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>&amp; Sales</tspan>
</text>
<text x="687" y="460" font-weight="600" fill="#4F46E5" text-anchor="middle">
<tspan>Advertising · Promotions</tspan>
</text>
<!-- Chev 5 rect center: x=(780+955)/2=867 -->
<text x="867" y="428" font-size="13" font-weight="800" fill="#3730A3" text-anchor="middle">
<tspan>Service</tspan>
</text>
<text x="867" y="452" font-weight="600" fill="#4F46E5" text-anchor="middle">
<tspan>Support · Maintenance</tspan>
</text>
</g>
<!-- ==================== MARGIN Label (placed at tail's widest point, near right) ==================== -->
<!-- Tail point at (1200, 312). Label centered at x ≈ 1070, y ≈ 312 where width ≈ 130 px -->
<g id="margin-label" data-pptx-bounds="975 277 150 61">
<text x="1050" y="306" font-size="24" font-weight="800" fill="#FFFFFF" text-anchor="middle" letter-spacing="3">
<tspan>MARGIN</tspan>
</text>
<text x="1050" y="328" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="2" opacity="0.85">
<tspan>VALUE COST</tspan>
</text>
</g>
<!-- ==================== Activity Counter Captions (bottom row) ==================== -->
<g id="caption-9" data-pptx-bounds="33 553 254 70" transform="translate(60, 560)">
<rect x="0" y="0" width="200" height="56" rx="10" fill="#F8FAFC"/>
<text x="20" y="34" font-size="26" font-weight="800" fill="#0F172A">
<tspan>9</tspan>
</text>
<text x="50" y="26" font-weight="700" fill="#475569" letter-spacing="1">
<tspan>ACTIVITY AREAS</tspan>
</text>
<text x="50" y="42" font-weight="600" fill="#94A3B8">
<tspan>4 support · 5 primary</tspan>
</text>
</g>
<g id="caption-coverage" data-pptx-bounds="251 553 278 70" transform="translate(280, 560)">
<rect x="0" y="0" width="220" height="56" rx="10" fill="#F8FAFC"/>
<text x="20" y="34" font-size="26" font-weight="800" fill="#3B82F6">
<tspan>100%</tspan>
</text>
<text x="90" y="26" font-weight="700" fill="#475569" letter-spacing="1">
<tspan>COVERAGE</tspan>
</text>
<text x="90" y="42" font-weight="600" fill="#94A3B8">
<tspan>All 9 areas reviewed</tspan>
</text>
</g>
<g id="caption-optimized" data-pptx-bounds="481 553 378 70" transform="translate(520, 560)">
<rect x="0" y="0" width="300" height="56" rx="10" fill="#F8FAFC"/>
<text x="20" y="34" font-size="26" font-weight="800" fill="#6366F1">
<tspan>6</tspan>
</text>
<text x="50" y="26" font-weight="700" fill="#475569" letter-spacing="1">
<tspan>OPTIMIZATION TARGETS</tspan>
</text>
<text x="50" y="42" font-weight="600" fill="#94A3B8">
<tspan>Cost-reduction or differentiation</tspan>
</text>
</g>
<!-- ==================== Footer ==================== -->
<g id="source" data-pptx-bounds="40 672 1200 38">
<text x="1220" y="695" font-weight="600" fill="#94A3B8" text-anchor="end">
<tspan>Source: Internal Operations Review · FY24</tspan>
</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -1,190 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Chevron Process Template
Usage: Linear stage process, step-by-step progression, methodology display
Scenarios: Sales funnel stages, project phases, methodology steps
Supports: 5-stage horizontal chevron (3-6 recommended)
-->
<defs>
<!-- Phase Gradients -->
<linearGradient id="gradPhase1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366F1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#4F46E5;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradPhase2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2563EB;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradPhase3" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0EA5E9;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0284C7;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradPhase4" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10B981;stop-opacity:1" />
<stop offset="100%" style="stop-color:#059669;stop-opacity:1" />
</linearGradient>
<linearGradient id="gradPhase5" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#F59E0B;stop-opacity:1" />
<stop offset="100%" style="stop-color:#D97706;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- Title -->
<g id="header" data-pptx-bounds="280 25 720 68">
<text x="640" y="55" text-anchor="middle"
font-size="30" font-weight="700" fill="#1E293B">Digital Transformation Methodology</text>
<text x="640" y="82" text-anchor="middle"
font-size="14" fill="#64748B">Chevron Process · Five-Stage Progressive Framework</text>
</g>
<g id="main-content" transform="translate(75, -60)" data-pptx-bounds="115 160 1050 440">
<g id="chevrons">
<!-- Chevron 1 (Leftmost) -->
<g id="chevron-1">
<polygon points="40,220 200,220 230,265 200,310 40,310" fill="url(#gradPhase1)"/>
<text x="130" y="260" text-anchor="middle"
font-size="14" font-weight="700" fill="#FFFFFF">Diagnostics</text>
<text x="130" y="280" text-anchor="middle" fill="#FFFFFF" fill-opacity="0.8">PHASE 01</text>
</g>
<!-- Chevron 2 -->
<g id="chevron-2">
<polygon points="235,220 400,220 430,265 400,310 235,310 265,265" fill="url(#gradPhase2)"/>
<text x="330" y="260" text-anchor="middle"
font-size="14" font-weight="700" fill="#FFFFFF">Planning &amp; Design</text>
<text x="330" y="280" text-anchor="middle" fill="#FFFFFF" fill-opacity="0.8">PHASE 02</text>
</g>
<!-- Chevron 3 -->
<g id="chevron-3">
<polygon points="435,220 610,220 640,265 610,310 435,310 465,265" fill="url(#gradPhase3)"/>
<text x="533" y="260" text-anchor="middle"
font-size="14" font-weight="700" fill="#FFFFFF">Pilot Validation</text>
<text x="533" y="280" text-anchor="middle" fill="#FFFFFF" fill-opacity="0.8">PHASE 03</text>
</g>
<!-- Chevron 4 -->
<g id="chevron-4">
<polygon points="645,220 830,220 860,265 830,310 645,310 675,265" fill="url(#gradPhase4)"/>
<text x="748" y="260" text-anchor="middle"
font-size="14" font-weight="700" fill="#FFFFFF">Scale &amp; Rollout</text>
<text x="748" y="280" text-anchor="middle" fill="#FFFFFF" fill-opacity="0.8">PHASE 04</text>
</g>
<!-- Chevron 5 -->
<g id="chevron-5">
<polygon points="865,220 1060,220 1090,265 1060,310 865,310 895,265" fill="url(#gradPhase5)"/>
<text x="973" y="260" text-anchor="middle"
font-size="14" font-weight="700" fill="#FFFFFF">Optimization</text>
<text x="973" y="280" text-anchor="middle" fill="#FFFFFF" fill-opacity="0.8">PHASE 05</text>
</g>
</g>
<!-- Phase Timeline Bar -->
<g id="timeline">
<rect x="40" y="325" width="190" height="20" rx="4" fill="#4F46E5" fill-opacity="0.15"/>
<text x="135" y="339" text-anchor="middle" font-weight="600" fill="#4F46E5">2-4 Weeks</text>
<rect x="245" y="325" width="195" height="20" rx="4" fill="#2563EB" fill-opacity="0.15"/>
<text x="342" y="339" text-anchor="middle" font-weight="600" fill="#2563EB">4-6 Weeks</text>
<rect x="450" y="325" width="195" height="20" rx="4" fill="#0284C7" fill-opacity="0.15"/>
<text x="547" y="339" text-anchor="middle" font-weight="600" fill="#0284C7">8-12 Weeks</text>
<rect x="655" y="325" width="210" height="20" rx="4" fill="#059669" fill-opacity="0.15"/>
<text x="760" y="339" text-anchor="middle" font-weight="600" fill="#059669">12-16 Weeks</text>
<rect x="875" y="325" width="215" height="20" rx="4" fill="#D97706" fill-opacity="0.15"/>
<text x="982" y="339" text-anchor="middle" font-weight="600" fill="#D97706">Ongoing</text>
</g>
<!-- Detail Cards Row -->
<g id="cards">
<!-- Card 1 -->
<g id="card-1">
<g>
<rect x="40" y="370" width="190" height="290" rx="10" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<rect x="40" y="370" width="190" height="5" rx="3" fill="#4F46E5"/>
<text x="135" y="405" text-anchor="middle"
font-size="15" font-weight="700" fill="#1E293B">Diagnostics</text>
<line x1="65" y1="425" x2="205" y2="425" stroke="#F0F0F0" stroke-width="1"/>
<text x="60" y="450" font-size="13" fill="#475569">· Current State Review</text>
<text x="60" y="478" font-size="13" fill="#475569">· Identify Pain Points</text>
<text x="60" y="506" font-size="13" fill="#475569">· Maturity Evaluation</text>
<text x="60" y="534" font-size="13" fill="#475569">· Gap Analysis</text>
<line x1="65" y1="560" x2="205" y2="560" stroke="#F0F0F0" stroke-width="1"/>
<text x="60" y="585" font-size="13" font-weight="600" fill="#4F46E5">Deliverables:</text>
<text x="60" y="608" fill="#94A3B8">Diagnostic Report</text>
<text x="60" y="630" fill="#94A3B8">Maturity Scorecard</text>
</g>
</g>
<!-- Card 2 -->
<g id="card-2">
<g>
<rect x="250" y="370" width="190" height="290" rx="10" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<rect x="250" y="370" width="190" height="5" rx="3" fill="#2563EB"/>
<text x="345" y="405" text-anchor="middle"
font-size="15" font-weight="700" fill="#1E293B">Planning &amp; Design</text>
<line x1="275" y1="425" x2="415" y2="425" stroke="#F0F0F0" stroke-width="1"/>
<text x="270" y="450" font-size="13" fill="#475569">· Architecture Design</text>
<text x="270" y="478" font-size="13" fill="#475569">· Tech Stack Eval.</text>
<text x="270" y="506" font-size="13" fill="#475569">· Roadmap Dev.</text>
<text x="270" y="534" font-size="13" fill="#475569">· ROI Calculation</text>
<line x1="275" y1="560" x2="415" y2="560" stroke="#F0F0F0" stroke-width="1"/>
<text x="270" y="585" font-size="13" font-weight="600" fill="#2563EB">Deliverables:</text>
<text x="270" y="608" fill="#94A3B8">Strategic Blueprint</text>
<text x="270" y="630" fill="#94A3B8">ROI Analysis</text>
</g>
</g>
<!-- Card 3 -->
<g id="card-3">
<g>
<rect x="460" y="370" width="190" height="290" rx="10" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<rect x="460" y="370" width="190" height="5" rx="3" fill="#0284C7"/>
<text x="555" y="405" text-anchor="middle"
font-size="15" font-weight="700" fill="#1E293B">Pilot Validation</text>
<line x1="485" y1="425" x2="625" y2="425" stroke="#F0F0F0" stroke-width="1"/>
<text x="480" y="450" font-size="13" fill="#475569">· Select Scenarios</text>
<text x="480" y="478" font-size="13" fill="#475569">· Rapid MVP Launch</text>
<text x="480" y="506" font-size="13" fill="#475569">· Performance Data</text>
<text x="480" y="534" font-size="13" fill="#475569">· Feedback Iteration</text>
<line x1="485" y1="560" x2="625" y2="560" stroke="#F0F0F0" stroke-width="1"/>
<text x="480" y="585" font-size="13" font-weight="600" fill="#0284C7">Deliverables:</text>
<text x="480" y="608" fill="#94A3B8">Pilot Summary</text>
<text x="480" y="630" fill="#94A3B8">Feasibility Report</text>
</g>
</g>
<!-- Card 4 -->
<g id="card-4">
<g>
<rect x="670" y="370" width="190" height="290" rx="10" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<rect x="670" y="370" width="190" height="5" rx="3" fill="#059669"/>
<text x="765" y="405" text-anchor="middle"
font-size="15" font-weight="700" fill="#1E293B">Scale &amp; Rollout</text>
<line x1="695" y1="425" x2="835" y2="425" stroke="#F0F0F0" stroke-width="1"/>
<text x="690" y="450" font-size="13" fill="#475569">· Full-Scale Rollout</text>
<text x="690" y="478" font-size="13" fill="#475569">· Org. Change Mgt.</text>
<text x="690" y="506" font-size="13" fill="#475569">· Team Training</text>
<text x="690" y="534" font-size="13" fill="#475569">· Build Standards</text>
<line x1="695" y1="560" x2="835" y2="560" stroke="#F0F0F0" stroke-width="1"/>
<text x="690" y="585" font-size="13" font-weight="600" fill="#059669">Deliverables:</text>
<text x="690" y="608" fill="#94A3B8">Rollout Plan</text>
<text x="690" y="630" fill="#94A3B8">Capability Playbook</text>
</g>
</g>
<!-- Card 5 -->
<g id="card-5">
<g>
<rect x="880" y="370" width="190" height="290" rx="10" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<rect x="880" y="370" width="190" height="5" rx="3" fill="#D97706"/>
<text x="975" y="405" text-anchor="middle"
font-size="15" font-weight="700" fill="#1E293B">Optimization</text>
<line x1="905" y1="425" x2="1045" y2="425" stroke="#F0F0F0" stroke-width="1"/>
<text x="900" y="450" font-size="13" fill="#475569">· Ops Monitoring</text>
<text x="900" y="478" font-size="13" fill="#475569">· Data-Driven Opt.</text>
<text x="900" y="506" font-size="13" fill="#475569">· Tech Upgrades</text>
<text x="900" y="534" font-size="13" fill="#475569">· Scenario Expansion</text>
<line x1="905" y1="560" x2="1045" y2="560" stroke="#F0F0F0" stroke-width="1"/>
<text x="900" y="585" font-size="13" font-weight="600" fill="#D97706">Deliverables:</text>
<text x="900" y="608" fill="#94A3B8">Ops Dashboard</text>
<text x="900" y="630" fill="#94A3B8">Quarterly Report</text>
</g>
</g>
</g>
</g>
<!-- Footer Note -->
<g id="footer" data-pptx-bounds="300 640 680 25">
<text x="640" y="660" text-anchor="middle" fill="#CBD5E1">Phase durations and deliverables can be flexibly adjusted based on actual enterprise situations.</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,145 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Circular Stages Template
Structure: Closed-loop ring of 4-6 stages connected by arrows
Use cases: PDCA, flywheel compounding loops (Attract → Engage → Delight), agile iteration, lifecycle, continuous improvement, any cyclical or self-reinforcing process
Supports: 5-stage cycle (4-6 recommended)
-->
<!-- Background -->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- Title Area -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="60" y="70"
font-size="32" font-weight="700" fill="#0F172A" letter-spacing="-0.5">Continuous Improvement Cycle</text>
<text x="60" y="100"
font-size="16" fill="#64748B">Continuous Improvement Cycle · Five-stage closed-loop drive</text>
</g>
<g id="cycle-frame" data-pptx-bounds="225 132 510 516">
<!-- Center guide ring (dashed) -->
<circle cx="480" cy="410" r="200" fill="none" stroke="#E0E0E0" stroke-width="2" stroke-dasharray="8,4"/>
<circle cx="480" cy="410" r="130" fill="none" stroke="#E0E0E0" stroke-width="1" stroke-dasharray="4,4"/>
<!-- Center text -->
<text x="480" y="398" text-anchor="middle" font-size="24" font-weight="800" fill="#0F172A">Continuous</text>
<text x="480" y="428" text-anchor="middle" font-size="15" fill="#94A3B8">Continuous Loop</text>
<!-- 5 nodes distributed along the ring (center 480,410 r=200) -->
<!-- Positions: Top(480,210), Top-right(670,350), Bottom-right(600,570), Bottom-left(360,570), Top-left(290,350) -->
<!-- Connecting arc arrows (curved path + polygon triangle) -->
<!-- 1→2 -->
<path d="M 510,220 Q 620,240 650,340" stroke="#8B5CF6" stroke-width="3" fill="none" stroke-opacity="0.5"/>
<polygon points="655,340 645,332 648,348" fill="#8B5CF6" fill-opacity="0.6"/>
<!-- 2→3 -->
<path d="M 670,380 Q 680,480 610,555" stroke="#10B981" stroke-width="3" fill="none" stroke-opacity="0.5"/>
<polygon points="605,558 618,552 610,565" fill="#10B981" fill-opacity="0.6"/>
<!-- 3→4 -->
<path d="M 575,580 Q 480,620 375,575" stroke="#F59E0B" stroke-width="3" fill="none" stroke-opacity="0.5"/>
<polygon points="372,572 380,584 368,580" fill="#F59E0B" fill-opacity="0.6"/>
<!-- 4→5 -->
<path d="M 340,555 Q 280,470 285,365" stroke="#F43F5E" stroke-width="3" fill="none" stroke-opacity="0.5"/>
<polygon points="287,360 278,372 294,368" fill="#F43F5E" fill-opacity="0.6"/>
<!-- 5→1 -->
<path d="M 310,340 Q 340,250 460,215" stroke="#3B82F6" stroke-width="3" fill="none" stroke-opacity="0.5"/>
<polygon points="462,213 452,206 454,222" fill="#3B82F6" fill-opacity="0.6"/>
</g>
<g id="nodes" data-pptx-bounds="186 135 588 510">
<g id="node-1">
<!-- Node 1: Research & Analysis (Top) -->
<circle cx="480" cy="210" r="45" fill="#8B5CF6"/>
<text x="480" y="205" text-anchor="middle"
font-size="32" font-weight="700" fill="#FFFFFF">01</text>
<text x="480" y="228" text-anchor="middle"
font-size="13" font-weight="600" fill="#FFFFFF" fill-opacity="0.85">Research</text>
</g>
<g id="node-2">
<!-- Node 2: Solution Design (Top-right) -->
<circle cx="670" cy="355" r="45" fill="#10B981"/>
<text x="670" y="350" text-anchor="middle"
font-size="32" font-weight="700" fill="#FFFFFF">02</text>
<text x="670" y="373" text-anchor="middle"
font-size="13" font-weight="600" fill="#FFFFFF" fill-opacity="0.85">Design</text>
</g>
<g id="node-3">
<!-- Node 3: Execution (Bottom-right) -->
<circle cx="600" cy="570" r="45" fill="#F59E0B"/>
<text x="600" y="565" text-anchor="middle"
font-size="32" font-weight="700" fill="#FFFFFF">03</text>
<text x="600" y="588" text-anchor="middle"
font-size="13" font-weight="600" fill="#FFFFFF" fill-opacity="0.85">Execution</text>
</g>
<g id="node-4">
<!-- Node 4: Evaluation (Bottom-left) -->
<circle cx="360" cy="570" r="45" fill="#F43F5E"/>
<text x="360" y="565" text-anchor="middle"
font-size="32" font-weight="700" fill="#FFFFFF">04</text>
<text x="360" y="588" text-anchor="middle"
font-size="13" font-weight="600" fill="#FFFFFF" fill-opacity="0.85">Evaluation</text>
</g>
<g id="node-5">
<!-- Node 5: Iteration (Top-left) -->
<circle cx="290" cy="355" r="45" fill="#3B82F6"/>
<text x="290" y="350" text-anchor="middle"
font-size="32" font-weight="700" fill="#FFFFFF">05</text>
<text x="290" y="373" text-anchor="middle"
font-size="13" font-weight="600" fill="#FFFFFF" fill-opacity="0.85">Iteration</text>
</g>
</g>
<g id="cards" data-pptx-bounds="724 121 552 493">
<!-- Right Description Panel -->
<g id="card-1">
<g>
<rect x="780" y="150" width="440" height="75" rx="8" fill="#FFFFFF"/>
<rect x="780" y="150" width="5" height="75" rx="3" fill="#8B5CF6"/>
<text x="802" y="180"
font-size="18" font-weight="700" fill="#8B5CF6">01 Research &amp; Analysis</text>
<text x="802" y="206"
font-size="14" fill="#64748B">Conduct research and collect user data</text>
</g>
</g>
<g id="card-2">
<g>
<rect x="780" y="240" width="440" height="75" rx="8" fill="#FFFFFF"/>
<rect x="780" y="240" width="5" height="75" rx="3" fill="#10B981"/>
<text x="802" y="270"
font-size="18" font-weight="700" fill="#10B981">02 Solution Design</text>
<text x="802" y="296"
font-size="14" fill="#64748B">Define clear goals and improvement paths</text>
</g>
</g>
<g id="card-3">
<g>
<rect x="780" y="330" width="440" height="75" rx="8" fill="#FFFFFF"/>
<rect x="780" y="330" width="5" height="75" rx="3" fill="#F59E0B"/>
<text x="802" y="360"
font-size="18" font-weight="700" fill="#D97706">03 Execution</text>
<text x="802" y="386"
font-size="14" fill="#64748B">Implement solutions and track project progress</text>
</g>
</g>
<g id="card-4">
<g>
<rect x="780" y="420" width="440" height="75" rx="8" fill="#FFFFFF"/>
<rect x="780" y="420" width="5" height="75" rx="3" fill="#F43F5E"/>
<text x="802" y="450"
font-size="18" font-weight="700" fill="#F43F5E">04 Evaluation</text>
<text x="802" y="476"
font-size="14" fill="#64748B">Measure outcomes against target objectives</text>
</g>
</g>
<g id="card-5">
<g>
<rect x="780" y="510" width="440" height="75" rx="8" fill="#FFFFFF"/>
<rect x="780" y="510" width="5" height="75" rx="3" fill="#3B82F6"/>
<text x="802" y="540"
font-size="18" font-weight="700" fill="#3B82F6">05 Iteration &amp; Optimization</text>
<text x="802" y="566"
font-size="14" fill="#64748B">Review lessons learned for the next cycle</text>
</g>
</g>
</g>
<!-- Footer Note -->
<g id="source" data-pptx-bounds="40 667 1200 38">
<text x="60" y="690"
fill="#94A3B8">With each completed cycle, overall capability leaps to a higher level</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.4 KiB

@@ -1,182 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Client-Server Flow Template
Usage: Left clients + Right servers, bidirectional arrows for key interactions
Scenarios: Request-response architecture, Frontend-backend flow, C/S protocol stack, API calls
Supports: 2-3 client modules (left) + 2-4 server modules (right) + 3-5 key interactions (middle)
Key convention: Each module card = Name + 1 line description; Each arrow = Action label (e.g. "Login" / "Return JWT"); DO NOT draw arrows without labels
-->
<defs>
<marker id="csArrowRight" markerWidth="10" markerHeight="10" refX="9" refY="5"
orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L10,5 L0,10 Z" fill="#3B82F6"/>
</marker>
<marker id="csArrowLeft" markerWidth="10" markerHeight="10" refX="9" refY="5"
orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L10,5 L0,10 Z" fill="#10B981"/>
</marker>
</defs>
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- Title -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="60" y="58"
font-size="28" font-weight="700" fill="#0F172A">Client-Server Flow</text>
<text x="60" y="84"
font-size="14" fill="#64748B">Client-Server Flow · Left side initiates requests, right side responds hierarchically</text>
</g>
<g id="client-group" data-pptx-bounds="19 98 402 584">
<!-- Left side: Client Group -->
<rect x="60" y="130" width="320" height="520" rx="16" fill="#EFF6FF" stroke="#BFDBFE" stroke-width="1.5"/>
<rect x="60" y="130" width="320" height="44" rx="16" fill="#3B82F6"/>
<rect x="60" y="158" width="320" height="16" fill="#3B82F6"/>
<text x="220" y="160" text-anchor="middle"
font-size="16" font-weight="700" fill="#FFFFFF">CLIENT</text>
<g id="client-1">
<!-- Client Module 1: Web -->
<g>
<rect x="90" y="200" width="260" height="100" rx="10" fill="#FFFFFF" stroke="#D4DFF7" stroke-width="1"/>
</g>
<rect x="90" y="200" width="6" height="100" fill="#3B82F6"/>
<text x="112" y="226"
font-size="18" font-weight="700" fill="#0F172A">Web Browser</text>
<text x="112" y="252"
font-size="13" fill="#475569">React SPA UI &amp; Interaction</text>
<text x="112" y="272"
font-size="13" fill="#475569">HTTPS + JWT Auth</text>
<text x="112" y="290"
font-size="13" fill="#64748B">Protocol: HTTPS / WebSocket</text>
</g>
<g id="client-2">
<!-- Client Module 2: Mobile -->
<g>
<rect x="90" y="320" width="260" height="100" rx="10" fill="#FFFFFF" stroke="#D4DFF7" stroke-width="1"/>
</g>
<rect x="90" y="320" width="6" height="100" fill="#3B82F6"/>
<text x="112" y="346"
font-size="18" font-weight="700" fill="#0F172A">Mobile App</text>
<text x="112" y="372"
font-size="13" fill="#475569">iOS/Android Native Client</text>
<text x="112" y="392"
font-size="13" fill="#475569">Offline Cache Supported</text>
<text x="112" y="410"
font-size="13" fill="#64748B">Protocol: HTTP/2 / Push</text>
</g>
<g id="client-3">
<!-- Client Module 3: Third-party -->
<g>
<rect x="90" y="440" width="260" height="100" rx="10" fill="#FFFFFF" stroke="#D4DFF7" stroke-width="1"/>
</g>
<rect x="90" y="440" width="6" height="100" fill="#3B82F6"/>
<text x="112" y="466"
font-size="18" font-weight="700" fill="#0F172A">3rd Party Client</text>
<text x="112" y="492"
font-size="13" fill="#475569">Open API Access</text>
<text x="112" y="512"
font-size="13" fill="#475569">OAuth2 / API Key Auth</text>
<text x="112" y="530"
font-size="13" fill="#64748B">Protocol: HTTPS / Webhook</text>
</g>
</g>
<g id="server-group" data-pptx-bounds="859 98 402 584">
<!-- Right side: Server Group -->
<rect x="900" y="130" width="320" height="520" rx="16" fill="#ECFDF5" stroke="#A7F3D0" stroke-width="1.5"/>
<rect x="900" y="130" width="320" height="44" rx="16" fill="#10B981"/>
<rect x="900" y="158" width="320" height="16" fill="#10B981"/>
<text x="1060" y="160" text-anchor="middle"
font-size="16" font-weight="700" fill="#FFFFFF">SERVER</text>
<g id="server-1">
<!-- Server Module 1: Gateway -->
<g>
<rect x="930" y="200" width="260" height="100" rx="10" fill="#FFFFFF" stroke="#CDEAD3" stroke-width="1"/>
</g>
<rect x="930" y="200" width="6" height="100" fill="#10B981"/>
<text x="952" y="226"
font-size="18" font-weight="700" fill="#0F172A">API Gateway</text>
<text x="952" y="252"
font-size="13" fill="#475569">Auth &amp; Rate Limiting</text>
<text x="952" y="272"
font-size="13" fill="#475569">Routing &amp; Tracing</text>
<text x="952" y="290"
font-size="13" fill="#64748B">Kong / Envoy</text>
</g>
<g id="server-2">
<!-- Server Module 2: Services -->
<g>
<rect x="930" y="320" width="260" height="100" rx="10" fill="#FFFFFF" stroke="#CDEAD3" stroke-width="1"/>
</g>
<rect x="930" y="320" width="6" height="100" fill="#10B981"/>
<text x="952" y="346"
font-size="18" font-weight="700" fill="#0F172A">Microservices</text>
<text x="952" y="372"
font-size="13" fill="#475569">Domain Service Cluster</text>
<text x="952" y="392"
font-size="13" fill="#475569">gRPC Internal Comm.</text>
<text x="952" y="410"
font-size="13" fill="#64748B">Go / Java / Node</text>
</g>
<g id="server-3">
<!-- Server Module 3: Data -->
<g>
<rect x="930" y="440" width="260" height="100" rx="10" fill="#FFFFFF" stroke="#CDEAD3" stroke-width="1"/>
</g>
<rect x="930" y="440" width="6" height="100" fill="#10B981"/>
<text x="952" y="466"
font-size="18" font-weight="700" fill="#0F172A">Database Layer</text>
<text x="952" y="492"
font-size="13" fill="#475569">Primary/Replica &amp; Cache</text>
<text x="952" y="512"
font-size="13" fill="#475569">Hot/Cold Data Tiering</text>
<text x="952" y="530"
font-size="13" fill="#64748B">PostgreSQL / Redis / S3</text>
</g>
</g>
<g id="interactions" data-pptx-bounds="312 121 682 433">
<!-- Middle Communication Channel Labels -->
<text x="640" y="160" text-anchor="middle"
font-size="14" font-weight="700" fill="#0F172A">Key Interactions</text>
<text x="640" y="180" text-anchor="middle"
font-size="13" fill="#64748B">Key Interactions</text>
<g id="interaction-1">
<!-- Interaction 1: Login Request -->
<line x1="380" y1="250" x2="926" y2="250" stroke="#3B82F6" stroke-width="2.5" marker-end="url(#csArrowRight)"/>
<rect x="500" y="216" width="280" height="26" rx="11" fill="#FFFFFF" stroke="#3B82F6" stroke-width="1"/>
<text x="640" y="234" text-anchor="middle"
font-size="14" font-weight="700" fill="#3B82F6">① Login Request · POST /auth/login</text>
</g>
<g id="interaction-2">
<!-- Interaction 2: Return Token -->
<line x1="926" y1="290" x2="384" y2="290" stroke="#10B981" stroke-width="2.5" marker-end="url(#csArrowLeft)"/>
<rect x="500" y="302" width="280" height="26" rx="11" fill="#FFFFFF" stroke="#10B981" stroke-width="1"/>
<text x="640" y="318" text-anchor="middle"
font-size="14" font-weight="700" fill="#10B981">② Return JWT Token · 2xx Response</text>
</g>
<g id="interaction-3">
<!-- Interaction 3: Business Call -->
<line x1="380" y1="370" x2="926" y2="370" stroke="#3B82F6" stroke-width="2.5" marker-end="url(#csArrowRight)"/>
<rect x="500" y="336" width="280" height="26" rx="11" fill="#FFFFFF" stroke="#3B82F6" stroke-width="1"/>
<text x="640" y="354" text-anchor="middle"
font-size="14" font-weight="700" fill="#3B82F6">③ Business Call · GET /api/resources</text>
</g>
<g id="interaction-4">
<!-- Interaction 4: Data Return -->
<line x1="926" y1="410" x2="384" y2="410" stroke="#10B981" stroke-width="2.5" marker-end="url(#csArrowLeft)"/>
<rect x="500" y="422" width="280" height="26" rx="11" fill="#FFFFFF" stroke="#10B981" stroke-width="1"/>
<text x="640" y="438" text-anchor="middle"
font-size="14" font-weight="700" fill="#10B981">④ Return Data · JSON Payload</text>
</g>
<g id="interaction-5">
<!-- Interaction 5: Push -->
<line x1="926" y1="490" x2="384" y2="490" stroke="#10B981" stroke-width="2.5" marker-end="url(#csArrowLeft)"/>
<rect x="500" y="502" width="280" height="26" rx="11" fill="#FFFFFF" stroke="#10B981" stroke-width="1"/>
<text x="640" y="518" text-anchor="middle"
font-size="14" font-weight="700" fill="#10B981">⑤ Real-time Push · WebSocket / SSE</text>
</g>
</g>
<!-- Footer Note -->
<g id="source" data-pptx-bounds="40 660 1200 40">
<text x="60" y="685"
font-size="14" fill="#64748B">Blue arrow = Client request; Green arrow = Server response; Every arrow MUST have an action label, do not draw bare lines</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

@@ -1,119 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
<!--
Multi-Column Comparison / Pricing Table Template
Usage: Service tier comparison, pricing plans, membership levels
Scenarios: SaaS pricing page, product version comparison, service tiers
Supports: 3 columns (2-4 recommended)
-->
<rect width="1280" height="720" fill="#F1F5F9"/>
<!-- Title -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="640" y="52" text-anchor="middle"
font-size="34" font-weight="700" fill="#0F172A"><tspan>Choose the Right Plan</tspan></text>
<text x="640" y="78" text-anchor="middle"
font-size="14" fill="#64748B"><tspan>Pricing Plans · Flexible plans to meet different scale needs</tspan></text>
</g>
<!-- Basic Plan (Left) -->
<g id="plan-basic" data-pptx-bounds="36 78 428 624">
<g>
<rect x="80" y="110" width="340" height="560" rx="16" fill="#FFFFFF"/>
<!-- Top-rounded Header (圆顶角分区头) -->
<path d="M 96 110 h 308 a 16 16 0 0 1 16 16 v 104 h -340 v -104 a 16 16 0 0 1 16 -16 Z" fill="#3B82F6" fill-opacity="0.06"/>
<text x="250" y="152" text-anchor="middle"
font-size="24" font-weight="700" fill="#3B82F6"><tspan>Basic</tspan></text>
<text x="250" y="175" text-anchor="middle"
font-size="15" fill="#94A3B8"><tspan>For individuals &amp; small teams</tspan></text>
<text x="250" y="210" text-anchor="middle">
<tspan font-size="42" font-weight="800" fill="#0F172A">$15</tspan>
<tspan font-size="14" fill="#94A3B8"> /mo</tspan>
</text>
<!-- Feature List -->
<line x1="110" y1="240" x2="390" y2="240" stroke="#F0F0F0" stroke-width="1"/>
<text x="118" y="275" text-anchor="start" font-size="15"><tspan fill="#3B82F6" font-weight="700"></tspan><tspan x="142" fill="#334155">5 Project Spaces</tspan></text>
<text x="118" y="310" text-anchor="start" font-size="15"><tspan fill="#3B82F6" font-weight="700"></tspan><tspan x="142" fill="#334155">10GB Storage</tspan></text>
<text x="118" y="345" text-anchor="start" font-size="15"><tspan fill="#3B82F6" font-weight="700"></tspan><tspan x="142" fill="#334155">Basic Reporting</tspan></text>
<text x="118" y="380" text-anchor="start" font-size="15"><tspan fill="#3B82F6" font-weight="700"></tspan><tspan x="142" fill="#334155">Email Support</tspan></text>
<text x="118" y="415" text-anchor="start" font-size="15"><tspan fill="#CBD5E1" font-weight="700"></tspan><tspan x="142" fill="#CBD5E1">API Integration</tspan></text>
<text x="118" y="450" text-anchor="start" font-size="15"><tspan fill="#CBD5E1" font-weight="700"></tspan><tspan x="142" fill="#CBD5E1">Custom Workflows</tspan></text>
<text x="118" y="485" text-anchor="start" font-size="15"><tspan fill="#CBD5E1" font-weight="700"></tspan><tspan x="142" fill="#CBD5E1">Dedicated Account Manager</tspan></text>
<text x="118" y="520" text-anchor="start" font-size="15"><tspan fill="#CBD5E1" font-weight="700"></tspan><tspan x="142" fill="#CBD5E1">SLA Guarantee</tspan></text>
<!-- Button -->
<rect x="150" y="560" width="200" height="44" rx="22" fill="#FFFFFF" stroke="#3B82F6" stroke-width="2"/>
<text x="250" y="587" text-anchor="middle"
font-size="14" font-weight="700" fill="#3B82F6"><tspan>Get Started</tspan></text>
</g>
</g>
<!-- Pro Plan (Middle, Recommended Highlight) -->
<g id="plan-pro" data-pptx-bounds="426 50 428 667">
<g>
<rect x="470" y="95" width="340" height="590" rx="16" fill="#FFFFFF"/>
<!-- Top-rounded Header (圆顶角分区头) -->
<path d="M 486 95 h 308 a 16 16 0 0 1 16 16 v 114 h -340 v -114 a 16 16 0 0 1 16 -16 Z" fill="#8B5CF6"/>
<!-- Recommended Tag -->
<rect x="600" y="82" width="80" height="26" rx="13" fill="#F59E0B"/>
<text x="640" y="99" text-anchor="middle"
font-size="12" font-weight="700" fill="#FFFFFF"><tspan>Most Popular</tspan></text>
<text x="640" y="142" text-anchor="middle"
font-size="24" font-weight="700" fill="#FFFFFF"><tspan>Pro</tspan></text>
<text x="640" y="165" text-anchor="middle"
font-size="15" fill="#FFFFFF" fill-opacity="0.85"><tspan>For growing businesses</tspan></text>
<text x="640" y="205" text-anchor="middle">
<tspan font-size="42" font-weight="800" fill="#0F172A">$49</tspan>
<tspan font-size="14" fill="#94A3B8"> /mo</tspan>
</text>
<line x1="500" y1="235" x2="780" y2="235" stroke="#F0F0F0" stroke-width="1"/>
<text x="508" y="270" text-anchor="start" font-size="15"><tspan fill="#8B5CF6" font-weight="700"></tspan><tspan x="532" fill="#334155">Unlimited Projects</tspan></text>
<text x="508" y="305" text-anchor="start" font-size="15"><tspan fill="#8B5CF6" font-weight="700"></tspan><tspan x="532" fill="#334155">100GB Storage</tspan></text>
<text x="508" y="340" text-anchor="start" font-size="15"><tspan fill="#8B5CF6" font-weight="700"></tspan><tspan x="532" fill="#334155">Advanced Reports + Dashboard</tspan></text>
<text x="508" y="375" text-anchor="start" font-size="15"><tspan fill="#8B5CF6" font-weight="700"></tspan><tspan x="532" fill="#334155">Priority Ticket Support</tspan></text>
<text x="508" y="410" text-anchor="start" font-size="15"><tspan fill="#8B5CF6" font-weight="700"></tspan><tspan x="532" fill="#334155">API Integration</tspan></text>
<text x="508" y="445" text-anchor="start" font-size="15"><tspan fill="#8B5CF6" font-weight="700"></tspan><tspan x="532" fill="#334155">Custom Workflows</tspan></text>
<text x="508" y="480" text-anchor="start" font-size="15"><tspan fill="#CBD5E1" font-weight="700"></tspan><tspan x="532" fill="#CBD5E1">Dedicated Account Manager</tspan></text>
<text x="508" y="515" text-anchor="start" font-size="15"><tspan fill="#CBD5E1" font-weight="700"></tspan><tspan x="532" fill="#CBD5E1">SLA Guarantee</tspan></text>
<rect x="540" y="555" width="200" height="44" rx="22" fill="#8B5CF6"/>
<text x="640" y="582" text-anchor="middle"
font-size="14" font-weight="700" fill="#FFFFFF"><tspan>Upgrade Now</tspan></text>
</g>
</g>
<!-- Enterprise Plan (Right) -->
<g id="plan-enterprise" data-pptx-bounds="816 78 428 624">
<g>
<rect x="860" y="110" width="340" height="560" rx="16" fill="#FFFFFF"/>
<!-- Top-rounded Header (圆顶角分区头) -->
<path d="M 876 110 h 308 a 16 16 0 0 1 16 16 v 104 h -340 v -104 a 16 16 0 0 1 16 -16 Z" fill="#0F172A" fill-opacity="0.04"/>
<text x="1030" y="152" text-anchor="middle"
font-size="24" font-weight="700" fill="#0F172A"><tspan>Enterprise</tspan></text>
<text x="1030" y="175" text-anchor="middle"
font-size="15" fill="#94A3B8"><tspan>For large organizations</tspan></text>
<text x="1030" y="210" text-anchor="middle"
font-size="36" font-weight="800" fill="#0F172A"><tspan>Contact Sales</tspan></text>
<line x1="890" y1="240" x2="1170" y2="240" stroke="#F0F0F0" stroke-width="1"/>
<text x="898" y="275" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">Unlimited Projects</tspan></text>
<text x="898" y="310" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">Unlimited Storage</tspan></text>
<text x="898" y="345" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">All Reports + BI Analytics</tspan></text>
<text x="898" y="380" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">24/7 Phone Support</tspan></text>
<text x="898" y="415" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">API Integration + Webhook</tspan></text>
<text x="898" y="450" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">Custom Workflows</tspan></text>
<text x="898" y="485" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">Dedicated Account Manager</tspan></text>
<text x="898" y="520" text-anchor="start" font-size="15"><tspan fill="#0F172A" font-weight="700"></tspan><tspan x="922" fill="#334155">99.9% SLA Guarantee</tspan></text>
<rect x="930" y="560" width="200" height="44" rx="22" fill="#FFFFFF" stroke="#0F172A" stroke-width="2"/>
<text x="1030" y="587" text-anchor="middle"
font-size="14" font-weight="700" fill="#0F172A"><tspan>Contact Us</tspan></text>
</g>
</g>
<!-- Footer -->
<g id="source" data-pptx-bounds="40 687 1200 33">
<text x="640" y="710" text-anchor="middle"
font-size="12" fill="#CBD5E1"><tspan>All plans include a 14-day free trial · Upgrade or downgrade anytime</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

@@ -1,143 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Multi-dimensional Feature Comparison Table Template
Usage: Multi-plan/product horizontal comparison, feature matrix
Scenarios: Product selection, competitor comparison, service tier comparison
Supports: 3 columns x 6 rows (2-4 columns, 4-8 rows recommended)
-->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- Title -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="640" y="55" text-anchor="middle"
font-size="30" font-weight="700" fill="#0F172A">Solution Comparison Assessment</text>
<text x="640" y="82" text-anchor="middle"
font-size="14" fill="#64748B">Comparison Matrix · Horizontal comparison of core metrics across three solutions</text>
</g>
<g id="row-header" data-pptx-bounds="0 85 1280 84">
<!-- Header Row -->
<rect x="60" y="105" width="240" height="55" rx="8" fill="#F1F5F9"/>
<text x="180" y="139" text-anchor="middle"
font-size="15" font-weight="700" fill="#475569">Evaluation Dimensions</text>
<!-- Plan A Column Header -->
<rect x="320" y="105" width="280" height="55" rx="8" fill="#64748B"/>
<text x="460" y="132" text-anchor="middle"
font-size="18" font-weight="700" fill="#FFFFFF">Plan A</text>
<text x="460" y="150" text-anchor="middle"
fill="#FFFFFF" fill-opacity="0.8">Self-built On-premise</text>
<!-- Plan B Column Header (Recommended) -->
<rect x="620" y="100" width="280" height="62" rx="8" fill="#10B981"/>
<rect x="750" y="92" width="56" height="20" rx="10" fill="#F59E0B"/>
<text x="778" y="105" text-anchor="middle"
font-weight="700" fill="#FFFFFF">Recommended</text>
<text x="760" y="131" text-anchor="middle"
font-size="18" font-weight="700" fill="#FFFFFF">Plan B</text>
<text x="760" y="150" text-anchor="middle"
fill="#FFFFFF" fill-opacity="0.8">Hybrid Cloud Arch</text>
<!-- Plan C Column Header -->
<rect x="920" y="105" width="280" height="55" rx="8" fill="#3B82F6"/>
<text x="1060" y="132" text-anchor="middle"
font-size="18" font-weight="700" fill="#FFFFFF">Plan C</text>
<text x="1060" y="150" text-anchor="middle"
fill="#FFFFFF" fill-opacity="0.8">Pure SaaS Subscription</text>
</g>
<!-- Data Rows -->
<g id="row-1" data-pptx-bounds="0 168 1280 89">
<!-- Row 1: Deployment Cost -->
<rect x="60" y="175" width="240" height="75" rx="6" fill="#F1F5F9"/>
<text x="180" y="218" text-anchor="middle"
font-size="15" font-weight="600" fill="#334155">Deployment Cost</text>
<rect x="320" y="175" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="460" y="210" text-anchor="middle"
font-size="22" font-weight="700" fill="#0F172A">$500K</text>
<text x="460" y="232" text-anchor="middle"
fill="#F43F5E">High</text>
<rect x="620" y="175" width="280" height="75" rx="6" fill="#10B981" fill-opacity="0.05" stroke="#10B981" stroke-width="1.5"/>
<text x="760" y="210" text-anchor="middle"
font-size="22" font-weight="700" fill="#0F172A">$180K</text>
<text x="760" y="232" text-anchor="middle"
fill="#10B981">Optimal</text>
<rect x="920" y="175" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="1060" y="210" text-anchor="middle"
font-size="22" font-weight="700" fill="#0F172A">$80K/yr</text>
<text x="1060" y="232" text-anchor="middle"
fill="#F59E0B">Medium</text>
</g>
<g id="row-2" data-pptx-bounds="0 253 1280 89">
<!-- Row 2: Data Security -->
<rect x="60" y="260" width="240" height="75" rx="6" fill="#F1F5F9"/>
<text x="180" y="303" text-anchor="middle"
font-size="15" font-weight="600" fill="#334155">Data Security</text>
<rect x="320" y="260" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="460" y="303" text-anchor="middle"
font-size="16" font-weight="600" fill="#10B981">★★★★★</text>
<rect x="620" y="260" width="280" height="75" rx="6" fill="#10B981" fill-opacity="0.05" stroke="#10B981" stroke-width="1.5"/>
<text x="760" y="303" text-anchor="middle"
font-size="16" font-weight="600" fill="#10B981">★★★★☆</text>
<rect x="920" y="260" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="1060" y="303" text-anchor="middle"
font-size="16" font-weight="600" fill="#F43F5E">★★★☆☆</text>
</g>
<g id="row-3" data-pptx-bounds="0 338 1280 89">
<!-- Row 3: Scalability -->
<rect x="60" y="345" width="240" height="75" rx="6" fill="#F1F5F9"/>
<text x="180" y="388" text-anchor="middle"
font-size="15" font-weight="600" fill="#334155">Scalability</text>
<rect x="320" y="345" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="460" y="388" text-anchor="middle"
font-size="16" font-weight="600" fill="#F43F5E">★★☆☆☆</text>
<rect x="620" y="345" width="280" height="75" rx="6" fill="#10B981" fill-opacity="0.05" stroke="#10B981" stroke-width="1.5"/>
<text x="760" y="388" text-anchor="middle"
font-size="16" font-weight="600" fill="#10B981">★★★★★</text>
<rect x="920" y="345" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="1060" y="388" text-anchor="middle"
font-size="16" font-weight="600" fill="#10B981">★★★★★</text>
</g>
<g id="row-4" data-pptx-bounds="0 423 1280 89">
<!-- Row 4: Time to Market -->
<rect x="60" y="430" width="240" height="75" rx="6" fill="#F1F5F9"/>
<text x="180" y="473" text-anchor="middle"
font-size="15" font-weight="600" fill="#334155">Time to Market</text>
<rect x="320" y="430" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="460" y="473" text-anchor="middle"
font-size="16" font-weight="700" fill="#0F172A">6-12 Months</text>
<rect x="620" y="430" width="280" height="75" rx="6" fill="#10B981" fill-opacity="0.05" stroke="#10B981" stroke-width="1.5"/>
<text x="760" y="473" text-anchor="middle"
font-size="16" font-weight="700" fill="#0F172A">3-5 Months</text>
<rect x="920" y="430" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="1060" y="473" text-anchor="middle"
font-size="16" font-weight="700" fill="#0F172A">1-2 Months</text>
</g>
<g id="row-5" data-pptx-bounds="0 508 1280 89">
<!-- Row 5: Customization -->
<rect x="60" y="515" width="240" height="75" rx="6" fill="#F1F5F9"/>
<text x="180" y="558" text-anchor="middle"
font-size="15" font-weight="600" fill="#334155">Customization Level</text>
<rect x="320" y="515" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="460" y="558" text-anchor="middle"
font-size="16" font-weight="600" fill="#10B981">★★★★★</text>
<rect x="620" y="515" width="280" height="75" rx="6" fill="#10B981" fill-opacity="0.05" stroke="#10B981" stroke-width="1.5"/>
<text x="760" y="558" text-anchor="middle"
font-size="16" font-weight="600" fill="#10B981">★★★★☆</text>
<rect x="920" y="515" width="280" height="75" rx="6" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="1"/>
<text x="1060" y="558" text-anchor="middle"
font-size="16" font-weight="600" fill="#F43F5E">★★☆☆☆</text>
</g>
<g id="row-total" data-pptx-bounds="0 603 1280 84">
<!-- Footer Total Score -->
<rect x="60" y="610" width="240" height="70" rx="8" fill="#F1F5F9"/>
<text x="180" y="650" text-anchor="middle"
font-size="15" font-weight="700" fill="#334155">Overall Score</text>
<rect x="320" y="610" width="280" height="70" rx="8" fill="#64748B" fill-opacity="0.08"/>
<text x="460" y="652" text-anchor="middle"
font-size="28" font-weight="800" fill="#64748B">72</text>
<rect x="620" y="610" width="280" height="70" rx="8" fill="#10B981" fill-opacity="0.1"/>
<text x="760" y="652" text-anchor="middle"
font-size="28" font-weight="800" fill="#10B981">91</text>
<rect x="920" y="610" width="280" height="70" rx="8" fill="#3B82F6" fill-opacity="0.08"/>
<text x="1060" y="652" text-anchor="middle"
font-size="28" font-weight="800" fill="#3B82F6">68</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.5 KiB

@@ -1,232 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="12">
<!--
Concentric Circles / Bullseye Chart Template
Structure: 3-5 nested rings expressing priority from core (deepest color) outward to periphery (lightest), paired with descriptor cards
Use cases: Target focus analysis, stakeholder circles, core-expansion-ecosystem priority layers, onion model, sphere-of-influence
Design Principles: Single-hue color progression (deeper at core = higher priority), tick-marked outer rim for depth, resource % as hero number on each card
-->
<defs>
<radialGradient id="coreGrad" cx="35%" cy="30%" r="80%">
<stop offset="0%" stop-color="#6366F1"/>
<stop offset="100%" stop-color="#3730A3"/>
</radialGradient>
</defs>
<!-- Background -->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="60 42 1180 60">
<rect x="60" y="42" width="4" height="60" rx="2" fill="#4338CA"/>
<text x="78" y="68" font-size="30" font-weight="800" fill="#0F172A">
<tspan>Product Strategy Priority Layers</tspan>
</text>
<text x="78" y="94" font-size="13" font-weight="700" fill="#64748B" letter-spacing="2">
<tspan>FY24 RESOURCE ALLOCATION FRAMEWORK · 4 PRIORITY TIERS</tspan>
</text>
<!-- Header right chip -->
<g transform="translate(1010, 50)">
<rect x="0" y="0" width="200" height="48" rx="14" fill="#EEF2FF"/>
<text x="22" y="32" font-size="22" font-weight="800" fill="#4338CA">
<tspan>100%</tspan>
</text>
<text x="92" y="22" font-weight="800" fill="#312E81" letter-spacing="1.5">
<tspan>ALLOCATED</tspan>
</text>
<text x="92" y="38" font-weight="600" fill="#6366F1">
<tspan>4 tiers · core → edge</tspan>
</text>
</g>
</g>
<!-- ==================== Tick-marked outer rim (depth) ==================== -->
<g id="ticks" data-pptx-bounds="108 78 664 619">
<!-- 12 tick marks at 30° intervals on radius 320, stroke-opacity per-element (no <g opacity>, see §6.2) -->
<line x1="440" y1="90" x2="440" y2="78" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="600" y1="133" x2="606" y2="123" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="717" y1="250" x2="727" y2="244" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="760" y1="410" x2="772" y2="410" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="717" y1="570" x2="727" y2="576" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="600" y1="687" x2="606" y2="697" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="280" y1="687" x2="274" y2="697" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="163" y1="570" x2="153" y2="576" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="120" y1="410" x2="108" y2="410" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="163" y1="250" x2="153" y2="244" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
<line x1="280" y1="133" x2="274" y2="123" stroke="#A5B4FC" stroke-width="2" stroke-opacity="0.4"/>
</g>
<!-- ==================== Concentric Bullseye (center 440, 410; single-hue Indigo progression) ==================== -->
<g id="bullseye" data-pptx-bounds="140 110 600 600">
<!-- Layer 4: Ecosystem (lightest, outermost) -->
<circle cx="440" cy="410" r="300" fill="#EEF2FF" stroke="#C7D2FE" stroke-width="2"/>
<!-- Layer 3: Growth Engine -->
<circle cx="440" cy="410" r="220" fill="#E0E7FF" stroke="#A5B4FC" stroke-width="2"/>
<!-- Layer 2: Core Support -->
<circle cx="440" cy="410" r="140" fill="#C7D2FE" stroke="#818CF8" stroke-width="2"/>
<!-- Layer 1: Core (deepest focal layer) -->
<circle cx="440" cy="410" r="68" fill="url(#coreGrad)"/>
<text x="440" y="403" font-weight="800" fill="#C7D2FE" text-anchor="middle" letter-spacing="2">
<tspan>TIER 01</tspan>
</text>
<text x="440" y="428" font-size="18" font-weight="800" fill="#FFFFFF" text-anchor="middle">
<tspan>Core</tspan>
</text>
<text x="440" y="448" font-size="18" font-weight="800" fill="#FFFFFF" text-anchor="middle">
<tspan>Product</tspan>
</text>
</g>
<!-- ==================== Ring Labels (at 12 o'clock, with leader dot) ==================== -->
<g id="ring-labels" data-pptx-bounds="330 104 220 198">
<!-- Layer 4 (outermost) at top -->
<circle cx="440" cy="110" r="4" fill="#4338CA"/>
<text x="440" y="138" font-weight="800" fill="#4338CA" text-anchor="middle" letter-spacing="2">
<tspan>ECOSYSTEM</tspan>
</text>
<!-- Layer 3 -->
<circle cx="440" cy="190" r="4" fill="#4338CA"/>
<text x="440" y="218" font-weight="800" fill="#4338CA" text-anchor="middle" letter-spacing="2">
<tspan>GROWTH ENGINE</tspan>
</text>
<!-- Layer 2 -->
<circle cx="440" cy="270" r="4" fill="#FFFFFF"/>
<text x="440" y="298" font-weight="800" fill="#312E81" text-anchor="middle" letter-spacing="2">
<tspan>CORE SUPPORT</tspan>
</text>
</g>
<!-- ==================== Right Cards: 4 priority tiers ==================== -->
<g id="cards" data-pptx-bounds="810 130 410 508">
<!-- Card 1: Tier 01 (Core) — focal -->
<g id="card-1" transform="translate(810, 130)">
<rect x="0" y="0" width="410" height="118" rx="12" fill="#FFFFFF" stroke="#E0E7FF" stroke-width="1.5"/>
<rect x="0" y="0" width="6" height="118" rx="3" fill="#4338CA"/>
<!-- Tier badge -->
<circle cx="40" cy="32" r="16" fill="#4338CA"/>
<text x="40" y="38" font-size="14" font-weight="800" fill="#FFFFFF" text-anchor="middle">
<tspan>01</tspan>
</text>
<text x="64" y="28" font-weight="800" fill="#4338CA" letter-spacing="2">
<tspan>HIGHEST PRIORITY · CORE</tspan>
</text>
<text x="64" y="46" font-size="16" font-weight="800" fill="#0F172A">
<tspan>Core Product</tspan>
</text>
<text x="22" y="70" font-size="13" font-weight="500" fill="#64748B">
<tspan>UX features driving retention </tspan><tspan x="22" dy="16">&amp; conversion</tspan>
</text>
<text x="22" y="108" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>BI-WEEKLY ITERATION</tspan>
</text>
<!-- Resource % hero -->
<text x="388" y="60" font-size="32" font-weight="800" fill="#4338CA" text-anchor="end">
<tspan>60%</tspan>
</text>
<text x="388" y="78" font-weight="800" fill="#64748B" text-anchor="end" letter-spacing="1.5">
<tspan>OF RESOURCES</tspan>
</text>
<!-- Mini bar -->
<rect x="280" y="92" width="110" height="6" rx="3" fill="#E0E7FF"/>
<rect x="280" y="92" width="66" height="6" rx="3" fill="#4338CA"/>
</g>
<!-- Card 2: Tier 02 (Core Support) -->
<g id="card-2" transform="translate(810, 260)">
<rect x="0" y="0" width="410" height="118" rx="12" fill="#FFFFFF" stroke="#E0E7FF" stroke-width="1.5"/>
<rect x="0" y="0" width="6" height="118" rx="3" fill="#6366F1"/>
<circle cx="40" cy="32" r="16" fill="#6366F1"/>
<text x="40" y="38" font-size="14" font-weight="800" fill="#FFFFFF" text-anchor="middle">
<tspan>02</tspan>
</text>
<text x="64" y="28" font-weight="800" fill="#6366F1" letter-spacing="2">
<tspan>HIGH PRIORITY · CORE SUPPORT</tspan>
</text>
<text x="64" y="46" font-size="16" font-weight="800" fill="#0F172A">
<tspan>Foundational Capabilities</tspan>
</text>
<text x="22" y="70" font-size="13" font-weight="500" fill="#64748B">
<tspan>Security, performance, </tspan><tspan x="22" dy="16">infrastructure</tspan>
</text>
<text x="22" y="108" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>MONTHLY ITERATION</tspan>
</text>
<text x="388" y="60" font-size="32" font-weight="800" fill="#6366F1" text-anchor="end">
<tspan>25%</tspan>
</text>
<text x="388" y="78" font-weight="800" fill="#64748B" text-anchor="end" letter-spacing="1.5">
<tspan>OF RESOURCES</tspan>
</text>
<rect x="280" y="92" width="110" height="6" rx="3" fill="#E0E7FF"/>
<rect x="280" y="92" width="27" height="6" rx="3" fill="#6366F1"/>
</g>
<!-- Card 3: Tier 03 (Growth Engine) -->
<g id="card-3" transform="translate(810, 390)">
<rect x="0" y="0" width="410" height="118" rx="12" fill="#FFFFFF" stroke="#E0E7FF" stroke-width="1.5"/>
<rect x="0" y="0" width="6" height="118" rx="3" fill="#818CF8"/>
<circle cx="40" cy="32" r="16" fill="#818CF8"/>
<text x="40" y="38" font-size="14" font-weight="800" fill="#FFFFFF" text-anchor="middle">
<tspan>03</tspan>
</text>
<text x="64" y="28" font-weight="800" fill="#6366F1" letter-spacing="2">
<tspan>MEDIUM PRIORITY · GROWTH ENGINE</tspan>
</text>
<text x="64" y="46" font-size="16" font-weight="800" fill="#0F172A">
<tspan>Acquisition &amp; Monetization</tspan>
</text>
<text x="22" y="70" font-size="13" font-weight="500" fill="#64748B">
<tspan>Channel ops, paywall, </tspan><tspan x="22" dy="16">conversion funnels</tspan>
</text>
<text x="22" y="108" font-weight="700" fill="#94A3B8" letter-spacing="1">
<tspan>QUARTERLY PLANNING</tspan>
</text>
<text x="388" y="60" font-size="32" font-weight="800" fill="#818CF8" text-anchor="end">
<tspan>10%</tspan>
</text>
<text x="388" y="78" font-weight="800" fill="#64748B" text-anchor="end" letter-spacing="1.5">
<tspan>OF RESOURCES</tspan>
</text>
<rect x="280" y="92" width="110" height="6" rx="3" fill="#E0E7FF"/>
<rect x="280" y="92" width="11" height="6" rx="3" fill="#818CF8"/>
</g>
<!-- Card 4: Tier 04 (Ecosystem) — exploratory -->
<g id="card-4" transform="translate(810, 520)">
<rect x="0" y="0" width="410" height="118" rx="12" fill="#FFFFFF" stroke="#E0E7FF" stroke-width="1.5" stroke-dasharray="6,4"/>
<rect x="0" y="0" width="6" height="118" rx="3" fill="#C7D2FE"/>
<circle cx="40" cy="32" r="16" fill="#FFFFFF" stroke="#C7D2FE" stroke-width="2.5"/>
<text x="40" y="38" font-size="14" font-weight="800" fill="#A5B4FC" text-anchor="middle">
<tspan>04</tspan>
</text>
<text x="64" y="28" font-weight="800" fill="#A5B4FC" letter-spacing="2">
<tspan>EXPLORATORY · ECOSYSTEM</tspan>
</text>
<text x="64" y="46" font-size="16" font-weight="800" fill="#334155">
<tspan>Open Platform &amp; Partnerships</tspan>
</text>
<text x="22" y="70" font-size="13" font-weight="500" fill="#94A3B8">
<tspan>ISV integrations, API </tspan><tspan x="22" dy="16">marketplace</tspan>
</text>
<text x="22" y="108" font-weight="700" fill="#CBD5E1" letter-spacing="1">
<tspan>SEMI-ANNUAL REVIEW</tspan>
</text>
<text x="388" y="60" font-size="32" font-weight="800" fill="#A5B4FC" text-anchor="end">
<tspan>5%</tspan>
</text>
<text x="388" y="78" font-weight="800" fill="#64748B" text-anchor="end" letter-spacing="1.5">
<tspan>OF RESOURCES</tspan>
</text>
<rect x="280" y="92" width="110" height="6" rx="3" fill="#F1F5F9"/>
<rect x="280" y="92" width="6" height="6" rx="3" fill="#A5B4FC"/>
</g>
</g>
<!-- ==================== Footer ==================== -->
<g id="footer" data-pptx-bounds="170 675 540 25">
<text x="440" y="695" font-weight="600" fill="#94A3B8" text-anchor="middle">
<tspan>Closer to the center → higher priority &amp; deeper resource concentration</tspan>
</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -1,121 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="Roboto, -apple-system, BlinkMacSystemFont, sans-serif"
font-size="12">
<!--
Consulting Data Table Template (Visual Integration)
Purpose: High-density data presentation with visual aids, favored by consulting firms
Use cases: Financial reports, regional performance, sales tracking
Design Principles: Left-align text, right-align numbers, weak borders, visual integration
-->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="80" y="70" font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="32" font-weight="800" fill="#0F172A">Global Revenue Performance</text>
<text x="80" y="100" font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="14" font-weight="700" fill="#64748B" letter-spacing="1">Q4 2025 · FINANCIAL DATA SUMMARY REPORT</text>
</g>
<!-- ==================== Table Structure ==================== -->
<g id="tableArea" data-pptx-bounds="0 128 1280 512" transform="translate(80, 150)">
<!-- Table Header (Weak borders, no vertical lines) -->
<line x1="0" y1="35" x2="1120" y2="35" stroke="#CBD5E1" stroke-width="2"/>
<text x="20" y="20" font-size="13" font-weight="800" fill="#64748B" letter-spacing="1">REGION / MARKET</text>
<text x="320" y="20" font-size="13" font-weight="800" fill="#64748B" letter-spacing="1" text-anchor="middle">STATUS</text>
<text x="620" y="20" font-size="13" font-weight="800" fill="#64748B" letter-spacing="1" text-anchor="end">REVENUE (USD)</text>
<text x="820" y="20" font-size="13" font-weight="800" fill="#64748B" letter-spacing="1" text-anchor="end">YOY GROWTH</text>
<text x="880" y="20" font-size="13" font-weight="800" fill="#64748B" letter-spacing="1">MARKET SHARE (VISUAL)</text>
<!-- Data Rows (Alternating faint background for readability, no borders) -->
<!-- Row 1 -->
<rect x="0" y="45" width="1120" height="60" fill="#F8FAFC" rx="6"/>
<!-- Left aligned: Variable length text -->
<text x="20" y="81" font-size="16" font-weight="700" fill="#0F172A">North America</text>
<!-- Center aligned: Uniform text -->
<rect x="280" y="65" width="80" height="24" rx="12" fill="#ECFDF5"/>
<text x="320" y="82" font-size="13" font-weight="700" fill="#059669" text-anchor="middle">Active</text>
<!-- Right aligned: Large numbers -->
<text x="620" y="82" font-size="18" font-weight="600" fill="#0F172A" text-anchor="end">$ 124,500,000</text>
<!-- Right aligned: Percentages -->
<text x="820" y="82" font-size="16" font-weight="700" fill="#10B981" text-anchor="end">+ 12.4%</text>
<!-- Visual Integration: Data Bar -->
<rect x="880" y="70" width="200" height="12" rx="6" fill="#E2E8F0"/>
<rect x="880" y="70" width="160" height="12" rx="6" fill="#2563EB"/>
<text x="1090" y="81" font-size="13" font-weight="700" fill="#64748B">42%</text>
<!-- Row 2 -->
<!-- Left aligned: Variable length text -->
<text x="20" y="141" font-size="16" font-weight="700" fill="#0F172A">Europe &amp; Middle East</text>
<!-- Center aligned: Uniform text -->
<rect x="280" y="125" width="80" height="24" rx="12" fill="#ECFDF5"/>
<text x="320" y="142" font-size="13" font-weight="700" fill="#059669" text-anchor="middle">Active</text>
<!-- Right aligned: Large numbers -->
<text x="620" y="142" font-size="18" font-weight="600" fill="#0F172A" text-anchor="end">$ 89,200,000</text>
<!-- Right aligned: Percentages -->
<text x="820" y="142" font-size="16" font-weight="700" fill="#10B981" text-anchor="end">+ 8.1%</text>
<!-- Visual Integration: Data Bar -->
<rect x="880" y="130" width="200" height="12" rx="6" fill="#E2E8F0"/>
<rect x="880" y="130" width="115" height="12" rx="6" fill="#60A5FA"/>
<text x="1090" y="141" font-size="13" font-weight="700" fill="#64748B">30%</text>
<!-- Row 3 -->
<rect x="0" y="165" width="1120" height="60" fill="#F8FAFC" rx="6"/>
<!-- Left aligned: Variable length text -->
<text x="20" y="201" font-size="16" font-weight="700" fill="#0F172A">Asia Pacific</text>
<!-- Center aligned: Uniform text -->
<rect x="280" y="185" width="80" height="24" rx="12" fill="#FFF1F2"/>
<text x="320" y="202" font-size="13" font-weight="700" fill="#E11D48" text-anchor="middle">Warning</text>
<!-- Right aligned: Large numbers -->
<text x="620" y="202" font-size="18" font-weight="600" fill="#0F172A" text-anchor="end">$ 45,600,000</text>
<!-- Right aligned: Percentages -->
<text x="820" y="202" font-size="16" font-weight="700" fill="#F43F5E" text-anchor="end">- 3.2%</text>
<!-- Visual Integration: Data Bar -->
<rect x="880" y="190" width="200" height="12" rx="6" fill="#E2E8F0"/>
<rect x="880" y="190" width="60" height="12" rx="6" fill="#F43F5E"/>
<text x="1090" y="201" font-size="13" font-weight="700" fill="#64748B">15%</text>
<!-- Row 4 -->
<!-- Left aligned: Variable length text -->
<text x="20" y="261" font-size="16" font-weight="700" fill="#0F172A">Latin America</text>
<!-- Center aligned: Uniform text -->
<rect x="280" y="245" width="80" height="24" rx="12" fill="#FFFBEB"/>
<text x="320" y="262" font-size="13" font-weight="700" fill="#D97706" text-anchor="middle">Pending</text>
<!-- Right aligned: Large numbers -->
<text x="620" y="262" font-size="18" font-weight="600" fill="#0F172A" text-anchor="end">$ 22,400,000</text>
<!-- Right aligned: Percentages -->
<text x="820" y="262" font-size="16" font-weight="700" fill="#10B981" text-anchor="end">+ 15.6%</text>
<!-- Visual Integration: Data Bar -->
<rect x="880" y="250" width="200" height="12" rx="6" fill="#E2E8F0"/>
<rect x="880" y="250" width="30" height="12" rx="6" fill="#F59E0B"/>
<text x="1090" y="261" font-size="13" font-weight="700" fill="#64748B">8%</text>
<!-- Row 5 -->
<rect x="0" y="285" width="1120" height="60" fill="#F8FAFC" rx="6"/>
<!-- Left aligned: Variable length text -->
<text x="20" y="321" font-size="16" font-weight="700" fill="#0F172A">Africa</text>
<!-- Center aligned: Uniform text -->
<rect x="280" y="305" width="80" height="24" rx="12" fill="#F1F5F9"/>
<text x="320" y="322" font-size="13" font-weight="700" fill="#475569" text-anchor="middle">Emerging</text>
<!-- Right aligned: Large numbers -->
<text x="620" y="322" font-size="18" font-weight="600" fill="#0F172A" text-anchor="end">$ 12,100,000</text>
<!-- Right aligned: Percentages -->
<text x="820" y="322" font-size="16" font-weight="700" fill="#10B981" text-anchor="end">+ 22.8%</text>
<!-- Visual Integration: Data Bar -->
<rect x="880" y="310" width="200" height="12" rx="6" fill="#E2E8F0"/>
<rect x="880" y="310" width="20" height="12" rx="6" fill="#94A3B8"/>
<text x="1090" y="321" font-size="13" font-weight="700" fill="#64748B">5%</text>
<!-- ==================== Emphasized Total Row ==================== -->
<!-- Thick separator line to emphasize the bottom block -->
<line x1="0" y1="370" x2="1120" y2="370" stroke="#0F172A" stroke-width="3"/>
<!-- Emphasized background card -->
<g>
<rect x="0" y="390" width="1120" height="70" fill="#0F172A" rx="8"/>
<!-- Accent line -->
<rect x="0" y="390" width="6" height="70" fill="#3B82F6" rx="3"/>
</g>
<text x="20" y="432" font-size="18" font-weight="800" fill="#FFFFFF" letter-spacing="1">GLOBAL TOTAL</text>
<text x="320" y="432" font-size="14" font-weight="600" fill="#94A3B8" text-anchor="middle">5 REGIONS</text>
<!-- Highly emphasized total number -->
<text x="620" y="434" font-size="28" font-weight="800" fill="#FFFFFF" text-anchor="end">$ 293,800,000</text>
<text x="820" y="432" font-size="20" font-weight="800" fill="#34D399" text-anchor="end">+ 8.7%</text>
<rect x="880" y="420" width="200" height="12" rx="6" fill="#1E293B"/>
<rect x="880" y="420" width="200" height="12" rx="6" fill="#2563EB"/>
<text x="1090" y="431" font-size="14" font-weight="800" fill="#FFFFFF">100%</text>
</g>
<!-- ==================== Footer ==================== -->
<g id="source" data-pptx-bounds="40 667 1200 38">
<text x="1200" y="690" text-anchor="end" font-weight="600" fill="#94A3B8">Data Source: Internal Finance System · Generated automatically</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

@@ -1,83 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="Roboto, -apple-system, BlinkMacSystemFont, sans-serif"
font-size="12">
<!--
Feature Matrix / Competitive Checklist Table
Purpose: Capability comparison, competitor benchmarking
Use cases: "Us vs Them", feature availability checklists, compliance matrices
Design Principles: Left-aligned features, centered checkmarks/crosses, alternating rows
-->
<rect width="1280" height="720" fill="#FFFFFF"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="40 15 1200 125">
<text x="80" y="70" font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="32" font-weight="800" fill="#0F172A">Competitive Feature Matrix</text>
<text x="80" y="100" font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="14" font-weight="700" fill="#64748B" letter-spacing="1">CAPABILITY CHECKLIST · OUR PLATFORM VS KEY COMPETITORS</text>
</g>
<!-- ==================== Table Structure ==================== -->
<g id="tableArea" data-pptx-bounds="0 118 1280 594" transform="translate(80, 150)">
<!-- Table Header (Top Line) -->
<line x1="0" y1="40" x2="1120" y2="40" stroke="#CBD5E1" stroke-width="2"/>
<text x="20" y="25" font-size="13" font-weight="800" fill="#64748B" letter-spacing="1">CORE CAPABILITIES</text>
<!-- Column 1: Our Product (Highlighted) -->
<rect x="440" y="0" width="180" height="40" rx="6" fill="#EEF2FF"/>
<text x="530" y="25" font-size="14" font-weight="800" fill="#4338CA" letter-spacing="1" text-anchor="middle">OUR PLATFORM</text>
<!-- Column 2: Competitor A -->
<text x="770" y="25" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="middle">COMPETITOR A</text>
<!-- Column 3: Competitor B -->
<text x="1010" y="25" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="middle">COMPETITOR B</text>
<!-- Vertical highlight background for "Our Platform" spanning all rows -->
<rect x="440" y="45" width="180" height="420" fill="#EEF2FF" fill-opacity="0.4" rx="6"/>
<!-- ==================== Data Rows ==================== -->
<!-- Category 1 -->
<rect x="0" y="55" width="1120" height="35" fill="#F8FAFC"/>
<text x="20" y="78" font-size="15" font-weight="800" fill="#0F172A">1. Security &amp; Compliance</text>
<!-- Row 1.1 -->
<line x1="0" y1="140" x2="1120" y2="140" stroke="#F1F5F9" stroke-width="1.5"/>
<text x="40" y="120" font-size="15" font-weight="600" fill="#334155">End-to-End Encryption (E2EE)</text>
<text x="530" y="122" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="770" y="122" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="1010" y="122" font-family="sans-serif" font-size="18" font-weight="800" fill="#F43F5E" text-anchor="middle"></text>
<!-- Row 1.2 -->
<line x1="0" y1="190" x2="1120" y2="190" stroke="#F1F5F9" stroke-width="1.5"/>
<text x="40" y="170" font-size="15" font-weight="600" fill="#334155">SOC2 Type II Certified</text>
<text x="530" y="172" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="770" y="172" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="1010" y="170" font-size="14" font-weight="600" fill="#F59E0B" text-anchor="middle">Planned Q4</text>
<!-- Category 2 -->
<rect x="0" y="205" width="1120" height="35" fill="#F8FAFC"/>
<text x="20" y="228" font-size="15" font-weight="800" fill="#0F172A">2. Artificial Intelligence</text>
<!-- Row 2.1 -->
<line x1="0" y1="290" x2="1120" y2="290" stroke="#F1F5F9" stroke-width="1.5"/>
<text x="40" y="270" font-size="15" font-weight="600" fill="#334155">Automated Workflow Generation</text>
<text x="530" y="272" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="770" y="272" font-family="sans-serif" font-size="18" font-weight="800" fill="#F43F5E" text-anchor="middle"></text>
<text x="1010" y="272" font-family="sans-serif" font-size="18" font-weight="800" fill="#F43F5E" text-anchor="middle"></text>
<!-- Row 2.2 -->
<line x1="0" y1="340" x2="1120" y2="340" stroke="#F1F5F9" stroke-width="1.5"/>
<text x="40" y="320" font-size="15" font-weight="600" fill="#334155">Predictive Analytics Dashboard</text>
<text x="530" y="322" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="770" y="320" font-size="14" font-weight="600" fill="#F59E0B" text-anchor="middle">Add-on Only</text>
<text x="1010" y="322" font-family="sans-serif" font-size="18" font-weight="800" fill="#F43F5E" text-anchor="middle"></text>
<!-- Category 3 -->
<rect x="0" y="355" width="1120" height="35" fill="#F8FAFC"/>
<text x="20" y="378" font-size="15" font-weight="800" fill="#0F172A">3. Ecosystem Integration</text>
<!-- Row 3.1 -->
<line x1="0" y1="440" x2="1120" y2="440" stroke="#F1F5F9" stroke-width="1.5"/>
<text x="40" y="420" font-size="15" font-weight="600" fill="#334155">Native Salesforce Sync</text>
<text x="530" y="422" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="770" y="422" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<text x="1010" y="422" font-family="sans-serif" font-size="18" font-weight="800" fill="#10B981" text-anchor="middle"></text>
<!-- Bottom Highlight Box -->
<g>
<rect x="0" y="470" width="1120" height="60" fill="#4F46E5" rx="8"/>
</g>
<text x="40" y="506" font-size="18" font-weight="800" fill="#FFFFFF">Total Advanced Features Met</text>
<text x="530" y="508" font-size="24" font-weight="800" fill="#FFFFFF" text-anchor="middle">100%</text>
<text x="770" y="508" font-size="24" font-weight="800" fill="#A5B4FC" text-anchor="middle">40%</text>
<text x="1010" y="508" font-size="24" font-weight="800" fill="#A5B4FC" text-anchor="middle">20%</text>
</g>
<!-- ==================== Footer ==================== -->
<g id="source" data-pptx-bounds="40 667 1200 38">
<text x="1200" y="690" text-anchor="end" font-weight="600" fill="#94A3B8">Data Source: Competitive Intelligence Report · Updated Q4 2025</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

@@ -1,216 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" width="1280" height="720"
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif"
font-size="12">
<!--
Financial Statement Table Template
Purpose: Strict tabular data presentation for P&L, balance sheets
Use cases: Quarterly earnings, budget reviews, cost analysis
Design Principles: Left-align labels with indent hierarchy, strict right-aligned numbers, strong bottom totals
-->
<rect width="1280" height="720" fill="#F8FAFC"/>
<!-- ==================== Header ==================== -->
<g id="header" data-pptx-bounds="80 35 820 75">
<text x="80" y="70" font-size="32" font-weight="800" fill="#0F172A">Income Statement Summary</text>
<text x="80" y="100" font-size="14" font-weight="700" fill="#64748B" letter-spacing="1">Q1 - Q3 2025 · IN MILLIONS USD (UNAUDITED)</text>
</g>
<!-- ==================== Table Structure ==================== -->
<g id="financial_statement_table" data-pptx-bounds="80 140 1120 550" data-pptx-replace-with="table">
<metadata type="application/json">
{
"name": "financial_statement_table",
"x": 80,
"y": 160,
"width": 1120,
"height": 530,
"strict_grid": true,
"column_widths": [340, 160, 180, 180, 180, 80],
"row_heights": [45, 42, 35, 35, 50, 45, 42, 35, 35, 35, 60],
"style": {
"font_family": "Roboto",
"font_size": 15,
"header_font_size": 13,
"header_fill": "#FFFFFF",
"header_text": "#475569",
"body_fill": "#FFFFFF",
"body_text": "#0F172A",
"band_fill": "#FFFFFF",
"border_color": "#E2E8F0",
"border_width": 0.5,
"padding": {"left": 10, "right": 10, "top": 4, "bottom": 4},
"valign": "middle"
},
"columns": [
{"text": "FINANCIAL LINE ITEM", "bold": true},
{"text": "Q1 2025", "align": "r", "bold": true},
{"text": "Q2 2025", "align": "r", "bold": true},
{"text": "Q3 2025", "align": "r", "bold": true},
{"text": "YTD TOTAL", "align": "r", "bold": true, "color": "#0F172A"},
{"text": "YOY%", "align": "r", "bold": true}
],
"rows": [
[
{"text": "Total Revenue", "bold": true},
{"text": "245.8", "align": "r", "bold": true},
{"text": "268.4", "align": "r", "bold": true},
{"text": "294.2", "align": "r", "bold": true},
{"text": "808.4", "align": "r", "bold": true},
{"text": "+24%", "align": "r", "color": "#10B981", "bold": true}
],
[
{"text": "Software Subscriptions", "color": "#475569"},
{"text": "180.5", "align": "r", "color": "#475569"},
{"text": "200.2", "align": "r", "color": "#475569"},
{"text": "225.0", "align": "r", "color": "#475569"},
{"text": "605.7", "align": "r", "color": "#334155", "bold": true},
{"text": "+32%", "align": "r", "color": "#10B981", "bold": true}
],
[
{"text": "Professional Services", "color": "#475569"},
{"text": "65.3", "align": "r", "color": "#475569"},
{"text": "68.2", "align": "r", "color": "#475569"},
{"text": "69.2", "align": "r", "color": "#475569"},
{"text": "202.7", "align": "r", "color": "#334155", "bold": true},
{"text": "+4%", "align": "r", "color": "#F59E0B", "bold": true}
],
[
{"text": "Cost of Revenue", "bold": true},
{"text": "(75.4)", "align": "r"},
{"text": "(80.1)", "align": "r"},
{"text": "(85.5)", "align": "r"},
{"text": "(241.0)", "align": "r", "bold": true},
{"text": "+12%", "align": "r", "color": "#F43F5E", "bold": true}
],
[
{"text": "Gross Profit", "bold": true, "fill": "#F1F5F9"},
{"text": "170.4", "align": "r", "bold": true, "fill": "#F1F5F9"},
{"text": "188.3", "align": "r", "bold": true, "fill": "#F1F5F9"},
{"text": "208.7", "align": "r", "bold": true, "fill": "#F1F5F9"},
{"text": "567.4", "align": "r", "bold": true, "fill": "#F1F5F9"},
{"text": "+28%", "align": "r", "color": "#10B981", "bold": true, "fill": "#F1F5F9"}
],
[
{"text": "Operating Expenses", "bold": true},
"",
"",
"",
"",
""
],
[
{"text": "Research &amp; Development", "color": "#475569"},
{"text": "(45.2)", "align": "r", "color": "#475569"},
{"text": "(48.5)", "align": "r", "color": "#475569"},
{"text": "(52.0)", "align": "r", "color": "#475569"},
{"text": "(145.7)", "align": "r", "color": "#334155", "bold": true},
""
],
[
{"text": "Sales &amp; Marketing", "color": "#475569"},
{"text": "(60.8)", "align": "r", "color": "#475569"},
{"text": "(65.2)", "align": "r", "color": "#475569"},
{"text": "(70.5)", "align": "r", "color": "#475569"},
{"text": "(196.5)", "align": "r", "color": "#334155", "bold": true},
""
],
[
{"text": "General &amp; Administrative", "color": "#475569"},
{"text": "(22.4)", "align": "r", "color": "#475569"},
{"text": "(23.1)", "align": "r", "color": "#475569"},
{"text": "(24.0)", "align": "r", "color": "#475569"},
{"text": "(69.5)", "align": "r", "color": "#334155", "bold": true},
""
],
[
{"text": "Net Operating Income", "bold": true, "font_size": 18},
{"text": "42.0", "align": "r", "bold": true, "font_size": 18},
{"text": "51.5", "align": "r", "bold": true, "font_size": 18},
{"text": "62.2", "align": "r", "bold": true, "font_size": 18},
{"text": "155.7", "align": "r", "color": "#2563EB", "bold": true, "font_size": 24},
{"text": "+48%", "align": "r", "color": "#10B981", "bold": true, "font_size": 18}
]
]
}
</metadata>
<g id="tableArea" transform="translate(80, 140)">
<!-- Table Header (Double Top Line) -->
<line x1="0" y1="20" x2="1120" y2="20" stroke="#0F172A" stroke-width="3"/>
<text x="20" y="45" font-size="13" font-weight="800" fill="#475569" letter-spacing="1">FINANCIAL LINE ITEM</text>
<text x="460" y="45" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="end">Q1 2025</text>
<text x="640" y="45" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="end">Q2 2025</text>
<text x="820" y="45" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="end">Q3 2025</text>
<text x="1000" y="45" font-size="13" font-weight="800" fill="#0F172A" letter-spacing="1" text-anchor="end">YTD TOTAL</text>
<text x="1100" y="45" font-size="13" font-weight="800" fill="#475569" letter-spacing="1" text-anchor="end">YOY%</text>
<line x1="0" y1="60" x2="1120" y2="60" stroke="#0F172A" stroke-width="1.5"/>
<!-- ==================== Data Rows ==================== -->
<!-- Section: Revenue -->
<text x="20" y="100" font-size="16" font-weight="800" fill="#0F172A">Total Revenue</text>
<text x="460" y="100" font-size="16" font-weight="600" fill="#0F172A" text-anchor="end">245.8</text>
<text x="640" y="100" font-size="16" font-weight="600" fill="#0F172A" text-anchor="end">268.4</text>
<text x="820" y="100" font-size="16" font-weight="600" fill="#0F172A" text-anchor="end">294.2</text>
<text x="1000" y="100" font-size="16" font-weight="800" fill="#0F172A" text-anchor="end">808.4</text>
<text x="1100" y="100" font-size="15" font-weight="700" fill="#10B981" text-anchor="end">+24%</text>
<!-- Sub-items (Indented) -->
<text x="40" y="140" font-size="15" font-weight="500" fill="#475569">Software Subscriptions</text>
<text x="460" y="140" font-size="15" font-weight="500" fill="#475569" text-anchor="end">180.5</text>
<text x="640" y="140" font-size="15" font-weight="500" fill="#475569" text-anchor="end">200.2</text>
<text x="820" y="140" font-size="15" font-weight="500" fill="#475569" text-anchor="end">225.0</text>
<text x="1000" y="140" font-size="15" font-weight="700" fill="#334155" text-anchor="end">605.7</text>
<text x="1100" y="140" font-size="14" font-weight="600" fill="#10B981" text-anchor="end">+32%</text>
<text x="40" y="175" font-size="15" font-weight="500" fill="#475569">Professional Services</text>
<text x="460" y="175" font-size="15" font-weight="500" fill="#475569" text-anchor="end">65.3</text>
<text x="640" y="175" font-size="15" font-weight="500" fill="#475569" text-anchor="end">68.2</text>
<text x="820" y="175" font-size="15" font-weight="500" fill="#475569" text-anchor="end">69.2</text>
<text x="1000" y="175" font-size="15" font-weight="700" fill="#334155" text-anchor="end">202.7</text>
<text x="1100" y="175" font-size="14" font-weight="600" fill="#F59E0B" text-anchor="end">+4%</text>
<!-- Section: COGS & Margin -->
<line x1="0" y1="205" x2="1120" y2="205" stroke="#CBD5E1" stroke-width="1"/>
<text x="20" y="235" font-size="16" font-weight="700" fill="#0F172A">Cost of Revenue</text>
<text x="460" y="235" font-size="16" font-weight="500" fill="#0F172A" text-anchor="end">(75.4)</text>
<text x="640" y="235" font-size="16" font-weight="500" fill="#0F172A" text-anchor="end">(80.1)</text>
<text x="820" y="235" font-size="16" font-weight="500" fill="#0F172A" text-anchor="end">(85.5)</text>
<text x="1000" y="235" font-size="16" font-weight="700" fill="#0F172A" text-anchor="end">(241.0)</text>
<text x="1100" y="235" font-size="15" font-weight="600" fill="#F43F5E" text-anchor="end">+12%</text>
<!-- Gross Profit (Subtotal Highlight) -->
<rect x="0" y="255" width="1120" height="45" fill="#F1F5F9" rx="4"/>
<text x="20" y="284" font-size="16" font-weight="800" fill="#0F172A">Gross Profit</text>
<text x="460" y="284" font-size="16" font-weight="700" fill="#0F172A" text-anchor="end">170.4</text>
<text x="640" y="284" font-size="16" font-weight="700" fill="#0F172A" text-anchor="end">188.3</text>
<text x="820" y="284" font-size="16" font-weight="700" fill="#0F172A" text-anchor="end">208.7</text>
<text x="1000" y="284" font-size="16" font-weight="800" fill="#0F172A" text-anchor="end">567.4</text>
<text x="1100" y="284" font-size="15" font-weight="700" fill="#10B981" text-anchor="end">+28%</text>
<!-- Section: Operating Expenses -->
<text x="20" y="340" font-size="16" font-weight="800" fill="#0F172A">Operating Expenses</text>
<text x="40" y="375" font-size="15" font-weight="500" fill="#475569">Research &amp; Development</text>
<text x="460" y="375" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(45.2)</text>
<text x="640" y="375" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(48.5)</text>
<text x="820" y="375" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(52.0)</text>
<text x="1000" y="375" font-size="15" font-weight="700" fill="#334155" text-anchor="end">(145.7)</text>
<text x="40" y="410" font-size="15" font-weight="500" fill="#475569">Sales &amp; Marketing</text>
<text x="460" y="410" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(60.8)</text>
<text x="640" y="410" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(65.2)</text>
<text x="820" y="410" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(70.5)</text>
<text x="1000" y="410" font-size="15" font-weight="700" fill="#334155" text-anchor="end">(196.5)</text>
<text x="40" y="445" font-size="15" font-weight="500" fill="#475569">General &amp; Administrative</text>
<text x="460" y="445" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(22.4)</text>
<text x="640" y="445" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(23.1)</text>
<text x="820" y="445" font-size="15" font-weight="500" fill="#475569" text-anchor="end">(24.0)</text>
<text x="1000" y="445" font-size="15" font-weight="700" fill="#334155" text-anchor="end">(69.5)</text>
<!-- ==================== Emphasized Net Income Row ==================== -->
<!-- Double lines for final total -->
<line x1="0" y1="475" x2="1120" y2="475" stroke="#0F172A" stroke-width="2"/>
<line x1="0" y1="480" x2="1120" y2="480" stroke="#0F172A" stroke-width="1"/>
<rect x="0" y="485" width="1120" height="60" fill="#FFFFFF"/>
<text x="20" y="522" font-size="18" font-weight="800" fill="#0F172A">Net Operating Income</text>
<text x="460" y="522" font-size="18" font-weight="700" fill="#0F172A" text-anchor="end">42.0</text>
<text x="640" y="522" font-size="18" font-weight="700" fill="#0F172A" text-anchor="end">51.5</text>
<text x="820" y="522" font-size="18" font-weight="700" fill="#0F172A" text-anchor="end">62.2</text>
<text x="1000" y="524" font-size="24" font-weight="800" fill="#2563EB" text-anchor="end">155.7</text>
<text x="1100" y="522" font-size="18" font-weight="800" fill="#10B981" text-anchor="end">+48%</text>
<line x1="0" y1="550" x2="1120" y2="550" stroke="#CBD5E1" stroke-width="1"/>
</g>
</g>
<!-- ==================== Footer ==================== -->
<g id="source" data-pptx-bounds="830 675 370 20">
<text x="1200" y="690" text-anchor="end" font-weight="600" fill="#94A3B8">Data Source: Q3 2025 Financial Earnings Report</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

Some files were not shown because too many files have changed in this diff Show More