diff --git a/.gitignore b/.gitignore index 37cb962..606cb74 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,8 @@ desktop.ini *.tmp.pptx output.pptx untitled*.pptx +spec_lock.md +/*.pptx # 用户本地工具脚本 / 规划文件 (不入库) 规划.docx diff --git a/PROGRESS.md b/PROGRESS.md index 454de94..b7510fd 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md` 阅读。本文件记录已完成的事、关键决策、与原设计的偏差。 -最后更新: 2026-05-06 (PPT skill 完善:references + scripts;v2 加图标系统 + 安全区 + 越界检测 + 默认红色主题) +最后更新: 2026-05-06 (PPT skill v3:红色硬约束 + ⛔ blocking + canvas 合并 + apply_brand 品牌条 + 强制尾页 + Iconify 图标库) --- @@ -68,15 +68,18 @@ - 三个 skill,均按 WHY+WHAT 风格写,不写 Step 1/2/3: - `skills/coding/SKILL.md` - `skills/ppt/` —— 完整渐进披露结构(借鉴 hugohe3/ppt-master 的两阶段 + spec lock 思路): - - `SKILL.md`(两阶段工作流 + 八条对齐 + 默认红色主题 + 反模式) - - `references/design_principles.md`(字号/配色/留白/图表 + §4.1 **字数预算表**) - - `references/canvas_presets.md`(16:9 / 4:3 / 9:16 等画布表) - - `references/layouts.md`(9 种轻量版式 + **safe area 起手** + assert_inside / TEXT_TO_FIT_SHAPE 兜底) - - `references/icons.md`(MSO_SHAPE 图标目录 + unicode 字形表 + 5 个标准图标 helper) - - `scripts/quality_check.py`(页数/标题/bullet/字号/配色 + **shape 越界 + 文本溢出估算**) - - `scripts/source_to_md.py`(PDF/DOCX/PPTX/URL → Markdown,策略阶段输入) - - `scripts/render_icon.py`(unicode 字形 → 透明 PNG,MSO_SHAPE 兜底) - - **默认配色**:商务红 PRIMARY `#C00000` / SECONDARY `#E15554` / ACCENT `#FFC107` + - `SKILL.md`(两阶段工作流 + ⛔ blocking 八条对齐 + 红色硬约束 + 强制封面/尾页 + 反模式) + - `references/design_principles.md`(§0 画布 + §1-9 字号/配色/留白/字数预算/图表) + - `references/layouts.md`(9 种版式 + `apply_brand(slide, kind)` 品牌条覆盖每页 + assert_inside) + - `references/icons.md`(业务图标两层:Iconify / unicode 兜底;MSO_SHAPE 退为 layouts.md helper 内部几何原语) + - `assets/icons/INDEX.md`(本地图标库索引 + 推荐清单) + - `scripts/fetch_icon.py`(Iconify CDN 拉个性化 SVG/PNG,主题色染色,缓存本地) + - `scripts/quality_check.py`(页数/标题/bullet/字号/配色 + shape 越界 + 文本溢出估算) + - `scripts/source_to_md.py`(PDF/DOCX/PPTX/URL → Markdown) + - `scripts/render_icon.py`(unicode 字形 → 透明 PNG,兜底) + - **配色**:商务红 PRIMARY `#C00000` / SECONDARY `#E15554` / ACCENT `#FFC107` —— v3 改为硬约束,模型不能基于"场景判断"自行换色 + - **品牌条**:v3 加 `apply_brand(slide, kind)`,所有版式起手必调,4 种模式 (cover/inner/section/end) 覆盖左竖条 + 浅底,不再裸白纸 + - **图标**:v3 加 Iconify CDN 拉取链路 (tabler/lucide/heroicons/material-symbols 等 150+ 集),本地缓存复用 - `skills/proposal/SKILL.md`(含工作目录约定 + 字数表 + python-docx 合并模板) ### 7. System Prompt diff --git a/skills/ppt/SKILL.md b/skills/ppt/SKILL.md index 20d6cc6..6ae57ea 100644 --- a/skills/ppt/SKILL.md +++ b/skills/ppt/SKILL.md @@ -8,79 +8,104 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P 把材料变成可演示的 .pptx。**先定调,再出稿,再验收** —— 不要一口气把整份 deck 丢出去。 ## 资源 -- `references/design_principles.md` —— 字号/颜色/层级/留白/字数预算等硬规则,出稿前先翻一遍 -- `references/canvas_presets.md` —— 16:9 / 4:3 / 9:16 / A4 等画布尺寸表 -- `references/layouts.md` —— 9 种常用版式的 python-pptx 起手代码 + safe area 辅助 (封面/目录/分章/要点/双栏/图表/图片/金句/结尾) -- `references/icons.md` —— MSO_SHAPE 图标目录 + unicode 字形表 (替代大色块的轻量装饰) -- `scripts/source_to_md.py` —— 可执行,把 PDF/DOCX/PPTX/URL 转成干净 Markdown 再做素材 -- `scripts/render_icon.py` —— 可执行,unicode 字形 → 透明 PNG (MSO_SHAPE 覆盖不到时兜底) -- `scripts/quality_check.py` —— 可执行,产物 .pptx 出来后跑一遍验收 (含越界 / 文本溢出检测) +- `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则 +- `references/layouts.md` —— 9 种版式的 python-pptx 起手代码 + 安全区/越界保护 + `apply_brand` 品牌条 +- `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底 +- `assets/icons/` —— 本地图标缓存 (Iconify 拉过的图存这,见 `INDEX.md` 推荐清单) +- `scripts/source_to_md.py` —— PDF/DOCX/PPTX/URL → 干净 Markdown +- `scripts/fetch_icon.py` —— 从 Iconify CDN 拉 SVG/PNG (染主题色,缓存本地) +- `scripts/render_icon.py` —— unicode 字形 → 透明 PNG (Iconify 没有时兜底) +- `scripts/quality_check.py` —— 产物 .pptx 验收 (越界 / 文本溢出 / 颜色一致) -## 默认主题 -**商务红** —— PRIMARY `#C00000` / SECONDARY `#E15554` / ACCENT `#FFC107`。除非 spec_lock 指定其它配色,layouts.md 起手代码就用这套。其它备选见 `design_principles.md` §2。 +## 默认主题 — 商务红 (硬约束) + +**主色 `#C00000` / 辅色 `#E15554` / 强调色 `#FFC107`。** + +⛔ **不允许擅自换色**。除非满足以下任一条件,否则 spec_lock 必须填这套红色: +- 用户在请求里**明确**点名其它配色 (例:"做成蓝色"、"用我们公司的紫色") +- 用户提供素材里有明确的 brand guideline / 配色卡 + +**禁止的自我合理化**(都属违规): +- "这个场景蓝色更专业" / "学术汇报红色不合适" / "财务用蓝更稳重" +- "我觉得 XX 主题更适合" + +要换色,**先问用户**,不要在 spec_lock 里塞自己的偏好。其它备选见 `design_principles.md §2`。 ## 两阶段工作流 -### 阶段一: 策略 (Strategist) -产物:`spec_lock.md` —— 整个 deck 的"宪法",执行阶段每生成一页前都要重读。 +### 阶段一: 策略 (Strategist) — 八条对齐 -**八条对齐**(不全部确认完,不开工): -1. **画布**: 16:9 / 4:3 / 9:16 (默认 16:9,见 canvas_presets.md) -2. **页数**: 默认 5-8 页;长报告再加,但每超 1 页就要问一次"这页非加不可吗" -3. **受众**: 领导汇报 / 同行评审 / 大众科普 / 客户 pitch —— 决定信息密度和措辞 -4. **风格**: 商务正式 / 学术严谨 / 现代简约 / 极简留白 (默认现代简约) -5. **配色**: 主色 + 辅色 + 强调色,三色封顶。给具体 hex,不要"蓝色系"这种话 -6. **字体**: 中文标题/正文,英文标题/正文。Win 默认 微软雅黑 + Arial -7. **图标/插图**: 是否要、风格 (线性/扁平/拟物)、来源 (用户提供 / 不用) -8. **图表**: 数据 ≥ 3 个点的页面默认配图;明确哪几页要图 +产物:`spec_lock.md` —— 整个 deck 的"宪法",阶段二每页前都要重读。 -写入 `spec_lock.md` 后给用户看一眼再继续。**spec_lock 写定后不要再改**,有冲突回头跟用户重新对齐。 +按下表**一次性给出推荐方案**,然后 ⛔ **BLOCKING:等用户确认/修改后才能进阶段二**。不要一条一条问。 -### 阶段二: 执行 (Executor) -**逐页生成**,不是一次性 dump 全 deck。每页前先读一次 `spec_lock.md`,然后: +| # | 项 | 默认值 | +|---|----|-------| +| 1 | 画布 | **16:9** (13.33×7.5 in) | +| 2 | 页数 | **封面 + 5-8 页正文 + 尾页(Q&A)** = 共 7-10 页。**封面 / 尾页强制必有**,不在 5-8 页预算里 | +| 3 | 受众 | 看材料推断:领导汇报 / 同行评审 / 客户 pitch | +| 4 | 风格 | **现代简约** (白底 + 细线 + 留白) | +| 5 | 配色 | **商务红** `#C00000` `#E15554` `#FFC107` (见上"默认主题") | +| 6 | 字体 | **微软雅黑 + Arial** | +| 7 | 图标 | **MSO_SHAPE 几何形状** (无外部图片资源) | +| 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 | -1. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx,append slide,save) -2. 跑完报这一页的:版式、标题、要点条数、是否含图 -3. 用户确认 / 微调后再下一页 +把这 8 项写进 `spec_lock.md`,以表格形式给用户预览,问一句"按这个开干?"。**spec_lock 写定后不再改**,有冲突回头跟用户重新对齐。 -**为什么逐页?** 一次性出全 deck 很容易越到后面越糊。逐页能让用户在第 2 页就发现风格不对,而不是看完 8 页才推翻重来。 +### 阶段二: 执行 (Executor) — 逐页生成 -**例外**: 用户明确说 "你别问,直接全做了" —— 那就一次跑完,但跑完后必须用 `quality_check.py` 验收。 +每页前 **必须 read 一次 `spec_lock.md`**,只用里面定的颜色/字体/图标 —— **不允许凭记忆或临时发挥**。这条规则是为了对抗长 deck 中的上下文漂移。 + +每页流程: +1. 读 `spec_lock.md` (即使刚读过) +2. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save) +3. 报这一页:版式、标题、要点条数、是否含图 +4. 用户确认 / 微调后再下一页 + +**为什么逐页?** 一次性出全 deck 中途改方向就要全推翻;逐页能让用户在第 2 页就发现问题。 + +**例外**: 用户明确说"别问,直接全做了" —— 一次跑完,但跑完必须用 `quality_check.py` 验收。 ### 阶段三: 验收 -- `python scripts/quality_check.py ` —— 检页数/标题/bullet 条数/文件大小 -- 不通过的项,回头 edit 对应页 -## 设计原则 (硬规则) +```bash +python scripts/quality_check.py --spec spec_lock.md +``` + +不通过的项,回头 edit 对应页。 + +## 设计原则 (硬规则速查) - **每页一个核心信息**: 一页讲一件事,塞两件就拆页 - **bullet ≤ 5 条**: 超过就拆页或改成图表/双栏 - **正文不写完整段落**: 列要点;长句留给演讲者口述 - **数据 ≥ 3 个点应有图表**: 用 matplotlib 生成 .png 嵌入 -- **中文标题 ≤ 30 字** / **英文标题 ≤ 12 词** -- **配色三色封顶**: 主色 + 辅色 + 强调色,其他都用灰阶 -- **少用大色块,多用细线 + 图标 + 留白**: 满铺色块只在封面/分章/结尾克制使用 -- **图标走 MSO_SHAPE**: 原生形状可编辑、可缩放;复杂图标走 `render_icon.py` -- **Shape 不能越界**: `layouts.md` 的起手代码用 `assert_inside` 在生成时即报错;最终必跑 `quality_check.py` -- **字数按预算来**: 写 bullet 前查 `design_principles.md §4.1` 的字数预算表,溢出靠拆条不靠收缩字号 +- **中文标题 ≤ 30 字** +- **配色三色封顶**: 主 + 辅 + 强调,其他用灰阶 +- **少用大色块,多用细线 + 图标 + 留白** +- **图标走 MSO_SHAPE**: 矢量、可编辑;复杂图标走 `render_icon.py` +- **Shape 不能越界**: `layouts.md` 起手代码用 `assert_inside` 在生成时即报错 +- **字数按预算来**: 写 bullet 前查 `design_principles.md §4.1` 字数预算表 - 详细规则见 `references/design_principles.md` ## 工作目录约定 ``` / -├── source.md # 阶段一: source_to_md.py 转出的素材 -├── spec_lock.md # 阶段一: 八条对齐落定 +├── source.md # source_to_md.py 转出的素材 +├── spec_lock.md # 八条对齐落定 ├── slides/ │ └── chart_p3.png # 各页用到的图片素材 -└── .pptx # 最终产物 (文件名按主题命名,不要 untitled.pptx) +└── .pptx # 最终产物 (按主题命名) ``` ## 反模式 - 用户没给材料就开始硬编内容 - 八条没对齐就跑 python-pptx +- **基于"场景判断"自行换配色**(见上"默认主题"违规清单) +- **缺封面 / 缺尾页(Q&A)** —— 两端都是强制项,不算在正文页数预算内 +- **裸白纸版式** —— 所有版式起手都必须 `apply_brand(slide, kind)`,见 layouts.md - 一个 `run_python` 出整 deck (中途改方向就要全推翻) - 跑完不做 `quality_check.py` 就交付 - 起名 `output.pptx` / `untitled.pptx` —— 务必按主题给文件名 -- 文字塞满整张幻灯片 —— 留白本身是设计 ## 输出 完成后告诉用户:文件路径、页数、用到的版式列表、是否有未满足的 spec 项。问一句要不要再改。 diff --git a/skills/ppt/assets/icons/.gitkeep b/skills/ppt/assets/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/ppt/assets/icons/INDEX.md b/skills/ppt/assets/icons/INDEX.md new file mode 100644 index 0000000..15f5dfb --- /dev/null +++ b/skills/ppt/assets/icons/INDEX.md @@ -0,0 +1,66 @@ +# 本地图标库 + +> 这里缓存通过 `scripts/fetch_icon.py` 从 Iconify 拉下来的图标。**首次为空**,模型按需下载缓存,再次用时直接读本地不再发请求。 + +## 缓存命名规约 + +``` +___.png +__.svg +``` + +例: `tabler_rocket_C00000_128.png` / `lucide_target_FFC107_96.svg` + +## 推荐图标清单 (按业务主题) + +按下面 3 行命令拉取首批最常用 18 个,够覆盖 80% 商务汇报场景: + +```bash +ICONS_DIR=skills/ppt/assets/icons + +# 战略 / 目标 / 启动 +for n in target rocket flag bulb; do + python skills/ppt/scripts/fetch_icon.py $n --set tabler --color C00000 --size 128 \ + -o "$ICONS_DIR/tabler_${n}_C00000_128.png" +done + +# 数据 / 趋势 / 报表 +for n in chart-bar chart-line trending-up calculator; do + python skills/ppt/scripts/fetch_icon.py $n --set tabler --color C00000 --size 128 \ + -o "$ICONS_DIR/tabler_${n}_C00000_128.png" +done + +# 团队 / 流程 / 时间 +for n in users settings calendar clock check shield-check arrow-right alert-triangle currency-yuan circle-check; do + python skills/ppt/scripts/fetch_icon.py $n --set tabler --color C00000 --size 128 \ + -o "$ICONS_DIR/tabler_${n}_C00000_128.png" +done +``` + +## 图标集对照 + +| 集名 | 风格 | 数量 | License | +|-----|-----|-----|---------| +| **tabler** ⭐ 推荐 | 描边、商务、克制 | 4500+ | MIT | +| lucide | 描边、克制 | 1500+ | ISC | +| heroicons | Tailwind 风、双重粗细 | 300+ | MIT | +| material-symbols | Google Material 描边/填充 | 3000+ | Apache 2.0 | +| carbon | IBM、克制专业 | 2000+ | Apache 2.0 | +| fluent | Microsoft、温和现代 | 4000+ | MIT | +| mdi | Material Design Icons 社区 | 7000+ | Apache 2.0 | + +## 浏览找名字 + +打开 https://icon-sets.iconify.design/ 搜中英文关键词,复制图标名 (如 `tabler:rocket`),回来用 `--set tabler rocket` 拉。 + +## 主题色变体 + +同一图标按主色/辅色/强调色/灰各拉一份,文件名只在 `` 段不同: +- `tabler_target_C00000_128.png` (主红) +- `tabler_target_E15554_128.png` (辅红) +- `tabler_target_FFC107_128.png` (强调金) +- `tabler_target_595959_128.png` (灰) + +## 用图标的硬规则 + +见 `references/icons.md §C` —— 风格统一、颜色限定、大小克制、不替表意、避 emoji。 diff --git a/skills/ppt/references/canvas_presets.md b/skills/ppt/references/canvas_presets.md deleted file mode 100644 index 5952197..0000000 --- a/skills/ppt/references/canvas_presets.md +++ /dev/null @@ -1,55 +0,0 @@ -# 画布尺寸预设 - -> 阶段一选画布时查这张表。**画布定了之后所有版式按这个尺寸算坐标**,不要中途改。 - -## 标准尺寸表 - -| 用途 | 比例 | 宽×高 (英寸) | python-pptx | 说明 | -|-----|------|------------|------------|------| -| **现代商务汇报** | 16:9 | 13.33 × 7.5 | `Inches(13.33), Inches(7.5)` | **默认选这个** | -| 老投影仪 | 4:3 | 10 × 7.5 | `Inches(10), Inches(7.5)` | 老会议室、教学场景 | -| 竖屏手机 / 朋友圈 | 9:16 | 7.5 × 13.33 | `Inches(7.5), Inches(13.33)` | 移动端阅读、视频号封面 | -| 小红书 | 3:4 | 7.5 × 10 | `Inches(7.5), Inches(10)` | 单图阅读 | -| 微信公众号长图 | 1:n | 7.5 × 7.5 起 | `Inches(7.5), Inches(7.5)` | 单页或拼接 | -| 海报 (A4 横) | √2:1 | 11.69 × 8.27 | `Inches(11.69), Inches(8.27)` | 打印 | -| 海报 (A4 竖) | 1:√2 | 8.27 × 11.69 | `Inches(8.27), Inches(11.69)` | 打印 | -| 大屏宣讲 | 16:9 高 dpi | 同 16:9 | 同上 | 字号上调 4-6pt | - -## 选画布的几条经验 - -- 不知道选哪个 —— **16:9**,99% 场合通吃 -- 用户在投影仪墙上看 —— 16:9 -- 用户在电脑屏幕上看 —— 16:9 或 4:3 -- 用户在手机上看 —— 9:16 -- 用户要打印散发 —— A4 横或 A4 竖 -- 用户说"做个图发朋友圈" —— 3:4 或 1:1,不是 PPT 范畴但 python-pptx 也能干 - -## python-pptx 画布初始化 - -```python -from pptx import Presentation -from pptx.util import Inches, Pt - -prs = Presentation() -# 16:9 默认 -prs.slide_width = Inches(13.33) -prs.slide_height = Inches(7.5) - -# 4:3 改这两行 -# prs.slide_width = Inches(10) -# prs.slide_height = Inches(7.5) - -# 9:16 改这两行 -# prs.slide_width = Inches(7.5) -# prs.slide_height = Inches(13.33) -``` - -## 安全边距 (各画布通用) - -- 左右边距: **画布宽 × 0.05** (16:9 即 0.67 寸) -- 上下边距: **画布高 × 0.07** (16:9 即 0.5 寸) -- 内容区域: 画布尺寸减去四周边距,所有元素都摆在这个矩形内 - -## 字号随画布缩放 - -如果画布超过 16:9 默认尺寸 (比如做 4K 大屏),**所有字号 × (实际宽 / 13.33)**。模型自己换算,不要硬抄默认表。 diff --git a/skills/ppt/references/design_principles.md b/skills/ppt/references/design_principles.md index e6623d7..944869b 100644 --- a/skills/ppt/references/design_principles.md +++ b/skills/ppt/references/design_principles.md @@ -2,6 +2,18 @@ > 出稿前过一遍。**这些不是建议,是工程约束** —— 模型生成 PPT 最常见的失败模式都是违反这些规则。 +## 0. 画布 (默认 16:9) + +| 用途 | 比例 | 宽×高 (英寸) | python-pptx | +|-----|------|------------|------------| +| **现代商务汇报** ⭐ 默认 | 16:9 | 13.33 × 7.5 | `Inches(13.33), Inches(7.5)` | +| 老投影 / 教学 | 4:3 | 10 × 7.5 | `Inches(10), Inches(7.5)` | +| 手机 / 视频号 | 9:16 | 7.5 × 13.33 | `Inches(7.5), Inches(13.33)` | +| 小红书 | 3:4 | 7.5 × 10 | `Inches(7.5), Inches(10)` | +| A4 横 / 竖 | √2:1 | 11.69 × 8.27 / 反 | 同左 | + +不知道选哪个 → **16:9**。安全边距统一:左右 0.7 in,上下 0.5 in。**画布定了不要中途改**,后续坐标全按这个尺寸算。画布超 16:9 默认尺寸时所有字号 × `(实际宽 / 13.33)`。 + ## 1. 字号 (16:9 标准) | 元素 | 字号 (Pt) | 备注 | diff --git a/skills/ppt/references/icons.md b/skills/ppt/references/icons.md index 5f5da1e..7c642eb 100644 --- a/skills/ppt/references/icons.md +++ b/skills/ppt/references/icons.md @@ -1,189 +1,97 @@ -# 图标系统 +# 图标系统 (两层) -> **首选 `MSO_SHAPE.*` —— PowerPoint 原生形状,矢量、可编辑、配色随主题。** 复杂图标(齿轮、放大镜、文件夹等无对应 MSO_SHAPE)再走 `render_icon.py` 用 unicode 字形栅格化为 PNG。 +> 几何装饰 (圆点、徽章、品牌条、装饰线) 已在 `layouts.md` 起手块以 helper 封装 (`add_dot` / `add_badge` / `add_accent_line` / `add_rect`),直接调用,**不要重写**,**也不要把它们当"图标"用**。本文档处理的是真正的**业务概念图标** (火箭 / 目标 / 雷达 / 齿轮 / 盾牌 ...) -## A. MSO_SHAPE 图标目录 +## 选图标两层降级 + +``` +1) Iconify 个性化图标 ── 业务概念 (火箭、目标、雷达、齿轮) → 见 §A +2) Unicode 字形兜底 ── Iconify 没有合适的 (✓ ✗ ★ → ↑) → 见 §B +``` + +整 deck 选**一个图标集**用到底,不要 tabler 跟 lucide 混用。 + +## §A. Iconify 个性化图标 (本地缓存 + 网络拉取) + +### A1. 本地库 +路径: `skills/ppt/assets/icons/`,详见 [INDEX.md](../assets/icons/INDEX.md)。 +命名规约: `___.png`(如 `tabler_rocket_C00000_128.png`) + +**用之前先 `glob` 检查本地有没有**,有就直接 `add_picture`,免去网络往返。 + +### A2. fetch_icon.py 拉新图标 +```bash +# 主红色 128px PNG (推荐) +python skills/ppt/scripts/fetch_icon.py rocket --set tabler --color C00000 \ + --size 128 -o skills/ppt/assets/icons/tabler_rocket_C00000_128.png + +# 强调色金黄 +python skills/ppt/scripts/fetch_icon.py target --set tabler --color FFC107 \ + --size 128 -o skills/ppt/assets/icons/tabler_target_FFC107_128.png +``` + +`--set` 默认 `tabler`(4500+ 商务图标,MIT)。其它选 `lucide / heroicons / material-symbols / carbon / fluent / mdi`。**整 deck 只用一个 set**。 + +PNG 转换需 `pip install cairosvg`(推荐)或 `pip install svglib`。没装也能拿 SVG。 + +### A3. 嵌入幻灯片 +```python +slide.shapes.add_picture( + "skills/ppt/assets/icons/tabler_rocket_C00000_128.png", + Inches(1.0), Inches(2.5), + width=Inches(0.8), # 装饰图标 0.5-1.5 in;别超 2 in +) +``` + +### A4. 浏览找名字 +打开 https://icon-sets.iconify.design/ 搜关键词,如 "rocket" / "数据" / "shield",拿到名字 (如 `tabler:rocket`) 直接给 fetch_icon.py。 + +### A5. 流程节点 (替代 PENTAGON) + +需要"调研→设计→开发→测试→上线"这种横向流程时,**不要用 PowerPoint 内置 PENTAGON**(视觉陈旧),改用 Iconify 的 `chevron-right` + 文本组合: ```python -from pptx.enum.shapes import MSO_SHAPE +from pptx.util import Inches, Pt +stages = ["调研","设计","开发","测试","上线"] +icon_path = "skills/ppt/assets/icons/tabler_chevron-right_C00000_64.png" +for i, label in enumerate(stages): + x = 0.7 + i * 2.4 + add_textbox(slide, x, 3.7, 1.8, 0.5, label, 16, bold=True, + color=PRIMARY, align=PP_ALIGN.CENTER, name=f"stage_{i}") + if i < len(stages) - 1: # 节点间放 chevron + slide.shapes.add_picture(icon_path, Inches(x + 1.85), Inches(3.7), + width=Inches(0.4)) ``` -### 标记类 (放在 bullet 前 / 标题旁) +## §B. Unicode 字形 (兜底) -| 用途 | MSO_SHAPE | 说明 | -|-----|-----------|------| -| 圆点 bullet | `OVAL` | 0.18×0.18 in,实心填充 | -| 方点 bullet | `RECTANGLE` | 0.16×0.16 in,实心 | -| 钻石点 | `DIAMOND` | 0.2×0.2 in | -| 对号 ✓ | `CHEVRON` 旋转 / 或用字形 | MSO 没有专门"check"形;用字形更清晰 | -| 加号 + | `MATH_PLUS` | 强调"新增"语境 | -| 星 ★ | `STAR_5_POINT` | 重点项;不要每页都用 | -| 心 | `HEART` | 用户向 / 软话题 | - -### 箭头类 (流程 / 趋势) - -| 用途 | MSO_SHAPE | 说明 | -|-----|-----------|------| -| 右箭头 → | `RIGHT_ARROW` | 流程下一步 | -| 上箭头 ↑ | `UP_ARROW` | 增长 | -| 下箭头 ↓ | `DOWN_ARROW` | 下降 | -| 双向箭头 ↔ | `LEFT_RIGHT_ARROW` | 对比 / 关联 | -| 折线右箭头 | `BENT_ARROW` / `CURVED_RIGHT_ARROW` | 转折 | -| 五边形流程 | `PENTAGON` | 流程节点(横排) | -| V 形 | `CHEVRON` | 流程节点(空间紧) | - -### 几何/装饰 - -| 用途 | MSO_SHAPE | 说明 | -|-----|-----------|------| -| 圆形头像底 | `OVAL` | 头像/数字徽章 | -| 圆角矩形 | `ROUNDED_RECTANGLE` | 标签 / 按钮态 | -| 标注气泡 | `ROUNDED_RECTANGULAR_CALLOUT` | 引述 | -| 雷电 | `LIGHTNING_BOLT` | 突破 / 创新 | -| 太阳 | `SUN` | 机会 / 启示 | -| 月亮 | `MOON` | 夜晚 / 安静主题 | -| 云 | `CLOUD` | SaaS / 网络主题 | -| 禁止 | `NO_SYMBOL` | 反模式 / 禁止 | -| 笑脸 | `SMILEY_FACE` | 用户满意 | - -### 引用/装饰 - -| 用途 | MSO_SHAPE | 说明 | -|-----|-----------|------| -| 大引号 | 字形 `"` 或 `LEFT_BRACE` | 金句页常用 | -| 横线分隔 | `RECTANGLE` 高 0.04 in | 标题下装饰线 | -| 竖线分隔 | `RECTANGLE` 宽 0.04 in | 双栏中线 | -| 三点 ⋯ | `OVAL` × 3 | 加载 / 进行中 | - -## B. 标准用法 - -### B1. 圆点 bullet - -```python -from pptx.enum.shapes import MSO_SHAPE -from pptx.util import Inches - -def add_dot(slide, x, y, size=0.18, color=ACCENT): - dot = slide.shapes.add_shape(MSO_SHAPE.OVAL, - Inches(x), Inches(y), - Inches(size), Inches(size)) - dot.fill.solid(); dot.fill.fore_color.rgb = color - dot.line.fill.background() - return dot +Iconify 都没合适的时候用。避 emoji,用单色符号: ``` - -### B2. 编号徽章 (圆 + 数字) - -```python -def add_badge(slide, x, y, num, diameter=0.7, - fill=PRIMARY, fg=RGBColor(255,255,255)): - circle = slide.shapes.add_shape(MSO_SHAPE.OVAL, - Inches(x), Inches(y), - Inches(diameter), Inches(diameter)) - circle.fill.solid(); circle.fill.fore_color.rgb = fill - circle.line.fill.background() - tf = circle.text_frame - tf.text = str(num) - p = tf.paragraphs[0] - p.alignment = PP_ALIGN.CENTER - r = p.runs[0] - r.font.bold = True - r.font.size = Pt(20) - r.font.color.rgb = fg - r.font.name = "Arial" - return circle -``` - -### B3. 流程节点 (五边形) - -```python -def add_pentagon(slide, x, y, w, h, text, fill=PRIMARY): - shp = slide.shapes.add_shape(MSO_SHAPE.PENTAGON, - Inches(x), Inches(y), - Inches(w), Inches(h)) - shp.fill.solid(); shp.fill.fore_color.rgb = fill - shp.line.fill.background() - tf = shp.text_frame - tf.text = text - p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER - r = p.runs[0]; r.font.size = Pt(14); r.font.bold = True - r.font.color.rgb = RGBColor(255,255,255); r.font.name = "微软雅黑" - return shp - -# 用法:水平排五个节点 -for i, label in enumerate(["调研","设计","开发","测试","上线"]): - add_pentagon(slide, 0.7 + i*2.4, 3.5, 2.2, 0.8, label) -``` - -### B4. 强调箭头 (右箭头) - -```python -def add_arrow_right(slide, x, y, w, h, fill=ACCENT): - a = slide.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, - Inches(x), Inches(y), - Inches(w), Inches(h)) - a.fill.solid(); a.fill.fore_color.rgb = fill - a.line.fill.background() - return a -``` - -### B5. 标题装饰线 - -```python -def add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=ACCENT): - """标题下面那条 1 寸长的强调横线 (替代大色块的轻量做法)""" - bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, - Inches(x), Inches(y), - Inches(length), Inches(thickness)) - bar.fill.solid(); bar.fill.fore_color.rgb = color - bar.line.fill.background() - return bar -``` - -## C. Unicode 字形 (MSO_SHAPE 没有的图形) - -某些图标 MSO_SHAPE 没有对应,用 unicode 字形渲染成 PNG 嵌入。Win/Mac 默认字体覆盖良好。 - -### 推荐字形 (避开 emoji,用单色符号) - -``` -✓ ✔ ✗ ✘ 对号 / 错号 -✦ ✧ ✪ ★ 星 -→ ← ↑ ↓ ↔ 箭头 -⬛ ⬜ ◆ ◇ 方块菱形 -● ○ ◉ ◎ 圆 -※ ◇ ⬢ ⬡ 装饰 -☰ ☱ ☲ ☳ 汉字六十四卦类(简洁) -∴ ∵ ⇒ ⇔ 数学 -№ ¶ § † 文档符号 +✓ ✔ ✗ ✘ 对号 / 错号 +★ ✦ ✧ ✪ 星 +→ ← ↑ ↓ ↔ 箭头 ↗ ↘ ↙ ↖ 斜箭头 -⌘ ⌥ ⌃ ⏎ 键盘 +● ○ ◉ ◎ 圆 +⬛ ⬜ ◆ ◇ 方块菱形 +∴ ∵ ⇒ ⇔ 数学 +№ ¶ § † 文档 ``` -### 用 render_icon.py 生成 - ```bash -# 生成对号 PNG (强调色,96px) -python scripts/render_icon.py "✓" --color "#38B2AC" --size 96 -o slides/check.png - -# 然后嵌入幻灯片 -slide.shapes.add_picture("slides/check.png", Inches(1), Inches(2), - width=Inches(0.5)) +# 强调色对号 96px → PNG +python scripts/render_icon.py "✓" --color "#C00000" --size 96 -o slides/check.png ``` -## D. 用图标的几条原则 +## §C. 硬规则 -1. **同一 deck 风格统一** —— 全用 MSO_SHAPE 或全用字形 PNG,不要混 -2. **颜色限定** —— 只用 PRIMARY / SECONDARY / ACCENT / GREY,不要每个图标独立配色 -3. **大小克制** —— bullet 前的 dot 0.15-0.2 in;独立装饰图标 0.5-1.5 in;不要超过 2 in -4. **间距统一** —— 图标右侧到文字的间距固定,通常 0.2-0.3 in -5. **不替换文字** —— 图标是辅助,不是表意主体;一个 ★ 不能代替"重点"两字 -6. **避免 emoji** —— emoji 在不同系统渲染差异大,且自带颜色与你的配色冲突 +1. **风格统一** —— 整 deck 只用一个 Iconify set;不要 tabler 跟 lucide 混 +2. **颜色限定** —— 只用 PRIMARY / SECONDARY / ACCENT / GREY,不要每图标独立配色 +3. **大小克制** —— 装饰图标 0.5-1.5 in;不超过 2 in +4. **不替表意** —— 一个 ★ 不能代替"重点"两字 +5. **避免 emoji** —— 跨系统渲染差异大,且自带颜色冲突主题 +6. **不要每页都堆** —— 装饰是配角,文字是主角 +7. **缓存复用** —— Iconify 拉的图标进 `assets/icons/` 缓存,下次直接用,不要重复请求 -## E. 不要做什么 +## §D. 不要把 layouts.md helper 当"图标" -- ❌ 在每页都堆图标 -- ❌ 用网上随便下载的彩色图标 (主题不统一) -- ❌ 用 emoji (🚀💡⚡) 当严肃汇报的图标 -- ❌ 图标尺寸大于标题字号高度的 2 倍 -- ❌ 用 STAR / HEART 装饰严肃议题 (融资额、合规) +`add_dot` / `add_badge` / `add_accent_line` / `add_rect` 是几何**装饰**(品牌条、圆点 bullet、编号徽章、装饰短线),不是业务图标。它们底层是 MSO_SHAPE.OVAL/RECTANGLE,但模型不要直接调 MSO_SHAPE —— 全部走 layouts.md 的 helper 接口。 diff --git a/skills/ppt/references/layouts.md b/skills/ppt/references/layouts.md index 8d5e474..c8f8a07 100644 --- a/skills/ppt/references/layouts.md +++ b/skills/ppt/references/layouts.md @@ -13,7 +13,9 @@ from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE from pptx.enum.shapes import MSO_SHAPE -# ---- 配色 (默认红色主题; spec_lock 里有覆盖以 spec_lock 为准) ---- +# ---- 配色 (商务红 — 硬约束默认) ---- +# ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec_lock 已写其它 hex, +# 否则就是这套商务红。禁止以"这个场景蓝色更专业"这类自我合理化做替换。 PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据 SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形 ACCENT = RGBColor(0xFF, 0xC1, 0x07) # 金黄 - 关键数据点/CTA @@ -122,6 +124,37 @@ def page_title(slide, text, page_num=None, total=None, footer="项目汇报"): add_textbox(slide, 12.0, 7.0, 1.2, 0.4, f"{page_num} / {total}", 11, color=GREY_LIGHT, align=PP_ALIGN.RIGHT, shrink=False, name="page_num") + +# ---- 品牌条 (每页起手必调,确保不是裸白纸) ---- +def apply_brand(slide, kind="inner"): + """统一品牌锚点。每个版式第一行调用,给一条窄的主色锚点 + 必要时浅底。 + kind: + cover —— 封面: 左侧主色长竖条 + 顶部短横 + inner —— 内页 (默认): 左侧主色窄条 (从标题到底部) + section —— 分章: 整页浅灰 + 左侧强调色粗竖条 + end —— 结尾: 整页浅灰 + 顶/底强调色短线 + """ + if kind == "cover": + # 顶部短主色横线 + 左侧主色长竖条 + 底部细灰线 + add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar") + add_rect(slide, 0.7, 0.6, 0.8, 0.06, PRIMARY, "brand_top_line") + add_rect(slide, SAFE_LEFT, 7.18, SAFE_W, 0.02, + RGBColor(0xDD, 0xDD, 0xDD), "brand_btm_hairline") + elif kind == "section": + add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg") + add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar") + add_rect(slide, 0.7, 2.5, 0.08, 2.5, ACCENT, "brand_section_bar") + elif kind == "end": + add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg") + add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line") + add_rect(slide, SAFE_RIGHT - 0.8, 6.85, 0.8, 0.06, ACCENT, + "brand_btm_line") + else: # inner — 默认内页 + # 左侧主色窄条 (从标题区到页脚) + add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar") + # 底部一条细灰线 (页脚分隔) + add_rect(slide, SAFE_LEFT, 7.18, SAFE_W, 0.02, + RGBColor(0xDD, 0xDD, 0xDD), "brand_btm_hairline") ``` > **要点**: @@ -131,23 +164,20 @@ def page_title(slide, text, page_num=None, total=None, footer="项目汇报"): --- -## L1 · 封面 (Cover) —— 极简,无大色块 +## L1 · 封面 (Cover) —— 主色长竖条锚点 ```python slide = prs.slides.add_slide(BLANK) +apply_brand(slide, "cover") # 左侧主色长竖条 + 顶部短横 -# 左上角小色块 + 标题左侧细色条 -add_rect(slide, 0.7, 0.7, 0.6, 0.06, PRIMARY) # 顶部短线 -add_rect(slide, 0.7, 1.05, 0.06, 1.5, ACCENT) # 左侧竖线 (装饰) - -# 主标题 -add_textbox(slide, 0.7, 2.6, 11.9, 1.4, "项目名称 / 演示主题", +# 主标题 (避开左竖条) +add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "项目名称 / 演示主题", 44, bold=True, color=INK, name="cover_title") # 副标题 (灰色,弱化) -add_textbox(slide, 0.7, 4.1, 11.9, 0.6, "一句话副标题或定位", +add_textbox(slide, 0.9, 4.1, 11.9, 0.6, "一句话副标题或定位", 22, color=GREY, name="cover_sub") # 汇报人 / 日期 -add_textbox(slide, 0.7, 6.4, 11.9, 0.4, +add_textbox(slide, 0.9, 6.4, 11.9, 0.4, "汇报人 · 部门 · 2026-05-06", 14, color=GREY_LIGHT, name="cover_meta") # 右下角小图标点缀 (五角星,可选) @@ -161,6 +191,7 @@ add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, ACCENT, ```python slide = prs.slides.add_slide(BLANK) +apply_brand(slide, "inner") page_title(slide, "目录") items = ["背景与现状", "核心问题", "解决方案", "实施计划", "预期成果"] @@ -178,10 +209,7 @@ for i, item in enumerate(items): ```python slide = prs.slides.add_slide(BLANK) -# 整页极浅灰 (替代深色满铺) -add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG) -# 左侧装饰竖条 -add_rect(slide, 0.7, 2.5, 0.08, 2.5, ACCENT) +apply_brand(slide, "section") # 整页浅灰 + 主色左竖条 + 强调装饰 # 大编号 (主色,描边视觉感) add_textbox(slide, 1.1, 2.0, 4, 2.5, "01", 160, bold=True, color=PRIMARY, font=EN_FONT, name="sec_num") @@ -204,6 +232,7 @@ add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, ACCENT, ```python slide = prs.slides.add_slide(BLANK) +apply_brand(slide, "inner") page_title(slide, "核心结论") bullets = [ @@ -226,6 +255,7 @@ for i, b in enumerate(bullets): ```python slide = prs.slides.add_slide(BLANK) +apply_brand(slide, "inner") page_title(slide, "现状 vs 改进后") mid_x = SLIDE_W / 2 @@ -266,6 +296,7 @@ for i, p in enumerate(right_pts): ```python # chart.png 已用 matplotlib 生成 (见 design_principles.md §7) slide = prs.slides.add_slide(BLANK) +apply_brand(slide, "inner") page_title(slide, "季度营收持续增长") # 一句话结论 add_textbox(slide, SAFE_LEFT, SAFE_TOP + 1.1, SAFE_W, 0.5, @@ -310,6 +341,7 @@ add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, ACCENT, ```python slide = prs.slides.add_slide(BLANK) +apply_brand(slide, "inner") # 左上大引号 (用 STAR 不合适;用字形) add_textbox(slide, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True, color=ACCENT, font=EN_FONT, shrink=False, name="quote_mark") @@ -326,14 +358,13 @@ add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025", --- -## L9 · 结尾 / Q&A —— 浅色 + 大字,不再满铺深色 +## L9 · 结尾 / Q&A —— 浅底 + 大字,**强制必有** + +> **不是可选** —— 任何 deck 都必须以这页收尾。无论是汇报、提案、路演,缺尾页等于"话没说完"。 ```python slide = prs.slides.add_slide(BLANK) -# 顶部 + 底部装饰短线 (代替整页色块) -add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "top_line") -add_rect(slide, SAFE_RIGHT - 0.8, 6.85, 0.8, 0.06, ACCENT, "bottom_line") - +apply_brand(slide, "end") # 整页浅灰 + 顶/底强调短线 add_textbox(slide, 0, 2.5, SLIDE_W, 1.6, "Thank You", 80, bold=True, color=PRIMARY, align=PP_ALIGN.CENTER, font=EN_FONT, name="thanks") diff --git a/skills/ppt/scripts/fetch_icon.py b/skills/ppt/scripts/fetch_icon.py new file mode 100644 index 0000000..577ba09 --- /dev/null +++ b/skills/ppt/scripts/fetch_icon.py @@ -0,0 +1,177 @@ +"""fetch_icon.py: 从 Iconify CDN 拉个性化图标,按主题色染色,缓存本地。 + +Iconify 聚合了 150+ 免费开源图标集,无需账号、无 API key: + tabler -- 现代描边 (Apache 2.0) ⭐ 推荐 + lucide -- 开源经典 (ISC) + heroicons -- Tailwind (MIT) + material-symbols -- Google (Apache 2.0) + carbon -- IBM (Apache 2.0) + fluent -- Microsoft (MIT) + mdi -- Material Design (Apache 2.0) + +每个集都有数千图标,在 https://icon-sets.iconify.design/ 浏览找名字。 + +用法: + # 推荐: 染主色,导出 PNG (需 cairosvg 或 svglib) + python fetch_icon.py rocket --set tabler --color C00000 --size 128 \\ + -o slides/rocket.png + + # 只要 SVG (PowerPoint 2016+ 支持嵌入 SVG) + python fetch_icon.py target --set lucide --color FFC107 \\ + -o slides/target.svg + + # 默认值: set=tabler, color=C00000(主红), size=128 + python fetch_icon.py chart-bar -o slides/chart_bar.png + +环境: + PNG 转换依赖任一: `pip install cairosvg` (推荐) 或 `pip install svglib` + 若都没有,会保存 .svg 到目标路径(扩展名自动改). + +退出码: + 0 = 成功 PNG/SVG + 1 = SVG 有了但 PNG 转换失败 (已保存 SVG) + 2 = 网络/图标名错误 (没拉到) +""" +from __future__ import annotations + +import argparse +import io +import sys +import urllib.parse +import urllib.request +from pathlib import Path + +ICONIFY_API = "https://api.iconify.design/{set}/{name}.svg" + + +def fetch_svg(name: str, icon_set: str, color: str, size: int) -> str: + """从 Iconify 拉 SVG,带主题色和大小参数。""" + params: dict[str, str] = {} + if color: + params["color"] = "#" + color.lstrip("#") + if size: + params["height"] = str(size) + params["width"] = str(size) + url = ICONIFY_API.format(set=icon_set, name=name) + if params: + url += "?" + urllib.parse.urlencode(params) + req = urllib.request.Request( + url, headers={"User-Agent": "ppt-skill-fetch_icon/1.0"} + ) + with urllib.request.urlopen(req, timeout=15) as resp: + body = resp.read().decode("utf-8") + return body + + +def svg_to_png(svg_text: str, out_path: Path, size: int) -> bool: + """SVG → PNG,降级链:cairosvg → svglib+reportlab → 失败。""" + # 路径 1: cairosvg (推荐,质量最好) + try: + import cairosvg # type: ignore + cairosvg.svg2png( + bytestring=svg_text.encode("utf-8"), + write_to=str(out_path), + output_width=size, + output_height=size, + ) + return True + except ImportError: + pass + except Exception as e: + print(f"[warn] cairosvg 渲染失败: {e}", file=sys.stderr) + + # 路径 2: svglib + reportlab + try: + from svglib.svglib import svg2rlg # type: ignore + from reportlab.graphics import renderPM # type: ignore + + drawing = svg2rlg(io.StringIO(svg_text)) + if drawing is None: + return False + renderPM.drawToFile(drawing, str(out_path), fmt="PNG") + return True + except ImportError: + pass + except Exception as e: + print(f"[warn] svglib 渲染失败: {e}", file=sys.stderr) + + return False + + +def main() -> int: + ap = argparse.ArgumentParser( + description="从 Iconify CDN 拉个性化 SVG/PNG 图标" + ) + ap.add_argument("name", help="图标名,见 https://icon-sets.iconify.design/") + ap.add_argument( + "--set", default="tabler", + help="图标集 (默认 tabler;可选 lucide/heroicons/material-symbols/carbon/fluent/mdi)", + ) + ap.add_argument( + "--color", default="C00000", + help="主题色 hex,无 # (默认 C00000 商务红主色)", + ) + ap.add_argument( + "--size", type=int, default=128, + help="像素 (默认 128,适合 0.5-1.0 in PPT 图标)", + ) + ap.add_argument( + "-o", "--out", required=True, type=Path, + help="输出路径 (.png 或 .svg)", + ) + ap.add_argument( + "--svg-only", action="store_true", + help="只输出 SVG,跳过 PNG 转换", + ) + args = ap.parse_args() + + args.out.parent.mkdir(parents=True, exist_ok=True) + + try: + svg = fetch_svg(args.name, args.set, args.color, args.size) + except urllib.error.HTTPError as e: + print( + f"[error] Iconify 返回 {e.code}: 图标 '{args.set}:{args.name}' " + f"可能不存在,在 https://icon-sets.iconify.design/{args.set}/ 搜", + file=sys.stderr, + ) + return 2 + except Exception as e: + print(f"[error] 拉取失败: {e}", file=sys.stderr) + return 2 + + if "