feat(ppt): 视觉系统升级为卡片式 —— 治"生成效果不太行"
学 hugohe3/ppt-master 后定位根因:被 python-pptx 原语(平矩形+左色条+ 圆点 bullet)摁死视觉天花板。选路径 B(升级 python-pptx 设计系统,保留 单脚本批量架构、原生可编辑)。 - pptx_helpers: 加 add_card(圆角+柔和投影)/add_gradient_rect/add_kpi/ add_icon_tile/add_pill/add_eyebrow/add_chevron/add_notes;set_palette 派生明暗色阶 WASH/SOFT/DARK;apply_brand 封面/章节改渐变大色块; 所有 helper 把 name= 写进形状 .name(原来只喂 assert_inside) - layouts.md: 9 版式重写成卡片式 + 扩到 13 种(KPI 卡/卡片网格/流程/大数字) - quality_check: 跟新设计语言对齐 —— 三色制按色相归桶、标签按 .name 豁免 小字号/bullet、≥40pt 展示字跳过溢出估算、bullet≤5 改按列判 - SKILL.md: opt-in 真实配图(imagegen, ¥0.22/张)+ 每页演讲者备注 - design_principles/SKILL_LIST/PROGRESS 同步 验证:13 版式全覆盖 demo + 6 页样例 deck quality_check 全过;单列 6-bullet 回归仍触发。未动 SVG 路线/live preview/动画(更大工程)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b95c247971
commit
c0169e7766
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-06-08
|
### 2026-06-08
|
||||||
|
|
||||||
|
- **ppt skill 视觉系统升级为「卡片式」(治"生成效果不太行")**:学 GitHub `hugohe3/ppt-master`(24.9k★)后定位根因——其好看的核心是「SVG 作画→转原生 PPTX」给足设计自由度,而 zcbot 被 python-pptx **原语**(平矩形+左色条+圆点 bullet)摁死了视觉天花板,出来就是"2010 办公模板"。岔路三选(A 自建 SVG→pptx 转换器=最高天花板但大工程且与"一脚本整建/少来回"冲突;B 升级 python-pptx 设计系统;C 混合),**选 B**(保留单脚本批量架构、原生可编辑、风险低)。落地:① `pptx_helpers.py` 加质感件——`add_card`(圆角矩形 `adjustments[0]` 调圆角 + `a:outerShdw` XML 柔和投影)/`add_gradient_rect`(`fill.gradient()` + 角度)/`add_kpi`(数字卡)/`add_icon_tile`(图标底块)/`add_pill`/`add_eyebrow`/`add_chevron`/`add_notes`(演讲者备注),`set_palette` 从主/辅/强调**派生明暗色阶** `PRIMARY_WASH/SOFT/DARK`+`ACCENT_SOFT`;`apply_brand` 封面/章节改**渐变大色块**;**所有 helper 把 `name=` 写进形状 `.name`**(原来只喂 assert_inside,导致 quality_check 拿不到语义名)。② `layouts.md` 9 版式重写成卡片式 + 扩到 **13 种**(加 L10 KPI 卡 / L11 卡片网格 / L12 流程 / L13 大数字论据)。③ **quality_check 跟新设计语言对齐**(否则每个 deck 淹在假警告里):三色制改**按色相归桶**判(主色深浅/wash tint 不算新色)、小字号/bullet 按 `.name` 豁免标签类、大号展示字(≥40pt)跳过溢出估算、bullet ≤5 改**按列**判(双栏 3+3 不误报、单列 6 仍抓)。④ SKILL.md 工作流加 opt-in 真实配图(走 imagegen,¥0.22/张,大纲标 `[img]`)+ 每页 `add_notes`;`design_principles.md` 加派生色阶/KPI 卡/图表透明底卡片化。验证:13 版式全覆盖 demo deck 建成 + quality_check 全过;单列 6-bullet 回归仍触发。改 `skills/ppt/{SKILL.md,scripts/pptx_helpers.py,scripts/quality_check.py,references/layouts.md,references/design_principles.md}` + `SKILL_LIST.md`。**未动**:SVG 路线(A)、live preview、动画——属更大工程,本轮不上。
|
||||||
- **system prompt 加「少来回」全局原则(广谱减轮)**:ppt 之外的长尾 task(改代码/跑数据/画图)没专属 skill 兜,加一条通用 `工作原则`:互相独立、不依赖中间结果的操作(建多页产物/批量改文件/生成整份产物)合到一个脚本或一轮并发 tool call 里做,别一步一 call(每轮重发整段上下文,轮数=token 体量线性乘数);但下一步输入要看上一步结果时(探索检索/按报错改/需用户确认)就老实分步,别硬批——精准措辞避免"过度批处理"踩掉该有的 checkpoint。定位是便宜补充(prompt 走缓存近零成本),不指望它动 100+ 轮大头(那靠结构改造)。改 `prompts/system/general_v1.md`。
|
- **system prompt 加「少来回」全局原则(广谱减轮)**:ppt 之外的长尾 task(改代码/跑数据/画图)没专属 skill 兜,加一条通用 `工作原则`:互相独立、不依赖中间结果的操作(建多页产物/批量改文件/生成整份产物)合到一个脚本或一轮并发 tool call 里做,别一步一 call(每轮重发整段上下文,轮数=token 体量线性乘数);但下一步输入要看上一步结果时(探索检索/按报错改/需用户确认)就老实分步,别硬批——精准措辞避免"过度批处理"踩掉该有的 checkpoint。定位是便宜补充(prompt 走缓存近零成本),不指望它动 100+ 轮大头(那靠结构改造)。改 `prompts/system/general_v1.md`。
|
||||||
- **ppt skill 工作流批量化(减高轮数 task 的来回)**:实测高成本 task 几乎全是 100+ 轮的"逐步 tool 调用循环"(rust→PPT 34 轮、文献采集 245 轮),每轮重发整段上下文,轮数是 token 体量的线性乘数。ppt 是最易压、风险最低的试点:原 §阶段二**逐页**(每页 `读spec→glob图标→一个 run_python 加页→等用户确认→下一页`,N 页 ~2N 轮)。改法:① 阶段一 spec 增「逐页大纲」表(页|版式|标题|要点|图标),作为**替代逐页确认的前置 checkpoint**——改文字大纲比建完 slide 再推翻便宜;② 阶段二改成**写一个 `build_deck.py` 一次建整 deck**(同进程 `new_presentation`→按大纲循环 `add_slide`→一次 `save`,坐标天然一致;`pptx_helpers` 模块化已消解原"逐页防漂移"理由),图标**全 deck 批量预取**(不逐页拉);③ quality_check 一次 → 改脚本重跑(不 edit 成品);④ 可选"风格探针"(先建封面+1 页看观感)兜视觉返工险。N 页从 ~2N 轮降到 ~3-4 轮。改 `skills/ppt/SKILL.md`(阶段一/二/三 + 反模式 + 文件树)、`references/layouts.md`(§通用起手换成"整 deck 单脚本"模板)、`SKILL_LIST.md`(流程描述/典型产物同步)。冒烟过:单脚本 `new_presentation`+循环 `add_slide`+`save` 建 2 页成功,API 调用与模板一致。**注**:数据采集类(host 工具中转免不了)是另一条路(拆采集/处理相位),未动。
|
- **ppt skill 工作流批量化(减高轮数 task 的来回)**:实测高成本 task 几乎全是 100+ 轮的"逐步 tool 调用循环"(rust→PPT 34 轮、文献采集 245 轮),每轮重发整段上下文,轮数是 token 体量的线性乘数。ppt 是最易压、风险最低的试点:原 §阶段二**逐页**(每页 `读spec→glob图标→一个 run_python 加页→等用户确认→下一页`,N 页 ~2N 轮)。改法:① 阶段一 spec 增「逐页大纲」表(页|版式|标题|要点|图标),作为**替代逐页确认的前置 checkpoint**——改文字大纲比建完 slide 再推翻便宜;② 阶段二改成**写一个 `build_deck.py` 一次建整 deck**(同进程 `new_presentation`→按大纲循环 `add_slide`→一次 `save`,坐标天然一致;`pptx_helpers` 模块化已消解原"逐页防漂移"理由),图标**全 deck 批量预取**(不逐页拉);③ quality_check 一次 → 改脚本重跑(不 edit 成品);④ 可选"风格探针"(先建封面+1 页看观感)兜视觉返工险。N 页从 ~2N 轮降到 ~3-4 轮。改 `skills/ppt/SKILL.md`(阶段一/二/三 + 反模式 + 文件树)、`references/layouts.md`(§通用起手换成"整 deck 单脚本"模板)、`SKILL_LIST.md`(流程描述/典型产物同步)。冒烟过:单脚本 `new_presentation`+循环 `add_slide`+`save` 建 2 页成功,API 调用与模板一致。**注**:数据采集类(host 工具中转免不了)是另一条路(拆采集/处理相位),未动。
|
||||||
- **修进度还原错乱 + 进度区移到对话区顶部(codex 式)**:根因(查 DB 实锤)= 上下文压缩把旧 `task_progress` tool_call 参数换成 `{"_compacted":true,"step_id":"sX"}` 这种"看着像合法调用"的标记,① 毒化模型让它后续照抄出残废 `update_step`(丢了 `step.status`)并入库,② 残废格式前端 `applyProgressAction` 读不到 `args.step` → s4/s5 永停 pending → 进度显示不对。修复:`context.py` 对 `task_progress` 参数**一律不压缩**(参数本就小,压缩省不了几个 token 却两头坏事);旧的 `_compact_task_progress_arguments` 整个删除。**进度展示重构**:删掉每条消息卡内联进度块(`renderProgressHtml`/`renderProgressInto` 移除),进度统一只在**对话区顶部**单一 `#task-progress-dock`(从 composer 上方移到 `chat-stream` 之上、`flex-shrink:0` 钉顶不滚)实时显示;**完成态折叠**——全部步骤 completed 时 dock 自动收成一行 `✓ 全部完成 · N/N 步`(`<details>` 点开看清单)。prompt + tool 描述改为"跑完把最后一步标 `completed`、不要 `clear`",留住全绿收尾。校验:`python -m unittest tests.test_context_compaction tests.test_task_progress_tool`(12 过,改写 `test_keeps_old_task_progress_arguments_intact` 断言参数原样保留);`node --test tests/frontend_task_progress.test.mjs`(2 过)。
|
- **修进度还原错乱 + 进度区移到对话区顶部(codex 式)**:根因(查 DB 实锤)= 上下文压缩把旧 `task_progress` tool_call 参数换成 `{"_compacted":true,"step_id":"sX"}` 这种"看着像合法调用"的标记,① 毒化模型让它后续照抄出残废 `update_step`(丢了 `step.status`)并入库,② 残废格式前端 `applyProgressAction` 读不到 `args.step` → s4/s5 永停 pending → 进度显示不对。修复:`context.py` 对 `task_progress` 参数**一律不压缩**(参数本就小,压缩省不了几个 token 却两头坏事);旧的 `_compact_task_progress_arguments` 整个删除。**进度展示重构**:删掉每条消息卡内联进度块(`renderProgressHtml`/`renderProgressInto` 移除),进度统一只在**对话区顶部**单一 `#task-progress-dock`(从 composer 上方移到 `chat-stream` 之上、`flex-shrink:0` 钉顶不滚)实时显示;**完成态折叠**——全部步骤 completed 时 dock 自动收成一行 `✓ 全部完成 · N/N 步`(`<details>` 点开看清单)。prompt + tool 描述改为"跑完把最后一步标 `completed`、不要 `clear`",留住全绿收尾。校验:`python -m unittest tests.test_context_compaction tests.test_task_progress_tool`(12 过,改写 `test_keeps_old_task_progress_arguments_intact` 断言参数原样保留);`node --test tests/frontend_task_progress.test.mjs`(2 过)。
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
||||||
### ppt
|
### ppt
|
||||||
**生成 PowerPoint 演示文稿 (.pptx)。**
|
**生成 PowerPoint 演示文稿 (.pptx)。**
|
||||||
|
|
||||||
把材料(汇报草稿 / 项目方案 / 调研报告)变成可演示的 .pptx。流程:**先定调(8 项 + 逐页大纲)→ 一个脚本建整 deck → quality_check 验收**。方向在大纲阶段对齐,执行阶段一把出稿(不逐页来回)。
|
把材料(汇报草稿 / 项目方案 / 调研报告)变成可演示的 .pptx。流程:**先定调(8 项 + 逐页大纲)→ 一个脚本建整 deck → quality_check 验收**。方向在大纲阶段对齐,执行阶段一把出稿(不逐页来回)。视觉走**卡片式系统**(圆角卡片 + 柔和投影 + 渐变 + 从主色派生的明暗色阶),原生可编辑,告别扁平办公模板观感。
|
||||||
|
|
||||||
**触发**:
|
**触发**:
|
||||||
- ✅ 用户明确点名 PPT / 幻灯片 / 演示文稿 / .pptx / slide / deck
|
- ✅ 用户明确点名 PPT / 幻灯片 / 演示文稿 / .pptx / slide / deck
|
||||||
|
|
@ -157,14 +157,16 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
|
||||||
| 4 | 风格 | 现代简约(白底 + 细线 + 留白) |
|
| 4 | 风格 | 现代简约(白底 + 细线 + 留白) |
|
||||||
| 5 | 配色 | 商务红 |
|
| 5 | 配色 | 商务红 |
|
||||||
| 6 | 字体 | 微软雅黑 + Arial |
|
| 6 | 字体 | 微软雅黑 + Arial |
|
||||||
| 7 | 图标 | Iconify `tabler` 集(主色染色,本地缓存) |
|
| 7 | 图标 | Iconify `tabler` 集(主色染色,本地缓存;概念页配图标底块) |
|
||||||
| 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 |
|
| 8 | 图表 / 配图 | 数据图 matplotlib / 少量数字上 KPI 卡;真实配图 opt-in 走 imagegen(每张 ¥0.22) |
|
||||||
|
|
||||||
**核心能力**:
|
**核心能力**:
|
||||||
- 9 种版式起手代码(封面 / 大标题 / 双栏 / 三栏 / 时间线 / 表格 / 图文混排 / 引子 / 尾页)
|
- **13 种卡片式版式**(封面 / 目录 / 章节 / 要点 / 双栏 / 图表 / 图片 / 金句 / 尾页 + KPI 数字卡 / 卡片网格 / 流程图 / 大数字论据)
|
||||||
|
- **质感工具箱** `pptx_helpers.py`:`add_card`(圆角+投影)/ `add_gradient_rect`(渐变)/ `add_kpi` / `add_icon_tile` / `add_pill` / 从主色自动派生明暗色阶
|
||||||
|
- 演讲者备注 `add_notes`(每页口述要点)
|
||||||
- 业务图标双层兜底(Iconify CDN 拉 SVG → 本地缓存 → unicode 字形)
|
- 业务图标双层兜底(Iconify CDN 拉 SVG → 本地缓存 → unicode 字形)
|
||||||
- `apply_brand` 品牌条 + 安全区 / 越界保护
|
- `apply_brand` 品牌锚点(封面/章节渐变)+ 安全区 / 越界保护
|
||||||
- `quality_check.py` 验收(越界 / 文本溢出 / 颜色一致)
|
- `quality_check.py` 验收(越界 / 文本溢出 / 按列 bullet ≤5 / 按色系三色制 / 内容重叠)
|
||||||
- 素材摄取走 markitdown 把 PDF/DOCX/PPTX/XLSX/HTML/URL 统一转 Markdown
|
- 素材摄取走 markitdown 把 PDF/DOCX/PPTX/XLSX/HTML/URL 统一转 Markdown
|
||||||
|
|
||||||
**典型产物**:`<task>.pptx` + `build_deck.py`(整 deck 构建脚本,改稿/修验收项都改它重跑)。
|
**典型产物**:`<task>.pptx` + `build_deck.py`(整 deck 构建脚本,改稿/修验收项都改它重跑)。
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx) 文件。✅ 触发:用户
|
||||||
进度展示建议:多页 deck 任务用 `task_progress` 标记「摄取素材 / 八条对齐 + 逐页大纲 / 图标预取 / 脚本建 deck / 质量检查 / 交付」等关键阶段;不要把每一页的内部写入都作为进度步骤。
|
进度展示建议:多页 deck 任务用 `task_progress` 标记「摄取素材 / 八条对齐 + 逐页大纲 / 图标预取 / 脚本建 deck / 质量检查 / 交付」等关键阶段;不要把每一页的内部写入都作为进度步骤。
|
||||||
|
|
||||||
## 资源
|
## 资源
|
||||||
- `scripts/pptx_helpers.py` —— **版式工具箱模块**:配色/字体常量 + `new_presentation`/`load`/`add_slide`/`set_palette` + `add_textbox`/`add_rect`/`add_dot`/`add_badge`/`page_title`/`apply_brand` 等 helper。每页 `import pptx_helpers as P` 调用,**不要把 helper 源码默写进 run_python**
|
- `scripts/pptx_helpers.py` —— **卡片式视觉工具箱模块**:配色/字体常量 + 从主色派生的明暗色阶(`PRIMARY_WASH/SOFT/DARK`)+ `new_presentation`/`load`/`set_palette` + 质感件 `add_card`(圆角卡片+柔和投影)/`add_gradient_rect`(渐变)/`add_kpi`(数字卡)/`add_icon_tile`(图标底块)/`add_pill`(胶囊标签)/`add_eyebrow` + `add_notes`(演讲者备注)+ 基础件 `add_textbox`/`add_dot`/`add_badge`/`page_title`/`apply_brand`。`import pptx_helpers as P` 调用,**不要把 helper 源码默写进脚本**。⚠️ helper 的 `name=` 会写进形状名,quality_check 靠它判标签/bullet —— 自己加元素时按 layouts.md 的命名习惯起名
|
||||||
- `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则
|
- `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则
|
||||||
- `references/layouts.md` —— 9 种版式的调用示例(基于 `pptx_helpers`)+ helper API 速查 + 安全区/越界保护 + `apply_brand` 品牌条
|
- `references/layouts.md` —— 9 种版式的调用示例(基于 `pptx_helpers`)+ helper API 速查 + 安全区/越界保护 + `apply_brand` 品牌条
|
||||||
- `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底
|
- `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底
|
||||||
|
|
@ -66,20 +66,23 @@ glob <task_dir>/*-<task_short_id>-*.spec.md → 按文件名字典序排,取最
|
||||||
| 4 | 风格 | **现代简约** (白底 + 细线 + 留白) |
|
| 4 | 风格 | **现代简约** (白底 + 细线 + 留白) |
|
||||||
| 5 | 配色 | **商务红** `#C00000` `#E15554` `#FFC107` (见上"默认主题") |
|
| 5 | 配色 | **商务红** `#C00000` `#E15554` `#FFC107` (见上"默认主题") |
|
||||||
| 6 | 字体 | **微软雅黑 + Arial** |
|
| 6 | 字体 | **微软雅黑 + Arial** |
|
||||||
| 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `<task_dir>/assets/icons/`) |
|
| 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `<task_dir>/assets/icons/`;业务概念页用 `add_icon_tile` 配图标底块) |
|
||||||
| 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 |
|
| 8 | 图表 / 配图 | 数据 ≥ 3 个点 → matplotlib 图(或 ≤4 个数字直接上 KPI 卡 L10);**真实配图 opt-in**:封面/章节/图片页可走 imagegen 生图(**每张 ¥0.22**,默认不开,要用在大纲里标 `[img]` 并经用户确认) |
|
||||||
|
|
||||||
把这 8 项写进上面那个 task 级 spec 文件,以表格形式给用户预览,问一句"按这个开干?"。**spec 写定后不再改**(要改就走 §0 的「重定调」分支,以 today 为前缀写新版,旧版保留)。
|
把这 8 项写进上面那个 task 级 spec 文件,以表格形式给用户预览,问一句"按这个开干?"。**spec 写定后不再改**(要改就走 §0 的「重定调」分支,以 today 为前缀写新版,旧版保留)。
|
||||||
|
|
||||||
**8 项之外,spec 还要含一张「逐页大纲」表** —— 这是阶段二一个脚本建整 deck 的输入,也是替代"逐页确认"的前置 checkpoint(改一行文字大纲,比建完一页 slide 再推翻便宜得多):
|
**8 项之外,spec 还要含一张「逐页大纲」表** —— 这是阶段二一个脚本建整 deck 的输入,也是替代"逐页确认"的前置 checkpoint(改一行文字大纲,比建完一页 slide 再推翻便宜得多):
|
||||||
|
|
||||||
| 页 | 版式 | 标题 | 核心信息 / 要点(≤5) | 图标 / 图表 |
|
| 页 | 版式 | 标题 | 核心信息 / 要点(≤5) | 图标 / 图表 / 配图 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 1 | L1 封面 | <主标题> | <副标题 / 定位> | — |
|
| 1 | L1 封面 | <主标题> | <副标题 / 定位> | 可选 `[img]` 主图 |
|
||||||
| 2 | L4 三栏 | <标题> | <要点 1 / 2 / 3> | `target` / `cpu` / `chart-bar` |
|
| 2 | L11 卡片网格 | <标题> | <要点 1 / 2 / 3> | `target` / `cpu` / `chart-bar` |
|
||||||
|
| 3 | L10 KPI 卡 | <标题> | <数字 1 / 2 / 3 / 4> | — |
|
||||||
| … | … | … | … | … |
|
| … | … | … | … | … |
|
||||||
| N | L9 尾页 | Q&A / 致谢 | <联系方式> | — |
|
| N | L9 尾页 | Q&A / 致谢 | <联系方式> | — |
|
||||||
|
|
||||||
|
> 版式从 layouts.md 的 L1-L13 里选(见 §选版式速查):**业务概念优先 L11 卡片网格(图标底块,别只摆圆点)**,**2-4 个关键数字优先 L10 KPI 卡(别硬画柱图)**,**单个震撼数字用 L13**。要真实配图的页在「图标/图表/配图」列标 `[img]` + 一句画面描述。
|
||||||
|
|
||||||
大纲连同 8 项一起给用户预览,**BLOCKING 等用户确认整份结构**(页数、每页讲什么、用什么版式/图标)后再进阶段二。用户在这一步推翻方向 = 改表格文字,零 slide 返工。
|
大纲连同 8 项一起给用户预览,**BLOCKING 等用户确认整份结构**(页数、每页讲什么、用什么版式/图标)后再进阶段二。用户在这一步推翻方向 = 改表格文字,零 slide 返工。
|
||||||
|
|
||||||
### 阶段二: 执行 (Executor) — 一个脚本建整 deck
|
### 阶段二: 执行 (Executor) — 一个脚本建整 deck
|
||||||
|
|
@ -89,10 +92,11 @@ glob <task_dir>/*-<task_short_id>-*.spec.md → 按文件名字典序排,取最
|
||||||
流程:
|
流程:
|
||||||
1. **读 current spec**(按 §0 的 glob 规则拿字典序最大那份),含 8 项 + 逐页大纲;只用里面定的颜色/字体/图标/页结构,**不凭记忆发挥**。
|
1. **读 current spec**(按 §0 的 glob 规则拿字典序最大那份),含 8 项 + 逐页大纲;只用里面定的颜色/字体/图标/页结构,**不凭记忆发挥**。
|
||||||
2. **图标批量预取(全 deck 一次,不逐页)**: 把大纲里所有页需要的图标概念汇总,`glob` 两处看现成 —— 种子库 `<skill_dir>/assets/icons/`(只读)+ 本 task `<task_dir>/assets/icons/`;缺的在**一个 `run_python` 里批量** `fetch_icon.py <name> --set tabler --color C00000 --size 128 -o <task_dir>/assets/icons/...` 拉齐。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper**。
|
2. **图标批量预取(全 deck 一次,不逐页)**: 把大纲里所有页需要的图标概念汇总,`glob` 两处看现成 —— 种子库 `<skill_dir>/assets/icons/`(只读)+ 本 task `<task_dir>/assets/icons/`;缺的在**一个 `run_python` 里批量** `fetch_icon.py <name> --set tabler --color C00000 --size 128 -o <task_dir>/assets/icons/...` 拉齐。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper**。
|
||||||
3. **写 `build_deck.py` 到 `<task_dir>`,一次建整 deck**: 顶部 `import pptx_helpers as P`(`sys.path` 指到 `<skill_dir>/scripts`)→ `prs = P.new_presentation(...)` → `P.set_palette(spec_path=...)` → **按大纲循环每页**(`P.add_slide` + 各 helper,每页一个小函数 `page_1(prs)`…清晰)→ 末尾一次 `prs.save(...)`。**helper 一律 `P.xxx`,不默写源码**。起手见 `layouts.md §通用起手(整 deck 单脚本)`。先 `write` 脚本文件再 `run_python(script_path=...)` 跑(避免大段源码进对话历史)。
|
3. **真实配图(opt-in,仅当大纲标了 `[img]`)**: 把标 `[img]` 的页(封面/章节/图片页)汇总,**load `imagegen` skill 走它自己的确认流程**逐张生成(每张 ¥0.22,有强制确认门,不要绕过),产物落 `<task_dir>/figures/`;build_deck 里 `add_picture(<figures 路径>)` 引用。**没标 `[img]` 的 deck 跳过这步**,图标/卡片/渐变已足够撑视觉。
|
||||||
4. **quality_check 一次**(见阶段三)→ 按报告**改 `build_deck.py` 重跑**(不要逐页 edit 成品 .pptx —— 改源脚本可复现、可再跑)。
|
4. **写 `build_deck.py` 到 `<task_dir>`,一次建整 deck**: 顶部 `import pptx_helpers as P`(`sys.path` 指到 `<skill_dir>/scripts`)→ `prs = P.new_presentation(...)` → `P.set_palette(spec_path=...)` → **按大纲循环每页**(`P.add_slide` + `P.apply_brand` + 各 helper,每页一个小函数 `page_1(prs)`…清晰;**每页 `P.add_notes(slide, ...)` 写 2-4 句演讲者口述要点**)→ 末尾一次 `prs.save(...)`。**helper 一律 `P.xxx`,不默写源码**;版式照 layouts.md L1-L13 选,**业务概念用 L11 卡片网格、数字用 L10 KPI 卡**。起手见 `layouts.md §通用起手`。先 `write` 脚本再 `run_python(script_path=...)`(避免大段源码进对话历史)。
|
||||||
5. 报整份 deck:页数、各页版式、用到的图标;问用户要不要改。
|
5. **quality_check 一次**(见阶段三)→ 按报告**改 `build_deck.py` 重跑**(不要逐页 edit 成品 .pptx —— 改源脚本可复现、可再跑)。
|
||||||
6. 用户确认了**实质改动**(改版式 / 换图标 / 改文案要点 / 增删页 / 调主色)后,追加一行到 `<task_dir>/REVISIONS.md` —— 见 §修订日志。
|
7. 报整份 deck:页数、各页版式、用到的图标/配图;问用户要不要改。
|
||||||
|
8. 用户确认了**实质改动**(改版式 / 换图标 / 改文案要点 / 增删页 / 调主色)后,追加一行到 `<task_dir>/REVISIONS.md` —— 见 §修订日志。
|
||||||
|
|
||||||
**风格探针(可选,降视觉返工险)**: 用户对观感没底、或这是全新风格时,可先只建**封面 + 1 内页**给用户看一眼,确认后把 `build_deck.py` 的页范围放开重跑补齐其余页 —— 仍是改一个脚本,不退回逐页。用户要快("直接全做")就跳过探针,整 deck 一把出。
|
**风格探针(可选,降视觉返工险)**: 用户对观感没底、或这是全新风格时,可先只建**封面 + 1 内页**给用户看一眼,确认后把 `build_deck.py` 的页范围放开重跑补齐其余页 —— 仍是改一个脚本,不退回逐页。用户要快("直接全做")就跳过探针,整 deck 一把出。
|
||||||
|
|
||||||
|
|
@ -108,15 +112,18 @@ python <skill_dir>/scripts/quality_check.py <task_dir>/<output.pptx> --spec <tas
|
||||||
|
|
||||||
## 设计原则 (硬规则速查)
|
## 设计原则 (硬规则速查)
|
||||||
- **每页一个核心信息**: 一页讲一件事,塞两件就拆页
|
- **每页一个核心信息**: 一页讲一件事,塞两件就拆页
|
||||||
- **bullet ≤ 5 条**: 超过就拆页或改成图表/双栏
|
- **内容装进卡片**: 内容页主力容器是 `add_card`(圆角+柔和投影),白底之上靠卡片浮起分层,别让元素裸贴白纸
|
||||||
- **正文不写完整段落**: 列要点;长句留给演讲者口述
|
- **概念配图标底块**: 业务概念(能力/模块/策略)用 L11 卡片网格 + `add_icon_tile`,**别只摆圆点 bullet**(视觉太单薄)
|
||||||
- **数据 ≥ 3 个点应有图表**: 用 matplotlib 生成 .png 嵌入
|
- **数字上 KPI 卡**: 2-4 个关键数字用 L10 `add_kpi`,优先于硬画柱状图;单个震撼数字用 L13
|
||||||
|
- **bullet ≤ 5 条/列**: 单列超过就拆页或改卡片网格;双栏对比左右各 ≤5
|
||||||
|
- **正文不写完整段落**: 列要点;长句留给演讲者口述(写进 `add_notes`)
|
||||||
|
- **数据 ≥ 3 个点应有图表**: matplotlib 生成 .png 嵌入(或转 KPI 卡)
|
||||||
- **中文标题 ≤ 30 字**
|
- **中文标题 ≤ 30 字**
|
||||||
- **配色三色封顶**: 主 + 辅 + 强调,其他用灰阶
|
- **配色三色封顶 + 派生阶**: 主 + 辅 + 强调三色系,浅底/卡片底走 `set_palette` 自动派生的 `PRIMARY_WASH/SOFT`,不算新色
|
||||||
- **少用大色块,多用细线 + 图标 + 留白**
|
- **渐变只用在大色块**: 封面/章节用 `apply_brand` 内置渐变;渐变深底上文字一律用白/`ACCENT_SOFT`
|
||||||
- **图标走 MSO_SHAPE**: 矢量、可编辑;复杂图标走 `render_icon.py`
|
- **每页演讲者备注**: `add_notes` 写 2-4 句口述要点(正式产物标配)
|
||||||
- **Shape 不能越界**: `layouts.md` 起手代码用 `assert_inside` 在生成时即报错
|
- **Shape 不能越界**: helper 内置 `assert_inside` 生成时即报错
|
||||||
- **字数按预算来**: 写 bullet 前查 `design_principles.md §4.1` 字数预算表
|
- **字数按预算来**: 写 bullet 前查 `design_principles.md §4.1` 字数预算表;卡片内按"卡宽 - 0.8"算框宽
|
||||||
- 详细规则见 `references/design_principles.md`
|
- 详细规则见 `references/design_principles.md`
|
||||||
|
|
||||||
## 工作目录约定
|
## 工作目录约定
|
||||||
|
|
@ -127,7 +134,9 @@ python <skill_dir>/scripts/quality_check.py <task_dir>/<output.pptx> --spec <tas
|
||||||
<task_dir>/
|
<task_dir>/
|
||||||
├── source/ # markitdown 转出的素材(同 working_dir 多 task 共享;用 markitdown -o <task_dir>/source/<name>.md)
|
├── source/ # markitdown 转出的素材(同 working_dir 多 task 共享;用 markitdown -o <task_dir>/source/<name>.md)
|
||||||
├── <today>-<task_short_id>-<task_name>.spec.md # 八条对齐落定,task 级宪法;命名见 system prompt 约定;按 short_id 主锚,重定调时写新日期,旧版保留
|
├── <today>-<task_short_id>-<task_name>.spec.md # 八条对齐落定,task 级宪法;命名见 system prompt 约定;按 short_id 主锚,重定调时写新日期,旧版保留
|
||||||
├── slides/ # 各页用到的图片素材 (chart_p3.png 等),多 task 时文件名前缀区分
|
├── slides/ # 各页 matplotlib 图表 (chart_p3.png 等),多 task 时文件名前缀区分
|
||||||
|
├── figures/ # imagegen 生成的真实配图 (opt-in;封面/章节主图),由 imagegen skill 落盘
|
||||||
|
├── assets/icons/ # fetch_icon.py 新拉的主题色图标(种子库在 skill 只读侧)
|
||||||
├── build_deck.py # 整 deck 构建脚本(一次建完所有页);改稿/修 quality_check 项都改它重跑
|
├── build_deck.py # 整 deck 构建脚本(一次建完所有页);改稿/修 quality_check 项都改它重跑
|
||||||
├── REVISIONS.md # 修订日志:每次卡点用户确认的实质改动,见 §修订日志
|
├── REVISIONS.md # 修订日志:每次卡点用户确认的实质改动,见 §修订日志
|
||||||
└── <topic>.pptx # 最终产物 (按主题命名,多 task 时主题必须不同)
|
└── <topic>.pptx # 最终产物 (按主题命名,多 task 时主题必须不同)
|
||||||
|
|
@ -185,7 +194,10 @@ python <skill_dir>/scripts/quality_check.py <task_dir>/<output.pptx> --spec <tas
|
||||||
- **基于"场景判断"自行换配色**(见上"默认主题"违规清单)
|
- **基于"场景判断"自行换配色**(见上"默认主题"违规清单)
|
||||||
- **缺封面 / 缺尾页(Q&A)** —— 两端都是强制项,不算在正文页数预算内
|
- **缺封面 / 缺尾页(Q&A)** —— 两端都是强制项,不算在正文页数预算内
|
||||||
- **裸白纸版式** —— 所有版式起手都必须 `apply_brand(slide, kind)`,见 layouts.md
|
- **裸白纸版式** —— 所有版式起手都必须 `apply_brand(slide, kind)`,见 layouts.md
|
||||||
- **业务概念页只用几何形状** —— 比如"战略目标"页只摆圆点 bullet 没有 `target` 图标,视觉太单薄;按 §阶段二第 2 步先拉 Iconify 图标再做页
|
- **业务概念页只用几何形状 / 裸圆点 bullet** —— "战略目标 / 三大能力"这类页摆光圆点没图标没卡片,视觉太单薄;用 L11 卡片网格 + `add_icon_tile`,图标按 §阶段二第 2 步先拉
|
||||||
|
- **数字页硬画柱图** —— 只有 2-4 个数字却画 bar chart 浪费版面,用 L10 KPI 卡
|
||||||
|
- **元素裸贴白纸不进卡片** —— 内容页一坨文字/图标直接铺白底,显扁平;装进 `add_card`(自带投影)分层
|
||||||
|
- **演讲者备注全空** —— 正式产物每页应有口述要点,`add_notes` 顺手写,别交白板
|
||||||
- **逐页 run_python 建 deck**(每页一轮来回烧轮数;改用一个 `build_deck.py` 整建,方向风险靠阶段一大纲 + 可选探针兜)
|
- **逐页 run_python 建 deck**(每页一轮来回烧轮数;改用一个 `build_deck.py` 整建,方向风险靠阶段一大纲 + 可选探针兜)
|
||||||
- **没经阶段一大纲对齐就直接整建** —— 大纲是替代逐页确认的 checkpoint,跳过它整建才会"改方向全推翻"
|
- **没经阶段一大纲对齐就直接整建** —— 大纲是替代逐页确认的 checkpoint,跳过它整建才会"改方向全推翻"
|
||||||
- 跑完不做 `quality_check.py` 就交付
|
- 跑完不做 `quality_check.py` 就交付
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,19 @@
|
||||||
| 现代简约 | #2D3748 | #4A5568 | #38B2AC | 互联网/SaaS |
|
| 现代简约 | #2D3748 | #4A5568 | #38B2AC | 互联网/SaaS |
|
||||||
| 科技深色 | #0A192F | #112240 | #64FFDA | 黑客松/技术大会 |
|
| 科技深色 | #0A192F | #112240 | #64FFDA | 黑客松/技术大会 |
|
||||||
|
|
||||||
|
### 派生色阶(卡片式视觉的层次来源)
|
||||||
|
`set_palette` 从主/辅/强调自动派生明暗阶,**这些不算"新色"**(quality_check 按色相归桶,同色系深浅收敛成一个):
|
||||||
|
- `PRIMARY_WASH`(主色兑 92% 白)—— 整页/大区域浅底(尾页、L13 论据卡)
|
||||||
|
- `PRIMARY_SOFT`(兑 80% 白)—— 卡片/图标底块/标签浅底
|
||||||
|
- `PRIMARY_DARK`(主色压暗)—— 封面/章节渐变深端
|
||||||
|
- `ACCENT_SOFT`(强调兑 78% 白)—— 渐变深底上的弱化文字
|
||||||
|
> 白底之上靠卡片(`add_card` 圆角+投影)+ 浅色阶分层,才有"现代咨询风"的层次;纯白底裸贴元素 = 扁平办公模板。
|
||||||
|
|
||||||
### 禁忌
|
### 禁忌
|
||||||
- 红配绿、紫配黄等高对比互补色不要直接用
|
- 红配绿、紫配黄等高对比互补色不要直接用
|
||||||
- 渐变只用在 accent 上,正文/标题不要渐变
|
- **渐变只用在大色块**(封面右块 / 章节整页,`apply_brand` 已内置);正文/标题/小图形不要渐变
|
||||||
- 一份 deck 主色不要换。封面是 A 色、内页变 B 色 —— 这是大忌
|
- 一份 deck 主色不要换。封面是 A 色、内页变 B 色 —— 这是大忌
|
||||||
|
- 渐变深底上文字一律用**白 / `ACCENT_SOFT`**,别用深灰 `INK`(看不清)
|
||||||
|
|
||||||
## 3. 留白
|
## 3. 留白
|
||||||
|
|
||||||
|
|
@ -67,7 +76,8 @@
|
||||||
| 目录 | 每条 ≤ 15 字 | 不要图 |
|
| 目录 | 每条 ≤ 15 字 | 不要图 |
|
||||||
| 分章页 | ≤ 20 字 | 大号数字 + 章节名 |
|
| 分章页 | ≤ 20 字 | 大号数字 + 章节名 |
|
||||||
| 要点页 | bullet ≤ 5 条,每条 ≤ 25 字 | 可选小图标 |
|
| 要点页 | bullet ≤ 5 条,每条 ≤ 25 字 | 可选小图标 |
|
||||||
| 数据页 | 标题 + 一句结论 | **必须有图表** |
|
| 数据页 | 标题 + 一句结论 | **必须有图表**;2-4 个数字优先 KPI 卡(L10)而非柱图 |
|
||||||
|
| 概念页 | 卡片标题 ≤6 字 + 说明 ≤2 行 | 图标底块 + 卡片网格(L11),别裸圆点 |
|
||||||
| 图片页 | ≤ 15 字标题 + 1-2 行说明 | 主体是图 |
|
| 图片页 | ≤ 15 字标题 + 1-2 行说明 | 主体是图 |
|
||||||
|
|
||||||
## 4.1 字数预算 (避免溢出)
|
## 4.1 字数预算 (避免溢出)
|
||||||
|
|
@ -116,10 +126,13 @@
|
||||||
|
|
||||||
## 7. 图表规则 (matplotlib)
|
## 7. 图表规则 (matplotlib)
|
||||||
|
|
||||||
|
> **先问要不要图表**:只有 2-4 个数字 → 用 KPI 卡(layouts L10),别画柱图;真有趋势/分布/多系列才上 matplotlib。图表 png 嵌进 `add_card` 白卡片里(L6)比裸图精致。
|
||||||
|
|
||||||
- 颜色用 spec 里定的主/辅/强调三色,**不要用 matplotlib 默认色板**
|
- 颜色用 spec 里定的主/辅/强调三色,**不要用 matplotlib 默认色板**
|
||||||
- 字号: 标题 16,坐标轴 12,刻度 10
|
- 字号: 标题 16,坐标轴 12,刻度 10
|
||||||
- 去掉上方和右方边框 (`ax.spines['top'/'right'].set_visible(False)`)
|
- **去四边框**,只留极淡横向网格 (`ax.spines[*].set_visible(False)` + `ax.grid(axis='y', color='#EEEEEE', lw=0.8)`)—— 比全框 + 默认网格干净,跟卡片观感一致
|
||||||
- 数据标签直接标在柱子/点上,优先于看坐标
|
- 数据标签直接标在柱子/点上,优先于看坐标
|
||||||
|
- 透明底:`fig.savefig(..., transparent=True)`,嵌白卡片上无白边
|
||||||
- 中文字体: `plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']`
|
- 中文字体: `plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']`
|
||||||
- 负号: `plt.rcParams['axes.unicode_minus'] = False`
|
- 负号: `plt.rcParams['axes.unicode_minus'] = False`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# 9 种常用版式 (16:9, 13.33×7.5 in)
|
# 版式库 (16:9, 13.33×7.5 in) — 卡片式视觉系统
|
||||||
|
|
||||||
> **要点**:版式 helper 已全部收进 `scripts/pptx_helpers.py`,**不要再把 helper 源码默写进每页的 run_python** —— 每页只 `import pptx_helpers as P` 然后调用。这样长 deck 里不会出现第 7 页和第 2 页的 `apply_brand` 坐标对不上的漂移,也省 token。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex —— 通过 `P.set_palette()` 注入,默认商务红。
|
> **要点**:版式 helper 全在 `scripts/pptx_helpers.py`,**不要把 helper 源码默写进 build_deck.py** —— 只 `import pptx_helpers as P` 然后调用。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex,通过 `P.set_palette(spec_path=...)` 注入,默认商务红 + 自动派生明暗色阶。
|
||||||
|
>
|
||||||
|
> **观感升级要点(相对老版"左色条 + 圆点 bullet")**:内容尽量装进**圆角卡片**(`add_card`,自带柔和投影),业务概念配**图标底块**(`add_icon_tile`),数据页优先**KPI 数字卡**(`add_kpi`)而非小柱图,封面/章节用**渐变大色块**(`apply_brand` 已内置)。白底之上靠卡片浮起 + 浅色阶分层,才不是"扁平办公模板"。
|
||||||
|
|
||||||
## 通用起手(整 deck 单脚本 — 默认路径)
|
## 通用起手(整 deck 单脚本 — 默认路径)
|
||||||
|
|
||||||
|
|
@ -10,24 +12,25 @@
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入的绝对路径替换
|
sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入的绝对路径替换
|
||||||
import pptx_helpers as P
|
import pptx_helpers as P
|
||||||
|
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
|
||||||
|
from pptx.enum.shapes import MSO_SHAPE
|
||||||
|
|
||||||
SPEC = "<task_dir>/<today>-<task_short_id>-<task_name>.spec.md"
|
SPEC = "<task_dir>/<today>-<task_short_id>-<task_name>.spec.md"
|
||||||
OUT = "<task_dir>/<topic>.pptx"
|
OUT = "<task_dir>/<topic>.pptx"
|
||||||
|
ICONS = "<task_dir>/assets/icons" # fetch_icon.py 拉到这;种子库在 <skill_dir>/assets/icons
|
||||||
|
|
||||||
def page_1_cover(prs):
|
def page_1_cover(prs):
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "cover")
|
P.apply_brand(s, "cover")
|
||||||
# ... 见 L1 封面 ...
|
# ... 见 L1 封面 ...
|
||||||
|
|
||||||
def page_2(prs):
|
def page_2(prs):
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
# ... 见对应 Lx 版式 ...
|
# ... 见对应 Lx 版式 ...
|
||||||
|
|
||||||
# ... 按大纲补齐 page_3 … page_N ...
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
prs = P.new_presentation("16:9") # 默认 16:9;可传 "4:3" / "9:16" / "3:4"
|
prs = P.new_presentation("16:9") # 默认 16:9;可传 "4:3" / "9:16" / "3:4"
|
||||||
P.set_palette(spec_path=SPEC) # 整 deck 设一次配色(同进程内常驻)
|
P.set_palette(spec_path=SPEC) # 整 deck 设一次配色 + 派生色阶(同进程常驻)
|
||||||
for build in (page_1_cover, page_2, ...): # 按逐页大纲顺序
|
for build in (page_1_cover, page_2, ...): # 按逐页大纲顺序
|
||||||
build(prs)
|
build(prs)
|
||||||
prs.save(OUT)
|
prs.save(OUT)
|
||||||
|
|
@ -35,114 +38,127 @@ def main():
|
||||||
main()
|
main()
|
||||||
```
|
```
|
||||||
|
|
||||||
跑法:先 `write` 这个脚本到 `<task_dir>/build_deck.py`,再 `run_python(script_path="<task_dir>/build_deck.py")`。要改(quality_check 报错 / 用户要调)→ 改脚本里对应 `page_x` 函数,重跑整脚本(可复现,不 edit 成品 .pptx)。
|
跑法:先 `write` 脚本到 `<task_dir>/build_deck.py`,再 `run_python(script_path=...)`。要改(quality_check 报错 / 用户要调)→ 改对应 `page_x` 函数重跑整脚本(可复现,不 edit 成品 .pptx)。
|
||||||
|
|
||||||
> **风格探针 / 增量补页**:要先看封面 + 1 页观感,把 `main()` 的循环临时缩到前 2 个函数跑一遍,确认后放开重跑;或对已存在的 deck 追加单页时用 `prs = P.load(OUT)` 载入再 `add_slide`(`load` 按文件实际尺寸回填画布常量)。**常规整建不用 `load`**。
|
> **风格探针 / 增量补页**:要先看封面 + 1 页观感,把 `main()` 循环临时缩到前 2 个函数跑一遍;或对已存在 deck 追加单页时 `prs = P.load(OUT)` 再 `add_slide`。**常规整建不用 `load`**。
|
||||||
|
|
||||||
⚠️ 一律用 `P.xxx`(不要 `from pptx_helpers import *`)—— `set_palette` 靠改模块属性覆盖配色,`import *` 会把旧绑定拷进页面命名空间导致覆盖不生效。
|
⚠️ 一律用 `P.xxx`(不要 `from pptx_helpers import *`)—— `set_palette` 靠改模块属性覆盖配色,`import *` 会把旧绑定拷进命名空间导致覆盖不生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Helper API 速查 (都在 `P.` 命名空间下)
|
## Helper API 速查 (都在 `P.` 命名空间下)
|
||||||
|
|
||||||
**画布 / 配色入口**
|
**画布 / 配色入口**
|
||||||
- `P.new_presentation(canvas="16:9")` → 建空 deck,设画布,回填 `P.SLIDE_W/H` 与安全区
|
- `P.new_presentation(canvas="16:9")` → 建空 deck,设画布,回填 `P.SLIDE_W/H` 与安全区
|
||||||
- `P.load(path)` → 载入已有 deck,按文件实际尺寸回填画布常量(逐页进程间自动同步)
|
- `P.load(path)` → 载入已有 deck,按文件实际尺寸回填画布常量
|
||||||
- `P.add_slide(prs)` → 追加一张空白版式(layout 6)slide
|
- `P.add_slide(prs)` → 追加空白 slide
|
||||||
- `P.set_palette(primary=, secondary=, accent=, cn_font=, en_font=, spec_path=)` → 覆盖主题色/字体;传 `spec_path` 自动从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调;**默认商务红,什么都不传无副作用**
|
- `P.set_palette(primary=, secondary=, accent=, cn_font=, en_font=, spec_path=)` → 覆盖主题色/字体并**重算派生色阶**;传 `spec_path` 自动取 spec 前 3 个 #hex;默认商务红
|
||||||
|
|
||||||
**颜色常量**:`P.PRIMARY` `P.SECONDARY` `P.ACCENT` `P.INK` `P.GREY` `P.GREY_LIGHT` `P.HAIRLINE` `P.BG` `P.WHITE`
|
**颜色常量**:`P.PRIMARY` `P.SECONDARY` `P.ACCENT` `P.INK` `P.GREY` `P.GREY_LIGHT` `P.HAIRLINE` `P.BG` `P.WHITE`
|
||||||
|
**派生色阶**(从主/辅/强调自动算):`P.PRIMARY_WASH`(整页/大区域浅底) `P.PRIMARY_SOFT`(卡片/标签浅底) `P.PRIMARY_DARK`(渐变深端) `P.ACCENT_SOFT`(高亮浅底) `P.SURFACE`(卡片白面)
|
||||||
**字体常量**:`P.CN_FONT`(微软雅黑) `P.EN_FONT`(Arial)
|
**字体常量**:`P.CN_FONT`(微软雅黑) `P.EN_FONT`(Arial)
|
||||||
**画布常量**:`P.SLIDE_W` `P.SLIDE_H` `P.SAFE_LEFT/TOP/RIGHT/BOTTOM` `P.SAFE_W` `P.SAFE_H`
|
**画布常量**:`P.SLIDE_W` `P.SLIDE_H` `P.SAFE_LEFT/TOP/RIGHT/BOTTOM` `P.SAFE_W` `P.SAFE_H`
|
||||||
|
**色阶工具**:`P.tint(color, pct)` 提亮 / `P.shade(color, pct)` 压暗(自定义中间色用)
|
||||||
|
|
||||||
**放置 helper**(全部内置 `assert_inside` 越界即报错)
|
**容器 / 质感**(卡片式核心)
|
||||||
- `P.add_textbox(slide, left, top, w, h, text, size, bold=False, color=P.INK, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, font=None, shrink=True, name=...)` → 文本框;`font=None` 自动 latin=Arial + 东亚=微软雅黑(**中文真落到雅黑靠这个**),传 `font` 则两槽都用它(纯英文大字/数字)
|
- `P.add_card(slide, l, t, w, h, fill=SURFACE, radius=0.12, shadow=True, border=False, accent=None)` → 圆角卡片(投影/边线/左强调条可选);内容再叠其上,内边距约 0.3-0.4
|
||||||
- `P.add_rect(slide, left, top, w, h, fill, name=...)` → 无边线实心矩形
|
- `P.add_round_rect(slide, l, t, w, h, fill, radius=0.10)` → 无投影圆角矩形
|
||||||
- `P.add_shape(slide, kind, left, top, w, h, fill, name=...)` → 任意 MSO_SHAPE(`kind` 用 `MSO_SHAPE.XXX`)
|
- `P.add_gradient_rect(slide, l, t, w, h, c1, c2, angle=90, rounded=False)` → 渐变块(封面/章节大色块;原生可编辑非图片)
|
||||||
- `P.add_dot(slide, x, y, size=0.18, color=P.ACCENT)` → 圆点(bullet 前缀)
|
- `P.set_shadow(shape, blur=0.10, dist=0.045, dir_deg=90, alpha=0.26)` → 给任意形状加柔和投影
|
||||||
- `P.add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=P.ACCENT)` → 强调短线
|
- `P.set_line(shape, color, weight=0.75)` → 描边(color=None 去边)
|
||||||
- `P.add_badge(slide, x, y, num, diameter=0.7, fill=P.PRIMARY, fg=P.WHITE)` → 编号徽章(圆+数字)
|
- `P.add_bg(slide, color=BG)` → 整页背景(`apply_brand` 已内置,一般不用手调)
|
||||||
- `P.page_title(slide, text, page_num=None, total=None, footer="项目汇报")` → 内页标题+强调线(+可选页脚页码)
|
|
||||||
- `P.apply_brand(slide, kind)` → 品牌锚点,`kind` ∈ `"cover"/"inner"/"section"/"end"`;**每页第一行必调**
|
|
||||||
- `P.assert_inside(left, top, w, h, name="")` → 手动越界校验(上面的 helper 已内置)
|
|
||||||
|
|
||||||
> `MSO_SHAPE` / `PP_ALIGN` / `MSO_ANCHOR` 等枚举若页面里要直接用,自行 `from pptx.enum.shapes import MSO_SHAPE` 等(`pptx_helpers` 内部已 import,但不重导出)。
|
**组件**
|
||||||
|
- `P.add_kpi(slide, l, t, w, h, value, label, sub=None, value_color=PRIMARY, card=True, value_size=40)` → KPI 数字卡(大数字+标签+小注)
|
||||||
|
- `P.add_icon_tile(slide, x, y, size=0.9, png_path=None, fill=PRIMARY_SOFT)` → 图标圆角底块 + 居中图标
|
||||||
|
- `P.add_icon(slide, png_path, x, y, size=0.6)` → 裸图标 PNG(方形源等比)
|
||||||
|
- `P.add_pill(slide, x, y, w, h, text, fill=PRIMARY, fg=WHITE, size=12)` → 胶囊标签 / chip
|
||||||
|
- `P.add_eyebrow(slide, x, y, text, color=PRIMARY, size=13)` → 标题上方小标签 / kicker
|
||||||
|
- `P.add_badge(slide, x, y, num, diameter=0.7)` → 编号徽章(圆+数字)
|
||||||
|
- `P.add_chevron(slide, x, y, w=0.55, h=0.5, color=GREY_LIGHT)` → 流程箭头
|
||||||
|
- `P.add_dot(slide, x, y, size=0.18, color=ACCENT)` → 圆点(bullet 前缀)
|
||||||
|
- `P.add_accent_line(slide, x, y, length=1.0, thickness=0.05)` → 强调短线
|
||||||
|
- `P.add_divider(slide, x, y, length, vertical=False)` → 细分隔线
|
||||||
|
|
||||||
|
**文本 / 标题 / 品牌 / 备注**
|
||||||
|
- `P.add_textbox(slide, l, t, w, h, text, size, bold=False, color=INK, align=, anchor=, font=None, shrink=True, name=)` → 文本框;`font=None` 自动 latin=Arial + 东亚=微软雅黑(**中文真落雅黑靠这个**),传 `font` 则两槽都用它(纯英文大字/数字)
|
||||||
|
- `P.page_title(slide, text, page_num=None, total=None, footer=, eyebrow=None)` → 内页标题+强调线(+可选 eyebrow / 页脚页码)
|
||||||
|
- `P.apply_brand(slide, kind)` → 品牌锚点,`kind` ∈ `"cover"/"inner"/"section"/"end"`;**每页第一行必调**(已含整页背景)
|
||||||
|
- `P.add_notes(slide, text)` → 演讲者备注(正式产物每页给 2-4 句口述要点)
|
||||||
|
- `P.assert_inside(l, t, w, h, name="")` → 手动越界校验(放置 helper 已内置)
|
||||||
|
|
||||||
|
> ⚠️ **给每个元素起语义 `name`**(`"bullet_1"`/`"kpi_val"`/`"eyebrow"`/`"pill"` 等)。quality_check 靠 name 判定"哪些是标签(小字号豁免)、哪些是真 bullet(计 ≤5)、谁压了谁",名字乱起会误报。helper 默认名已合理,自己加文本时照着命名。
|
||||||
|
|
||||||
|
> `MSO_SHAPE` / `PP_ALIGN` / `MSO_ANCHOR` 页面里要直接用就自行 import(`pptx_helpers` 内部已 import 但不重导出)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L1 · 封面 (Cover) —— 主色长竖条锚点
|
## L1 · 封面 (Cover) —— 渐变大色块 + 左侧标题区
|
||||||
|
|
||||||
```python
|
```python
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "cover") # 左侧主色长竖条 + 顶部短横
|
P.apply_brand(s, "cover") # 右侧 40% 主色→深主色渐变块 + 左上强调短线 + 底细线
|
||||||
|
# 左侧标题区(避开右侧渐变块,文字区约 7.4 寸宽)
|
||||||
# 主标题 (避开左竖条)
|
P.add_eyebrow(s, 0.9, 2.0, "2026 年度技术汇报") # kicker 小标签
|
||||||
P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "项目名称 / 演示主题",
|
P.add_textbox(s, 0.9, 2.5, 7.2, 1.6, "项目名称 / 演示主题",
|
||||||
44, bold=True, color=P.INK, name="cover_title")
|
42, bold=True, color=P.INK, name="cover_title")
|
||||||
# 副标题 (灰色,弱化)
|
P.add_textbox(s, 0.9, 4.4, 7.0, 0.6, "一句话副标题或定位",
|
||||||
P.add_textbox(slide, 0.9, 4.1, 11.9, 0.6, "一句话副标题或定位",
|
20, color=P.GREY, name="cover_sub")
|
||||||
22, color=P.GREY, name="cover_sub")
|
P.add_textbox(s, 0.9, 6.4, 7.0, 0.4, "汇报人 · 部门 · 2026-06-08",
|
||||||
# 汇报人 / 日期
|
14, color=P.GREY_LIGHT, name="cover_meta")
|
||||||
P.add_textbox(slide, 0.9, 6.4, 11.9, 0.4,
|
P.add_notes(s, "开场白:点出主题与本次汇报要解决的核心问题。")
|
||||||
"汇报人 · 部门 · 2026-05-06", 14, color=P.GREY_LIGHT,
|
|
||||||
name="cover_meta")
|
|
||||||
# 右下角小图标点缀 (五角星,可选)
|
|
||||||
from pptx.enum.shapes import MSO_SHAPE
|
|
||||||
P.add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, P.ACCENT,
|
|
||||||
"deco_star")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 有合适主图时(见 SKILL.md §配图),可把右侧渐变块换成**真实图片**:`s.shapes.add_picture(hero, Inches(P.SLIDE_W*0.6), Inches(0), height=Inches(7.5))`,再在图上叠半透明主色块保证文字区干净。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L2 · 目录 (Agenda) —— 编号徽章 + 文字
|
## L2 · 目录 (Agenda) —— 编号徽章 + 文字
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.enum.text import MSO_ANCHOR
|
from pptx.enum.text import MSO_ANCHOR
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "inner")
|
P.apply_brand(s, "inner")
|
||||||
P.page_title(slide, "目录")
|
P.page_title(s, "目录")
|
||||||
|
|
||||||
items = ["背景与现状", "核心问题", "解决方案", "实施计划", "预期成果"]
|
items = ["背景与现状", "核心问题", "解决方案", "实施计划", "预期成果"]
|
||||||
for i, item in enumerate(items):
|
for i, item in enumerate(items):
|
||||||
y = 1.9 + i * 0.95
|
y = 1.9 + i * 0.95
|
||||||
P.add_badge(slide, P.SAFE_LEFT, y, i + 1, diameter=0.65)
|
P.add_badge(s, P.SAFE_LEFT, y, i + 1, diameter=0.65)
|
||||||
P.add_textbox(slide, P.SAFE_LEFT + 1.0, y, P.SAFE_W - 1.0, 0.65,
|
P.add_textbox(s, P.SAFE_LEFT + 1.0, y, P.SAFE_W - 1.0, 0.65, item, 22,
|
||||||
item, 22, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
|
color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name=f"agenda_{i}")
|
||||||
name=f"agenda_{i}")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L3 · 章节分隔 (Section Divider) —— 浅色背景 + 大字编号
|
## L3 · 章节分隔 (Section Divider) —— 渐变整页 + 大字编号(白字)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.enum.text import MSO_ANCHOR
|
from pptx.enum.text import MSO_ANCHOR
|
||||||
from pptx.enum.shapes import MSO_SHAPE
|
s = P.add_slide(prs)
|
||||||
slide = P.add_slide(prs)
|
P.apply_brand(s, "section") # 主色→深主色整页渐变 + 强调装饰条
|
||||||
P.apply_brand(slide, "section") # 整页浅灰 + 主色左竖条 + 强调装饰
|
# 大编号(白色;font=EN_FONT 让数字走 Arial)
|
||||||
# 大编号 (主色;font=EN_FONT 让数字走 Arial)
|
P.add_textbox(s, 1.1, 2.0, 4, 2.5, "01", 150, bold=True, color=P.WHITE,
|
||||||
P.add_textbox(slide, 1.1, 2.0, 4, 2.5, "01", 160, bold=True,
|
font=P.EN_FONT, name="sec_num")
|
||||||
color=P.PRIMARY, font=P.EN_FONT, name="sec_num")
|
# 章节名(白色)
|
||||||
# 章节名
|
P.add_textbox(s, 5.3, 2.8, 7, 1.0, "背景与现状", 44, bold=True,
|
||||||
P.add_textbox(slide, 5.5, 2.8, 7, 1.0, "背景与现状",
|
color=P.WHITE, anchor=MSO_ANCHOR.MIDDLE, name="sec_title")
|
||||||
44, bold=True, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
|
# 引言(强调浅色,渐变深底上可读)
|
||||||
name="sec_title")
|
P.add_textbox(s, 5.3, 4.0, 7, 0.6, "本章讨论行业现状与机会窗口", 18,
|
||||||
# 引言
|
color=P.ACCENT_SOFT, name="sec_lead")
|
||||||
P.add_textbox(slide, 5.5, 4.0, 7, 0.6,
|
|
||||||
"本章讨论行业现状与机会窗口", 18, color=P.GREY,
|
|
||||||
name="sec_lead")
|
|
||||||
# 装饰小图标
|
|
||||||
P.add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, P.ACCENT,
|
|
||||||
"sec_arrow")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 渐变深底上文字一律用 **白 / `ACCENT_SOFT`** 等浅色,不要用 `INK` 深灰(看不清)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L4 · 要点 (Bullets) —— 圆点 + 文字,无大块色
|
## L4 · 要点 (Bullets) —— 圆点 + 文字;≥3 条建议升级成卡片(见 L11)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.enum.text import MSO_ANCHOR
|
from pptx.enum.text import MSO_ANCHOR
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "inner")
|
P.apply_brand(s, "inner")
|
||||||
P.page_title(slide, "核心结论")
|
P.page_title(s, "核心结论")
|
||||||
|
|
||||||
bullets = [
|
bullets = [
|
||||||
"结论一:用一句话讲清楚",
|
"结论一:用一句话讲清楚",
|
||||||
|
|
@ -152,141 +168,235 @@ bullets = [
|
||||||
]
|
]
|
||||||
for i, b in enumerate(bullets):
|
for i, b in enumerate(bullets):
|
||||||
y = 2.0 + i * 0.95
|
y = 2.0 + i * 0.95
|
||||||
P.add_dot(slide, P.SAFE_LEFT + 0.05, y + 0.22, size=0.18)
|
P.add_dot(s, P.SAFE_LEFT + 0.05, y + 0.22, size=0.18)
|
||||||
P.add_textbox(slide, P.SAFE_LEFT + 0.45, y, P.SAFE_W - 0.45, 0.6,
|
P.add_textbox(s, P.SAFE_LEFT + 0.45, y, P.SAFE_W - 0.45, 0.6, b, 22,
|
||||||
b, 22, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
|
color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name=f"bullet_{i}")
|
||||||
name=f"bullet_{i}")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 纯圆点 bullet 偏单薄。**业务概念类要点(能力/模块/策略)优先用 L11 卡片网格 + 图标底块**,视觉重量足。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L5 · 双栏对比 (Two-Column) —— 中线分隔,小色块标签
|
## L5 · 双栏对比 (Two-Column) —— 两张卡片,左中右灰
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "inner")
|
P.apply_brand(s, "inner")
|
||||||
P.page_title(slide, "现状 vs 改进后")
|
P.page_title(s, "现状 vs 改进后")
|
||||||
|
|
||||||
mid_x = P.SLIDE_W / 2
|
cw = (P.SAFE_W - 0.5) / 2 # 两卡 + 中间 0.5 间隙
|
||||||
|
ly, lh = 2.0, 4.5
|
||||||
# 中间细分隔线 (替代两块大矩形)
|
# 左卡:现状(中性灰底,弱化)
|
||||||
P.add_rect(slide, mid_x - 0.02, 2.0, 0.04, 4.8, P.HAIRLINE, "divider")
|
P.add_card(s, P.SAFE_LEFT, ly, cw, lh, fill=P.BG, border=True, shadow=False)
|
||||||
|
P.add_pill(s, P.SAFE_LEFT + 0.35, ly + 0.35, 1.1, 0.36, "现状", fill=P.GREY)
|
||||||
# 左栏小标签 (色块只占小区域)
|
|
||||||
P.add_rect(slide, P.SAFE_LEFT, 2.0, 0.8, 0.35, P.GREY, "left_tag")
|
|
||||||
P.add_textbox(slide, P.SAFE_LEFT, 2.0, 0.8, 0.35, "现状", 14, bold=True,
|
|
||||||
color=P.WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
|
|
||||||
shrink=False, name="left_label")
|
|
||||||
left_pts = ["问题 A:描述", "问题 B:描述", "问题 C:描述"]
|
left_pts = ["问题 A:描述", "问题 B:描述", "问题 C:描述"]
|
||||||
for i, p in enumerate(left_pts):
|
for i, p in enumerate(left_pts):
|
||||||
P.add_dot(slide, P.SAFE_LEFT + 0.05, 2.7 + i * 0.7 + 0.18, color=P.GREY)
|
yy = ly + 1.1 + i * 0.7
|
||||||
P.add_textbox(slide, P.SAFE_LEFT + 0.45, 2.7 + i * 0.7,
|
P.add_dot(s, P.SAFE_LEFT + 0.4, yy + 0.16, color=P.GREY)
|
||||||
mid_x - P.SAFE_LEFT - 0.7, 0.55, p, 18, color=P.INK,
|
P.add_textbox(s, P.SAFE_LEFT + 0.8, yy, cw - 1.1, 0.55, p, 17,
|
||||||
anchor=MSO_ANCHOR.MIDDLE, name=f"l_pt_{i}")
|
color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name=f"l_pt_{i}")
|
||||||
|
# 右卡:改进后(主色强调条 + 浅底,突出)
|
||||||
# 右栏小标签
|
rx = P.SAFE_LEFT + cw + 0.5
|
||||||
P.add_rect(slide, mid_x + 0.3, 2.0, 0.8, 0.35, P.PRIMARY, "right_tag")
|
P.add_card(s, rx, ly, cw, lh, fill=P.SURFACE, accent=P.PRIMARY)
|
||||||
P.add_textbox(slide, mid_x + 0.3, 2.0, 0.8, 0.35, "改进后", 14, bold=True,
|
P.add_pill(s, rx + 0.5, ly + 0.35, 1.3, 0.36, "改进后", fill=P.PRIMARY)
|
||||||
color=P.WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
|
|
||||||
shrink=False, name="right_label")
|
|
||||||
right_pts = ["改善 A:描述", "改善 B:描述", "改善 C:描述"]
|
right_pts = ["改善 A:描述", "改善 B:描述", "改善 C:描述"]
|
||||||
for i, p in enumerate(right_pts):
|
for i, p in enumerate(right_pts):
|
||||||
P.add_dot(slide, mid_x + 0.35, 2.7 + i * 0.7 + 0.18, color=P.ACCENT)
|
yy = ly + 1.1 + i * 0.7
|
||||||
P.add_textbox(slide, mid_x + 0.75, 2.7 + i * 0.7,
|
P.add_dot(s, rx + 0.55, yy + 0.16, color=P.ACCENT)
|
||||||
P.SAFE_RIGHT - mid_x - 0.75, 0.55, p, 18, color=P.INK,
|
P.add_textbox(s, rx + 0.95, yy, cw - 1.3, 0.55, p, 17, color=P.INK,
|
||||||
anchor=MSO_ANCHOR.MIDDLE, name=f"r_pt_{i}")
|
anchor=MSO_ANCHOR.MIDDLE, name=f"r_pt_{i}")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L6 · 图表为主 (Chart-focus) —— 标题 + 一句结论 + 大图
|
## L6 · 图表为主 (Chart-focus) —— 标题 + 一句结论 + 大图嵌卡片
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.util import Inches
|
from pptx.util import Inches
|
||||||
from pptx.enum.text import PP_ALIGN
|
from pptx.enum.text import PP_ALIGN
|
||||||
# chart.png 已用 matplotlib 生成(见 design_principles.md §7)
|
# chart.png 已用 matplotlib 生成(见 design_principles.md §7)
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "inner")
|
P.apply_brand(s, "inner")
|
||||||
P.page_title(slide, "季度营收持续增长")
|
P.page_title(s, "季度营收持续增长")
|
||||||
# 一句话结论
|
P.add_textbox(s, P.SAFE_LEFT, P.SAFE_TOP + 1.1, P.SAFE_W, 0.5,
|
||||||
P.add_textbox(slide, P.SAFE_LEFT, P.SAFE_TOP + 1.1, P.SAFE_W, 0.5,
|
|
||||||
"Q4 同比增长 158%,创历史新高", 18, color=P.GREY, name="lead")
|
"Q4 同比增长 158%,创历史新高", 18, color=P.GREY, name="lead")
|
||||||
# 图表 (居中,占 8.9 寸宽,高度自适应 —— 只给 width 等比缩放)
|
# 图表衬一张白卡片(浮起,比裸图精致)
|
||||||
slide.shapes.add_picture("<task_dir>/slides/chart.png", Inches(2.2),
|
P.add_card(s, 2.0, 2.4, 9.3, 4.3, fill=P.SURFACE)
|
||||||
Inches(2.4), width=Inches(8.9))
|
s.shapes.add_picture("<task_dir>/slides/chart.png", Inches(2.4),
|
||||||
# 数据来源 (右下角弱化)
|
Inches(2.7), width=Inches(8.5))
|
||||||
P.add_textbox(slide, P.SAFE_LEFT, 6.95, P.SAFE_W, 0.4,
|
P.add_textbox(s, P.SAFE_LEFT, 6.95, P.SAFE_W, 0.4, "数据来源:公司年报 2025",
|
||||||
"数据来源: 公司年报 2025", 11, color=P.GREY_LIGHT,
|
11, color=P.GREY_LIGHT, align=PP_ALIGN.RIGHT, shrink=False,
|
||||||
align=PP_ALIGN.RIGHT, shrink=False, name="source")
|
name="source")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L7 · 图片为主 (Image-focus) —— 文字在图旁,不压图
|
## L7 · 图片为主 (Image-focus) —— 图占 58%,文字独立区
|
||||||
|
|
||||||
> 之前用满铺图 + 半透明遮罩,效果不稳定。改成"图占 60% + 文字独立区"。
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.util import Inches
|
from pptx.util import Inches
|
||||||
from pptx.enum.shapes import MSO_SHAPE
|
from pptx.enum.shapes import MSO_SHAPE
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
# 左侧图占 60% 宽 (只给 width 或 height 一项,避免变形;此处图需正好铺满左 8 寸高 7.5 寸时按素材比例取舍)
|
P.add_bg(s, P.WHITE)
|
||||||
slide.shapes.add_picture("<task_dir>/slides/hero.jpg", Inches(0), Inches(0),
|
# 左侧图(只给 height 等比铺满,避免变形)
|
||||||
|
s.shapes.add_picture("<task_dir>/slides/hero.jpg", Inches(0), Inches(0),
|
||||||
height=Inches(7.5))
|
height=Inches(7.5))
|
||||||
# 右侧浅灰背景区放文字
|
# 右侧浅底文字区
|
||||||
P.add_rect(slide, 8, 0, 5.33, 7.5, P.BG, "text_panel")
|
P.add_rect(s, 7.7, 0, 5.63, 7.5, P.PRIMARY_WASH, "text_panel")
|
||||||
P.add_rect(slide, 8.4, 1.0, 0.06, 0.8, P.ACCENT, "deco_bar") # 装饰短线
|
P.add_eyebrow(s, 8.1, 1.4, "PRODUCT")
|
||||||
P.add_textbox(slide, 8.4, 2.0, 4.6, 1.6, "走进未来", 36,
|
P.add_textbox(s, 8.1, 1.9, 4.9, 1.0, "走进未来", 36, bold=True, color=P.INK,
|
||||||
bold=True, color=P.INK, name="img_title")
|
name="img_title")
|
||||||
P.add_textbox(slide, 8.4, 3.8, 4.6, 1.5,
|
P.add_accent_line(s, 8.1, 3.0, length=0.6)
|
||||||
"用一两句话点出主旨,不要把演讲稿搬上来。",
|
P.add_textbox(s, 8.1, 3.4, 4.9, 1.6, "用一两句话点出主旨,不要把演讲稿搬上来。",
|
||||||
18, color=P.GREY, name="img_caption")
|
18, color=P.GREY, name="img_caption")
|
||||||
# 图标:右下角的箭头,引导视线
|
P.add_shape(s, MSO_SHAPE.RIGHT_ARROW, 8.1, 6.4, 0.7, 0.35, P.ACCENT, "img_cta")
|
||||||
P.add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, P.ACCENT,
|
|
||||||
"img_cta")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L8 · 金句 / 大字 (Quote) —— 留白主导,装饰极简
|
## L8 · 金句 / 大字 (Quote) —— 留白主导
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.enum.text import MSO_ANCHOR
|
from pptx.enum.text import MSO_ANCHOR
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "inner")
|
P.apply_brand(s, "inner")
|
||||||
# 左上大引号 (用字形;font=EN_FONT 走 Arial)
|
P.add_textbox(s, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True, color=P.ACCENT,
|
||||||
P.add_textbox(slide, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True,
|
font=P.EN_FONT, shrink=False, name="quote_mark")
|
||||||
color=P.ACCENT, font=P.EN_FONT, shrink=False, name="quote_mark")
|
P.add_textbox(s, 1.5, 2.7, 10.5, 2.0, "把复杂留给我们,把简单留给用户。", 36,
|
||||||
# 金句 (深色,留白多)
|
bold=True, color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name="quote_text")
|
||||||
P.add_textbox(slide, 1.5, 2.7, 10.5, 2.0,
|
P.add_accent_line(s, 1.5, 5.0, length=0.5)
|
||||||
"把复杂留给我们,把简单留给用户。", 36, bold=True,
|
P.add_textbox(s, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025", 16, color=P.GREY,
|
||||||
color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name="quote_text")
|
name="quote_attr")
|
||||||
# 装饰短线
|
|
||||||
P.add_accent_line(slide, 1.5, 5.0, length=0.5)
|
|
||||||
# 出处
|
|
||||||
P.add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025",
|
|
||||||
16, color=P.GREY, name="quote_attr")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## L9 · 结尾 / Q&A —— 浅底 + 大字,**强制必有**
|
## L9 · 结尾 / Q&A —— 浅底 + 大字,**强制必有**
|
||||||
|
|
||||||
> **不是可选** —— 任何 deck 都必须以这页收尾。无论是汇报、提案、路演,缺尾页等于"话没说完"。
|
> **不是可选** —— 任何 deck 都必须以这页收尾。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pptx.enum.text import PP_ALIGN
|
from pptx.enum.text import PP_ALIGN
|
||||||
slide = P.add_slide(prs)
|
s = P.add_slide(prs)
|
||||||
P.apply_brand(slide, "end") # 整页浅灰 + 顶/底强调短线
|
P.apply_brand(s, "end") # PRIMARY_WASH 浅底 + 顶/底强调短线
|
||||||
P.add_textbox(slide, 0, 2.5, P.SLIDE_W, 1.6, "Thank You", 80, bold=True,
|
P.add_textbox(s, 0, 2.5, P.SLIDE_W, 1.6, "Thank You", 80, bold=True,
|
||||||
color=P.PRIMARY, align=PP_ALIGN.CENTER, font=P.EN_FONT,
|
color=P.PRIMARY, align=PP_ALIGN.CENTER, font=P.EN_FONT,
|
||||||
name="thanks")
|
name="thanks")
|
||||||
P.add_textbox(slide, 0, 4.3, P.SLIDE_W, 0.6, "欢迎提问与讨论",
|
P.add_textbox(s, 0, 4.3, P.SLIDE_W, 0.6, "欢迎提问与讨论", 22, color=P.INK,
|
||||||
22, color=P.ACCENT, align=PP_ALIGN.CENTER, name="qa")
|
align=PP_ALIGN.CENTER, name="qa")
|
||||||
P.add_textbox(slide, 0, 6.2, P.SLIDE_W, 0.5,
|
P.add_textbox(s, 0, 6.2, P.SLIDE_W, 0.5, "联系方式 / 邮箱 / 公众号", 14,
|
||||||
"联系方式 / 邮箱 / 公众号", 14, color=P.GREY_LIGHT,
|
color=P.GREY_LIGHT, align=PP_ALIGN.CENTER, name="contact")
|
||||||
align=PP_ALIGN.CENTER, name="contact")
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L10 · KPI 数字卡 (Metrics) —— 2-4 张并排,数据页主力
|
||||||
|
|
||||||
|
> 数据页**优先用这个**,不要为 2-4 个数字硬画柱状图。大数字 + 标签 + 同比小注,信息密度与质感俱佳。
|
||||||
|
|
||||||
|
```python
|
||||||
|
s = P.add_slide(prs)
|
||||||
|
P.apply_brand(s, "inner")
|
||||||
|
P.page_title(s, "平台运行关键指标", eyebrow="运行数据 / 2025")
|
||||||
|
|
||||||
|
data = [("158%", "实验吞吐同比", "↑ 较去年"),
|
||||||
|
("27天", "配方迭代周期", "↓ 缩短 40%"),
|
||||||
|
("92.3%", "中试一次通过率", "↑ +11pt"),
|
||||||
|
("4.2万", "累计实验记录", "条")]
|
||||||
|
n = len(data)
|
||||||
|
gap = 0.3
|
||||||
|
cw = (P.SAFE_W - gap * (n - 1)) / n
|
||||||
|
for i, (v, lab, sub) in enumerate(data):
|
||||||
|
P.add_kpi(s, P.SAFE_LEFT + i * (cw + gap), 2.6, cw, 2.7, v, lab, sub=sub)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 想突出某张卡:传 `value_color=P.ACCENT` 或给那张卡 `add_card(..., accent=P.ACCENT)` 后 `add_kpi(..., card=False)` 叠上。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L11 · 卡片网格 (Card Grid) —— 图标底块 + 标题 + 说明,业务概念主力
|
||||||
|
|
||||||
|
> 能力 / 模块 / 策略 / 价值点这类**业务概念**用它,替代单薄的圆点 bullet。2-4 列均可;图标走 `add_icon_tile`(图标先按 SKILL.md §阶段二第 2 步批量 `fetch_icon.py` 拉到 `<task_dir>/assets/icons`)。
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
s = P.add_slide(prs)
|
||||||
|
P.apply_brand(s, "inner")
|
||||||
|
P.page_title(s, "三大核心能力")
|
||||||
|
|
||||||
|
items = [("target", "数据底座", "统一实验/表征/工艺数据湖,一处录入处处可用"),
|
||||||
|
("cpu", "智能配方", "贝叶斯优化叠加机理约束,迭代更快更稳"),
|
||||||
|
("chart-bar", "中试放大", "小试到中试参数迁移模型,放大不失真")]
|
||||||
|
n = len(items)
|
||||||
|
gap = 0.35
|
||||||
|
cw = (P.SAFE_W - gap * (n - 1)) / n
|
||||||
|
for i, (icon, h, body) in enumerate(items):
|
||||||
|
x = P.SAFE_LEFT + i * (cw + gap)
|
||||||
|
P.add_card(s, x, 2.3, cw, 3.6, accent=P.PRIMARY)
|
||||||
|
png = os.path.join(ICONS, f"tabler_{icon}_C00000_128.png") # 主色染色后的图标
|
||||||
|
P.add_icon_tile(s, x + 0.4, 2.7, 0.95, png_path=png)
|
||||||
|
P.add_textbox(s, x + 0.4, 3.85, cw - 0.8, 0.5, h, 20, bold=True,
|
||||||
|
color=P.INK, name=f"card_h_{i}")
|
||||||
|
P.add_textbox(s, x + 0.4, 4.45, cw - 0.8, 1.1, body, 15, color=P.GREY,
|
||||||
|
name=f"card_b_{i}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L12 · 流程 / 步骤 (Process) —— 卡片 + chevron 箭头串联
|
||||||
|
|
||||||
|
```python
|
||||||
|
s = P.add_slide(prs)
|
||||||
|
P.apply_brand(s, "inner")
|
||||||
|
P.page_title(s, "实施四步走", eyebrow="路线图")
|
||||||
|
|
||||||
|
steps = [("01", "调研", "梳理现状与痛点"),
|
||||||
|
("02", "建模", "搭数据底座与模型"),
|
||||||
|
("03", "试点", "单产线小批验证"),
|
||||||
|
("04", "推广", "全厂复制与运维")]
|
||||||
|
n = len(steps)
|
||||||
|
arrow_w = 0.5
|
||||||
|
cw = (P.SAFE_W - arrow_w * (n - 1) - 0.2 * (n - 1)) / n
|
||||||
|
y, h = 2.8, 2.6
|
||||||
|
for i, (num, title, body) in enumerate(steps):
|
||||||
|
x = P.SAFE_LEFT + i * (cw + arrow_w + 0.2)
|
||||||
|
P.add_card(s, x, y, cw, h, fill=P.SURFACE)
|
||||||
|
P.add_textbox(s, x + 0.3, y + 0.3, cw - 0.6, 0.7, num, 34, bold=True,
|
||||||
|
color=P.PRIMARY, font=P.EN_FONT, name=f"step_num_{i}")
|
||||||
|
P.add_textbox(s, x + 0.3, y + 1.1, cw - 0.6, 0.5, title, 19, bold=True,
|
||||||
|
color=P.INK, name=f"step_t_{i}")
|
||||||
|
P.add_textbox(s, x + 0.3, y + 1.65, cw - 0.6, 0.8, body, 14, color=P.GREY,
|
||||||
|
name=f"step_b_{i}")
|
||||||
|
if i < n - 1:
|
||||||
|
P.add_chevron(s, x + cw + 0.1, y + h / 2 - 0.25, arrow_w, 0.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L13 · 大数字 + 论据 (Stat Highlight) —— 单个震撼数字撑半屏
|
||||||
|
|
||||||
|
> 一个核心数字要砸出冲击力时用。左侧超大数字,右侧三两条支撑论据卡。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pptx.enum.text import MSO_ANCHOR
|
||||||
|
s = P.add_slide(prs)
|
||||||
|
P.apply_brand(s, "inner")
|
||||||
|
P.page_title(s, "一年走完三年的路", eyebrow="成效")
|
||||||
|
# 左:超大数字(主色)
|
||||||
|
P.add_textbox(s, P.SAFE_LEFT, 2.4, 5.2, 2.4, "3.6×", 140, bold=True,
|
||||||
|
color=P.PRIMARY, font=P.EN_FONT, anchor=MSO_ANCHOR.MIDDLE,
|
||||||
|
name="big_stat")
|
||||||
|
P.add_textbox(s, P.SAFE_LEFT, 4.9, 5.2, 0.5, "研发效率提升", 20, color=P.INK,
|
||||||
|
name="big_stat_label")
|
||||||
|
# 右:支撑论据(浅底小卡堆叠)
|
||||||
|
facts = ["实验自动排程,人力释放 60%", "失败配方提前预警,返工 ↓45%", "知识沉淀复用,新人上手周期减半"]
|
||||||
|
for i, f in enumerate(facts):
|
||||||
|
yy = 2.5 + i * 1.25
|
||||||
|
P.add_card(s, 6.6, yy, 6.0, 1.05, fill=P.PRIMARY_WASH, shadow=False)
|
||||||
|
P.add_dot(s, 6.95, yy + 0.45, color=P.PRIMARY)
|
||||||
|
P.add_textbox(s, 7.35, yy, 5.0, 1.05, f, 16, color=P.INK,
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE, name=f"fact_{i}")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -294,19 +404,25 @@ P.add_textbox(slide, 0, 6.2, P.SLIDE_W, 0.5,
|
||||||
## 选版式速查
|
## 选版式速查
|
||||||
|
|
||||||
```
|
```
|
||||||
有数据 ≥ 3 点 → L6 (Chart-focus)
|
封面 → L1 (Cover)
|
||||||
对比类 (前/后, A/B) → L5 (Two-Column)
|
目录 → L2 (Agenda)
|
||||||
要点 ≤ 5 条 → L4 (Bullets)
|
|
||||||
转场 / 换章 → L3 (Section Divider)
|
转场 / 换章 → L3 (Section Divider)
|
||||||
首页 → L1 (Cover)
|
要点 ≤ 5 条(纯文字) → L4 (Bullets)
|
||||||
末页 → L9 (Q&A)
|
对比类 (前/后, A/B) → L5 (Two-Column)
|
||||||
|
有数据图表 → L6 (Chart-focus)
|
||||||
有大图 / 视觉优先 → L7 (Image-focus)
|
有大图 / 视觉优先 → L7 (Image-focus)
|
||||||
观点强调 / 名言 → L8 (Quote)
|
观点强调 / 名言 → L8 (Quote)
|
||||||
|
末页 → L9 (Q&A) [强制]
|
||||||
|
2-4 个关键数字 → L10 (KPI 数字卡) ← 优先于硬画柱图
|
||||||
|
业务概念(能力/模块) → L11 (卡片网格 + 图标) ← 优先于圆点 bullet
|
||||||
|
流程 / 步骤 → L12 (Process)
|
||||||
|
单个震撼数字 → L13 (Stat Highlight)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 三个常犯的越界场景
|
## 三个常犯的越界场景
|
||||||
|
|
||||||
1. **bullet 字数超额** —— 22pt 在 11.5 寸宽下每行约 50 个中文字。超过 1 行就溢出 0.7 in 高的框。`add_textbox` 内置 `assert_inside` + shrink-to-fit 兜底;但根本解法是**字数压缩**(见 design_principles.md §字数预算)
|
1. **bullet 字数超额** —— 22pt 在 11.5 寸宽下每行约 50 个中文字,超 1 行就溢出 0.6 in 框。根本解法是**字数压缩**(见 design_principles.md §字数预算),不要靠 `auto_size` 收字号兜底。
|
||||||
2. **标题占两行** —— 标题在 0.7 in 高的框里,32pt 单行高约 0.45 in,**两行就溢出**。中文标题 ≤ 30 字
|
2. **卡片内容超出卡片** —— 卡片内文字按 `卡宽 - 2×0.4` 内边距算框宽;标题/正文字数超了会顶出卡片下边缘。卡片高度留够(KPI 卡 ≥2.5,概念卡 ≥3.4)。
|
||||||
3. **图片不等比拉伸** —— `add_picture(width=, height=)` 同时给会变形;**只给 width 或 height 一项**
|
3. **图片不等比拉伸** —— `add_picture(width=, height=)` 同时给会变形;**只给 width 或 height 一项**。
|
||||||
|
4. **渐变深底上用深色字** —— L3 章节页 / cover 渐变块上的文字必须 `WHITE` / `ACCENT_SOFT`,用 `INK` 看不清。
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
"""pptx_helpers.py — PPT skill 的共享版式工具箱。
|
"""pptx_helpers.py — PPT skill 的共享版式工具箱(卡片式视觉系统)。
|
||||||
|
|
||||||
逐页生成时**每页一个 run_python**(载入已有 .pptx → append 一页 → save),
|
整 deck 在一个 `build_deck.py` 里构建,每页一个小函数,这些 helper 统一在
|
||||||
这些 helper 以前要在每页里重新默写一遍 —— 既烧 token 又会在长 deck 里漂移
|
`P.` 命名空间下调用 —— 既省 token,又保证长 deck 里坐标/配色不漂移。
|
||||||
(第 7 页的 apply_brand 坐标和第 2 页写得不一样)。收进本模块后,每页只 import。
|
|
||||||
|
|
||||||
用法(在 run_python block 顶部):
|
用法(在 build_deck.py 顶部):
|
||||||
|
|
||||||
import sys; sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入值
|
import sys; sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入值
|
||||||
import pptx_helpers as P
|
import pptx_helpers as P
|
||||||
|
|
||||||
# —— 第一页(创建)——
|
|
||||||
prs = P.new_presentation("16:9") # 默认 16:9,可传 4:3 / 9:16 / 3:4
|
prs = P.new_presentation("16:9") # 默认 16:9,可传 4:3 / 9:16 / 3:4
|
||||||
P.set_palette(spec_path="<task_dir>/...spec.md") # 默认商务红;spec 覆盖了才需要
|
P.set_palette(spec_path="<task_dir>/...spec.md") # 默认商务红;spec 覆盖了才需要
|
||||||
slide = P.add_slide(prs)
|
slide = P.add_slide(prs)
|
||||||
|
|
@ -17,12 +15,12 @@
|
||||||
P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "标题", 44, bold=True, color=P.INK)
|
P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "标题", 44, bold=True, color=P.INK)
|
||||||
prs.save("<task_dir>/<topic>.pptx")
|
prs.save("<task_dir>/<topic>.pptx")
|
||||||
|
|
||||||
# —— 后续页(追加)——
|
视觉系统(相对老版"平矩形 + 圆点 bullet"的升级):
|
||||||
prs = P.load("<task_dir>/<topic>.pptx") # 从文件实际尺寸回填画布常量
|
- **卡片**:`add_card` 圆角 + 柔和投影 + 可选底色/边线/强调条 —— 内容页主力容器
|
||||||
P.set_palette(spec_path="<task_dir>/...spec.md") # 每页都重读 spec(同 SKILL.md 规则)
|
- **色阶**:`set_palette` 从主/辅/强调派生 wash/soft/dark 明暗阶,白底之外有层次
|
||||||
slide = P.add_slide(prs)
|
- **渐变**:`add_gradient_rect` 用于封面/章节大色块(原生可编辑,非图片)
|
||||||
...
|
- **组件**:`add_kpi`(数字卡) `add_pill`(胶囊标签) `add_icon_tile`(图标底块)
|
||||||
prs.save("<task_dir>/<topic>.pptx")
|
`add_eyebrow`(小标签) `add_chevron`(流程箭头) `add_notes`(演讲者备注)
|
||||||
|
|
||||||
⚠️ 一律用 `P.xxx` 访问颜色常量与函数 —— set_palette 靠改模块属性生效,
|
⚠️ 一律用 `P.xxx` 访问颜色常量与函数 —— set_palette 靠改模块属性生效,
|
||||||
`from pptx_helpers import *` 会把旧绑定拷进页面命名空间,覆盖配色不生效。
|
`from pptx_helpers import *` 会把旧绑定拷进页面命名空间,覆盖配色不生效。
|
||||||
|
|
@ -33,7 +31,7 @@ import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pptx import Presentation
|
from pptx import Presentation
|
||||||
from pptx.util import Inches, Pt
|
from pptx.util import Inches, Pt, Emu
|
||||||
from pptx.dml.color import RGBColor
|
from pptx.dml.color import RGBColor
|
||||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
|
||||||
from pptx.enum.shapes import MSO_SHAPE
|
from pptx.enum.shapes import MSO_SHAPE
|
||||||
|
|
@ -54,6 +52,14 @@ HAIRLINE = RGBColor(0xDD, 0xDD, 0xDD) # 细分隔线
|
||||||
BG = RGBColor(0xFA, 0xFA, 0xFA) # 背景近白
|
BG = RGBColor(0xFA, 0xFA, 0xFA) # 背景近白
|
||||||
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
||||||
|
|
||||||
|
# —— 从主/辅/强调派生的明暗色阶 (set_palette 里按当前三色重算) ——
|
||||||
|
# 卡片底色 / 章节渐变 / 标签底 都从这套阶取,避免"白底 + 纯红"两极、缺中间层次。
|
||||||
|
PRIMARY_WASH = RGBColor(0xF7, 0xE6, 0xE6) # 主色 92% 兑白 —— 整页/大区域浅底
|
||||||
|
PRIMARY_SOFT = RGBColor(0xF0, 0xCC, 0xCC) # 主色 80% 兑白 —— 卡片/标签浅底
|
||||||
|
PRIMARY_DARK = RGBColor(0x8A, 0x00, 0x00) # 主色压暗 —— 渐变深端
|
||||||
|
ACCENT_SOFT = RGBColor(0xFF, 0xEC, 0xB8) # 强调 80% 兑白 —— 高亮底
|
||||||
|
SURFACE = RGBColor(0xFF, 0xFF, 0xFF) # 卡片面(白,衬在 BG 上靠投影浮起)
|
||||||
|
|
||||||
CN_FONT = "微软雅黑" # 中文字形走 <a:ea> 槽位
|
CN_FONT = "微软雅黑" # 中文字形走 <a:ea> 槽位
|
||||||
EN_FONT = "Arial" # 拉丁字形走 <a:latin> 槽位
|
EN_FONT = "Arial" # 拉丁字形走 <a:latin> 槽位
|
||||||
|
|
||||||
|
|
@ -118,16 +124,44 @@ def add_slide(prs: Presentation):
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 配色覆盖 (默认商务红;spec 写了别的色才调)
|
# 配色覆盖 (默认商务红;spec 写了别的色才调) + 色阶派生
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def _to_rgb(h: str) -> RGBColor:
|
def _to_rgb(h: str) -> RGBColor:
|
||||||
return RGBColor.from_string(h.lstrip("#").upper())
|
return RGBColor.from_string(h.lstrip("#").upper())
|
||||||
|
|
||||||
|
|
||||||
|
def _mix(c1: RGBColor, c2: RGBColor, t: float) -> RGBColor:
|
||||||
|
"""线性混合:t=0 → c1,t=1 → c2。"""
|
||||||
|
return RGBColor(
|
||||||
|
round(c1[0] + (c2[0] - c1[0]) * t),
|
||||||
|
round(c1[1] + (c2[1] - c1[1]) * t),
|
||||||
|
round(c1[2] + (c2[2] - c1[2]) * t),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tint(c: RGBColor, pct: float) -> RGBColor:
|
||||||
|
"""提亮:pct=0.85 → 兑 85% 白(越大越浅)。"""
|
||||||
|
return _mix(c, WHITE, pct)
|
||||||
|
|
||||||
|
|
||||||
|
def shade(c: RGBColor, pct: float) -> RGBColor:
|
||||||
|
"""压暗:pct=0.2 → 混 20% 黑(越大越深)。"""
|
||||||
|
return _mix(c, RGBColor(0, 0, 0), pct)
|
||||||
|
|
||||||
|
|
||||||
|
def _recompute_ramp() -> None:
|
||||||
|
"""按当前 PRIMARY/ACCENT 重算明暗色阶。set_palette 末尾调。"""
|
||||||
|
global PRIMARY_WASH, PRIMARY_SOFT, PRIMARY_DARK, ACCENT_SOFT
|
||||||
|
PRIMARY_WASH = tint(PRIMARY, 0.92)
|
||||||
|
PRIMARY_SOFT = tint(PRIMARY, 0.80)
|
||||||
|
PRIMARY_DARK = shade(PRIMARY, 0.22)
|
||||||
|
ACCENT_SOFT = tint(ACCENT, 0.78)
|
||||||
|
|
||||||
|
|
||||||
def set_palette(primary: str | None = None, secondary: str | None = None,
|
def set_palette(primary: str | None = None, secondary: str | None = None,
|
||||||
accent: str | None = None, cn_font: str | None = None,
|
accent: str | None = None, cn_font: str | None = None,
|
||||||
en_font: str | None = None, spec_path=None) -> None:
|
en_font: str | None = None, spec_path=None) -> None:
|
||||||
"""覆盖主题色 / 字体。逐页生成时每页都调一次(对齐 SKILL.md「每页重读 spec」)。
|
"""覆盖主题色 / 字体,并重算派生色阶。整 deck 设一次。
|
||||||
|
|
||||||
- 显式传 primary/secondary/accent(hex,带不带 # 都行)即覆盖对应色。
|
- 显式传 primary/secondary/accent(hex,带不带 # 都行)即覆盖对应色。
|
||||||
- 传 spec_path:从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调
|
- 传 spec_path:从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调
|
||||||
|
|
@ -155,6 +189,7 @@ def set_palette(primary: str | None = None, secondary: str | None = None,
|
||||||
CN_FONT = cn_font
|
CN_FONT = cn_font
|
||||||
if en_font:
|
if en_font:
|
||||||
EN_FONT = en_font
|
EN_FONT = en_font
|
||||||
|
_recompute_ramp()
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -213,6 +248,7 @@ def add_textbox(slide, left, top, width, height, text, size,
|
||||||
assert_inside(left, top, width, height, name)
|
assert_inside(left, top, width, height, name)
|
||||||
tb = slide.shapes.add_textbox(Inches(left), Inches(top),
|
tb = slide.shapes.add_textbox(Inches(left), Inches(top),
|
||||||
Inches(width), Inches(height))
|
Inches(width), Inches(height))
|
||||||
|
tb.name = name # 语义名写进 pptx —— quality_check 按名豁免标签 / 计 bullet 靠这个
|
||||||
tf = tb.text_frame
|
tf = tb.text_frame
|
||||||
tf.vertical_anchor = anchor
|
tf.vertical_anchor = anchor
|
||||||
tf.word_wrap = True
|
tf.word_wrap = True
|
||||||
|
|
@ -229,6 +265,7 @@ def add_rect(slide, left, top, width, height, fill, name="rect"):
|
||||||
assert_inside(left, top, width, height, name)
|
assert_inside(left, top, width, height, name)
|
||||||
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top),
|
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top),
|
||||||
Inches(width), Inches(height))
|
Inches(width), Inches(height))
|
||||||
|
s.name = name
|
||||||
s.fill.solid()
|
s.fill.solid()
|
||||||
s.fill.fore_color.rgb = fill
|
s.fill.fore_color.rgb = fill
|
||||||
s.line.fill.background()
|
s.line.fill.background()
|
||||||
|
|
@ -239,6 +276,7 @@ def add_shape(slide, kind, left, top, width, height, fill, name="shape"):
|
||||||
assert_inside(left, top, width, height, name)
|
assert_inside(left, top, width, height, name)
|
||||||
s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
|
s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
|
||||||
Inches(width), Inches(height))
|
Inches(width), Inches(height))
|
||||||
|
s.name = name
|
||||||
s.fill.solid()
|
s.fill.solid()
|
||||||
s.fill.fore_color.rgb = fill
|
s.fill.fore_color.rgb = fill
|
||||||
s.line.fill.background()
|
s.line.fill.background()
|
||||||
|
|
@ -269,13 +307,244 @@ def add_badge(slide, x, y, num, diameter=0.7, fill=None, fg=None):
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 视觉质感:投影 / 圆角 / 渐变 / 描边
|
||||||
|
# ============================================================
|
||||||
|
def set_shadow(shape, blur=0.10, dist=0.045, dir_deg=90, alpha=0.26,
|
||||||
|
color="000000") -> None:
|
||||||
|
"""给形状加柔和外投影(写 <a:effectLst><a:outerShdw>)。
|
||||||
|
|
||||||
|
blur/dist 单位英寸;dir_deg 投影方向(90=正下,默认);alpha 不透明度(0-1)。
|
||||||
|
卡片靠这个从背景"浮起",是平矩形与卡片观感的关键差。
|
||||||
|
"""
|
||||||
|
spPr = shape._element.spPr
|
||||||
|
for el in spPr.findall(qn("a:effectLst")):
|
||||||
|
spPr.remove(el)
|
||||||
|
eff = spPr.makeelement(qn("a:effectLst"), {})
|
||||||
|
shd = eff.makeelement(qn("a:outerShdw"), {
|
||||||
|
"blurRad": str(int(Inches(blur))),
|
||||||
|
"dist": str(int(Inches(dist))),
|
||||||
|
"dir": str(int(dir_deg * 60000)),
|
||||||
|
"rotWithShape": "0",
|
||||||
|
})
|
||||||
|
clr = shd.makeelement(qn("a:srgbClr"), {"val": color})
|
||||||
|
a = clr.makeelement(qn("a:alpha"), {"val": str(int(alpha * 100000))})
|
||||||
|
clr.append(a)
|
||||||
|
shd.append(clr)
|
||||||
|
eff.append(shd)
|
||||||
|
spPr.append(eff)
|
||||||
|
|
||||||
|
|
||||||
|
def set_line(shape, color, weight=0.75) -> None:
|
||||||
|
"""给形状描边(weight 单位 pt)。weight=0 / color=None 走无边线。"""
|
||||||
|
if color is None:
|
||||||
|
shape.line.fill.background()
|
||||||
|
return
|
||||||
|
shape.line.color.rgb = color
|
||||||
|
shape.line.width = Pt(weight)
|
||||||
|
|
||||||
|
|
||||||
|
def _round_adj(shape, radius_in) -> None:
|
||||||
|
"""把圆角矩形的圆角设成约 radius_in 英寸(adjustments[0] 是相对短边的比例)。"""
|
||||||
|
try:
|
||||||
|
short = min(shape.width, shape.height) / 914400.0
|
||||||
|
if short > 0:
|
||||||
|
shape.adjustments[0] = max(0.0, min(0.5, radius_in / short))
|
||||||
|
except (IndexError, ZeroDivisionError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def add_round_rect(slide, left, top, width, height, fill, radius=0.10,
|
||||||
|
name="round_rect"):
|
||||||
|
"""无边线圆角矩形。radius 单位英寸(约 0.08-0.14 观感最稳)。"""
|
||||||
|
assert_inside(left, top, width, height, name)
|
||||||
|
s = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
||||||
|
Inches(left), Inches(top),
|
||||||
|
Inches(width), Inches(height))
|
||||||
|
s.name = name
|
||||||
|
s.fill.solid()
|
||||||
|
s.fill.fore_color.rgb = fill
|
||||||
|
s.line.fill.background()
|
||||||
|
_round_adj(s, radius)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def add_gradient_rect(slide, left, top, width, height, c1, c2, angle=90,
|
||||||
|
rounded=False, radius=0.10, name="gradient"):
|
||||||
|
"""渐变矩形(原生可编辑,非图片)。封面/章节大色块用。
|
||||||
|
|
||||||
|
angle:渐变方向(度,0=左→右,90=上→下)。rounded=True 走圆角。
|
||||||
|
"""
|
||||||
|
assert_inside(left, top, width, height, name)
|
||||||
|
kind = MSO_SHAPE.ROUNDED_RECTANGLE if rounded else MSO_SHAPE.RECTANGLE
|
||||||
|
s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
|
||||||
|
Inches(width), Inches(height))
|
||||||
|
s.name = name
|
||||||
|
s.line.fill.background()
|
||||||
|
if rounded:
|
||||||
|
_round_adj(s, radius)
|
||||||
|
s.fill.gradient()
|
||||||
|
stops = s.fill.gradient_stops
|
||||||
|
stops[0].color.rgb = c1
|
||||||
|
stops[0].position = 0.0
|
||||||
|
stops[1].color.rgb = c2
|
||||||
|
stops[1].position = 1.0
|
||||||
|
try:
|
||||||
|
s.fill.gradient_angle = float(angle)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def add_bg(slide, color=None):
|
||||||
|
"""整页背景色块(每页第一笔铺,后续元素叠其上)。默认近白 BG。"""
|
||||||
|
return add_rect(slide, 0, 0, SLIDE_W, SLIDE_H,
|
||||||
|
BG if color is None else color, "bg")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 卡片 (内容页主力容器) + 组件
|
||||||
|
# ============================================================
|
||||||
|
def add_card(slide, left, top, width, height, fill=None, radius=0.12,
|
||||||
|
shadow=True, border=False, accent=None, accent_w=0.07,
|
||||||
|
name="card"):
|
||||||
|
"""圆角卡片:白面 + 柔和投影 + 可选发丝边 + 可选左侧强调竖条。
|
||||||
|
|
||||||
|
- fill:卡片底色,默认 SURFACE(白)。想要浅色卡传 PRIMARY_SOFT / PRIMARY_WASH。
|
||||||
|
- accent:给个颜色则在卡片左内缘加一条竖强调条(常用 PRIMARY / ACCENT)。
|
||||||
|
内容(标题/正文/图标)再叠到卡片上,自己按 left+0.3 内边距摆。
|
||||||
|
"""
|
||||||
|
card = add_round_rect(slide, left, top, width, height,
|
||||||
|
SURFACE if fill is None else fill, radius, name)
|
||||||
|
if shadow:
|
||||||
|
set_shadow(card)
|
||||||
|
if border:
|
||||||
|
set_line(card, HAIRLINE, 0.75)
|
||||||
|
if accent is not None:
|
||||||
|
# 左内缘一条圆角竖强调条(无投影,纯色)
|
||||||
|
add_round_rect(slide, left + 0.18, top + 0.22, accent_w,
|
||||||
|
max(0.4, height - 0.44), accent, radius=0.04,
|
||||||
|
name=name + "_accent")
|
||||||
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
def add_icon_tile(slide, x, y, size=0.9, png_path=None, fill=None,
|
||||||
|
radius=0.12, name="icon_tile"):
|
||||||
|
"""图标底块:圆角浅色方块 + 居中图标 PNG(没 PNG 就只出底块)。
|
||||||
|
|
||||||
|
fill 默认 PRIMARY_SOFT(主色浅底)。图标按 ~58% 居中,留呼吸。
|
||||||
|
业务概念页(战略/能力/模块)用它替代"光秃秃图标"或"只有圆点"。
|
||||||
|
"""
|
||||||
|
tile = add_round_rect(slide, x, y, size, size,
|
||||||
|
PRIMARY_SOFT if fill is None else fill, radius,
|
||||||
|
name)
|
||||||
|
if png_path and Path(str(png_path)).exists():
|
||||||
|
ic = size * 0.56
|
||||||
|
off = (size - ic) / 2
|
||||||
|
slide.shapes.add_picture(str(png_path), Inches(x + off),
|
||||||
|
Inches(y + off), width=Inches(ic))
|
||||||
|
return tile
|
||||||
|
|
||||||
|
|
||||||
|
def add_icon(slide, png_path, x, y, size=0.6):
|
||||||
|
"""直接摆图标 PNG(方形源,只给 width 等比)。底块版用 add_icon_tile。"""
|
||||||
|
if png_path and Path(str(png_path)).exists():
|
||||||
|
return slide.shapes.add_picture(str(png_path), Inches(x), Inches(y),
|
||||||
|
width=Inches(size))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_pill(slide, x, y, width, height, text, fill=None, fg=None, size=12,
|
||||||
|
name="pill"):
|
||||||
|
"""胶囊标签 / chip:全圆角小块 + 居中文字。分类标签、状态、eyebrow 用。"""
|
||||||
|
s = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
||||||
|
Inches(x), Inches(y),
|
||||||
|
Inches(width), Inches(height))
|
||||||
|
s.name = name
|
||||||
|
s.fill.solid()
|
||||||
|
s.fill.fore_color.rgb = PRIMARY if fill is None else fill
|
||||||
|
s.line.fill.background()
|
||||||
|
s.adjustments[0] = 0.5 # 全圆角
|
||||||
|
tf = s.text_frame
|
||||||
|
tf.word_wrap = False
|
||||||
|
tf.margin_top = tf.margin_bottom = 0
|
||||||
|
set_text(tf, text, size, bold=True, color=WHITE if fg is None else fg,
|
||||||
|
align=PP_ALIGN.CENTER)
|
||||||
|
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
||||||
|
s.text_frame.vertical_anchor = MSO_ANCHOR.MIDDLE
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def add_eyebrow(slide, x, y, text, color=None, size=13, width=4.0):
|
||||||
|
"""小标签 / kicker:标题上方一行弱化前缀(如『核心结论 / 01』)。"""
|
||||||
|
return add_textbox(slide, x, y, width, 0.35, text, size, bold=True,
|
||||||
|
color=PRIMARY if color is None else color,
|
||||||
|
shrink=False, name="eyebrow")
|
||||||
|
|
||||||
|
|
||||||
|
def add_kpi(slide, left, top, width, height, value, label, sub=None,
|
||||||
|
value_color=None, card=True, value_size=40, name="kpi"):
|
||||||
|
"""KPI 数字卡:大号数字 + 下方标签 + 可选小注(同比/单位)。
|
||||||
|
|
||||||
|
数据页把"小柱状图 / 一行结论"升级成 2-4 张并排数字卡,信息密度与质感都更高。
|
||||||
|
value 走 EN_FONT(数字/百分号更紧致)。
|
||||||
|
"""
|
||||||
|
if card:
|
||||||
|
add_card(slide, left, top, width, height, fill=SURFACE, radius=0.12,
|
||||||
|
shadow=True, name=name + "_card")
|
||||||
|
pad = 0.28
|
||||||
|
vh = height * 0.5
|
||||||
|
add_textbox(slide, left + pad, top + pad, width - 2 * pad, vh, str(value),
|
||||||
|
value_size, bold=True,
|
||||||
|
color=PRIMARY if value_color is None else value_color,
|
||||||
|
font=EN_FONT, anchor=MSO_ANCHOR.BOTTOM, shrink=False,
|
||||||
|
name=name + "_val")
|
||||||
|
add_textbox(slide, left + pad, top + pad + vh, width - 2 * pad, 0.4, label,
|
||||||
|
15, color=INK, anchor=MSO_ANCHOR.TOP, name=name + "_label")
|
||||||
|
if sub:
|
||||||
|
add_textbox(slide, left + pad, top + height - 0.5, width - 2 * pad,
|
||||||
|
0.35, sub, 12, color=GREY_LIGHT, shrink=False,
|
||||||
|
name=name + "_sub")
|
||||||
|
|
||||||
|
|
||||||
|
def add_chevron(slide, x, y, width=0.55, height=0.5, color=None):
|
||||||
|
"""流程箭头(步骤之间的指向)。"""
|
||||||
|
return add_shape(slide, MSO_SHAPE.CHEVRON, x, y, width, height,
|
||||||
|
(GREY_LIGHT if color is None else color), "chevron")
|
||||||
|
|
||||||
|
|
||||||
|
def add_divider(slide, x, y, length, vertical=False, color=None):
|
||||||
|
"""细分隔线(横/竖)。"""
|
||||||
|
c = HAIRLINE if color is None else color
|
||||||
|
if vertical:
|
||||||
|
return add_rect(slide, x, y, 0.02, length, c, "divider")
|
||||||
|
return add_rect(slide, x, y, length, 0.02, c, "divider")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 演讲者备注
|
||||||
|
# ============================================================
|
||||||
|
def add_notes(slide, text) -> None:
|
||||||
|
"""写演讲者备注(演示时可见,正式产物标配)。每页给 2-4 句口述要点。"""
|
||||||
|
slide.notes_slide.notes_text_frame.text = text or ""
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 标题套件 (内页通用)
|
# 标题套件 (内页通用)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def page_title(slide, text, page_num=None, total=None, footer="项目汇报"):
|
def page_title(slide, text, page_num=None, total=None, footer="项目汇报",
|
||||||
add_textbox(slide, SAFE_LEFT, SAFE_TOP, SAFE_W, 0.7, text,
|
eyebrow=None):
|
||||||
|
"""内页标题 + 强调线 (+ 可选 eyebrow 小标签 + 页脚页码)。
|
||||||
|
|
||||||
|
eyebrow:标题上方一行弱化前缀(章节名 / 分类),给则标题整体下移。
|
||||||
|
"""
|
||||||
|
ty = SAFE_TOP
|
||||||
|
if eyebrow:
|
||||||
|
add_eyebrow(slide, SAFE_LEFT, SAFE_TOP, eyebrow)
|
||||||
|
ty = SAFE_TOP + 0.4
|
||||||
|
add_textbox(slide, SAFE_LEFT, ty, SAFE_W, 0.7, text,
|
||||||
32, bold=True, color=PRIMARY, name="title")
|
32, bold=True, color=PRIMARY, name="title")
|
||||||
add_accent_line(slide, SAFE_LEFT, SAFE_TOP + 0.85, length=0.8)
|
add_accent_line(slide, SAFE_LEFT, ty + 0.85, length=0.8)
|
||||||
if page_num is not None and total is not None:
|
if page_num is not None and total is not None:
|
||||||
add_textbox(slide, SAFE_LEFT, SLIDE_H - 0.5, 6, 0.4, footer,
|
add_textbox(slide, SAFE_LEFT, SLIDE_H - 0.5, 6, 0.4, footer,
|
||||||
11, color=GREY_LIGHT, shrink=False, name="footer")
|
11, color=GREY_LIGHT, shrink=False, name="footer")
|
||||||
|
|
@ -289,28 +558,34 @@ def page_title(slide, text, page_num=None, total=None, footer="项目汇报"):
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def apply_brand(slide, kind="inner"):
|
def apply_brand(slide, kind="inner"):
|
||||||
"""统一品牌锚点。每个版式第一行调用。
|
"""统一品牌锚点。每个版式第一行调用。
|
||||||
cover —— 左侧主色长竖条 + 顶部短横 + 底部细灰线
|
|
||||||
inner —— (默认) 左侧主色窄条 + 底部细灰线
|
cover —— 右侧主色→深主色渐变大块 + 左侧细强调短线(现代封面)
|
||||||
section —— 整页浅灰 + 左侧主色竖条 + 强调色粗竖条
|
inner —— (默认) 近白底 + 左侧主色窄条 + 底部细灰线
|
||||||
end —— 整页浅灰 + 顶/底强调色短线
|
section —— 主色→深主色整页渐变 + 强调装饰条(章节大色块)
|
||||||
|
end —— 浅底 + 顶/底强调短线
|
||||||
"""
|
"""
|
||||||
btm = SLIDE_H - 0.32
|
btm = SLIDE_H - 0.32
|
||||||
if kind == "cover":
|
if kind == "cover":
|
||||||
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
|
add_bg(slide, WHITE)
|
||||||
add_rect(slide, 0.7, 0.6, 0.8, 0.06, PRIMARY, "brand_top_line")
|
# 右侧约 38% 宽的渐变色块,封面从"白纸加条"升级成有视觉重量的构图
|
||||||
add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
|
bw = SLIDE_W * 0.40
|
||||||
"brand_btm_hairline")
|
add_gradient_rect(slide, SLIDE_W - bw, 0, bw, SLIDE_H,
|
||||||
|
PRIMARY, PRIMARY_DARK, angle=60, name="cover_block")
|
||||||
|
add_rect(slide, SAFE_LEFT, 0.7, 0.55, 0.07, ACCENT, "brand_top_line")
|
||||||
|
add_rect(slide, SAFE_LEFT, btm, SLIDE_W - bw - SAFE_LEFT - 0.3, 0.02,
|
||||||
|
HAIRLINE, "brand_btm_hairline")
|
||||||
elif kind == "section":
|
elif kind == "section":
|
||||||
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
|
add_gradient_rect(slide, 0, 0, SLIDE_W, SLIDE_H,
|
||||||
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
|
PRIMARY, PRIMARY_DARK, angle=55, name="section_bg")
|
||||||
add_rect(slide, 0.7, SLIDE_H / 3, 0.08, SLIDE_H / 3, ACCENT,
|
add_rect(slide, 0.7, SLIDE_H / 3, 0.09, SLIDE_H / 3, ACCENT,
|
||||||
"brand_section_bar")
|
"brand_section_bar")
|
||||||
elif kind == "end":
|
elif kind == "end":
|
||||||
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
|
add_bg(slide, PRIMARY_WASH)
|
||||||
add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line")
|
add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line")
|
||||||
add_rect(slide, SAFE_RIGHT - 0.8, SLIDE_H - 0.65, 0.8, 0.06, ACCENT,
|
add_rect(slide, SAFE_RIGHT - 0.8, SLIDE_H - 0.65, 0.8, 0.06, ACCENT,
|
||||||
"brand_btm_line")
|
"brand_btm_line")
|
||||||
else: # inner
|
else: # inner
|
||||||
|
add_bg(slide, BG)
|
||||||
add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar")
|
add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||||
add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
|
add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
|
||||||
"brand_btm_hairline")
|
"brand_btm_hairline")
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import colorsys
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -47,14 +48,46 @@ _OVERLAP_MIN_RATIO = 0.25 # 交叠面积 / 较小形状面积 超过此比例
|
||||||
|
|
||||||
|
|
||||||
# ---- 颜色辅助 ----
|
# ---- 颜色辅助 ----
|
||||||
|
# 三色制按"色系数"判定,不是"hex 数":主/辅常同色系,主色的明暗阶(深红 #8A0000)、
|
||||||
|
# 浅底(wash/soft tint #F2CCCC)都从那三色派生,不该被算成"新色"。所以:
|
||||||
|
# - 低饱和的浅色/灰阶 → 中性(卡片底、wash 底),不计入彩色
|
||||||
|
# - 高饱和的算"彩色",但按色相(hue)归桶 —— 同色系(红的深浅)收敛成一个
|
||||||
|
# 这样"白底+红卡片+深红渐变+金强调"= 2 个色系,不会误报超 3 色。
|
||||||
|
|
||||||
def _is_neutral(hex6: str) -> bool:
|
def _hsv(hex6: str):
|
||||||
"""灰阶/黑/白判定:R/G/B 极差 ≤ 12 即视为中性色(三色制里不计入彩色)。"""
|
r, g, b = (int(hex6[0:2], 16) / 255, int(hex6[2:4], 16) / 255,
|
||||||
|
int(hex6[4:6], 16) / 255)
|
||||||
|
return colorsys.rgb_to_hsv(r, g, b) # h,s,v ∈ [0,1]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_chromatic(hex6: str) -> bool:
|
||||||
|
"""是否计入"彩色"。低饱和(浅底/wash/灰阶)或近黑 → 中性,不计。"""
|
||||||
try:
|
try:
|
||||||
r, g, b = int(hex6[0:2], 16), int(hex6[2:4], 16), int(hex6[4:6], 16)
|
_h, s, v = _hsv(hex6)
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
return False
|
return False
|
||||||
return max(r, g, b) - min(r, g, b) <= 12
|
return s >= 0.30 and v >= 0.18
|
||||||
|
|
||||||
|
|
||||||
|
def _hue_family(hex6: str) -> int:
|
||||||
|
"""色相归桶(30° 一桶)。同色系的深浅落同一桶,收敛成一个色。"""
|
||||||
|
h, _s, _v = _hsv(hex6)
|
||||||
|
return int((h * 360) // 30)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_neutral(hex6: str) -> bool:
|
||||||
|
"""保留旧名:非彩色(中性)= 不计入三色制。"""
|
||||||
|
return not _is_chromatic(hex6)
|
||||||
|
|
||||||
|
|
||||||
|
# 标签类形状名:这些天然用小字号(eyebrow/胶囊/页脚/数据来源/KPI 小注),
|
||||||
|
# 不参与"字号 < 14pt"与"bullet ≤ 5"的统计 —— 它们不是正文 bullet。
|
||||||
|
_LABEL_NAME_RE = re.compile(
|
||||||
|
r"(pill|eyebrow|footer|page_num|source|meta|_sub|kpi_sub|badge|tag|label)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
# bullet 类形状名:真正的要点列表才计入 bullet 数。
|
||||||
|
_BULLET_NAME_RE = re.compile(r"(bullet|_pt_|agenda|list|item)", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def _shape_fill_hex(shape) -> str | None:
|
def _shape_fill_hex(shape) -> str | None:
|
||||||
|
|
@ -132,8 +165,8 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
||||||
|
|
||||||
for idx, slide in enumerate(prs.slides, 1):
|
for idx, slide in enumerate(prs.slides, 1):
|
||||||
title_text = None
|
title_text = None
|
||||||
bullet_count = 0
|
|
||||||
small_font_count = 0
|
small_font_count = 0
|
||||||
|
bullet_xs: list = [] # 每个 bullet 项的 x 中心 —— 末尾按列分组判 ≤5
|
||||||
content_shapes: list = [] # (l, t, w, h, label, head) — 有文字 / 图片的形状
|
content_shapes: list = [] # (l, t, w, h, label, head) — 有文字 / 图片的形状
|
||||||
|
|
||||||
for s_i, shape in enumerate(slide.shapes):
|
for s_i, shape in enumerate(slide.shapes):
|
||||||
|
|
@ -211,7 +244,11 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
||||||
break
|
break
|
||||||
if first_size_pt:
|
if first_size_pt:
|
||||||
break
|
break
|
||||||
if first_size_pt and w_in > 0.5 and h_in > 0.2:
|
# 大号展示字(标题/KPI 大数字/章节编号 ≥ 40pt)单行短文本,
|
||||||
|
# 按"每行字数"估折行会假阳(每行才 1-2 字),跳过 —— 标题长度另有
|
||||||
|
# ≤30 字检查兜底。
|
||||||
|
if (first_size_pt and first_size_pt < 40
|
||||||
|
and w_in > 0.5 and h_in > 0.2):
|
||||||
chars_per_line = max(1, int(w_in * 72 / first_size_pt))
|
chars_per_line = max(1, int(w_in * 72 / first_size_pt))
|
||||||
est_lines = 0
|
est_lines = 0
|
||||||
for para in tf.paragraphs:
|
for para in tf.paragraphs:
|
||||||
|
|
@ -233,14 +270,26 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
||||||
except (AttributeError, TypeError, ValueError):
|
except (AttributeError, TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
is_label = bool(_LABEL_NAME_RE.search(shape_label))
|
||||||
|
is_bullet_shape = bool(_BULLET_NAME_RE.search(shape_label))
|
||||||
|
nonempty_paras = [
|
||||||
|
p for p in tf.paragraphs if (p.text or "").strip()
|
||||||
|
and (p.text or "").strip() != title_text
|
||||||
|
]
|
||||||
|
# bullet 只统计"真要点列表":名字像 bullet 的,或一个框里 ≥2 段的列表。
|
||||||
|
# KPI 卡 / 卡片标题 / 胶囊这类结构化短文本(单段、非 bullet 名)不算 bullet,
|
||||||
|
# 否则一页 4 张 KPI 卡会被误报成 "12 条 bullet"。
|
||||||
|
if not is_label and (is_bullet_shape or len(nonempty_paras) >= 2):
|
||||||
|
cx = left_in + w_in / 2 # x 中心,供按列分组
|
||||||
|
bullet_xs.extend([cx] * len(nonempty_paras))
|
||||||
|
|
||||||
for para in tf.paragraphs:
|
for para in tf.paragraphs:
|
||||||
ptxt = (para.text or "").strip()
|
ptxt = (para.text or "").strip()
|
||||||
if not ptxt:
|
if not ptxt:
|
||||||
continue
|
continue
|
||||||
if len(ptxt) > 1 and ptxt != title_text:
|
|
||||||
bullet_count += 1
|
|
||||||
for run in para.runs:
|
for run in para.runs:
|
||||||
if run.font.size:
|
# 标签类(eyebrow/胶囊/页脚/小注)天然小字,不算"投影看不清"
|
||||||
|
if run.font.size and not is_label:
|
||||||
if run.font.size < Pt(14):
|
if run.font.size < Pt(14):
|
||||||
small_font_count += 1
|
small_font_count += 1
|
||||||
if run.font.color and run.font.color.type:
|
if run.font.color and run.font.color.type:
|
||||||
|
|
@ -258,9 +307,15 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
||||||
f"第 {idx} 页标题过长 ({len(title_text)} 字): {title_text[:20]}..."
|
f"第 {idx} 页标题过长 ({len(title_text)} 字): {title_text[:20]}..."
|
||||||
)
|
)
|
||||||
|
|
||||||
if bullet_count > 5:
|
# bullet ≤5 按"列"判:双栏对比天生左 3 + 右 3,不该当整页 6 条报。
|
||||||
|
# 按 slide 中线把 bullet 分左右两列,任一列 > 5 才警告(单列列表也走这条)。
|
||||||
|
mid = slide_w_in / 2
|
||||||
|
left_n = sum(1 for x in bullet_xs if x < mid)
|
||||||
|
right_n = len(bullet_xs) - left_n
|
||||||
|
max_col = max(left_n, right_n)
|
||||||
|
if max_col > 5:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"第 {idx} 页 bullet {bullet_count} 条 (上限 5),建议拆页或转图表"
|
f"第 {idx} 页单列 bullet {max_col} 条 (上限 5),建议拆页或转图表"
|
||||||
)
|
)
|
||||||
|
|
||||||
if small_font_count > 0:
|
if small_font_count > 0:
|
||||||
|
|
@ -287,20 +342,23 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
||||||
f'{alab}("{ahead}") × {blab}("{bhead}")'
|
f'{alab}("{ahead}") × {blab}("{bhead}")'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 三色制按"非灰阶色"判定:灰/黑/白不计 (design_principles §2「其他全部用灰阶」)
|
# 三色制按"色系数"判定:同色系深浅(主色/深红渐变/浅红卡片底)收敛成一桶,
|
||||||
chromatic = {c for c in seen_colors if not _is_neutral(c)}
|
# 低饱和浅色/灰阶不计。这样卡片式设计的派生色阶不会被误报超 3 色。
|
||||||
if len(chromatic) > 3:
|
chromatic = {c for c in seen_colors if _is_chromatic(c)}
|
||||||
|
families = {_hue_family(c) for c in chromatic}
|
||||||
|
if len(families) > 3:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"非灰阶色 {len(chromatic)} 种 (三色制上限 3): "
|
f"彩色色系 {len(families)} 个 (三色制上限 3): "
|
||||||
f"{', '.join('#' + c for c in sorted(chromatic))};收敛到主/辅/强调三色"
|
f"{', '.join('#' + c for c in sorted(chromatic))};收敛到主/辅/强调三色系"
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec_colors:
|
if spec_colors:
|
||||||
spec_chromatic = {c for c in spec_colors if not _is_neutral(c)}
|
spec_families = {_hue_family(c) for c in spec_colors if _is_chromatic(c)}
|
||||||
extra = chromatic - spec_chromatic
|
extra = {c for c in chromatic if _hue_family(c) not in spec_families}
|
||||||
if extra:
|
if extra:
|
||||||
|
spec_chromatic = {c for c in spec_colors if _is_chromatic(c)}
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"出现 spec 之外的非灰阶色 {', '.join('#' + c for c in sorted(extra))};"
|
f"出现 spec 之外的色系 {', '.join('#' + c for c in sorted(extra))};"
|
||||||
f"擅自换色 / 非主题色 (spec 定的是 "
|
f"擅自换色 / 非主题色 (spec 定的是 "
|
||||||
f"{', '.join('#' + c for c in sorted(spec_chromatic))})"
|
f"{', '.join('#' + c for c in sorted(spec_chromatic))})"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue