fix(ppt): 修生成 PPT 缺图标(图标管线四层断点)+ 沙箱 SVG 预览渲染(bump 0.33.3)
查真实用户两个「ppt生成」任务的 DB 执行轨迹:24 页 SVG 共 0 个 <use data-icon>。 根因是图标管线四环节无一强制图标落地——策略层(有时)锁图标,执行层不放、 质检层不拦、工具层还断着。四层一起修: - B 工具断点:references/SKILL 23 处路径仍指向已不存在的 skills/ppt-master/ (zcbot 是 skills/ppt/)→ 模型 `ls .../icons/<lib>/|grep` 验名得空集 → 放弃图标; 且 strategist 强制用的 icon_sync.py 在 zcbot 根本没有(GATE 空转,正是某任务连 图标都没锁的原因)。修:全量改路径(保留上游署名)+ 新建 icon_sync.py(复用 embed_icons 解析,验名+拷进 project/icons,缺名非零退出)。 - A 质检兜底(硬门):svg_quality_checker 加图标校验——锁了 icons.library + 非空 inventory 但全 deck 0 图标 → deck 级 error 退非零(逼回执行重写);单页 0 图标 → warning(封面/分节/breathing/尾页豁免)。 - C 执行强制:executor-base §4 + SKILL 执行纪律改为"内容页必须放 1–3 个 inventory 图标"(自由设计无模板可继承图标,只能逐页手写)。 - D 导出兜底(纵深):svg_to_pptx 导出前预扫,锁了 inventory 却 0 图标 → stderr 大声 [WARN](非致命,防跳过质检直接导出)。核实 native 转换器本就自己从图标库展开 <use data-icon>,故原设想的"finalize 硬前置"前提不成立,D 改成与 A 同源的导出层警告。 同版附带修 svg_preview.py 在沙箱里渲不出 SVG(报"未找到 Chrome / Edge"):移植自 ppt-master 的 find_browser() 只认 Windows chrome/msedge,不认镜像自带 /usr/bin/chromium (给 mermaid 装的)→ 视觉验收这关在容器里全程失效。对齐 rendering/pdf.py 发现逻辑 (认 chromium/chromium-browser/google-chrome + $CHROMIUM 覆盖);render() 补容器必需的 --disable-dev-shm-usage + 临时 --user-data-dir;并修一个静默已久的 bug——--screenshot 传相对路径 chromium 写不出文件(原代码吞 stderr,看着和"没浏览器"一样),改传绝对路径 并暴露 chromium stderr。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
001f9af96f
commit
5d23ee682b
12
PROGRESS.md
12
PROGRESS.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-06-30(look_at_image 超时透明重试 + 超时 60→120s + bump 0.33.2)
|
最后更新:2026-06-30(ppt skill 修图标管线四层断点:executor 不放图标/质检不拦/工具断/导出不兜底 + bump 0.33.3)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,6 +21,16 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-06-30 / ppt skill 修「生成的 PPT 缺图标」四层断点(bump 0.33.3)
|
||||||
|
查真实用户(caoqianming@foxmail.com)两个「ppt生成」任务的 DB 执行轨迹:24 页 SVG 共 0 个 `<use data-icon>`。根因是图标管线四个环节没有一个强制图标落地——**策略层(有时)锁图标,执行层不放、质检层不拦、工具层还断着**。四层一起修:
|
||||||
|
- **B 工具断点**:references/SKILL 里 23 处路径仍指向已不存在的 `skills/ppt-master/`(zcbot 是 `skills/ppt/`)→ 模型按文档 `ls .../icons/<lib>/|grep` 验名得空集 → 放弃图标;且 strategist 强制用的 `icon_sync.py` 在 zcbot 根本没有(GATE 空转,正是某任务连图标都没锁的原因)。修:全量改路径 + 新建 `skills/ppt/scripts/icon_sync.py`(复用 embed_icons 解析,验名+拷进 project/icons,缺名非零退出)。
|
||||||
|
- **A 质检兜底(硬门)**:`svg_quality_checker.py` 加图标校验——spec_lock 锁了 `icons.library`+非空 `inventory` 但全 deck 0 图标 → **deck 级 error 退非零**(逼回执行重写);单页 0 图标 → warning(封面/分节/breathing/尾页豁免)。
|
||||||
|
- **C 执行强制**:executor-base §4 + SKILL 执行纪律第 4 条从"怎么写图标"改为"**内容页必须放 1–3 个 inventory 图标**"(自由设计无模板可继承图标,只能逐页手写)。
|
||||||
|
- **D 导出兜底(纵深)**:`svg_to_pptx` 导出前预扫,锁了 inventory 却 0 图标 → stderr 大声 [WARN](非致命,防跳过质检直接导出)。
|
||||||
|
> 附:核实 native 转换器(`drawingml_converter` 调 `use_expander`)本就自己从图标库展开 `<use data-icon>`,故 svg_output 保留原始占位符是正确的——原设想的"finalize 硬前置防丢图标"前提不成立,D 改成 A 同源的导出层警告。
|
||||||
|
|
||||||
|
同版附带修 **svg_preview.py 在沙箱里渲不出 SVG**(报"未找到 Chrome / Edge"):移植自 ppt-master 的 `find_browser()` 只认 Windows `chrome/msedge`,不认沙箱镜像自带的 `/usr/bin/chromium`(给 mermaid 装的)→ 视觉验收这关在容器里全程失效。对齐 `rendering/pdf.py` 的发现逻辑(认 `chromium`/`chromium-browser`/`google-chrome` + `$CHROMIUM` 覆盖);`render()` 补容器必需的 `--disable-dev-shm-usage` + 临时 `--user-data-dir`(cap-dropped 容器 /dev/shm 仅 64MB,否则 chromium 渲染中途崩);顺带挖出并修一个静默已久的 bug——`--screenshot` 传相对路径 chromium 写不出文件(原代码吞 stderr 看着和"没浏览器"一样),改传**绝对路径**并把 chromium stderr 暴露出来。skills 是 `/sandbox/skills:ro` bind 挂载,改动下次 exec 即生效,无需重建镜像。
|
||||||
|
|
||||||
### 2026-06-30 / look_at_image 偶发超时:tool 内透明重试 + 超时上限提到 120s(bump 0.33.2)
|
### 2026-06-30 / look_at_image 偶发超时:tool 内透明重试 + 超时上限提到 120s(bump 0.33.2)
|
||||||
Seed 2.0 Lite 非流式,长 OCR 首字节可能逼近 60s read timeout → 偶发超时,且返 `[Error]` 会触发主模型重发整个 tool call(图 base64 重传、输入 token 再付一次,正中"报错重试烧 token"根因)。修法:`ark_client` 新增 `ArkTimeoutError(ArkError)` 子类(仅超时/网络抖动抛它,HTTP 4xx/5xx 业务错误仍抛普通 `ArkError` 不重试);`look_at_image` 对该子类退避重试(`timeout_retries` 默认 1 次,退避 2^n s),在 tool 内消化掉不抛给主模型;`doubao.yaml` vision `request_timeout_s` 60→120。子类仍是 `ArkError`,seedream 等现有 `except ArkError` 不受影响。
|
Seed 2.0 Lite 非流式,长 OCR 首字节可能逼近 60s read timeout → 偶发超时,且返 `[Error]` 会触发主模型重发整个 tool call(图 base64 重传、输入 token 再付一次,正中"报错重试烧 token"根因)。修法:`ark_client` 新增 `ArkTimeoutError(ArkError)` 子类(仅超时/网络抖动抛它,HTTP 4xx/5xx 业务错误仍抛普通 `ArkError` 不重试);`look_at_image` 对该子类退避重试(`timeout_retries` 默认 1 次,退避 2^n s),在 tool 内消化掉不抛给主模型;`doubao.yaml` vision `request_timeout_s` 60→120。子类仍是 `ArkError`,seedream 等现有 `except ArkError` 不受影响。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.33.2"
|
__version__ = "0.33.3"
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
|
||||||
1. **逐页串行手写,不批量、不脚本生成**:每页由当前主 agent 在同一上下文里手写 SVG;**禁止写循环脚本批量产 SVG**(跨页视觉一致性靠逐页带上游上下文,生成器做不到),也不要 5 页一组。
|
1. **逐页串行手写,不批量、不脚本生成**:每页由当前主 agent 在同一上下文里手写 SVG;**禁止写循环脚本批量产 SVG**(跨页视觉一致性靠逐页带上游上下文,生成器做不到),也不要 5 页一组。
|
||||||
2. **每页前重读 `spec_lock.md`**:颜色/字体/图标/图片只能来自它;查本页 `page_rhythm`/`page_layouts`/`page_charts`。抗上下文压缩漂移。
|
2. **每页前重读 `spec_lock.md`**:颜色/字体/图标/图片只能来自它;查本页 `page_rhythm`/`page_layouts`/`page_charts`。抗上下文压缩漂移。
|
||||||
3. **模板供结构不供皮**(非 mirror):继承几何/标签位置/编码逻辑,**重新上 visual_style + spec_lock.colors 的皮**;字号按 spec_lock 角色锁定值,不继承模板占位字号。
|
3. **模板供结构不供皮**(非 mirror):继承几何/标签位置/编码逻辑,**重新上 visual_style + spec_lock.colors 的皮**;字号按 spec_lock 角色锁定值,不继承模板占位字号。
|
||||||
4. **图标**:写 `<use data-icon="<lib>/<name>" x= y= width= height= fill= [stroke-width=]>`,name 必须在 inventory 内、文件在 `templates/icons/<lib>/`。
|
4. **图标(锁了就必须用,非可选装饰)**:spec_lock 有 `icons.library` + 非空 `inventory` 时,**每个内容页必须放 1–3 个 inventory 内的图标**(KPI/列表/流程/对比/特性网格版式尤其要,常一卡一图标)——自由设计没有模板可继承图标,只能逐页手写 `<use data-icon>` 才有图标。封面/纯排版分节页/单数字·金句 breathing 页/尾页可不放。写法:`<use data-icon="<lib>/<name>" x= y= width= height= fill= [stroke-width=]>`,name 必须在 inventory 内、文件在 `templates/icons/<lib>/`。**质检会硬卡**:锁了 inventory 但全 deck 0 图标 → error 退非零(见阶段四)。
|
||||||
5. **配图**:`<image href="../images/<file>">`,croppable 用 `preserveAspectRatio="xMidYMid slice"`,`| no-crop` 行用 `meet`;意图与版式见 image-layout-*。
|
5. **配图**:`<image href="../images/<file>">`,croppable 用 `preserveAspectRatio="xMidYMid slice"`,`| no-crop` 行用 `meet`;意图与版式见 image-layout-*。
|
||||||
|
|
||||||
逐页写到 `<project_dir>/svg_output/<NN>_<page>.svg`。**演讲者备注**写 `<project_dir>/notes/total.md`(每页 2–4 句结论先行口语)。
|
逐页写到 `<project_dir>/svg_output/<NN>_<page>.svg`。**演讲者备注**写 `<project_dir>/notes/total.md`(每页 2–4 句结论先行口语)。
|
||||||
|
|
@ -137,7 +137,7 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
|
||||||
```
|
```
|
||||||
.venv/Scripts/python.exe <skill_dir>/scripts/svg_quality_checker.py <project_dir>
|
.venv/Scripts/python.exe <skill_dir>/scripts/svg_quality_checker.py <project_dir>
|
||||||
```
|
```
|
||||||
- **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移等)必须改:回阶段三重写该页再跑**,不放过。
|
- **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移 / **锁了图标 inventory 却全 deck 0 图标** 等)必须改:回阶段三重写该页再跑**,不放过。
|
||||||
- `warning`(低分辨率图 / 非 PPT 安全字体等):能顺手改就改,否则知会后放行。
|
- `warning`(低分辨率图 / 非 PPT 安全字体等):能顺手改就改,否则知会后放行。
|
||||||
- 跑 `svg_output/`(不要在 finalize 后跑 —— finalize 改写 SVG 会掩盖源级违规)。
|
- 跑 `svg_output/`(不要在 finalize 后跑 —— finalize 改写 SVG 会掩盖源级违规)。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@ Run the standalone [`customize-animations`](../workflows/customize-animations.md
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build an editable scaffold from real top-level <g id> anchors
|
# Build an editable scaffold from real top-level <g id> anchors
|
||||||
python3 skills/ppt-master/scripts/animation_config.py scaffold <project>
|
python3 skills/ppt/scripts/animation_config.py scaffold <project>
|
||||||
|
|
||||||
# Validate references before export
|
# Validate references before export
|
||||||
python3 skills/ppt-master/scripts/animation_config.py validate <project>
|
python3 skills/ppt/scripts/animation_config.py validate <project>
|
||||||
|
|
||||||
# Export reads <project>/animations.json automatically when present
|
# Export reads <project>/animations.json automatically when present
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project>
|
python3 skills/ppt/scripts/svg_to_pptx.py <project>
|
||||||
```
|
```
|
||||||
|
|
||||||
Minimal sidecar:
|
Minimal sidecar:
|
||||||
|
|
@ -60,13 +60,13 @@ Rules:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pick a different effect
|
# Pick a different effect
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> -t push --transition-duration 0.6
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> -t push --transition-duration 0.6
|
||||||
|
|
||||||
# Disable
|
# Disable
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> -t none
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> -t none
|
||||||
|
|
||||||
# Auto-advance every 5 seconds (kiosk-style playback)
|
# Auto-advance every 5 seconds (kiosk-style playback)
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> --auto-advance 5
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> --auto-advance 5
|
||||||
```
|
```
|
||||||
|
|
||||||
Available effects: `fade`, `push`, `wipe`, `split`, `strips`, `cover`, `random`.
|
Available effects: `fade`, `push`, `wipe`, `split`, `strips`, `cover`, `random`.
|
||||||
|
|
@ -87,23 +87,23 @@ Off by default — enable deck-wide with `-a auto` (or another effect). Once ena
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default behavior (no flags): page transitions only, no per-element builds
|
# Default behavior (no flags): page transitions only, no per-element builds
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project>
|
python3 skills/ppt/scripts/svg_to_pptx.py <project>
|
||||||
|
|
||||||
# Enable per-element animation deck-wide (auto effect + after-previous cascade)
|
# Enable per-element animation deck-wide (auto effect + after-previous cascade)
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> -a auto
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> -a auto
|
||||||
|
|
||||||
# Enable with a single effect (cascades via the after-previous trigger)
|
# Enable with a single effect (cascades via the after-previous trigger)
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> --animation fade
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> --animation fade
|
||||||
|
|
||||||
# Enable and switch to on-click for live presentations (presenter controls pacing)
|
# Enable and switch to on-click for live presentations (presenter controls pacing)
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> -a auto --animation-trigger on-click
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> -a auto --animation-trigger on-click
|
||||||
|
|
||||||
# Custom pacing
|
# Custom pacing
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> --animation mixed \
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> --animation mixed \
|
||||||
--animation-stagger 0.7 --animation-duration 0.5
|
--animation-stagger 0.7 --animation-duration 0.5
|
||||||
|
|
||||||
# All groups animate in unison on slide entry
|
# All groups animate in unison on slide entry
|
||||||
python3 skills/ppt-master/scripts/svg_to_pptx.py <project> --animation-trigger with-previous
|
python3 skills/ppt/scripts/svg_to_pptx.py <project> --animation-trigger with-previous
|
||||||
```
|
```
|
||||||
|
|
||||||
22 single effects: `appear`, `fade`, `fly`, `cut`, `zoom`, `wipe`, `split`, `blinds`, `checkerboard`, `dissolve`, `random_bars`, `peek`, `wheel`, `box`, `circle`, `diamond`, `plus`, `strips`, `wedge`, `stretch`, `expand`, `swivel`. Plus three auto-vary modes:
|
22 single effects: `appear`, `fade`, `fly`, `cut`, `zoom`, `wipe`, `split`, `blinds`, `checkerboard`, `dissolve`, `random_bars`, `peek`, `wheel`, `box`, `circle`, `diamond`, `plus`, `strips`, `wedge`, `stretch`, `expand`, `swivel`. Plus three auto-vary modes:
|
||||||
|
|
@ -137,7 +137,7 @@ Aim for **3–8 content groups per slide**. This is also the granularity PowerPo
|
||||||
- ≤ 8 visible top-level primitives → each becomes one anchor (capped to avoid 70+ atom cascades on dense pages).
|
- ≤ 8 visible top-level primitives → each becomes one anchor (capped to avoid 70+ atom cascades on dense pages).
|
||||||
- > 8 → animation is skipped on that slide. The slide still renders, just without entrance animation.
|
- > 8 → animation is skipped on that slide. The slide still renders, just without entrance animation.
|
||||||
|
|
||||||
Executors should wrap logical sections in `<g id>` regardless of whether you plan to animate. The Executor reference (`skills/ppt-master/references/shared-standards.md`) requires it.
|
Executors should wrap logical sections in `<g id>` regardless of whether you plan to animate. The Executor reference (`skills/ppt/references/shared-standards.md`) requires it.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,12 @@ Examples: `01_封面.svg` / `02_目录.svg` / `03_核心优势.svg`; `01_cover.s
|
||||||
|
|
||||||
Strategist chooses the library and inventory; Executor only implements. Library details and one-library rule: [`../templates/icons/README.md`](../templates/icons/README.md). This section defines placeholder syntax.
|
Strategist chooses the library and inventory; Executor only implements. Library details and one-library rule: [`../templates/icons/README.md`](../templates/icons/README.md). This section defines placeholder syntax.
|
||||||
|
|
||||||
|
> 🚧 **MANDATE — icons are not optional in free-design mode.** When `spec_lock.md` declares an `icons.library` + non-empty `inventory`, **every content page MUST place icons from that inventory** — they are part of the design, not garnish. In free-design mode there is no mirror template to copy icons from, so the only way icons reach the deck is you authoring `<use data-icon>` on each page. Concretely:
|
||||||
|
> - **Content pages** (KPI cards, lists, process / flow steps, comparison columns, feature grids, section dividers with a concept) → place **1–3** inventory icons that label the content (one per card / step / list item is the common pattern). A dense content page with zero icons reads flat and is a quality regression.
|
||||||
|
> - **Legitimately icon-less** (do NOT force icons): the cover, a pure-typography section break, a single-number / single-quote `breathing` page, and the closing/thanks page.
|
||||||
|
> - The strategist already validated each inventory name exists (via `icon_sync.py`); use those names verbatim — do not invent new ones.
|
||||||
|
> - **Enforcement**: `svg_quality_checker.py` fails the deck (hard error, non-zero exit) when an inventory is locked but the deck authors **zero** `<use data-icon>` across all pages, and warns per page that references none. Don't ship past it by deleting the icon lock — place the icons.
|
||||||
|
|
||||||
> **Resolution is project-first.** Strategist copied the chosen icons into `<project_path>/icons/<lib>/` (via `icon_sync.py`); `finalize_svg.py embed-icons` embeds from there, falling back to the global library per-icon. **Custom icons**: drop an `.svg` into `<project_path>/icons/<lib>/` (any `<lib>`, e.g. `custom/`) and reference it as `data-icon="<lib>/<name>"` — it embeds like any other. Reference only icons in the `spec_lock.md` inventory.
|
> **Resolution is project-first.** Strategist copied the chosen icons into `<project_path>/icons/<lib>/` (via `icon_sync.py`); `finalize_svg.py embed-icons` embeds from there, falling back to the global library per-icon. **Custom icons**: drop an `.svg` into `<project_path>/icons/<lib>/` (any `<lib>`, e.g. `custom/`) and reference it as `data-icon="<lib>/<name>"` — it embeds like any other. Reference only icons in the `spec_lock.md` inventory.
|
||||||
|
|
||||||
**Built-in icons — Placeholder method (recommended)**:
|
**Built-in icons — Placeholder method (recommended)**:
|
||||||
|
|
@ -262,11 +268,11 @@ Strategist chooses the library and inventory; Executor only implements. Library
|
||||||
|
|
||||||
**Searching for icons** — use terminal, zero token cost:
|
**Searching for icons** — use terminal, zero token cost:
|
||||||
```bash
|
```bash
|
||||||
ls skills/ppt-master/templates/icons/chunk-filled/ | grep home
|
ls skills/ppt/templates/icons/chunk-filled/ | grep home
|
||||||
ls skills/ppt-master/templates/icons/tabler-filled/ | grep home
|
ls skills/ppt/templates/icons/tabler-filled/ | grep home
|
||||||
ls skills/ppt-master/templates/icons/tabler-outline/ | grep chart
|
ls skills/ppt/templates/icons/tabler-outline/ | grep chart
|
||||||
ls skills/ppt-master/templates/icons/phosphor-duotone/ | grep house
|
ls skills/ppt/templates/icons/phosphor-duotone/ | grep house
|
||||||
ls skills/ppt-master/templates/icons/simple-icons/ | grep github
|
ls skills/ppt/templates/icons/simple-icons/ | grep github
|
||||||
```
|
```
|
||||||
|
|
||||||
**Abstract concept → icon name** (names for `chunk-filled`; tabler libraries use their own equivalents — verify with `ls | grep`):
|
**Abstract concept → icon name** (names for `chunk-filled`; tabler libraries use their own equivalents — verify with `ls | grep`):
|
||||||
|
|
|
||||||
|
|
@ -157,9 +157,9 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
|
||||||
> **After all eight confirmations are approved — when writing `design_spec.md` §VI / `spec_lock.md`**, then materialize the icon inventory:
|
> **After all eight confirmations are approved — when writing `design_spec.md` §VI / `spec_lock.md`**, then materialize the icon inventory:
|
||||||
>
|
>
|
||||||
> 3. Enumerate the concepts the deck actually needs (home, chart, users, …) based on the confirmed outline.
|
> 3. Enumerate the concepts the deck actually needs (home, chart, users, …) based on the confirmed outline.
|
||||||
> 4. Search for each concept's filename in the chosen library: `ls skills/ppt-master/templates/icons/<chosen-library>/ | grep <keyword>`
|
> 4. Search for each concept's filename in the chosen library: `ls skills/ppt/templates/icons/<chosen-library>/ | grep <keyword>`
|
||||||
> 5. Use the verified filename (without `.svg`) as the icon name; always include the library prefix (e.g., `chunk-filled/home`).
|
> 5. Use the verified filename (without `.svg`) as the icon name; always include the library prefix (e.g., `chunk-filled/home`).
|
||||||
> 6. **Copy each chosen icon into the project as you confirm it** — `python3 skills/ppt-master/scripts/icon_sync.py <project_path> <lib/name> [<lib/name> …]`. This populates `<project>/icons/<lib>/` (the set the Executor embeds from) and, more importantly, **validates existence on the spot**.
|
> 6. **Copy each chosen icon into the project as you confirm it** — `python3 skills/ppt/scripts/icon_sync.py <project_path> <lib/name> [<lib/name> …]`. This populates `<project>/icons/<lib>/` (the set the Executor embeds from) and, more importantly, **validates existence on the spot**.
|
||||||
> 7. List the final icon inventory and chosen library in `design_spec.md` §VI; record the same in `spec_lock.md icons` (including `stroke_width` for stroke-style libraries). Executor may only use icons from this list.
|
> 7. List the final icon inventory and chosen library in `design_spec.md` §VI; record the same in `spec_lock.md icons` (including `stroke_width` for stroke-style libraries). Executor may only use icons from this list.
|
||||||
>
|
>
|
||||||
> 🚧 **GATE — missing icon = re-pick now**: if `icon_sync.py` reports any name as missing (non-zero exit), that icon is not in the library — re-pick a real filename via `ls … | grep`, fix `§VI` / `spec_lock.md`, and re-run until it exits clean. Never carry a missing icon forward to generation. Over-copying candidates is harmless — finalize embeds only the icons actually referenced by `<use data-icon>`.
|
> 🚧 **GATE — missing icon = re-pick now**: if `icon_sync.py` reports any name as missing (non-zero exit), that icon is not in the library — re-pick a real filename via `ls … | grep`, fix `§VI` / `spec_lock.md`, and re-run until it exits clean. Never carry a missing icon forward to generation. Over-copying candidates is harmless — finalize embeds only the icons actually referenced by `<use data-icon>`.
|
||||||
|
|
@ -307,7 +307,7 @@ Formula rendering is part of Typography confirmation. Recommend one policy and l
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p <project_path>/images
|
mkdir -p <project_path>/images
|
||||||
python3 skills/ppt-master/scripts/latex_render.py <project_path>
|
python3 skills/ppt/scripts/latex_render.py <project_path>
|
||||||
```
|
```
|
||||||
|
|
||||||
Write the manifest first at `<project_path>/images/formula_manifest.json`. Use this shape:
|
Write the manifest first at `<project_path>/images/formula_manifest.json`. Use this shape:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Icon validation + project sync (strategist GATE tool).
|
||||||
|
|
||||||
|
Strategist runs this while locking the icon inventory (design_spec.md §VI /
|
||||||
|
spec_lock.md `icons`). It does two things, in order of importance:
|
||||||
|
|
||||||
|
1. **Validate** each ``lib/name`` against the global icon library
|
||||||
|
(``templates/icons/``). A name that does not resolve to a real file is a
|
||||||
|
hard error — the script exits non-zero listing every miss, so the
|
||||||
|
strategist's "missing icon = re-pick now" GATE actually fires instead of
|
||||||
|
carrying a phantom icon into generation.
|
||||||
|
2. **Copy** each resolved icon into ``<project>/icons/<lib>/`` so the project
|
||||||
|
carries its own icon set. This is belt-and-suspenders: ``finalize_svg.py``
|
||||||
|
and the native ``svg_to_pptx`` converter both resolve project-first with a
|
||||||
|
fallback to the global library (see ``svg_finalize.embed_icons``), so the
|
||||||
|
copy is not strictly required for embedding — but it keeps the project
|
||||||
|
self-contained and validates existence on the spot.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/icon_sync.py <project_path> <lib/name> [<lib/name> ...]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python3 scripts/icon_sync.py /workspace/ppt生成/deck tabler-outline/brain tabler-outline/cpu
|
||||||
|
|
||||||
|
Exit code:
|
||||||
|
0 every icon resolved (and copied)
|
||||||
|
1 one or more icons missing / unresolved
|
||||||
|
2 bad arguments
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Windows GBK console safety (mirrors other ppt scripts).
|
||||||
|
try: # pragma: no cover - platform dependent
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reuse the single source of truth for name resolution (handles the chunk/
|
||||||
|
# alias and the un-prefixed legacy layout) so validation matches what the
|
||||||
|
# embedder will actually load at finalize / export time.
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from svg_finalize.embed_icons import resolve_icon_path # noqa: E402
|
||||||
|
|
||||||
|
GLOBAL_ICONS_DIR = Path(__file__).resolve().parent.parent / "templates" / "icons"
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
if len(args) < 2:
|
||||||
|
print("usage: icon_sync.py <project_path> <lib/name> [<lib/name> ...]")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
project_path = Path(args[0])
|
||||||
|
names = args[1:]
|
||||||
|
if not project_path.exists():
|
||||||
|
print(f"[ERROR] project path not found: {project_path}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
project_icons = project_path / "icons"
|
||||||
|
missing: list[str] = []
|
||||||
|
copied = 0
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
src, _base = resolve_icon_path(name, GLOBAL_ICONS_DIR)
|
||||||
|
if not src.exists():
|
||||||
|
missing.append(name)
|
||||||
|
print(f"[MISSING] {name} (no file under {GLOBAL_ICONS_DIR})")
|
||||||
|
continue
|
||||||
|
# Mirror into <project>/icons/<lib>/<name>.svg, preserving the lib dir.
|
||||||
|
rel = src.relative_to(GLOBAL_ICONS_DIR)
|
||||||
|
dst = project_icons / rel
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(src, dst)
|
||||||
|
copied += 1
|
||||||
|
print(f"[OK] {name} -> {dst}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"[Summary] {copied} copied, {len(missing)} missing")
|
||||||
|
if missing:
|
||||||
|
print("[GATE] missing icons — re-pick real filenames via "
|
||||||
|
"`ls templates/icons/<lib>/ | grep <keyword>`, fix spec_lock.md, re-run:")
|
||||||
|
for m in missing:
|
||||||
|
print(f" - {m}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -25,6 +25,8 @@ try: # zcbot: Windows GBK 控制台兼容,避免 emoji/© 等触发 UnicodeEn
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_CHROME_CANDIDATES = [
|
_CHROME_CANDIDATES = [
|
||||||
|
|
@ -33,19 +35,37 @@ _CHROME_CANDIDATES = [
|
||||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
||||||
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||||
]
|
]
|
||||||
|
# Linux / zcbot sandbox: the image ships chromium at /usr/bin/chromium (installed
|
||||||
|
# for mermaid-cli). The original ppt-master discovery only knew Windows
|
||||||
|
# chrome/edge, so SVG preview always failed inside the container. Mirror the
|
||||||
|
# discovery zcbot already uses in rendering/pdf.py so it resolves the bundled
|
||||||
|
# chromium instead.
|
||||||
|
_LINUX_CANDIDATES = [
|
||||||
|
"/usr/bin/chromium", "/usr/bin/chromium-browser",
|
||||||
|
"/usr/bin/google-chrome", "/usr/bin/google-chrome-stable",
|
||||||
|
]
|
||||||
|
_WHICH_NAMES = (
|
||||||
|
"chrome", "chrome.exe", "msedge", "msedge.exe",
|
||||||
|
"chromium", "chromium-browser", "google-chrome", "google-chrome-stable",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def find_browser() -> str:
|
def find_browser() -> str:
|
||||||
for c in _CHROME_CANDIDATES:
|
# Explicit override first (matches rendering/pdf.py's CHROMIUM/CHROME env).
|
||||||
|
env = os.environ.get("CHROMIUM") or os.environ.get("CHROME")
|
||||||
|
if env and (shutil.which(env) or Path(env).exists()):
|
||||||
|
return shutil.which(env) or env
|
||||||
|
for c in _CHROME_CANDIDATES + _LINUX_CANDIDATES:
|
||||||
if Path(c).exists():
|
if Path(c).exists():
|
||||||
return c
|
return c
|
||||||
import shutil
|
for name in _WHICH_NAMES:
|
||||||
for name in ("chrome", "chrome.exe", "msedge", "msedge.exe"):
|
|
||||||
p = shutil.which(name)
|
p = shutil.which(name)
|
||||||
if p:
|
if p:
|
||||||
return p
|
return p
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"[fatal] 未找到 Chrome / Edge,无法渲染 SVG 预览。请安装其一,或用浏览器手动打开 svg_output/*.svg 验收。"
|
"[fatal] 未找到 Chrome / Edge / Chromium,无法渲染 SVG 预览。"
|
||||||
|
"沙箱镜像应自带 /usr/bin/chromium;本机可装 Chrome,或设 CHROMIUM 环境变量,"
|
||||||
|
"或用浏览器手动打开 svg_output/*.svg 验收。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,31 +99,48 @@ def _wrap_html(svg_path: Path, w: float, h: float) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def render(browser: str, svg_path: Path, out_png: Path, scale: float = 2.0) -> None:
|
def render(browser: str, svg_path: Path, out_png: Path, scale: float = 2.0) -> bool:
|
||||||
|
"""Render one SVG to PNG via headless chromium. Returns True on success.
|
||||||
|
|
||||||
|
On failure prints a concise warning with the chromium stderr tail (the
|
||||||
|
original code silenced stderr, so a broken render looked identical to a
|
||||||
|
missing browser — impossible to diagnose). The caller keeps going so one
|
||||||
|
bad page doesn't abort the whole batch.
|
||||||
|
"""
|
||||||
svg_text = svg_path.read_text(encoding="utf-8", errors="ignore")
|
svg_text = svg_path.read_text(encoding="utf-8", errors="ignore")
|
||||||
w, h = _dims(svg_text)
|
w, h = _dims(svg_text)
|
||||||
html = _wrap_html(svg_path, w, h)
|
html = _wrap_html(svg_path, w, h)
|
||||||
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f:
|
# chromium resolves --screenshot against its own cwd, not ours; a relative
|
||||||
f.write(html)
|
# path silently fails to write ("系统找不到指定的路径"). Always pass absolute.
|
||||||
html_path = Path(f.name)
|
out_png = out_png.resolve()
|
||||||
try:
|
|
||||||
out_png.parent.mkdir(parents=True, exist_ok=True)
|
out_png.parent.mkdir(parents=True, exist_ok=True)
|
||||||
subprocess.run(
|
# TemporaryDirectory holds both the wrapper HTML and a throwaway
|
||||||
|
# --user-data-dir; auto-cleaned on exit.
|
||||||
|
with tempfile.TemporaryDirectory(prefix="svgprev-") as tmp:
|
||||||
|
html_path = Path(tmp) / "page.html"
|
||||||
|
html_path.write_text(html, encoding="utf-8")
|
||||||
|
proc = subprocess.run(
|
||||||
[
|
[
|
||||||
browser, "--headless", "--disable-gpu", "--no-sandbox",
|
browser, "--headless", "--disable-gpu", "--no-sandbox",
|
||||||
|
# Required in the cap-dropped sandbox: chromium's own setuid
|
||||||
|
# sandbox can't start (--no-sandbox), and the container's 64MB
|
||||||
|
# /dev/shm is too small (--disable-dev-shm-usage), else chromium
|
||||||
|
# crashes mid-render. Same flags rendering/pdf.py uses.
|
||||||
|
"--disable-dev-shm-usage", "--user-data-dir=%s" % (Path(tmp) / "cr"),
|
||||||
"--hide-scrollbars", "--force-device-scale-factor=%s" % scale,
|
"--hide-scrollbars", "--force-device-scale-factor=%s" % scale,
|
||||||
"--window-size=%d,%d" % (round(w), round(h)),
|
"--window-size=%d,%d" % (round(w), round(h)),
|
||||||
"--default-background-color=FFFFFFFF",
|
"--default-background-color=FFFFFFFF",
|
||||||
"--screenshot=%s" % str(out_png),
|
"--screenshot=%s" % str(out_png),
|
||||||
html_path.as_uri(),
|
html_path.as_uri(),
|
||||||
],
|
],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60,
|
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=120, check=False,
|
||||||
)
|
)
|
||||||
finally:
|
if out_png.exists() and out_png.stat().st_size > 0:
|
||||||
try:
|
return True
|
||||||
html_path.unlink()
|
tail = (proc.stderr or b"").decode("utf-8", "replace").strip()[-400:]
|
||||||
except OSError:
|
print(f" [warn] 渲染失败 {svg_path.name}(rc={proc.returncode})"
|
||||||
pass
|
+ (f": {tail}" if tail else ""))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _collect(target: Path) -> tuple[list[Path], Path]:
|
def _collect(target: Path) -> tuple[list[Path], Path]:
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,16 @@ class SVGQualityChecker:
|
||||||
# severity is 'error' or 'warning'. Printed in print_summary.
|
# severity is 'error' or 'warning'. Printed in print_summary.
|
||||||
self._template_issues: List[Tuple[str, str, str]] = []
|
self._template_issues: List[Tuple[str, str, str]] = []
|
||||||
self._animation_issues: List[Tuple[str, str]] = []
|
self._animation_issues: List[Tuple[str, str]] = []
|
||||||
|
# Icon-usage aggregation (non-template mode). When spec_lock declares an
|
||||||
|
# icon library + inventory, the strategist intends the deck to use icons.
|
||||||
|
# The native exporter and finalize both expand <use data-icon> from the
|
||||||
|
# library, so an authored placeholder reliably becomes a real icon — but
|
||||||
|
# only if the executor writes one. A deck that locks an inventory yet
|
||||||
|
# authors ZERO placeholders ships flat/icon-less; this is the missing
|
||||||
|
# feedback loop that catches the executor silently skipping icons.
|
||||||
|
self._icon_inventory_declared = False # any page's spec_lock locked icons
|
||||||
|
self._deck_icon_total = 0 # total <use data-icon> across the deck
|
||||||
|
self._pages_missing_icons: List[str] = [] # declared-but-icon-less pages
|
||||||
|
|
||||||
def check_file(self, svg_file: str, expected_format: str = None) -> Dict:
|
def check_file(self, svg_file: str, expected_format: str = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -301,6 +311,11 @@ class SVGQualityChecker:
|
||||||
if not self.template_mode:
|
if not self.template_mode:
|
||||||
self._check_sourced_image_attribution(content, svg_path, result)
|
self._check_sourced_image_attribution(content, svg_path, result)
|
||||||
|
|
||||||
|
# 11. Check declared-vs-used icons. Templates don't ship a
|
||||||
|
# spec_lock.md; skip in template mode.
|
||||||
|
if not self.template_mode:
|
||||||
|
self._check_icon_usage(content, svg_path, result)
|
||||||
|
|
||||||
# Determine pass/fail
|
# Determine pass/fail
|
||||||
result['passed'] = len(result['errors']) == 0
|
result['passed'] = len(result['errors']) == 0
|
||||||
|
|
||||||
|
|
@ -1386,6 +1401,60 @@ class SVGQualityChecker:
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
def _check_icon_usage(self, content: str, svg_path: Path, result: Dict) -> None:
|
||||||
|
"""Warn when a page references no icons despite spec_lock locking an
|
||||||
|
inventory, and feed the deck-level zero-icons gate.
|
||||||
|
|
||||||
|
Section / cover / closing pages legitimately ship without icons, so a
|
||||||
|
single icon-less page is only a soft per-page warning. The hard failure
|
||||||
|
is deck-wide (every page icon-less while an inventory is locked) and is
|
||||||
|
emitted in :py:meth:`_print_icon_summary`.
|
||||||
|
"""
|
||||||
|
lock = self._get_spec_lock(svg_path)
|
||||||
|
if not lock:
|
||||||
|
return
|
||||||
|
icons = lock.get('icons') or {}
|
||||||
|
library = (icons.get('library') or '').strip().lower()
|
||||||
|
inventory = (icons.get('inventory') or '').strip().lower()
|
||||||
|
_empty = ('', 'none', '(none)', '-', 'n/a')
|
||||||
|
declared = library not in _empty and inventory not in _empty
|
||||||
|
if not declared:
|
||||||
|
return
|
||||||
|
self._icon_inventory_declared = True
|
||||||
|
count = len(re.findall(r'<use\b[^>]*\bdata-icon\s*=', content))
|
||||||
|
result.setdefault('info', {})['icon_count'] = count
|
||||||
|
self._deck_icon_total += count
|
||||||
|
if count == 0:
|
||||||
|
self._pages_missing_icons.append(svg_path.name)
|
||||||
|
result['warnings'].append(
|
||||||
|
f"spec_lock locks an icon library ({icons.get('library')}) + inventory "
|
||||||
|
f"but this page references no <use data-icon> — content pages should place "
|
||||||
|
f"1-3 inventory icons (cover / section / closing pages may omit)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _print_icon_summary(self):
|
||||||
|
"""Deck-level icon-usage gate.
|
||||||
|
|
||||||
|
Declared inventory + zero icons deck-wide is a hard error (the locked
|
||||||
|
icons are unused and the deck renders flat). Bumps ``summary['errors']``
|
||||||
|
so the process exits non-zero, mirroring ``_print_animation_summary``.
|
||||||
|
"""
|
||||||
|
if not self._icon_inventory_declared:
|
||||||
|
return
|
||||||
|
if self._deck_icon_total == 0:
|
||||||
|
self.summary['errors'] += 1
|
||||||
|
print("\n[ERROR] Icon usage: spec_lock locks an icon library + inventory, "
|
||||||
|
"but the deck authors ZERO <use data-icon> across all pages.")
|
||||||
|
print(" The locked icons are unused — the deck renders flat / icon-less.")
|
||||||
|
print(" Fix: in the executor, place inventory icons on content pages "
|
||||||
|
"(KPI / list / process / comparison layouts especially), then re-run.")
|
||||||
|
elif self._pages_missing_icons:
|
||||||
|
print(f"\n[INFO] Icon usage: {self._deck_icon_total} icon(s) deck-wide; "
|
||||||
|
f"{len(self._pages_missing_icons)} page(s) reference none "
|
||||||
|
f"({', '.join(self._pages_missing_icons)}).")
|
||||||
|
print(" Cover / section / closing pages may legitimately omit icons; "
|
||||||
|
"verify dense content pages aren't missing them.")
|
||||||
|
|
||||||
def print_summary(self):
|
def print_summary(self):
|
||||||
"""Print check summary"""
|
"""Print check summary"""
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
@ -1414,6 +1483,9 @@ class SVGQualityChecker:
|
||||||
# Animation config aggregation.
|
# Animation config aggregation.
|
||||||
self._print_animation_summary()
|
self._print_animation_summary()
|
||||||
|
|
||||||
|
# Deck-level icon-usage gate (declared inventory but icon-less deck).
|
||||||
|
self._print_icon_summary()
|
||||||
|
|
||||||
# Fix suggestions
|
# Fix suggestions
|
||||||
if self.summary['errors'] > 0 or self.summary['warnings'] > 0:
|
if self.summary['errors'] > 0 or self.summary['warnings'] > 0:
|
||||||
print(f"\n[TIP] Common fixes:")
|
print(f"\n[TIP] Common fixes:")
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,50 @@ def _recorded_narration_on_click_slides(
|
||||||
return blocked
|
return blocked
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_if_icons_unused(project_path: Path, svg_files: list[Path]) -> None:
|
||||||
|
"""Export-boundary defense-in-depth (mirrors svg_quality_checker's icon gate).
|
||||||
|
|
||||||
|
If ``spec_lock.md`` locks an icon library + non-empty inventory but the source
|
||||||
|
SVGs carry zero ``<use data-icon>`` placeholders, the deck exports flat /
|
||||||
|
icon-less. Warn loudly on stderr so it isn't silent when someone exports
|
||||||
|
without first running ``svg_quality_checker.py`` (the hard gate). Non-fatal:
|
||||||
|
export still proceeds — the lock may be stale or icons intentionally absent.
|
||||||
|
Fully defensive: any failure here must never break the export.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import re
|
||||||
|
|
||||||
|
lock_path = project_path / 'spec_lock.md'
|
||||||
|
if not lock_path.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from update_spec import parse_lock
|
||||||
|
icons = (parse_lock(lock_path) or {}).get('icons') or {}
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
library = (icons.get('library') or '').strip().lower()
|
||||||
|
inventory = (icons.get('inventory') or '').strip().lower()
|
||||||
|
_empty = ('', 'none', '(none)', '-', 'n/a')
|
||||||
|
if library in _empty or inventory in _empty:
|
||||||
|
return
|
||||||
|
total = 0
|
||||||
|
for p in svg_files:
|
||||||
|
try:
|
||||||
|
total += len(re.findall(r'<use\b[^>]*\bdata-icon\s*=', p.read_text(encoding='utf-8')))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if total == 0:
|
||||||
|
print(
|
||||||
|
"[WARN] spec_lock locks an icon library + inventory, but the source SVGs "
|
||||||
|
"contain ZERO <use data-icon> — this deck exports flat / icon-less. "
|
||||||
|
"Run svg_quality_checker.py and add inventory icons to content pages "
|
||||||
|
"before delivering.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
"""CLI entry point for the SVG to PPTX conversion tool."""
|
"""CLI entry point for the SVG to PPTX conversion tool."""
|
||||||
transition_choices = (
|
transition_choices = (
|
||||||
|
|
@ -307,6 +351,10 @@ Recorded narration:
|
||||||
print("Error: No SVG files found")
|
print("Error: No SVG files found")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# Export-boundary icon check: warn (non-fatal) if an inventory is locked but
|
||||||
|
# no <use data-icon> is authored — defense-in-depth behind the quality gate.
|
||||||
|
_warn_if_icons_unused(project_path, ref_files)
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
backup_dir: Path | None = None
|
backup_dir: Path | None = None
|
||||||
|
|
|
||||||
|
|
@ -64,4 +64,4 @@ The `icons/` directory contains 11,600+ vector icons across five libraries:
|
||||||
| `simple-icons` | brand logos (company / product marks) | 3400+ |
|
| `simple-icons` | brand logos (company / product marks) | 3400+ |
|
||||||
|
|
||||||
- **Usage & style rules**: [icons/README.md](./icons/README.md)
|
- **Usage & style rules**: [icons/README.md](./icons/README.md)
|
||||||
- **Search icons**: `ls skills/ppt-master/templates/icons/<library>/ | grep <keyword>`
|
- **Search icons**: `ls skills/ppt/templates/icons/<library>/ | grep <keyword>`
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Brand application follows the **same explicit-path rule as all template kinds**
|
||||||
Run the standalone workflow:
|
Run the standalone workflow:
|
||||||
|
|
||||||
```
|
```
|
||||||
Read skills/ppt-master/workflows/create-brand.md
|
Read skills/ppt/workflows/create-brand.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Three input paths are supported: brand asset (logo / brand site URL / branded PPTX / brand PDF), verbal spec dictated in chat, or empty skeleton for the user to fill in later.
|
Three input paths are supported: brand asset (logo / brand site URL / branded PPTX / brand PDF), verbal spec dictated in chat, or empty skeleton for the user to fill in later.
|
||||||
|
|
|
||||||
|
|
@ -445,9 +445,9 @@ font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Micr
|
||||||
```bash
|
```bash
|
||||||
# 一键校验
|
# 一键校验
|
||||||
f="your_chart.svg"
|
f="your_chart.svg"
|
||||||
xmllint --noout "skills/ppt-master/templates/charts/$f" && echo "XML OK" || echo "XML FAIL"
|
xmllint --noout "skills/ppt/templates/charts/$f" && echo "XML OK" || echo "XML FAIL"
|
||||||
echo "Old colors:" && grep -c '#2C3E50\|#7F8C8D\|#95A5A6\|#5D6D7E\|#000000' "skills/ppt-master/templates/charts/$f"
|
echo "Old colors:" && grep -c '#2C3E50\|#7F8C8D\|#95A5A6\|#5D6D7E\|#000000' "skills/ppt/templates/charts/$f"
|
||||||
echo "Small fonts:" && grep -c 'font-size="[0-9]"' "skills/ppt-master/templates/charts/$f"
|
echo "Small fonts:" && grep -c 'font-size="[0-9]"' "skills/ppt/templates/charts/$f"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Full data model: [`docs/zh/templates-architecture.md`](../../../../docs/zh/templ
|
||||||
|
|
||||||
## Trigger rule
|
## Trigger rule
|
||||||
|
|
||||||
Deck selection is **opt-in by explicit path**. The main workflow defaults to free design. A deck is only used when the user gives an explicit directory path in their initial message (e.g. `skills/ppt-master/templates/decks/招商银行/`). Bare names do not trigger. See [`SKILL.md`](../../SKILL.md) Step 3.
|
Deck selection is **opt-in by explicit path**. The main workflow defaults to free design. A deck is only used when the user gives an explicit directory path in their initial message (e.g. `skills/ppt/templates/decks/招商银行/`). Bare names do not trigger. See [`SKILL.md`](../../SKILL.md) Step 3.
|
||||||
|
|
||||||
`decks_index.json` is a **discovery aid**, not a trigger — it lets the AI answer "what decks exist?" by listing ids and paths. Listing alone never advances the pipeline.
|
`decks_index.json` is a **discovery aid**, not a trigger — it lets the AI answer "what decks exist?" by listing ids and paths. Listing alone never advances the pipeline.
|
||||||
|
|
||||||
|
|
@ -58,8 +58,8 @@ When the user gives a deck path **with** a brand path or layout path, identity /
|
||||||
|
|
||||||
1. Run [`workflows/create-template.md`](../../workflows/create-template.md) (default kind is `deck`)
|
1. Run [`workflows/create-template.md`](../../workflows/create-template.md) (default kind is `deck`)
|
||||||
2. Resulting directory lands under `templates/decks/<id>/`
|
2. Resulting directory lands under `templates/decks/<id>/`
|
||||||
3. Validate: `python3 skills/ppt-master/scripts/svg_quality_checker.py templates/decks/<id> --template-mode --format ppt169`
|
3. Validate: `python3 skills/ppt/scripts/svg_quality_checker.py templates/decks/<id> --template-mode --format ppt169`
|
||||||
4. Register: `python3 skills/ppt-master/scripts/register_template.py <id> --kind deck`
|
4. Register: `python3 skills/ppt/scripts/register_template.py <id> --kind deck`
|
||||||
|
|
||||||
The register step updates [`decks_index.json`](./decks_index.json) — the single source of truth for deck discovery.
|
The register step updates [`decks_index.json`](./decks_index.json) — the single source of truth for deck discovery.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ This directory provides **11,600+ high-quality SVG icons** across five libraries
|
||||||
This directory is the **global library**. At selection time the Strategist copies the chosen icons into the deck's own `<project>/icons/<lib>/` with `icon_sync.py`:
|
This directory is the **global library**. At selection time the Strategist copies the chosen icons into the deck's own `<project>/icons/<lib>/` with `icon_sync.py`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 skills/ppt-master/scripts/icon_sync.py <project_path> chunk-filled/home tabler-outline/bulb
|
python3 skills/ppt/scripts/icon_sync.py <project_path> chunk-filled/home tabler-outline/bulb
|
||||||
```
|
```
|
||||||
|
|
||||||
A name the library does not have is reported and the command exits non-zero — re-pick a real one then, not at export. `finalize_svg.py embed-icons` embeds **project-first** (from `<project>/icons/`), falling back to this global library per-icon.
|
A name the library does not have is reported and the command exits non-zero — re-pick a real one then, not at export. `finalize_svg.py embed-icons` embeds **project-first** (from `<project>/icons/`), falling back to this global library per-icon.
|
||||||
|
|
@ -66,11 +66,11 @@ python3 scripts/svg_finalize/embed_icons.py svg_output/*.svg
|
||||||
Use `ls | grep` — zero token cost:
|
Use `ls | grep` — zero token cost:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls skills/ppt-master/templates/icons/chunk-filled/ | grep home
|
ls skills/ppt/templates/icons/chunk-filled/ | grep home
|
||||||
ls skills/ppt-master/templates/icons/tabler-filled/ | grep home
|
ls skills/ppt/templates/icons/tabler-filled/ | grep home
|
||||||
ls skills/ppt-master/templates/icons/tabler-outline/ | grep chart
|
ls skills/ppt/templates/icons/tabler-outline/ | grep chart
|
||||||
ls skills/ppt-master/templates/icons/phosphor-duotone/ | grep house
|
ls skills/ppt/templates/icons/phosphor-duotone/ | grep house
|
||||||
ls skills/ppt-master/templates/icons/simple-icons/ | grep github
|
ls skills/ppt/templates/icons/simple-icons/ | grep github
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Full data model: [`docs/zh/templates-architecture.md`](../../../../docs/zh/templ
|
||||||
|
|
||||||
## Trigger rule
|
## Trigger rule
|
||||||
|
|
||||||
Layout selection is **opt-in by explicit path**. The main workflow defaults to free design. A layout is only used when the user gives an explicit directory path in their initial message (e.g. `skills/ppt-master/templates/layouts/academic_defense/`). Bare names do not trigger. See [`SKILL.md`](../../SKILL.md) Step 3.
|
Layout selection is **opt-in by explicit path**. The main workflow defaults to free design. A layout is only used when the user gives an explicit directory path in their initial message (e.g. `skills/ppt/templates/layouts/academic_defense/`). Bare names do not trigger. See [`SKILL.md`](../../SKILL.md) Step 3.
|
||||||
|
|
||||||
`layouts_index.json` is a **discovery aid**, not a trigger — it lets the AI answer "what layouts exist?" by listing ids and paths. Listing alone never advances the pipeline.
|
`layouts_index.json` is a **discovery aid**, not a trigger — it lets the AI answer "what layouts exist?" by listing ids and paths. Listing alone never advances the pipeline.
|
||||||
|
|
||||||
|
|
@ -68,8 +68,8 @@ Templates use `{{PLACEHOLDER}}` to mark replaceable content. New layouts should
|
||||||
|
|
||||||
1. Run [`workflows/create-template.md`](../../workflows/create-template.md) (default produces a deck; explicit "structure only / no identity" option produces a layout)
|
1. Run [`workflows/create-template.md`](../../workflows/create-template.md) (default produces a deck; explicit "structure only / no identity" option produces a layout)
|
||||||
2. Resulting directory lands under `templates/layouts/<id>/`
|
2. Resulting directory lands under `templates/layouts/<id>/`
|
||||||
3. Validate: `python3 skills/ppt-master/scripts/svg_quality_checker.py templates/layouts/<id> --template-mode --format ppt169`
|
3. Validate: `python3 skills/ppt/scripts/svg_quality_checker.py templates/layouts/<id> --template-mode --format ppt169`
|
||||||
4. Register: `python3 skills/ppt-master/scripts/register_template.py <id> --kind layout`
|
4. Register: `python3 skills/ppt/scripts/register_template.py <id> --kind layout`
|
||||||
|
|
||||||
The register step updates [`layouts_index.json`](./layouts_index.json) — the single source of truth for layout discovery.
|
The register step updates [`layouts_index.json`](./layouts_index.json) — the single source of truth for layout discovery.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue