PPT skill v3: 红色硬约束 + 品牌条 + 强制尾页 + Iconify 图标库

修复上一轮生成的实际问题: 模型擅自把红色换成蓝色 / 内页全裸白 / 缺 Q&A 尾页;
并补齐"个性化图标"能力 (此前只有 MSO_SHAPE + unicode 字形,业务概念图标缺位)。

- SKILL.md: 红色主题改硬约束 +  BLOCKING 八条对齐 (bundled 推荐, 等用户拍板),
  封面/尾页改强制项, 不算在 5-8 页正文预算内
- layouts.md: 加 apply_brand(slide, kind) 4 模式品牌条 (cover/inner/section/end),
  9 个版式起手必调, 消灭裸白页
- 图标库: 新增 fetch_icon.py 走 Iconify CDN (tabler/lucide/heroicons 等 150+ 集),
  主题色染色, 缓存到 assets/icons/, 配 INDEX.md 推荐清单
- icons.md: 移除 MSO_SHAPE 当业务图标的部分 (PENTAGON/LIGHTNING_BOLT 等视觉陈旧),
  三层降级 → 两层 (Iconify / unicode 兜底); MSO_SHAPE 退为 layouts.md helper 内部原语
- canvas_presets.md 并入 design_principles.md §0 (减少零碎文件)
- .gitignore: spec_lock.md 与根目录 *.pptx 不入库 (PPT skill 工作产物)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-05-06 12:23:06 +08:00
parent 3a66849953
commit 0971a500e7
10 changed files with 466 additions and 297 deletions

2
.gitignore vendored
View File

