From 5d23ee682b4d08e9d326927b90f739397ffced42 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 30 Jun 2026 13:59:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(ppt):=20=E4=BF=AE=E7=94=9F=E6=88=90=20PPT?= =?UTF-8?q?=20=E7=BC=BA=E5=9B=BE=E6=A0=87(=E5=9B=BE=E6=A0=87=E7=AE=A1?= =?UTF-8?q?=E7=BA=BF=E5=9B=9B=E5=B1=82=E6=96=AD=E7=82=B9)+=20=E6=B2=99?= =?UTF-8?q?=E7=AE=B1=20SVG=20=E9=A2=84=E8=A7=88=E6=B8=B2=E6=9F=93(bump=200?= =?UTF-8?q?.33.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 查真实用户两个「ppt生成」任务的 DB 执行轨迹:24 页 SVG 共 0 个 。 根因是图标管线四环节无一强制图标落地——策略层(有时)锁图标,执行层不放、 质检层不拦、工具层还断着。四层一起修: - B 工具断点:references/SKILL 23 处路径仍指向已不存在的 skills/ppt-master/ (zcbot 是 skills/ppt/)→ 模型 `ls .../icons//|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 转换器本就自己从图标库展开 ,故原设想的"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) --- PROGRESS.md | 12 ++- core/__init__.py | 2 +- skills/ppt/SKILL.md | 4 +- skills/ppt/references/animations.md | 26 ++--- skills/ppt/references/executor-base.md | 16 +++- skills/ppt/references/strategist.md | 6 +- skills/ppt/scripts/icon_sync.py | 95 +++++++++++++++++++ skills/ppt/scripts/svg_preview.py | 71 ++++++++++---- skills/ppt/scripts/svg_quality_checker.py | 72 ++++++++++++++ skills/ppt/scripts/svg_to_pptx/pptx_cli.py | 48 ++++++++++ skills/ppt/templates/README.md | 2 +- skills/ppt/templates/brands/README.md | 2 +- .../ppt/templates/charts/CHART_STYLE_GUIDE.md | 6 +- skills/ppt/templates/decks/README.md | 6 +- skills/ppt/templates/icons/README.md | 12 +-- skills/ppt/templates/layouts/README.md | 6 +- 16 files changed, 327 insertions(+), 59 deletions(-) create mode 100644 skills/ppt/scripts/icon_sync.py diff --git a/PROGRESS.md b/PROGRESS.md index 03e392d..614c3a9 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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 个 ``。根因是图标管线四个环节没有一个强制图标落地——**策略层(有时)锁图标,执行层不放、质检层不拦、工具层还断着**。四层一起修: +- **B 工具断点**:references/SKILL 里 23 处路径仍指向已不存在的 `skills/ppt-master/`(zcbot 是 `skills/ppt/`)→ 模型按文档 `ls .../icons//|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`)本就自己从图标库展开 ``,故 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` 不受影响。 diff --git a/core/__init__.py b/core/__init__.py index 3632376..1dc6803 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.33.2" +__version__ = "0.33.3" diff --git a/skills/ppt/SKILL.md b/skills/ppt/SKILL.md index 3b6bc0a..4f87f7b 100644 --- a/skills/ppt/SKILL.md +++ b/skills/ppt/SKILL.md @@ -127,7 +127,7 @@ references/visual-styles/.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. **图标**:写 ``,name 必须在 inventory 内、文件在 `templates/icons//`。 +4. **图标(锁了就必须用,非可选装饰)**:spec_lock 有 `icons.library` + 非空 `inventory` 时,**每个内容页必须放 1–3 个 inventory 内的图标**(KPI/列表/流程/对比/特性网格版式尤其要,常一卡一图标)——自由设计没有模板可继承图标,只能逐页手写 `` 才有图标。封面/纯排版分节页/单数字·金句 breathing 页/尾页可不放。写法:``,name 必须在 inventory 内、文件在 `templates/icons//`。**质检会硬卡**:锁了 inventory 但全 deck 0 图标 → error 退非零(见阶段四)。 5. **配图**:``,croppable 用 `preserveAspectRatio="xMidYMid slice"`,`| no-crop` 行用 `meet`;意图与版式见 image-layout-*。 逐页写到 `/svg_output/_.svg`。**演讲者备注**写 `/notes/total.md`(每页 2–4 句结论先行口语)。 @@ -137,7 +137,7 @@ references/visual-styles/.md # 锁定的视觉风格 ``` .venv/Scripts/python.exe /scripts/svg_quality_checker.py ``` -- **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移等)必须改:回阶段三重写该页再跑**,不放过。 +- **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移 / **锁了图标 inventory 却全 deck 0 图标** 等)必须改:回阶段三重写该页再跑**,不放过。 - `warning`(低分辨率图 / 非 PPT 安全字体等):能顺手改就改,否则知会后放行。 - 跑 `svg_output/`(不要在 finalize 后跑 —— finalize 改写 SVG 会掩盖源级违规)。 diff --git a/skills/ppt/references/animations.md b/skills/ppt/references/animations.md index 091c17f..2d78018 100644 --- a/skills/ppt/references/animations.md +++ b/skills/ppt/references/animations.md @@ -19,13 +19,13 @@ Run the standalone [`customize-animations`](../workflows/customize-animations.md ```bash # Build an editable scaffold from real top-level anchors -python3 skills/ppt-master/scripts/animation_config.py scaffold +python3 skills/ppt/scripts/animation_config.py scaffold # Validate references before export -python3 skills/ppt-master/scripts/animation_config.py validate +python3 skills/ppt/scripts/animation_config.py validate # Export reads /animations.json automatically when present -python3 skills/ppt-master/scripts/svg_to_pptx.py +python3 skills/ppt/scripts/svg_to_pptx.py ``` Minimal sidecar: @@ -60,13 +60,13 @@ Rules: ```bash # Pick a different effect -python3 skills/ppt-master/scripts/svg_to_pptx.py -t push --transition-duration 0.6 +python3 skills/ppt/scripts/svg_to_pptx.py -t push --transition-duration 0.6 # Disable -python3 skills/ppt-master/scripts/svg_to_pptx.py -t none +python3 skills/ppt/scripts/svg_to_pptx.py -t none # Auto-advance every 5 seconds (kiosk-style playback) -python3 skills/ppt-master/scripts/svg_to_pptx.py --auto-advance 5 +python3 skills/ppt/scripts/svg_to_pptx.py --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 +python3 skills/ppt/scripts/svg_to_pptx.py # Enable per-element animation deck-wide (auto effect + after-previous cascade) -python3 skills/ppt-master/scripts/svg_to_pptx.py -a auto +python3 skills/ppt/scripts/svg_to_pptx.py -a auto # Enable with a single effect (cascades via the after-previous trigger) -python3 skills/ppt-master/scripts/svg_to_pptx.py --animation fade +python3 skills/ppt/scripts/svg_to_pptx.py --animation fade # Enable and switch to on-click for live presentations (presenter controls pacing) -python3 skills/ppt-master/scripts/svg_to_pptx.py -a auto --animation-trigger on-click +python3 skills/ppt/scripts/svg_to_pptx.py -a auto --animation-trigger on-click # Custom pacing -python3 skills/ppt-master/scripts/svg_to_pptx.py --animation mixed \ +python3 skills/ppt/scripts/svg_to_pptx.py --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 --animation-trigger with-previous +python3 skills/ppt/scripts/svg_to_pptx.py --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 **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 → animation is skipped on that slide. The slide still renders, just without entrance animation. -Executors should wrap logical sections in `` 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 `` regardless of whether you plan to animate. The Executor reference (`skills/ppt/references/shared-standards.md`) requires it. ## Limitations diff --git a/skills/ppt/references/executor-base.md b/skills/ppt/references/executor-base.md index c15e049..8dc2e19 100644 --- a/skills/ppt/references/executor-base.md +++ b/skills/ppt/references/executor-base.md @@ -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 `` 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** `` 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 `/icons//` (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 `/icons//` (any ``, e.g. `custom/`) and reference it as `data-icon="/"` — 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`): diff --git a/skills/ppt/references/strategist.md b/skills/ppt/references/strategist.md index 0cfd352..faff063 100644 --- a/skills/ppt/references/strategist.md +++ b/skills/ppt/references/strategist.md @@ -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// | grep ` +> 4. Search for each concept's filename in the chosen library: `ls skills/ppt/templates/icons// | grep ` > 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 [ …]`. This populates `/icons//` (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 [ …]`. This populates `/icons//` (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 ``. @@ -307,7 +307,7 @@ Formula rendering is part of Typography confirmation. Recommend one policy and l ```bash mkdir -p /images -python3 skills/ppt-master/scripts/latex_render.py +python3 skills/ppt/scripts/latex_render.py ``` Write the manifest first at `/images/formula_manifest.json`. Use this shape: diff --git a/skills/ppt/scripts/icon_sync.py b/skills/ppt/scripts/icon_sync.py new file mode 100644 index 0000000..ca5c855 --- /dev/null +++ b/skills/ppt/scripts/icon_sync.py @@ -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 ``/icons//`` 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 [ ...] + +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 [ ...]") + 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 /icons//.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// | grep `, fix spec_lock.md, re-run:") + for m in missing: + print(f" - {m}") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/ppt/scripts/svg_preview.py b/skills/ppt/scripts/svg_preview.py index 24cc5f4..daca6bf 100644 --- a/skills/ppt/scripts/svg_preview.py +++ b/skills/ppt/scripts/svg_preview.py @@ -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]: diff --git a/skills/ppt/scripts/svg_quality_checker.py b/skills/ppt/scripts/svg_quality_checker.py index 642eb77..e558e43 100644 --- a/skills/ppt/scripts/svg_quality_checker.py +++ b/skills/ppt/scripts/svg_quality_checker.py @@ -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 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 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']*\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 — 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 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:") diff --git a/skills/ppt/scripts/svg_to_pptx/pptx_cli.py b/skills/ppt/scripts/svg_to_pptx/pptx_cli.py index 240163e..b779a4b 100644 --- a/skills/ppt/scripts/svg_to_pptx/pptx_cli.py +++ b/skills/ppt/scripts/svg_to_pptx/pptx_cli.py @@ -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 ```` 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']*\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 — 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 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 diff --git a/skills/ppt/templates/README.md b/skills/ppt/templates/README.md index e68d32c..fefb1ce 100644 --- a/skills/ppt/templates/README.md +++ b/skills/ppt/templates/README.md @@ -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// | grep ` +- **Search icons**: `ls skills/ppt/templates/icons// | grep ` diff --git a/skills/ppt/templates/brands/README.md b/skills/ppt/templates/brands/README.md index fc2acc4..455d891 100644 --- a/skills/ppt/templates/brands/README.md +++ b/skills/ppt/templates/brands/README.md @@ -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. diff --git a/skills/ppt/templates/charts/CHART_STYLE_GUIDE.md b/skills/ppt/templates/charts/CHART_STYLE_GUIDE.md index 1d17514..4d51510 100644 --- a/skills/ppt/templates/charts/CHART_STYLE_GUIDE.md +++ b/skills/ppt/templates/charts/CHART_STYLE_GUIDE.md @@ -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" ``` --- diff --git a/skills/ppt/templates/decks/README.md b/skills/ppt/templates/decks/README.md index 1af1488..c112112 100644 --- a/skills/ppt/templates/decks/README.md +++ b/skills/ppt/templates/decks/README.md @@ -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//` -3. Validate: `python3 skills/ppt-master/scripts/svg_quality_checker.py templates/decks/ --template-mode --format ppt169` -4. Register: `python3 skills/ppt-master/scripts/register_template.py --kind deck` +3. Validate: `python3 skills/ppt/scripts/svg_quality_checker.py templates/decks/ --template-mode --format ppt169` +4. Register: `python3 skills/ppt/scripts/register_template.py --kind deck` The register step updates [`decks_index.json`](./decks_index.json) — the single source of truth for deck discovery. diff --git a/skills/ppt/templates/icons/README.md b/skills/ppt/templates/icons/README.md index 754e5f1..fd3c501 100644 --- a/skills/ppt/templates/icons/README.md +++ b/skills/ppt/templates/icons/README.md @@ -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 `/icons//` with `icon_sync.py`: ```bash -python3 skills/ppt-master/scripts/icon_sync.py chunk-filled/home tabler-outline/bulb +python3 skills/ppt/scripts/icon_sync.py 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 `/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 ``` --- diff --git a/skills/ppt/templates/layouts/README.md b/skills/ppt/templates/layouts/README.md index 008c972..7db1633 100644 --- a/skills/ppt/templates/layouts/README.md +++ b/skills/ppt/templates/layouts/README.md @@ -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//` -3. Validate: `python3 skills/ppt-master/scripts/svg_quality_checker.py templates/layouts/ --template-mode --format ppt169` -4. Register: `python3 skills/ppt-master/scripts/register_template.py --kind layout` +3. Validate: `python3 skills/ppt/scripts/svg_quality_checker.py templates/layouts/ --template-mode --format ppt169` +4. Register: `python3 skills/ppt/scripts/register_template.py --kind layout` The register step updates [`layouts_index.json`](./layouts_index.json) — the single source of truth for layout discovery.