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:
caoqianming 2026-06-30 13:59:00 +08:00
parent 001f9af96f
commit 5d23ee682b
16 changed files with 327 additions and 59 deletions

View File

@ -2,7 +2,7 @@
> 配合 `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 条从"怎么写图标"改为"**内容页必须放 13 个 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)
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` 不受影响。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.33.2"
__version__ = "0.33.3"

View File

@ -127,7 +127,7 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
1. **逐页串行手写,不批量、不脚本生成**:每页由当前主 agent 在同一上下文里手写 SVG;**禁止写循环脚本批量产 SVG**(跨页视觉一致性靠逐页带上游上下文,生成器做不到),也不要 5 页一组。
2. **每页前重读 `spec_lock.md`**:颜色/字体/图标/图片只能来自它;查本页 `page_rhythm`/`page_layouts`/`page_charts`。抗上下文压缩漂移。
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` 时,**每个内容页必须放 13 个 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-*。
逐页写到 `<project_dir>/svg_output/<NN>_<page>.svg`。**演讲者备注**写 `<project_dir>/notes/total.md`(每页 24 句结论先行口语)。
@ -137,7 +137,7 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
```
.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 安全字体等):能顺手改就改,否则知会后放行。
- 跑 `svg_output/`(不要在 finalize 后跑 —— finalize 改写 SVG 会掩盖源级违规)。

View File

@ -19,13 +19,13 @@ Run the standalone [`customize-animations`](../workflows/customize-animations.md
```bash
# 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
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
python3 skills/ppt-master/scripts/svg_to_pptx.py <project>
python3 skills/ppt/scripts/svg_to_pptx.py <project>
```
Minimal sidecar:
@ -60,13 +60,13 @@ Rules:
```bash
# 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
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)
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`.
@ -87,23 +87,23 @@ Off by default — enable deck-wide with `-a auto` (or another effect). Once ena
```bash
# 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)
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)
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)
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
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
# 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:
@ -137,7 +137,7 @@ Aim for **38 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 → 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

View File

@ -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.
> 🚧 **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 **13** 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.
**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:
```bash
ls skills/ppt-master/templates/icons/chunk-filled/ | grep home
ls skills/ppt-master/templates/icons/tabler-filled/ | grep home
ls skills/ppt-master/templates/icons/tabler-outline/ | grep chart
ls skills/ppt-master/templates/icons/phosphor-duotone/ | grep house
ls skills/ppt-master/templates/icons/simple-icons/ | grep github
ls skills/ppt/templates/icons/chunk-filled/ | grep home
ls skills/ppt/templates/icons/tabler-filled/ | grep home
ls skills/ppt/templates/icons/tabler-outline/ | grep chart
ls skills/ppt/templates/icons/phosphor-duotone/ | grep house
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`):

View File

@ -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:
>
> 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`).
> 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.
>
> 🚧 **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
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:

View File

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

View File

@ -25,6 +25,8 @@ try: # zcbot: Windows GBK 控制台兼容,避免 emoji/© 等触发 UnicodeEn
except Exception:
pass
import tempfile
import os
import shutil
from pathlib import Path
_CHROME_CANDIDATES = [
@ -33,19 +35,37 @@ _CHROME_CANDIDATES = [
r"C:\Program Files\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:
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():
return c
import shutil
for name in ("chrome", "chrome.exe", "msedge", "msedge.exe"):
for name in _WHICH_NAMES:
p = shutil.which(name)
if p:
return p
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")
w, h = _dims(svg_text)
html = _wrap_html(svg_path, w, h)
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f:
f.write(html)
html_path = Path(f.name)
try:
out_png.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
# chromium resolves --screenshot against its own cwd, not ours; a relative
# path silently fails to write ("系统找不到指定的路径"). Always pass absolute.
out_png = out_png.resolve()
out_png.parent.mkdir(parents=True, exist_ok=True)
# 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",
# 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,
"--window-size=%d,%d" % (round(w), round(h)),
"--default-background-color=FFFFFFFF",
"--screenshot=%s" % str(out_png),
html_path.as_uri(),
],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=120, check=False,
)
finally:
try:
html_path.unlink()
except OSError:
pass
if out_png.exists() and out_png.stat().st_size > 0:
return True
tail = (proc.stderr or b"").decode("utf-8", "replace").strip()[-400:]
print(f" [warn] 渲染失败 {svg_path.name}(rc={proc.returncode})"
+ (f": {tail}" if tail else ""))
return False
def _collect(target: Path) -> tuple[list[Path], Path]:

