diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index 72d84d97..6b5b06e5 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -42,8 +42,8 @@ "repo": "https://github.com/JuliusBrussee/caveman.git", "ref": "main", "adapter": "codex-plugin", - "commit": "367fdb7f0f8f8e7994b5aab632c7ce5014802b32", - "syncedAt": "2026-09-04T16:00:00Z" + "commit": "5184b3d11ac6a1acb7d44b9bfaa31698157cff97", + "syncedAt": "2026-09-05T16:00:00Z" }, { "id": "taste-skill", @@ -96,8 +96,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "8295ca1416ad17cf25ed17765024bc7ba411782c", - "syncedAt": "2026-09-04T16:00:00Z" + "commit": "d3d81fe3cf4cc642de225159586308bbe98eeb4d", + "syncedAt": "2026-09-05T16:00:00Z" }, { "id": "grill-me", @@ -132,8 +132,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "602a2aba900e45fb2edd694f463f435430678e1f", - "syncedAt": "2026-09-04T16:00:00Z" + "commit": "6685283fe8533a469ee1a9455e2bc4047c7453cb", + "syncedAt": "2026-09-05T16:00:00Z" }, { "id": "shuorenhua", diff --git a/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json index 69931f4e..fb4e8650 100644 --- a/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "caveman", "repo": "https://github.com/JuliusBrussee/caveman.git", "ref": "main", - "commit": "367fdb7f0f8f8e7994b5aab632c7ce5014802b32", + "commit": "5184b3d11ac6a1acb7d44b9bfaa31698157cff97", "adapter": "codex-plugin", "sourcePath": "plugins/caveman", - "syncedAt": "2026-09-04T16:00:00Z" + "syncedAt": "2026-09-05T16:00:00Z" } diff --git a/plugins/codex/plugins/caveman/skills/caveman-compress/scripts/compress.py b/plugins/codex/plugins/caveman/skills/caveman-compress/scripts/compress.py index b5ba60a8..66cf4258 100644 --- a/plugins/codex/plugins/caveman/skills/caveman-compress/scripts/compress.py +++ b/plugins/codex/plugins/caveman/skills/caveman-compress/scripts/compress.py @@ -362,6 +362,32 @@ from .validate import validate MAX_RETRIES = 2 + +def _is_smaller_than_body(candidate_body: str, body: str) -> bool: + """True when `candidate_body` actually compresses `body`. + + The non-expansion invariant for #776. It lives in a helper because it has + to hold for EVERY candidate, not just the first one: a candidate that fails + validation is sent back to Claude for repair, and the repaired text is what + gets written if it validates. Checking only the first candidate left the + retry path able to write a longer file and report it as a successful + compression — the original bug, one branch over. + + Always compares bodies with frontmatter already removed. Frontmatter is + preserved verbatim, so counting it on one side and not the other would + measure the wrong thing. + """ + candidate_len = len(candidate_body.strip()) + body_len = len(body.strip()) + if candidate_len >= body_len: + print( + "❌ Compression aborted: output is not smaller than input " + f"({candidate_len} >= {body_len} chars)." + ) + return False + return True + + # Bounds each individual Claude call so a stalled CLI (dropped network, an # auth prompt with no TTY to answer it) can't hang past what LOCK_WAIT_SECONDS # assumes for the whole run's worst case (MAX_RETRIES+1 calls). @@ -649,6 +675,16 @@ def _compress_file_locked(filepath: Path) -> bool: print(" already in caveman form. Original file is untouched (no backup created).") return False + # A rewrite that is structurally faithful but LONGER than the input passes + # every check below (validate() only checks structural invariants, not + # length) and would otherwise be written over the original and reported + # as a successful compression — the opposite of what this tool exists to + # do (issue #776). Same length is also a reject: a compression that saved + # nothing isn't a compression. + if not _is_smaller_than_body(compressed_body, body): + print(" Original file is untouched (no backup created).") + return False + # Reassemble: frontmatter (verbatim) + compressed body compressed = frontmatter + compressed_body @@ -712,6 +748,18 @@ def _compress_file_locked(filepath: Path) -> bool: print(" Possible preamble leak. Skipping this attempt.") continue + # The repaired candidate is what gets written if it validates, so the + # non-expansion invariant has to hold for it too — a repair that + # restores the structure validate() asked for by padding the prose back + # out is exactly the "compression" #776 is about. `fixed` is a whole + # file (build_fix_prompt is given one, and the anchor check above + # requires it to start with the original's first line), so its + # frontmatter is split off to compare like against like. + _, fixed_body = split_frontmatter(fixed) + if not _is_smaller_than_body(fixed_body, body): + print(" Skipping this attempt.") + continue + compressed = fixed return False diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 5133915d..9724e80f 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-09-04T16:02:13Z" + "syncedAt": "2026-09-05T16:02:05Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index ac9d4c22..cda12061 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "602a2aba900e45fb2edd694f463f435430678e1f", + "commit": "6685283fe8533a469ee1a9455e2bc4047c7453cb", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-09-04T16:00:00Z" + "syncedAt": "2026-09-05T16:00:00Z" } diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index 33462b36..f3cf883c 100644 --- a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ppt-master", "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", - "commit": "8295ca1416ad17cf25ed17765024bc7ba411782c", + "commit": "d3d81fe3cf4cc642de225159586308bbe98eeb4d", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-09-04T16:00:00Z" + "syncedAt": "2026-09-05T16:00:00Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/.env.example b/plugins/codex/plugins/ppt-master/skills/ppt-master/.env.example index f5af2d98..8c529b77 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/.env.example +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/.env.example @@ -23,6 +23,11 @@ # 推荐核心后端:openai / gemini / qwen / zhipu / volcengine # IMAGE_BACKEND=openai +# Max concurrent requests in --manifest batch mode (default 3). +# Auto-halves on rate-limit; 1 is the serial fallback. +# --manifest 批量模式下的最大并发数(默认 3)。命中限流自动减半,最低 1(=串行)。 +# IMAGE_CONCURRENCY=3 + # You may also provide the same variables directly via the current process environment. # 也可以不写 .env,而是直接通过当前运行进程的环境变量提供同样的配置。 @@ -39,6 +44,8 @@ # IMAGE_BACKEND=openai # OPENAI_API_KEY=sk-xxx # OPENAI_MODEL=gpt-image-2 +# Legacy ChatGPT image API model / 旧版 ChatGPT 生图 API 模型 +# OPENAI_MODEL=chatgpt-image-latest # Deprecated by OpenAI / OpenAI 已弃用 # OPENAI_BASE_URL=http://127.0.0.1:3000/v1 # OpenAI-compatible providers may need these compatibility knobs. # OpenAI-compatible 第三方平台可按需开启以下兼容参数。 @@ -53,6 +60,9 @@ # OPENAI_BACKGROUND=auto # auto / low # OPENAI_MODERATION=auto +# Reference edits except gpt-image-2: high / low +# 除 gpt-image-2 外的参考图编辑:high / low +# OPENAI_INPUT_FIDELITY=high # ───────────────────────────────────────────────────────────── # Example: Gemini / Gemini 示例 @@ -97,6 +107,21 @@ # VOLCENGINE_MODEL=doubao-seedream-4-5-251128 # VOLCENGINE_BASE_URL=https://operator.las.cn-beijing.volces.com/api/v1 +# ───────────────────────────────────────────────────────────── +# Example: Tencent Cloud TokenHub / 腾讯云 TokenHub 生图示例 +# ───────────────────────────────────────────────────────────── +# IMAGE_BACKEND=tencent +# TENCENT_API_KEY=your-tokenhub-key +# Alternative key / 备选密钥:TOKENHUB_API_KEY +# TENCENT_MODEL=hy-image-v3 +# TENCENT_BASE_URL=https://tokenhub.tencentmaas.com +# International base / 国际基址:https://tokenhub-intl.tencentmaas.com +# Hy: hy-image-v3 uses /v1/wand/hunyuan-image/v3-generation / 混元模型使用此端点 +# Seedream: seedream-image-v5.0-pro / seedream-image-v5.0-lite use /v1/wand/si-image/generation / 即梦模型使用此端点 +# TENCENT_REVISE=true # Hy only / 仅混元 +# TENCENT_WATERMARK=false # Seedream only / 仅即梦 +# TENCENT_OUTPUT_FORMAT=png # Seedream: png / jpeg / 即梦输出格式 + # ───────────────────────────────────────────────────────────── # Extended / Experimental backends # 扩展 / 实验后端 @@ -170,12 +195,13 @@ # Use cloud providers only when you want high-quality or cloned voices — # all four (ElevenLabs / MiniMax / Qwen / CosyVoice) accept a cloned voice_id. # Clone the voice in the provider's console first, then pass --voice-id to -# notes_to_audio.py. See workflows/stages/generate-audio.md. +# notes_to_audio.py. See workflows/stages/generate-audio.md (installed skill) +# or docs/audio-narration.md "Use a cloned voice" (repository clone). # edge-tts 是默认旁白后端,无需 API Key。 # 需要高质量云端旁白或复刻音色时再配置云端提供商—— # ElevenLabs / MiniMax / Qwen / CosyVoice 四家都支持传入复刻 voice_id。 # 先在 provider 控制台复刻得到 voice_id,再用 --voice-id 传给 notes_to_audio.py。 -# 详见 workflows/stages/generate-audio.md。 +# 详见 workflows/stages/generate-audio.md(安装的 skill)或 docs/zh/audio-narration.md "使用复刻音色"(仓库克隆)。 # # ELEVENLABS_API_KEY=your-elevenlabs-api-key # MINIMAX_API_KEY=your-minimax-key diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md index 8ff776e8..f0555ccd 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md @@ -2,7 +2,7 @@ name: ppt-master description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶段演示文稿生成工作流。" metadata: - version: "6.2.0" + version: "6.3.0" copyright: "Copyright (c) 2025-2026 Hugo He" license: "MIT" official_repository: "https://github.com/hugohe3/ppt-master" diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md index 52f89e9c..912ef9f2 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md @@ -271,7 +271,7 @@ Mechanical repetition comes from reusing one carrier and topology without a page - **Element grouping (Mandatory)**: wrap each logical Slide-local body unit in a descriptive, page-unique top-level `` with root-coordinate `data-pptx-bounds="x y width height"`. A helper-authored preset atom stays top-level with `data-pptx-frame` and no bounds; nested groups need none. Give a root background image or full-canvas scrim/decoration rectangle a stable `id` plus `data-pptx-role="background"` / `"decoration"` instead of a wrapper. Thresholds, exemptions, and the Morph staging marker: [`shared-standards-core.md`](./shared-standards-core.md) §4.3. - **Reference — nested edit groups**: a top-level group may contain descriptive nested `` groups for meaningful subunits; they need no bounds and create no animation step, with no default depth or quota. -- **Default — bounds are the module zone, not a glyph box (may skip when no text is estimable)**: make each zone as generous as the canvas and siblings allow without overlap. A group's bounds are the union of every child's geometry — text lines, preset frames, image frames, line stroke half-width — plus margin, never the title text alone. An untransformed line spans `y − 0.85 × font_size` to `y + 0.35 × font_size`. Write the sentence first, then fit the zone to it; recompute the bounds after any font-size, line-count, or child-geometry change. +- **Default — bounds are the module zone, not a glyph box (may skip when no text is estimable)**: make each zone as generous as the canvas and siblings allow without overlap. A group's bounds are the union of every child's geometry — text lines, preset frames, image frames, line stroke half-width — plus margin, never the title text alone. An untransformed line spans `y − 0.85 × font_size` to `y + 0.35 × font_size`, so a zone's top sits at or above the first baseline − 0.85 × size and its bottom at or below the last baseline + 0.35 × size (a 16px footer on baseline 676 needs a bottom of 682, not 680). Write the sentence first, then fit the zone to it; recompute the bounds after any font-size, line-count, or child-geometry change. - **Width estimation**: width is estimated, never measured per page. The route calibrates every role once before P01 (Default Step 6, Quick §3) and keeps a per-role chars-per-100-px table in context; size each zone as characters ÷ that rate × 100; a line mixing CJK with Latin words or digits adds the two parts, CJK chars ÷ CJK rate + other chars ÷ Latin rate (acronyms and uppercase words use the CAPS rate, numbers the DIGITS rate; the rates are sample averages while the checker measures real glyphs, so keep about 5% below the bounds width; a line rewritten after calibration — an expanded title, a longer label — is re-estimated the same way, the outline column covers planned wording only). Never trim wording to satisfy an estimate. When text does not fit, expand a zone with unused space first, then reflow or switch texture (prose → points) before dropping a qualifier; larger bounds never repair off-canvas text. - **Spec adherence**: binding color, canvas, typography, identity, resource, and template anchors hold; layout and other References apply under §2.1 without becoming locks. - **Template structure**: inherit the native framework only for `template_reuse_scope: mirror|layout`; `style` uses the flat route. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md index 7d9b5403..415157ee 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md @@ -34,7 +34,7 @@ python3 skills/ppt-master/scripts/stamp_native_fallbacks.py " ``` -**Hard rule — project by the selected authority**: SVG-first metadata describes the same data and visible chrome as its fallback — for a chart every category/point and series value, x/y/size data, visible point colors and labels, line/area treatment, title/axis/legend chrome, companion text, bounds, and typography native export cannot infer; for a table every resolved cell, header/summary line, rectangular span, cell style, alignment, bounds, and typography. JSON-first metadata is the authority and its preview may be approximate. Never simplify the artwork to fit the payload: when the closed payload cannot carry required data or topology, Default returns the native-ready decision upstream and Quick revises it before drawing; only the resolved non-native object stays unmarked, and an explicit `=yes` is never silently ignored. **Per-page verification**: every `=yes` key matches exactly one marker with one JSON child, and `=no` / unlisted objects have none — `rg -n 'data-pptx-replace-with="(chart|table)"|' /svg_output/.svg`. +**Hard rule — project by the selected authority**: SVG-first metadata describes the same data and visible chrome as its fallback — for a chart every category/point and series value (`null` for a break in a line), x/y/size data, visible point colors and markers (`line_style` / `point_colors`), line/area treatment (a line over a translucent fill is a `combo` of an area plot and a line plot), title/axis/legend chrome, companion text, bounds, and typography native export cannot infer; bar categories follow payload order top-down, and bar spacing, clustered overlap, and where a line starts inside `plot_area` are read from the fallback; for a table every resolved cell, header/summary line, rectangular span, cell style, alignment, bounds, and typography. JSON-first metadata is the authority and its preview may be approximate. Never simplify the artwork to fit the payload: when the closed payload cannot carry required data or topology, Default returns the native-ready decision upstream and Quick revises it before drawing; only the resolved non-native object stays unmarked, and an explicit `=yes` is never silently ignored. **Per-page verification**: every `=yes` key matches exactly one marker with one JSON child, and `=no` / unlisted objects have none — `rg -n 'data-pptx-replace-with="(chart|table)"|' /svg_output/.svg`. ### Table schema — `ppt-master.semantic-table.v2` -Every payload carries that exact `schema`; `columns` holds the optional header row and `rows` the body rows; `column_widths` / `row_heights` are relative weights. A cell is a string or an object with `text` (or `paragraphs` / `runs` for rich text), `fill`, `color`, `align` (`l` / `ctr` / `r`), `valign` (`top` / `middle` / `bottom`), `bold`, `font_size`, `padding`, and per-side `borders` (each side is `{"style": "none"}` or `{"style": "solid", "color", "width"}`; a border without `style` is rejected); exact repetition may be factored into `defaults.cell` / `defaults.paragraph` / `defaults.run` (cell fields such as `align`, `valign`, `padding`, `font_size` go under `defaults.cell`, never directly under `defaults`) and named `cell_styles`. Merged cells use positive `row_span` / `col_span` on the anchor with every covered cell as `{"merge_continuation": true}`. The complete field grammar: [`native-data.md`](../scripts/docs/native-data.md). +Every payload carries that exact `schema`; `columns` holds the optional header row and `rows` the body rows; `column_widths` / `row_heights` are relative weights. A cell is a string or an object with `text` (or `paragraphs` for rich text), `fill`, `color`, `align` (`l` / `ctr` / `r`), `valign` (`top` / `middle` / `bottom`), `bold`, `font_size`, `padding`, and per-side `borders` (each side is `{"style": "none"}` or `{"style": "solid", "color", "width"}`; a border without `style` is rejected); exact repetition may be factored into `defaults.cell` / `defaults.paragraph` / `defaults.run` (cell fields such as `align`, `valign`, `padding`, `font_size` go under `defaults.cell`, never directly under `defaults`) and named `cell_styles`. Merged cells use positive `row_span` / `col_span` on the anchor with every covered cell as `{"merge_continuation": true}`. The complete field grammar: [`native-data.md`](../scripts/docs/native-data.md). **Hard rule — the table payload is complete**: every row, summary line, value, and cell style that must survive `--native-charts-and-tables` is in `columns` / `rows`, because fallback text is discarded on that route. A payload holding only `font_size` and a uniform border is not complete when the fallback draws a header band, row or column fills, first-column emphasis, non-uniform row heights, or sparse rules. Numeric or currency columns use cell objects with `align: "r"` (`text-anchor="end"` does not carry). diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md index 5285ea29..29d41f52 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md @@ -175,7 +175,7 @@ These forms are needed only when the stated PPT behavior matters: | Desired behavior | Required form | |---|---| -| One editable PPT text frame with mixed formatting or multiline prose | Use one `` per logical paragraph and non-positional `` children for inline runs. Per-run `fill` / `font-weight` / `font-size` is retained: export walks nested runs and emits one DrawingML run per styled segment, so an emphasised phrase stays inside the same editable frame, and a positioned line-break `` may itself contain inline runs. Keep the first line as direct text; later lines use direct positioned `` children that repeat parent `x` with positive relative `dy`; an all-`` form may start at `dy="0"`. Default retains these breaks without PowerPoint wrapping; `--reflow-text` may join eligible lines. A font-size change, list marker, or larger accepted gap starts another paragraph. Sibling `` elements are not a paragraph's line breaks — the checker reports likely cases as a warning because the detection is heuristic, and they are repaired before export; sibling frames remain valid for independent text. | +| One editable PPT text frame with mixed formatting or multiline prose | Use one `` per logical paragraph and non-positional `` children for inline runs. Per-run `fill` / `font-weight` / `font-size` is retained: export walks nested runs and emits one DrawingML run per styled segment, so an emphasised phrase stays inside the same editable frame, and a positioned line-break `` may itself contain inline runs. Keep the first line as direct text; later lines use direct positioned `` children that repeat parent `x` with positive relative `dy`; an all-`` form may start at `dy="0"`, and it is the form to use when the first line itself carries an inline run. Default retains these breaks without PowerPoint wrapping; `--reflow-text` may join eligible lines. A font-size change, list marker, or larger accepted gap starts another paragraph. Sibling `` elements are not a paragraph's line breaks — the checker reports likely cases as a warning because the detection is heuristic, and they are repaired before export; sibling frames remain valid for independent text. | | Stable object grouping or object-level animation anchor | Wrap the intended object in ``. Content grouping is **mandatory** per §4.3 — a top-level `` is also the animation anchor; it is not an optional convenience. | | Native PowerPoint background promotion | Outside structured mode, make the first visual layer a direct full-canvas `` (or one inside a simple single-child group) with a solid, linear/radial gradient, or preset-pattern fill and no transform, filter, clip, rounding, or visible stroke; export writes it as Slide `p:bg`. Structured routes follow [`pptx-structure-interface.md`](./pptx-structure-interface.md). | | Free-design / brand-only PowerPoint structure | Use `pptx_structure.mode: flat`: keep objects Slide-local and author no Master/Layout identities, layers, or slots; export emits one clean Master plus Blank Layout. | @@ -189,7 +189,7 @@ These forms are needed only when the stated PPT behavior matters: **Hard rule — root groups protect body-text layout**: every visible direct root `` except a compact helper-authored preset atom declares positive root-coordinate `data-pptx-bounds="x y width height"` sized as the intended module zone. On flat pages, maximize ordinary zones within canvas/sibling space without overlap; the checker fails root-group overlap beyond `1px`, warns on module text overflow through `5%` and fails above it, and fails any larger root-`viewBox` text overflow. Bounds do not clip or reflow. A native plate, caption, or label laid over a picture belongs inside that picture's root group, so it takes no separate zone and creates no root-group overlap, and a clipped picture's zone is its visible region; shrinking the picture to free a text zone is not the repair. Structured slots, structural-role groups, and a wholly off-canvas Morph endpoint marked `data-pptx-morph-staging="true"` are overlap-exempt only; thresholds and estimator: [`svg-contract.md`](../scripts/docs/svg-contract.md) §4. -Wrap each logical Slide-local body unit in one descriptive top-level ``; group count follows the page's semantic units, and each group becomes one stable animation target when animation is enabled. Nested implementation groups may remain anonymous, need no bounds, and create no animation step; use them only when internal subunits (icon + title, value + label, repeated rows) are useful to edit — there is no default nesting pattern, depth, or quota. Titles, direct atomic Master/Layout elements, and canvas-level static framing — background images and full-canvas scrim/decoration rectangles — may remain root primitives; on flat pages give such framing a stable `id` plus `data-pptx-role="background"` / `"decoration"` and never add a `` solely to silence an ungrouped-element advisory. +Wrap each logical Slide-local body unit in one descriptive top-level ``; group count follows the page's semantic units, and each group becomes one stable animation target when animation is enabled. Nested implementation groups may remain anonymous, need no bounds, and create no animation step; use them only when internal subunits (icon + title, value + label, repeated rows) are useful to edit — there is no default nesting pattern, depth, or quota. Titles, direct atomic Master/Layout elements, and canvas-level static framing — background images and full-canvas scrim/decoration rectangles — may remain root primitives; on flat pages give such framing a stable `id` plus `data-pptx-role="background"` / `"decoration"` and never add a `` solely to silence an ungrouped-element advisory; a `` that carries the role is still a root group and declares `data-pptx-bounds` — only a root primitive is exempt. **Structural atoms and slots are excluded automatically.** `data-pptx-layer` and `data-pptx-placeholder` semantics are read first; otherwise explicit `data-pptx-role` values (`background`, `decoration`, `header`, `footer`, `chrome`, `watermark`, `page-number`, `logo`) mark Slide-local static framing (§4.1, [`semantic-svg.md`](semantic-svg.md)). A normal slot group has exactly one direct compatible carrier; several drawing atoms require the explicit composite `object` proxy fallback. Native chart/table carrier groups retain their specialized [`native-data-interface.md`](./native-data-interface.md) contract. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/apply_template.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/apply_template.py new file mode 100644 index 00000000..f15a1ab4 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/apply_template.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +"""Install selected Brand / Style / Layout / Deck workspaces into a project. + +Runs the install half of ``workflows/stages/apply-template-workspace.md`` §4 +as one command: validate every root, map each spec to its kind-qualified +project name, resolve the structural owner (Layout over Deck), copy the owner's +roster and every root's ``images/`` / ``icons/`` once, refuse destination +collisions and duplicate kinds before writing anything, prepend one provenance +line under each copied spec's H1, and print the §5.3 completion receipt. + +Usage:: + + python3 scripts/apply_template.py --root [--root ...] [--dry-run] + +A root is a workspace directory exposing ``templates/design_spec.md`` (library +shape) or one ``templates/design_spec...md`` per kind (project +shape). A root that resolves to the target project is consumed in place. The +tool selects nothing: which roots to pass is the Stage-1 / Quick decision. +""" + +from __future__ import annotations + +import argparse +import filecmp +import json +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +if str(SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPT_DIR)) + +from attribution_guard import require_skill_integrity # noqa: E402 +from console_encoding import configure_utf8_stdio # noqa: E402 +from register_template import ( # noqa: E402 + KIND_CONFIG, + SpecParseError, + _read_spec, + _validate_spec_shape, + validate_qualified_spec_identity, +) + +configure_utf8_stdio() + +SKILL_DIR = SCRIPT_DIR.parent +REPO_ROOT = SKILL_DIR.parent.parent +CHECKER = SCRIPT_DIR / "svg_quality_checker.py" +KINDS = ("brand", "style", "layout", "deck") +STRUCTURAL_KINDS = ("layout", "deck") +ASSET_DIRS = ("images", "icons") +BITMAP_SUFFIXES = { + ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp", +} + + +class ApplyTemplateError(Exception): + """A contract violation that blocks installation.""" + + +@dataclass +class SpecRecord: + kind: str + template_id: str + path: Path + frontmatter: dict + + +@dataclass +class RootRecord: + supplied: str + root: Path + source: str # library | explicit + specs: list[SpecRecord] + in_place: bool = False + + @property + def kinds(self) -> list[str]: + return [spec.kind for spec in self.specs] + + @property + def templates_dir(self) -> Path: + return self.root / "templates" + + @property + def display(self) -> str: + if self.source == "library": + kind = self.specs[0].kind + kind_dir = KIND_CONFIG[kind]["dir"].name + return f"skills/ppt-master/templates/{kind_dir}/{self.specs[0].template_id}/" + return f"{self.root}/" + + +@dataclass +class Mapping: + src: Path + dst: Path + content: bytes | None = None # rewritten specs carry their bytes + status: str = "copy" # copy | identical | in-place + + +@dataclass +class InstallPlan: + project: Path + roots: list[RootRecord] + owner: RootRecord | None + owner_kind: str | None + mappings: list[Mapping] = field(default_factory=list) + removals: list[Path] = field(default_factory=list) + + @property + def installed_specs(self) -> list[str]: + return [ + f"design_spec.{spec.kind}.{spec.template_id}.md" + for root in self.roots + for spec in root.specs + ] + + +# --------------------------------------------------------------------------- +# Discovery and validation +# --------------------------------------------------------------------------- + + +def _frontmatter_identity(spec_path: Path) -> tuple[str, str, dict]: + frontmatter, _body = _read_spec(spec_path) + fm = frontmatter or {} + kind = str(fm.get("kind") or "").strip() + if kind not in KINDS: + raise ApplyTemplateError( + f"{spec_path} declares no kind in its frontmatter; a legacy or " + "semantic-only package needs a new workspace from Create Template" + ) + template_id = str(fm.get(KIND_CONFIG[kind]["id_key"]) or "").strip() + if not template_id: + raise ApplyTemplateError( + f"{spec_path} declares kind {kind!r} without {KIND_CONFIG[kind]['id_key']}" + ) + return kind, template_id, fm + + +def _discover_specs(templates_dir: Path) -> list[SpecRecord]: + bare = templates_dir / "design_spec.md" + try: + qualified = _validate_spec_shape(templates_dir) + except SpecParseError as exc: + raise ApplyTemplateError(str(exc)) from exc + if bare.is_file(): + kind, template_id, fm = _frontmatter_identity(bare) + return [SpecRecord(kind, template_id, bare, fm)] + specs: list[SpecRecord] = [] + for path, _kind in qualified: + try: + kind, template_id, fm, _body = validate_qualified_spec_identity(path) + except SpecParseError as exc: + raise ApplyTemplateError(str(exc)) from exc + specs.append(SpecRecord(kind, template_id, path, fm)) + if not specs: + raise ApplyTemplateError( + f"{templates_dir} holds neither design_spec.md nor " + "design_spec...md" + ) + return specs + + +def _source_label(root: Path, specs: list[SpecRecord]) -> str: + if len(specs) != 1: + return "explicit" + spec = specs[0] + config = KIND_CONFIG[spec.kind] + library_root = (config["dir"] / spec.template_id).resolve() + if root.resolve() != library_root: + return "explicit" + index_path = config["index"] + try: + index = json.loads(index_path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return "explicit" + return "library" if spec.template_id in index else "explicit" + + +def _load_root(supplied: str, project: Path) -> RootRecord: + root = Path(supplied).expanduser() + if not root.is_dir(): + raise ApplyTemplateError(f"workspace root is not a directory: {supplied}") + if root.name == "templates" and (root / "design_spec.md").is_file(): + raise ApplyTemplateError( + f"{supplied} is an inner templates/ directory; pass its workspace " + "root so sibling images/ and icons/ install with it" + ) + root = root.resolve() + templates_dir = root / "templates" + if not templates_dir.is_dir(): + raise ApplyTemplateError(f"{supplied} has no templates/ directory") + specs = _discover_specs(templates_dir) + in_place = root == project.resolve() + if in_place and any(spec.path.name == "design_spec.md" for spec in specs): + raise ApplyTemplateError( + "an in-place project root must already hold kind-qualified specs " + "(design_spec...md); templates/design_spec.md is a " + "library shape" + ) + record = RootRecord( + supplied=supplied, + root=root, + source="explicit" if in_place else _source_label(root, specs), + specs=specs, + in_place=in_place, + ) + if record.source == "library" and specs[0].kind == "style": + for extra in (*ASSET_DIRS, "exports"): + if (root / extra).exists(): + raise ApplyTemplateError( + f"Style-only library package {record.display} carries " + f"{extra}/; a Style contributes only its spec" + ) + return record + + +def _validate_root(record: RootRecord) -> None: + command = [ + sys.executable, + str(CHECKER), + str(record.templates_dir), + "--template-mode", + "--canonical-authoring", + ] + result = subprocess.run( + command, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + if result.returncode != 0: + tail = "\n".join( + line for line in (result.stdout + result.stderr).splitlines()[-25:] + ) + raise ApplyTemplateError( + f"template validation failed for {record.display} " + f"(exit {result.returncode}):\n{tail}" + ) + + +def _check_kind_cardinality(roots: list[RootRecord]) -> None: + seen: dict[str, str] = {} + for record in roots: + for kind in record.kinds: + if kind in seen: + raise ApplyTemplateError( + f"kind {kind!r} is contributed by both {seen[kind]} and " + f"{record.display}; select one root per kind" + ) + seen[kind] = record.display + + +# --------------------------------------------------------------------------- +# Planning +# --------------------------------------------------------------------------- + + +def _provenance_line(record: RootRecord) -> str: + return f"> **Installed from**: `{record.display}` ({record.source})" + + +def _spec_with_provenance(spec_path: Path, record: RootRecord) -> bytes: + text = spec_path.read_text(encoding="utf-8") + lines = text.split("\n") + body_start = 0 + if lines and lines[0].strip() == "---": + for idx in range(1, len(lines)): + if lines[idx].strip() == "---": + body_start = idx + 1 + break + h1 = next( + (idx for idx in range(body_start, len(lines)) if lines[idx].startswith("# ")), + None, + ) + if h1 is None: + raise ApplyTemplateError(f"{spec_path} has no H1 to anchor the provenance line") + rewritten = lines[: h1 + 1] + ["", _provenance_line(record)] + lines[h1 + 1 :] + return "\n".join(rewritten).encode("utf-8") + + +def _iter_files(directory: Path) -> list[Path]: + return sorted(path for path in directory.rglob("*") if path.is_file()) + + +def _is_spec_file(path: Path) -> bool: + return path.name == "design_spec.md" or ( + path.name.startswith("design_spec.") and path.suffix == ".md" + ) + + +def _structural_files(record: RootRecord) -> list[Path]: + return [ + path + for path in _iter_files(record.templates_dir) + if not _is_spec_file(path) and path.suffix.lower() not in BITMAP_SUFFIXES + ] + + +def _plan(project: Path, roots: list[RootRecord]) -> InstallPlan: + owner = next((r for r in roots if "layout" in r.kinds), None) + owner_kind = "layout" if owner else None + if owner is None: + owner = next((r for r in roots if "deck" in r.kinds), None) + owner_kind = "deck" if owner else None + plan = InstallPlan(project=project, roots=roots, owner=owner, owner_kind=owner_kind) + project_templates = project / "templates" + + for record in roots: + if record.in_place: + continue + for spec in record.specs: + plan.mappings.append( + Mapping( + src=spec.path, + dst=project_templates / f"design_spec.{spec.kind}.{spec.template_id}.md", + content=_spec_with_provenance(spec.path, record), + ) + ) + for asset_dir in ASSET_DIRS: + source_dir = record.root / asset_dir + if not source_dir.is_dir(): + continue + for path in _iter_files(source_dir): + plan.mappings.append( + Mapping(src=path, dst=project / asset_dir / path.relative_to(source_dir)) + ) + if owner is not None and not owner.in_place: + for path in _structural_files(owner): + plan.mappings.append( + Mapping(src=path, dst=project_templates / path.relative_to(owner.templates_dir)) + ) + in_place_deck = next( + (r for r in roots if r.in_place and "deck" in r.kinds), None + ) + if owner_kind == "layout" and in_place_deck is not None: + plan.removals.extend(_structural_files(in_place_deck)) + return plan + + +def _preflight(plan: InstallPlan) -> None: + by_dst: dict[Path, Mapping] = {} + for mapping in plan.mappings: + if mapping.dst in by_dst: + raise ApplyTemplateError( + f"two selected files map to {mapping.dst}: " + f"{by_dst[mapping.dst].src} and {mapping.src}" + ) + by_dst[mapping.dst] = mapping + removals = set(plan.removals) + collisions: list[str] = [] + for mapping in plan.mappings: + if mapping.dst in removals or not mapping.dst.exists(): + continue + if mapping.content is not None: + identical = mapping.dst.read_bytes() == mapping.content + else: + identical = filecmp.cmp(mapping.src, mapping.dst, shallow=False) + if identical: + mapping.status = "identical" + else: + collisions.append(f"{mapping.dst} (from {mapping.src})") + if collisions: + raise ApplyTemplateError( + "destination collision; remove or rename before installing:\n " + + "\n ".join(collisions) + ) + + +# --------------------------------------------------------------------------- +# Writing and receipt +# --------------------------------------------------------------------------- + + +def _write(plan: InstallPlan) -> None: + for path in plan.removals: + path.unlink() + for mapping in plan.mappings: + if mapping.status == "identical": + continue + mapping.dst.parent.mkdir(parents=True, exist_ok=True) + if mapping.content is not None: + mapping.dst.write_bytes(mapping.content) + else: + shutil.copy2(mapping.src, mapping.dst) + + +def _receipt(plan: InstallPlan) -> str: + kinds_present = {kind for root in plan.roots for kind in root.kinds} + identity = ( + "brand" if "brand" in kinds_present + else "deck" if "deck" in kinds_present + else "current-project" + ) + structure = plan.owner_kind or "free-design" + application = "deck" if "deck" in kinds_present else "none" + direction = "style" if "style" in kinds_present else "unresolved" + active_roster = ( + f"{plan.owner_kind}:{plan.owner.display}" if plan.owner else "none" + ) + install = "in-place" if all(root.in_place for root in plan.roots) else "copied" + return ( + "roots=" + ";".join(root.display for root in plan.roots) + + "; sources=" + ";".join(root.source for root in plan.roots) + + "; kinds=" + ";".join(",".join(root.kinds) for root in plan.roots) + + f"; segments=identity:{identity},structure:{structure}," + f"application_context:{application},direction:{direction}" + + f"; active_roster={active_roster}" + + f"; install={install}" + + "; installed_specs=" + ",".join(plan.installed_specs) + ) + + +def apply_templates( + project_path: str | Path, + root_args: list[str], + *, + dry_run: bool = False, + validate: bool = True, +) -> InstallPlan: + """Plan, preflight, and (unless ``dry_run``) install the selected roots.""" + project = Path(project_path).expanduser().resolve() + if not project.is_dir(): + raise ApplyTemplateError(f"project path is not a directory: {project_path}") + if not root_args: + raise ApplyTemplateError("pass at least one --root workspace") + roots = [_load_root(arg, project) for arg in root_args] + unique = {record.root for record in roots} + if len(unique) != len(roots): + raise ApplyTemplateError("the same workspace root was passed more than once") + _check_kind_cardinality(roots) + if validate: + for record in roots: + _validate_root(record) + plan = _plan(project, roots) + _preflight(plan) + if not dry_run: + _write(plan) + return plan + + +def main(argv: list[str] | None = None) -> int: + require_skill_integrity() + parser = argparse.ArgumentParser( + description=( + "Install selected Brand/Style/Layout/Deck workspaces into a " + "project's templates/, images/, and icons/ (apply-template-workspace §4)." + ), + ) + parser.add_argument("project_path", help="Initialized project root") + parser.add_argument( + "--root", + action="append", + default=[], + metavar="WORKSPACE_ROOT", + help="Workspace root to install; repeat for several kinds (one root per kind)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate, plan, and print the mapping and receipt without writing", + ) + parser.add_argument( + "--skip-validation", + action="store_true", + help="Skip the per-root svg_quality_checker --template-mode run (already validated in this turn)", + ) + args = parser.parse_args(argv) + try: + plan = apply_templates( + args.project_path, + args.root, + dry_run=args.dry_run, + validate=not args.skip_validation, + ) + except ApplyTemplateError as exc: + print(f"[ERROR] {exc}", file=sys.stderr) + return 1 + verb = "would install" if args.dry_run else "installed" + for path in plan.removals: + print(f"[remove] {path}") + same = 0 + for mapping in plan.mappings: + if mapping.status == "identical": + same += 1 + label = "[same]" if mapping.status == "identical" else "[copy]" + print(f"{label} {mapping.dst}") + copied = len(plan.mappings) - same + note = f", {same} already present" if same else "" + print(f"[OK] {verb} {copied} file(s) into {plan.project}{note}") + print(_receipt(plan)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md index c6f41466..8901d230 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md @@ -58,14 +58,20 @@ Useful options: output path is known. With multiple inputs, each successful conversion prints its own JSON line after that source finishes. - At the unified `source_to_md.py` entry, `--images all|filtered|none`, - `--no-images`, and `--filter-images` map to the PDF image mode. The web - backend exposes its own direct `--no-images` option described below. + `--no-images`, and `--filter-images` map to the PDF image mode. + `--no-images` (or `--images none`) also applies to web pages (images stay + remote links, no `_files/`) and is a no-op on Markdown/text. +- A `.md` / `.markdown` / `.txt` URL whose body is not HTML is saved verbatim + under a `Source:` header, named by the URL's filename stem. - Unknown backend-specific flags are passed through to each selected converter. - `-o/--output` selects one Markdown file for one input, or an output directory for multiple inputs / directory inputs. A path that names an existing directory, or ends in `/`, is always treated as a directory, even for a single input: the file keeps its default `.md` - name inside it. + name inside it; an extension-less `-o` for one input gains `.md`. + Local batch outputs are planned together; input/output collisions gain `_2`, + `_3`, etc. suffixes and are reported on stderr. A single-input file `-o` + refuses an existing file except its own Markdown/text passthrough. For multi-source project intake, use `project_manager.py import-sources` with all source paths / URLs. For local files, the default is to keep generated diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md index 5c0afd44..1b902529 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md @@ -57,6 +57,7 @@ python3 scripts/image_gen.py "A cinematic portrait" --backend minimax python3 scripts/image_gen.py "A product launch hero image" --backend qwen python3 scripts/image_gen.py "科技感背景图" --backend zhipu python3 scripts/image_gen.py "A product KV in cinematic style" --backend volcengine +python3 scripts/image_gen.py "A mountain landscape" --backend tencent ``` Configuration sources: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/native-data.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/native-data.md index 21b61a52..69888ab8 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/native-data.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/native-data.md @@ -14,24 +14,25 @@ file no longer repeats. Section titles mirror the owning section of ## §2 Metadata placement and bounds -one child `` (attribute `data-pptx-json` is read-compatible, not canonical); the marker's `data-pptx-replace-with` selects the schema. Provide `x`, `y`, `width`, `height` in metadata or as `data-pptx-x/y/width/height` on the group; omitted bounds are inferred from the fallback geometry (then marker/ancestor `translate` / `scale` apply), complete explicit bounds are absolute slide coordinates. `x` / `y` are finite and inside the 32-bit DrawingML range; `width` / `height` additionally resolve to at least one EMU (tables: per resolved row and column). JSON-first markers require all four bounds in metadata. Classic charts accept root `plot_area` (`x`, `y`, `width`, `height`, a positive rectangle inside the frame → `c:manualLayout`); ChartEx rejects it. `svg_quality_checker.py` validates marker kind, JSON, bounds/fallback, table rows/columns, chart type and data shape, the authority contract, and (SVG-first) baseline freshness. +one child `` (attribute `data-pptx-json` is read-compatible, not canonical); the marker's `data-pptx-replace-with` selects the schema. Provide `x`, `y`, `width`, `height` in metadata or as `data-pptx-x/y/width/height` on the group; omitted bounds are inferred from the fallback geometry (then marker/ancestor `translate` / `scale` apply), complete explicit bounds are absolute slide coordinates. `x` / `y` are finite and inside the 32-bit DrawingML range; `width` / `height` additionally resolve to at least one EMU (tables: per resolved row and column). JSON-first markers require all four bounds in metadata. Classic charts accept root `plot_area` (`x`, `y`, `width`, `height`, a positive rectangle inside the frame → `c:manualLayout`; the frame grows past the plot on the side that carries tick labels until that strip is two axis-font ems tall, because PowerPoint drops the manual vertical layout when its labels do not fit and auto-fits the plot to the frame top instead); ChartEx rejects it. `svg_quality_checker.py` validates marker kind, JSON, bounds/fallback, table rows/columns, chart type and data shape, the authority contract, and (SVG-first) baseline freshness. ## §2 Table schema — `ppt-master.semantic-table.v2` -Every payload carries that exact `schema`. Native tables are rectangular grids: `columns` for the optional header row, `rows` for body rows (shorter rows padded unless `strict_grid: true`; at most 1000 resolved rows and columns); `column_widths` / `row_heights` are finite non-negative relative weights matching the grid with one positive value; `header_rows` is an integer in range; `strict_grid`, `style.band_row`, and `bold` are JSON booleans. Exact repetition may be factored into `defaults.cell` / `defaults.paragraph` / `defaults.run` and kebab-case `cell_styles` selected by `cell_style` (precedence: cell defaults → named style → cell fields; object-valued `padding` merges, everything else replaces; `