@ -40,6 +40,8 @@ desktop.ini
*.tmp.pptx
output.pptx
untitled*.pptx
spec_lock.md
/*.pptx
# 用户本地工具脚本 / 规划文件 (不入库)
规划.docx

View File

@ -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

View File

@ -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 <output.pptx>` —— 检页数/标题/bullet 条数/文件大小
- 不通过的项,回头 edit 对应页
## 设计原则 (硬规则)
```bash
python scripts/quality_check.py <output.pptx> --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`
## 工作目录约定
```
<task_dir>/
├── source.md # 阶段一: source_to_md.py 转出的素材
├── spec_lock.md # 阶段一: 八条对齐落定
├── source.md # source_to_md.py 转出的素材
├── spec_lock.md # 八条对齐落定
├── slides/
│ └── chart_p3.png # 各页用到的图片素材
└── <topic>.pptx # 最终产物 (文件名按主题命名,不要 untitled.pptx)
└── <topic>.pptx # 最终产物 (按主题命名)
```
## 反模式
- 用户没给材料就开始硬编内容
- 八条没对齐就跑 python-pptx
- **基于"场景判断"自行换配色**(见上"默认主题"违规清单)
- **缺封面 / 缺尾页(Q&A)** —— 两端都是强制项,不算在正文页数预算内
- **裸白纸版式** —— 所有版式起手都必须 `apply_brand(slide, kind)`,见 layouts.md
- 一个 `run_python` 出整 deck (中途改方向就要全推翻)
- 跑完不做 `quality_check.py` 就交付
- 起名 `output.pptx` / `untitled.pptx` —— 务必按主题给文件名
- 文字塞满整张幻灯片 —— 留白本身是设计
## 输出
完成后告诉用户:文件路径、页数、用到的版式列表、是否有未满足的 spec 项。问一句要不要再改。

View File

View File

@ -0,0 +1,66 @@
# 本地图标库
> 这里缓存通过 `scripts/fetch_icon.py` 从 Iconify 拉下来的图标。**首次为空**,模型按需下载缓存,再次用时直接读本地不再发请求。
## 缓存命名规约
```
<set>_<name>_<colorhex>_<sizepx>.png
<set>_<name>_<colorhex>.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` 拉。
## 主题色变体
同一图标按主色/辅色/强调色/灰各拉一份,文件名只在 `<colorhex>` 段不同:
- `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。

View File

@ -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)**。模型自己换算,不要硬抄默认表。

View File

@ -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) | 备注 |

View File

@ -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 图标目录
## 选图标两层降级
```python
from pptx.enum.shapes import MSO_SHAPE
```
1) Iconify 个性化图标 ── 业务概念 (火箭、目标、雷达、齿轮) → 见 §A
2) Unicode 字形兜底 ── Iconify 没有合适的 (✓ ✗ ★ → ↑) → 见 §B
```
### 标记类 (放在 bullet 前 / 标题旁)
整 deck 选**一个图标集**用到底,不要 tabler 跟 lucide 混用。
| 用途 | 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` | 用户向 / 软话题 |
## §A. Iconify 个性化图标 (本地缓存 + 网络拉取)
### 箭头类 (流程 / 趋势)
### A1. 本地库
路径: `skills/ppt/assets/icons/`,详见 [INDEX.md](../assets/icons/INDEX.md)。
命名规约: `<set>_<name>_<colorhex>_<sizepx>.png`(如 `tabler_rocket_C00000_128.png`)
| 用途 | MSO_SHAPE | 说明 |
|-----|-----------|------|
| 右箭头 → | `RIGHT_ARROW` | 流程下一步 |
| 上箭头 ↑ | `UP_ARROW` | 增长 |
| 下箭头 ↓ | `DOWN_ARROW` | 下降 |
| 双向箭头 ↔ | `LEFT_RIGHT_ARROW` | 对比 / 关联 |
| 折线右箭头 | `BENT_ARROW` / `CURVED_RIGHT_ARROW` | 转折 |
| 五边形流程 | `PENTAGON` | 流程节点(横排) |
| V 形 | `CHEVRON` | 流程节点(空间紧) |
**用之前先 `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
| 用途 | 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
# 强调色金黄
python skills/ppt/scripts/fetch_icon.py target --set tabler --color FFC107 \
--size 128 -o skills/ppt/assets/icons/tabler_target_FFC107_128.png
```
### B2. 编号徽章 (圆 + 数字)
`--set` 默认 `tabler`(4500+ 商务图标,MIT)。其它选 `lucide / heroicons / material-symbols / carbon / fluent / mdi`。**整 deck 只用一个 set**。
PNG 转换需 `pip install cairosvg`(推荐)或 `pip install svglib`。没装也能拿 SVG。
### A3. 嵌入幻灯片
```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
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
)
```
### B3. 流程节点 (五边形)
### A4. 浏览找名字
打开 https://icon-sets.iconify.design/ 搜关键词,如 "rocket" / "数据" / "shield",拿到名字 (如 `tabler:rocket`) 直接给 fetch_icon.py。
### A5. 流程节点 (替代 PENTAGON)
需要"调研→设计→开发→测试→上线"这种横向流程时,**不要用 PowerPoint 内置 PENTAGON**(视觉陈旧),改用 Iconify 的 `chevron-right` + 文本组合:
```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)
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))
```
### 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,用单色符号)
## §B. Unicode 字形 (兜底)
Iconify 都没合适的时候用。避 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 接口。

View File

@ -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")

View File

@ -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 "<svg" not in svg:
print(
f"[error] 返回不是 SVG: 图标 '{args.set}:{args.name}' 不存在",
file=sys.stderr,
)
return 2
out: Path = args.out
want_svg = args.svg_only or out.suffix.lower() == ".svg"
if want_svg:
if out.suffix.lower() != ".svg":
out = out.with_suffix(".svg")
out.write_text(svg, encoding="utf-8")
print(f"[ok] SVG → {out}")
return 0
if svg_to_png(svg, out, args.size):
print(f"[ok] PNG → {out} ({args.set}:{args.name} #{args.color})")
return 0
# PNG 转换失败,保存 SVG 兜底
svg_alt = out.with_suffix(".svg")
svg_alt.write_text(svg, encoding="utf-8")
print(
f"[warn] PNG 转换不可用 (装 `pip install cairosvg` 或 `pip install svglib`)\n"
f" 已保存 SVG → {svg_alt}\n"
f" PowerPoint 2016+ 直接 add_picture(svg) 也可以",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
sys.exit(main())