View File

@ -222,6 +222,16 @@ class SVGQualityChecker:
# severity is 'error' or 'warning'. Printed in print_summary.
self._template_issues: List[Tuple[str, 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:
"""
@ -301,6 +311,11 @@ class SVGQualityChecker:
if not self.template_mode:
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
result['passed'] = len(result['errors']) == 0
@ -1386,6 +1401,60 @@ class SVGQualityChecker:
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):
"""Print check summary"""
print("=" * 80)
@ -1414,6 +1483,9 @@ class SVGQualityChecker:
# Animation config aggregation.
self._print_animation_summary()
# Deck-level icon-usage gate (declared inventory but icon-less deck).
self._print_icon_summary()
# Fix suggestions
if self.summary['errors'] > 0 or self.summary['warnings'] > 0:
print(f"\n[TIP] Common fixes:")

View File

@ -63,6 +63,50 @@ def _recorded_narration_on_click_slides(
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:
"""CLI entry point for the SVG to PPTX conversion tool."""
transition_choices = (
@ -307,6 +351,10 @@ Recorded narration:
print("Error: No SVG files found")
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")
backup_dir: Path | None = None

View File

@ -64,4 +64,4 @@ The `icons/` directory contains 11,600+ vector icons across five libraries:
| `simple-icons` | brand logos (company / product marks) | 3400+ |
- **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>`

View File

@ -24,7 +24,7 @@ Brand application follows the **same explicit-path rule as all template kinds**
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.

View File

@ -445,9 +445,9 @@ font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Micr
```bash
# 一键校验
f="your_chart.svg"
xmllint --noout "skills/ppt-master/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 "Small fonts:" && grep -c 'font-size="[0-9]"' "skills/ppt-master/templates/charts/$f"
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/templates/charts/$f"
echo "Small fonts:" && grep -c 'font-size="[0-9]"' "skills/ppt/templates/charts/$f"
```
---

View File

@ -10,7 +10,7 @@ Full data model: [`docs/zh/templates-architecture.md`](../../../../docs/zh/templ
## 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.
@ -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`)
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`
4. Register: `python3 skills/ppt-master/scripts/register_template.py <id> --kind deck`
3. Validate: `python3 skills/ppt/scripts/svg_quality_checker.py templates/decks/<id> --template-mode --format ppt169`
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.

View File

@ -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`:
```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.
@ -66,11 +66,11 @@ python3 scripts/svg_finalize/embed_icons.py svg_output/*.svg
Use `ls | grep` — zero token cost:
```bash
ls skills/ppt-master/templates/icons/chunk-filled/ | grep home
ls skills/ppt-master/templates/icons/tabler-filled/ | grep home
ls skills/ppt-master/templates/icons/tabler-outline/ | grep chart
ls skills/ppt-master/templates/icons/phosphor-duotone/ | grep house
ls skills/ppt-master/templates/icons/simple-icons/ | grep github
ls skills/ppt/templates/icons/chunk-filled/ | grep home
ls skills/ppt/templates/icons/tabler-filled/ | grep home
ls skills/ppt/templates/icons/tabler-outline/ | grep chart
ls skills/ppt/templates/icons/phosphor-duotone/ | grep house
ls skills/ppt/templates/icons/simple-icons/ | grep github
```
---

View File

@ -10,7 +10,7 @@ Full data model: [`docs/zh/templates-architecture.md`](../../../../docs/zh/templ
## 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.
@ -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)
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`
4. Register: `python3 skills/ppt-master/scripts/register_template.py <id> --kind layout`
3. Validate: `python3 skills/ppt/scripts/svg_quality_checker.py templates/layouts/<id> --template-mode --format ppt169`
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.