From 8f32dfa7a1de0015cd8fbfb0eb7b30376882eeb6 Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Sun, 21 Jun 2026 00:01:37 +0800 Subject: [PATCH] Sync third-party and MCP marketplace plugins Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed. --- config/external-sources.lock.json | 4 +- .../plugins/mcp-playwright/MCP_SOURCE.json | 2 +- plugins/codex/plugins/ppt-master/README.md | 2 +- .../ppt-master/THIRD_PARTY_SOURCE.json | 4 +- .../ppt-master/skills/ppt-master/SKILL.md | 49 ++- .../ppt-master/references/executor-base.md | 4 +- .../ppt-master/references/image-generator.md | 3 +- .../ppt-master/references/image-searcher.md | 32 +- .../ppt-master/references/shared-standards.md | 2 +- .../ppt-master/references/strategist.md | 25 +- .../references/visual-styles/chalkboard.md | 2 +- .../references/visual-styles/dark-tech.md | 2 + .../references/visual-styles/editorial.md | 2 + .../references/visual-styles/glassmorphism.md | 2 +- .../references/visual-styles/ink-notes.md | 2 +- .../references/visual-styles/ink-wash.md | 2 +- .../references/visual-styles/paper-cut.md | 2 +- .../references/visual-styles/sketch-notes.md | 2 +- .../references/visual-styles/soft-rounded.md | 2 + .../skills/ppt-master/scripts/README.md | 8 +- .../ppt-master/scripts/analyze_images.py | 10 +- .../ppt-master/scripts/beautify_identity.py | 2 +- .../ppt-master/scripts/beautify_inventory.py | 2 +- .../scripts/confirm_ui/static/app.js | 33 +- .../ppt-master/scripts/docs/confirm_ui.md | 17 +- .../ppt-master/scripts/docs/conversion.md | 23 ++ .../skills/ppt-master/scripts/docs/image.md | 2 + .../skills/ppt-master/scripts/docs/project.md | 5 + .../skills/ppt-master/scripts/image_search.py | 345 +++++++++++++++++- .../skills/ppt-master/scripts/pptx_intake.py | 231 ++++++++++++ .../ppt-master/scripts/project_manager.py | 130 ++++++- .../scripts/source_to_md/doc_to_md.py | 61 ++++ .../scripts/source_to_md/pdf_to_md.py | 51 +++ .../scripts/source_to_md/ppt_to_md.py | 75 +++- .../scripts/source_to_md/web_to_md.py | 39 ++ .../scripts/svg_to_pptx/drawingml_elements.py | 11 +- .../scripts/svg_to_pptx/drawingml_utils.py | 15 + .../scripts/svg_to_pptx/pptx_notes.py | 9 +- .../ppt-master/scripts/template_fill_pptx.py | 6 +- .../scripts/template_fill_pptx/chart_fill.py | 10 +- .../scripts/template_fill_pptx/chart_read.py | 43 ++- .../scripts/template_fill_pptx/checker.py | 16 + .../scripts/template_fill_pptx/cli.py | 6 +- .../templates/design_spec_reference.md | 12 +- .../ppt-master/workflows/beautify-pptx.md | 41 +-- .../workflows/template-fill-pptx.md | 18 +- 46 files changed, 1252 insertions(+), 114 deletions(-) create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_intake.py diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index be00f6fe8..69192f2be 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -69,8 +69,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "ae6d9096d64d0291933c7a3e6bba2ae4eb533741", - "syncedAt": "2026-06-19T16:00:00Z" + "commit": "c948611f54ae8aae089083bf18b02b10bdb79957", + "syncedAt": "2026-06-20T16:00:01Z" }, { "id": "next-skills", diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 592cf19f7..ad92eb4a3 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-06-19T16:01:21Z" + "syncedAt": "2026-06-20T16:01:37Z" } diff --git a/plugins/codex/plugins/ppt-master/README.md b/plugins/codex/plugins/ppt-master/README.md index 24bb556a6..850a3c4b4 100644 --- a/plugins/codex/plugins/ppt-master/README.md +++ b/plugins/codex/plugins/ppt-master/README.md @@ -1,6 +1,6 @@ # PPT Master — AI generates natively editable PPTX from any document -[![Version](https://img.shields.io/badge/version-v2.10.0-blue.svg)](https://github.com/hugohe3/ppt-master/releases) +[![Version](https://img.shields.io/badge/version-v2.11.0-blue.svg)](https://github.com/hugohe3/ppt-master/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub stars](https://img.shields.io/github/stars/hugohe3/ppt-master.svg)](https://github.com/hugohe3/ppt-master/stargazers) [![AtomGit stars](https://atomgit.com/hugohe3/ppt-master/star/badge.svg)](https://atomgit.com/hugohe3/ppt-master) diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index 1676cfcff..9a491b445 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": "ae6d9096d64d0291933c7a3e6bba2ae4eb533741", + "commit": "c948611f54ae8aae089083bf18b02b10bdb79957", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-06-19T16:00:00Z" + "syncedAt": "2026-06-20T16:00:01Z" } 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 20c1ac537..521b61258 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md @@ -45,6 +45,7 @@ description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶 | `${SKILL_DIR}/scripts/source_to_md/doc_to_md.py` | Documents to Markdown — native Python for DOCX/HTML/EPUB/IPYNB, pandoc fallback for legacy formats (.doc/.odt/.rtf/.tex/.rst/.org/.typ) | | `${SKILL_DIR}/scripts/source_to_md/excel_to_md.py` | Excel workbooks to Markdown — supports .xlsx/.xlsm; legacy .xls should be resaved as .xlsx | | `${SKILL_DIR}/scripts/source_to_md/ppt_to_md.py` | PowerPoint to Markdown | +| `${SKILL_DIR}/scripts/pptx_intake.py` | Standard PPTX intake enrichment — canvas / identity / slide geometry / tables / native chart data | | `${SKILL_DIR}/scripts/source_to_md/web_to_md.py` | Web page to Markdown (supports WeChat via `curl_cffi`) | | `${SKILL_DIR}/scripts/project_manager.py` | Project init / validate / manage | | `${SKILL_DIR}/scripts/icon_sync.py` | Copy chosen library icons into `/icons/` at selection time; missing names reported + non-zero (re-pick gate) | @@ -85,6 +86,21 @@ For complete tool documentation, see `${SKILL_DIR}/scripts/README.md`. | `live-preview` | `workflows/live-preview.md` | Browser-based live preview — auto-started during generation and re-enterable any time the user mentions "live preview", "preview", "看效果", or wants to click/select a slide element | | `visual-review` | `workflows/visual-review.md` | Per-page rubric-based visual self-check — run only when the user explicitly asks for a visual re-pass on the generated SVGs (between Executor and post-processing). Opt-in only; never invoked by the main pipeline. | +### PPTX Route Boundary + +When the user provides an existing `.pptx`, route by the role of the source deck: + +| User intent | Route | Contract | +|---|---|---| +| Preserve the deck's page split, page order, and per-slide wording; improve layout / hierarchy / whitespace | `beautify` | Source page count and order are 1:1; text and data values are frozen; visual identity is inherited after confirmation | +| Treat the deck as source material; rethink the story, merge / split / drop / reorder pages, or change page count | Main pipeline | `ppt_to_md` + PPTX intake provide content facts and candidates; Strategist may re-architect freely | +| Reuse the deck's native design with new material | `template-fill` | Clone selected source slides and replace text / table / chart data directly in OOXML; no SVG generation | +| Harvest the deck as a reusable future template | `create-template` | Build a template package, not a one-off generated deck | + +**Deciding axis (beautify vs main pipeline) — one question, one discriminator**: is the source's page split a finished artifact to preserve, or a draft structure to overturn? The concrete discriminator is **page count / order**: if it changes at all — any split, merge, drop, or reorder — it is the **main pipeline**, never beautify. Beautify is **strictly 1:1**: same page count, same order, text verbatim, only layout / hierarchy / whitespace redone. Edge case made explicit: "keep all the content but split a crowded page so it reads better" still changes page count, so it is the **main pipeline** (re-pagination is re-architecture), not beautify. + +Ambiguous requests such as "make this PPT more professional" or "optimize this deck" MUST be clarified with one question before routing: "Should the original page count/order and each slide's wording be preserved, or should the deck be treated as source material and restructured into a new story?" Preserve → `beautify`; restructure → main pipeline. + --- ## Workflow @@ -103,7 +119,7 @@ When the user provides non-Markdown content, convert immediately: | DOCX / Word / Office document | `python3 ${SKILL_DIR}/scripts/source_to_md/doc_to_md.py ` | | XLSX / XLSM / Excel workbook | `python3 ${SKILL_DIR}/scripts/source_to_md/excel_to_md.py ` | | CSV / TSV | Read directly as plain-text table source | -| PPTX / PowerPoint deck | `python3 ${SKILL_DIR}/scripts/source_to_md/ppt_to_md.py ` | +| PPTX / PowerPoint deck | `python3 ${SKILL_DIR}/scripts/source_to_md/ppt_to_md.py ` for Markdown content; after Step 2 `import-sources`, standard PPTX intake is also written to `/analysis/` | | EPUB / HTML / LaTeX / RST / other | `python3 ${SKILL_DIR}/scripts/source_to_md/doc_to_md.py ` | | Web link | `python3 ${SKILL_DIR}/scripts/source_to_md/web_to_md.py ` | | WeChat / high-security site | `python3 ${SKILL_DIR}/scripts/source_to_md/web_to_md.py ` (requires `curl_cffi`, included in `requirements.txt`) | @@ -144,6 +160,16 @@ Import source content (choose based on the situation): | Has source files (PDF/MD/etc.) | `python3 ${SKILL_DIR}/scripts/project_manager.py import-sources --move` | | User provided text directly in conversation | No import needed — content is already in conversation context; subsequent steps can reference it directly | +For PPTX sources, `import-sources` automatically runs the standard intake enrichment: + +```bash +python3 ${SKILL_DIR}/scripts/pptx_intake.py /sources/ -o /analysis +``` + +For each PPTX it writes `.identity.json` (canvas, theme palette/fonts, observed usage) and `.slide_library.json` (text slots, geometry, native tables, native chart caches), and merges that deck's Strategist-facing digest into the single multi-deck index `analysis/source_profile.json` (`decks[]`, one self-contained entry per source deck, with prefixed artifact pointers). In the main generation path these are source facts and recommendation candidates, not replica constraints; beautify and template-fill workflows decide separately which fields become locked constraints. + +Multi-deck: several PPTX files may be imported into one main-pipeline project — each gets its own `.*` artifacts and a deck entry in `source_profile.json`. `source_profile.json` stays the single must-read index (one entry for a one-deck project, several for a combined-source project). Stems must be distinct; re-importing the same stem replaces that deck's entry. The beautify / template-fill workflows remain single-deck (1:1 to one chosen source deck) and read that deck's `.*` artifacts. + > ⚠️ **MUST use `--move`** (not copy): all source files — Step 1's generated Markdown, original PDFs / MDs / images — go into `sources/` via `import-sources --move`. After execution they no longer exist at the original location. Intermediate artifacts (e.g., `_files/`) are handled automatically. **✅ Checkpoint — Confirm project structure created successfully, `sources/` contains all source files, converted materials are ready. Proceed to Step 3.** @@ -278,6 +304,10 @@ Read references/strategist.md > ⚠️ **Mandatory gate**: before writing `design_spec.md`, Strategist MUST `read_file templates/design_spec_reference.md` and follow its full I–XI section structure. See `strategist.md` Section 1. +**`/analysis/` is the project's intermediate-analysis folder: the canonical home for machine-extracted source/asset facts — the PPTX intake bundle (`source_profile.json` index + per-deck `.identity.json` / `.slide_library.json`) and `image_analysis.csv`. It holds facts, not design contracts — `design_spec.md` / `spec_lock.md` stay at the project root.** The MUST-read contract covers only the **compact structured data files (`.json` / `.csv`)**; other artifacts that may live under `analysis/` (e.g. a beautify `source_svg_import/` vector reference package) are NOT bulk-read — they are read selectively only when a specific workflow step calls for them. Before the Eight Confirmations, Strategist MUST read the auto-extracted fact files already in `analysis/` — currently `source_profile.json` (PPTX intake), when present. This file is the multi-deck index: read it once for the `decks[]` digests (canvas / chart / table entries per source deck), then open a specific deck's `.identity.json` / `.slide_library.json` only if you need its full raw facts. Use these entries as **factual source context** (format default + content facts); when several decks are present, synthesize across all of them. The source's **palette / typography / visual identity are a reference, not a constraint**: the main pipeline may inherit them where they fit the content and the confirmed style, or design fresh where they don't — the Strategist's judgment, never an obligation to either keep or discard. (Template-fill preserves the native source design by editing cloned slides directly; beautify defaults to the source identity but still follows the confirmed values; the main pipeline treats source identity as reference only and defaults to fresh design.) (`image_analysis.csv` lands later, at the image-analysis step below, and is the authoritative regenerated image-fact view there — re-derived from the live `images/` folder, not a durable store.) + +**Channel ownership — read each fact once from its owning channel.** In the main pipeline the **content contract is the Markdown** (`sources/.md`): text, tables, and chart data values all come from there (`ppt_to_md` now transcribes native chart data into Markdown tables). The `analysis/` chart / table entries are a **structural digest** for outline decisions (which slides carried charts, type, series names) — not a second copy of the values; do NOT also pull chart values from `.slide_library.json` in the main pipeline. The `.slide_library.json` full structured data is owned by the direct-PPTX workflows: template-fill uses it as the native fill contract; beautify uses it for native chart / table data while keeping slide text from the Markdown. + **Eight Confirmations** (full template: `templates/design_spec_reference.md`): ⛔ **BLOCKING**: present the Eight Confirmations as a single bundled recommendation set and **wait for explicit user confirmation or modification** before outputting Design Specification & Content Outline. This is the single core confirmation point — once confirmed, all subsequent steps proceed automatically. @@ -293,7 +323,7 @@ Read references/strategist.md **Confirm UI Auto-Launch (Mandatory — default visual confirmation surface)**: by default the Eight Confirmations are presented through an interactive local page (color swatches, live font previews, candidate picks); the chat path is the always-valid fallback. Steps: -1. Write the recommendations to `/confirm_ui/recommendations.json` (full schema + field mapping: [`scripts/docs/confirm_ui.md`](scripts/docs/confirm_ui.md)). Two kinds of field: **enumerable** (canvas / mode / visual_style / icons / formula policy / generation mode; plus image usage with a Custom path; plus AI source only when image usage may include `ai`) — the page lists common options from `confirm_ui/static/catalogs.json`, so you only name the recommended canonical `id` in a `recommend` block (canvas may be a catalog id like `ppt169` or a custom size/prose; style = `mode` + `visual_style`, two independent picks; icon ids are real libraries such as `tabler-outline`, or `emoji` for system emoji; image usage uses `ai` / `web` / `provided` / `placeholder` / `none`, or a custom prose plan when several sources must be combined; never write bare `"custom"` for image usage — write the actual mixed plan, e.g. "AI cover + user product assets + web industry images"; write `image_ai_path` only when recommending `image_usage: "ai"` or a custom plan that includes AI); **generative** (color, typography, generated-image style) — author **≥3 candidates** each (creative recommendations always offer real choice, never a single silent option — same rule as strategist h.5; fewer than 3 only on the honest-shortfall exception, with a stated reason) (color: user-facing core `palette` with background/secondary_bg/primary/accent/secondary_accent/body_text; typography: CJK + Latin for `heading` and `body` with `css` preview stacks, plus `body_size` as the body baseline px; when recommending generated images, `image_strategy.candidates` with rendering × palette combinations from strategist h.5). `page_count` / `audience` are plain values. Only open fields show a Custom box: `canvas`, `mode`, `visual_style`, `icons`, `image_usage`, and typography custom text. Closed fields (`image_ai_path`, `formula_policy`, `generation_mode`, `refine_spec`) stay finite. Set `lang` to the page language; visible candidate text should match `lang`, or provide bilingual `name_zh` / `name_en` and `note_zh` / `note_en` fields. Reuse the same candidate thinking as strategist h.5. +1. Write the recommendations to `/confirm_ui/recommendations.json` (full schema + field mapping: [`scripts/docs/confirm_ui.md`](scripts/docs/confirm_ui.md)). Two kinds of field: **enumerable** (canvas / mode / visual_style / icons / formula policy / generation mode; plus image usage with a Custom path; plus AI source only when image usage may include `ai`) — the page lists common options from `confirm_ui/static/catalogs.json`, so you only name the recommended canonical `id` in a `recommend` block (canvas may be a catalog id like `ppt169` or a custom size/prose; style = `mode` + `visual_style`, two independent picks; icon ids are real libraries such as `tabler-outline`, or `emoji` for system emoji; image usage uses `ai` / `web` / `provided` / `placeholder` / `none`, or a custom prose plan when several sources must be combined; never write bare `"custom"` for image usage — write the actual mixed plan, e.g. "AI cover + user product assets + web industry images"; write `image_ai_path` only when recommending `image_usage: "ai"` or a custom plan that includes AI); **generative** (color, typography, generated-image style) — author **≥3 candidates** each (creative recommendations always offer real choice, never a single silent option — same rule as strategist h.5; fewer than 3 only on the honest-shortfall exception, with a stated reason) (color: user-facing core `palette` with background/secondary_bg/primary/accent/secondary_accent/body_text; typography: CJK + Latin for `heading` and `body` with `css` preview stacks, plus `body_size` as the body baseline px; when recommending generated images, `image_strategy.candidates` with rendering × palette combinations from strategist h.5). `page_count` / `audience` / `content_divergence` are plain values (free text). Only open fields show a Custom box: `canvas`, `mode`, `visual_style`, `icons`, `image_usage`, and typography custom text. Closed fields (`image_ai_path`, `formula_policy`, `generation_mode`, `refine_spec`) stay finite. `content_divergence` is a **free-text** field shown under audience in §c — the user states in their own words how closely to follow the source vs how freely to reshape it (blank = balanced; facts stay sourced at every level). Write it as `content_divergence: { "value": "" }`. It is consumed by Strategist when authoring `§IX`, recorded in `design_spec.md §I`, carries no page-count coupling, and is **not** written to `spec_lock.md`. Set `lang` to the page language; visible candidate text should match `lang`, or provide bilingual `name_zh` / `name_en` and `note_zh` / `note_en` fields. Reuse the same candidate thinking as strategist h.5. 2. Launch the page **in the background and wait for the browser confirmation** (the child server runs detached; the parent command returns after `result.json` is freshly written). **Run this command with a long tool timeout — 600000 ms** — so the `--wait` (≈590 s budget) can complete: ```bash python3 ${SKILL_DIR}/scripts/confirm_ui/server.py --daemon --wait @@ -354,12 +384,14 @@ After the Eight Confirmations are approved and **before outputting `design_spec. The formula renderer uses a provider fallback chain by default: `codecogs,quicklatex,mathpad,wikimedia`. The first three are color-aware; Wikimedia is an availability fallback. Formula PNGs are transparent by default: manifest `background` is the temporary render matte and transparency-removal reference, not a retained final background unless `transparent: false` is set for that item. Do not scan `spec_lock.md` for `$...$` or `$$...$$`. Dollar-delimited math in source material is only a signal for Strategist; the renderer consumes the explicit manifest. -If the user provided images or formula PNGs were rendered, run analysis **before outputting the design spec**: +If the user provided images or formula PNGs were rendered, run analysis **before outputting the design spec**. It writes `analysis/image_analysis.csv` — the authoritative regenerated image-fact view in the `analysis/` folder, which MUST be read before authoring §VIII: ```bash python3 ${SKILL_DIR}/scripts/analyze_images.py /images ``` -> ⚠️ **Image handling**: NEVER directly read / open / view image files (`.jpg`, `.png`, etc.). All image info comes from `analyze_images.py` output or the Design Spec's Image Resource List. +> 🔁 **Image facts are regenerated on demand, never a durable store.** `images/` is a live working folder — pictures are extracted from the source at import, the user may drop or replace files at any time, and Step 5 writes web/AI images into it. The single source of truth is therefore the **current contents of `images/`**, and `analysis/image_analysis.csv` is a *regenerated view* of it, not a fact to keep in sync. Re-run `analyze_images.py /images` immediately **before any step that reads image facts** so the view reflects the live folder: before the §h image-usage recommendation (see [strategist.md](references/strategist.md) §h), here before authoring §VIII, after Step 5 acquisition (so web/AI files join the view), and again any time the user says they added or replaced images. This is the staleness strategy — re-derive on use, no cache to invalidate. + +> ⚠️ **Image handling**: NEVER directly read / open / view image files (`.jpg`, `.png`, etc.). All image info comes from `analyze_images.py` output (`analysis/image_analysis.csv`) or the Design Spec's Image Resource List. **Output**: - `/design_spec.md` — human-readable design narrative @@ -368,6 +400,7 @@ python3 ${SKILL_DIR}/scripts/analyze_images.py /images **✅ Checkpoint — Phase deliverables complete, auto-proceed to next step**: ```markdown ## ✅ Strategist Phase Complete +- [x] Read the auto-extracted facts already in `analysis/` (e.g. `source_profile.json`) before the Eight Confirmations - [x] Eight Confirmations completed (user confirmed via Confirm UI `result.json` or chat fallback) - [x] Split-mode note appended below the eight items (heavy or normal variant) - [x] Spec-refinement opt-in line appended (default OFF; only the user's explicit request enters the refine-spec workflow) @@ -395,13 +428,15 @@ Then **lazy-load the path-specific reference** for each row that actually needs | Acquire Via | Load reference (only if any such row exists) | Run | |---|---|---| | `ai` | `references/image-generator.md` | `python3 ${SKILL_DIR}/scripts/image_gen.py --manifest /images/image_prompts.json` | -| `web` | `references/image-searcher.md` | `python3 ${SKILL_DIR}/scripts/image_search.py ...` | +| `web` | `references/image-searcher.md` | `python3 ${SKILL_DIR}/scripts/image_search.py ...` (≥2 web rows → `--batch images/image_queries.json`) | | `user` / `placeholder` | (skip) | (skip) | A deck with only `ai` rows never loads `image-searcher.md`; a deck with only `web` rows never loads `image-generator.md`. A mixed deck loads both, processes each row through its own path, and writes both `image_prompts.json` and `image_sources.json`. > ⚠️ **In-pipeline ai path MUST use manifest mode** — even when only 1 ai row exists. Write `images/image_prompts.json` first, then run `image_gen.py --manifest`, then `image_gen.py --render-md` to produce the `image_prompts.md` sidecar. The positional form (`image_gen.py "prompt" ...`) is reserved for **out-of-pipeline one-off testing / single-image fixups** — it skips manifest + sidecar, leaving no audit trail. +> ⚠️ **web path — batch multiple rows**: when ≥2 rows are `Acquire Via: web`, write all queries into `images/image_queries.json` and run `image_search.py --batch` once (concurrent acquisition, status written back), instead of one CLI call per row. A single web row may use the positional single-query form. See [image-searcher.md](references/image-searcher.md) §5. + > ⚠️ **Honor the confirmed image source**: the `ai` generation path (Path A = `image_gen.py` API / Path B = host-native tool / Offline Manual) is **not** auto-only — a confirmed choice other than `auto` wins, whether it came from chat (canonical) or, when the page was used, `result.json.image_ai_path`. `host-native` forces Path B even when `IMAGE_BACKEND` is configured; `api` forces Path A; `manual` forces offline. The `--manifest` command above is Path A. Full selection rule: [image-generator.md](references/image-generator.md) §7 Path Selection. Workflow: @@ -409,6 +444,7 @@ Workflow: 1. Extract all rows with `Status: Pending` and `Acquire Via ∈ {ai, web}` from the design spec 2. Generate prompts (ai rows) and/or run search (web rows) per [image-base.md](references/image-base.md) §2 dispatch table 3. Verify every row reaches a terminal status: `Generated` (ai success), `Sourced` (web success), or `Needs-Manual` +4. Re-derive image facts now that web / AI files are in the folder — `python3 ${SKILL_DIR}/scripts/analyze_images.py /images` — so `analysis/image_analysis.csv` reflects every acquired image (real measured sizes) before the Executor lays them out. Image facts are regenerated on use, never a stale store (see Step 4's image-facts note). **✅ Checkpoint — Confirm acquisition attempted for every row**: ```markdown @@ -417,6 +453,7 @@ Workflow: - [x] image_prompts.md sidecar rendered (when any ai rows processed) - [x] image_sources.json created (when any web rows processed) - [x] Each row: status is `Generated` / `Sourced` / `Needs-Manual` (no `Pending` remaining) +- [x] analyze_images.py re-run so image_analysis.csv covers the acquired web / AI images ``` **Default — auto-proceed to Step 6.** Only when the user's Step 4 response explicitly opted into split mode (in chat or via Confirm UI `result.json` with `generation_mode: "split"`), output the Phase A hand-off below and stop this conversation: @@ -460,6 +497,8 @@ python3 ${SKILL_DIR}/scripts/svg_editor/server.py --live **Pre-generation Batch Read (Mandatory)**: before the first SVG, batch-read every distinct layout SVG referenced in `spec_lock.page_layouts` and every distinct chart SVG referenced in `spec_lock.page_charts` (plus any §VII backup charts). One read per file, up front — do not re-read these during page generation. See executor-base.md §1.0. +> Image facts: trust the `analysis/image_analysis.csv` regenerated at the end of Step 5. If `images/` changed since (the user swapped or added files), re-run `python3 ${SKILL_DIR}/scripts/analyze_images.py /images` before laying images out — facts are re-derived on use, never a stale store (Step 4 image-facts note). + **Per-page spec_lock re-read (Mandatory)**: before **each** SVG page, `read_file /spec_lock.md` and use only its colors / fonts / icons / images, plus the per-page `page_rhythm` / `page_layouts` / `page_charts` lookups (resolves to template SVGs already loaded in the batch read above). Resists context-compression drift on long decks. See executor-base.md §2.1. > ⚠️ **Main-agent only**: SVG generation MUST stay in the current main agent — page design depends on full upstream context. Do NOT delegate to sub-agents. 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 04371adc8..57278240b 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 @@ -118,7 +118,7 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P` | Tag | Layout discipline | |-----|-------------------| -| `anchor` | Structural page (cover / chapter / TOC / ending). Follow the matching template verbatim. | +| `anchor` | Structural page (cover / chapter / TOC / ending). With a template, follow the matching template verbatim. In free design (no template), realize the page's §IX intent — for the cover deliver its `Cover impact` and for a closing page its `Closing impact` (the committed hook / takeaway + composition), never a default centered title + subtitle or a generic "Thank you" sign-off. | | `dense` | Information-heavy. Card grids, multi-column layouts, KPI dashboards, tables, and charts are all permitted. This is the baseline behavior. | | `breathing` | Low-density impact page. Avoid **multi-card grid layouts** — do not organize content as multiple parallel rounded containers (3-card row, 4-card KPI grid, 2×2 matrix rendered as cards). Use naked text blocks, dividers, whitespace, or full-bleed imagery as the content structure. Single rounded visual elements (hero image corners, callouts, tags, one emphasis block) are fine — the rule is about grid structure, not about the `rx` attribute. Proportions follow information weight (not a preset ratio). Typical forms: hero quote, single large number with one-line interpretation, full-bleed image with floating caption, section transition. | @@ -331,7 +331,7 @@ Handle images by their status in the Design Spec's Image Resource List. Status e **`no-crop` images**: when a `spec_lock.md images` entry ends with ` | no-crop`, size the container to the image's native ratio (from `analyze_images.py` or file dims) and use `preserveAspectRatio="xMidYMid meet"`. Untagged entries are croppable — default to `slice`. -**Formula images**: rows with `Acquire Via: formula` or `Type: Latex Formula` MUST be treated as no-crop even if a legacy `spec_lock.md` forgot the flag. Use the dimensions from `design_spec.md §VIII`, `image_analysis.csv`, or `images/formula_manifest.json`; do not normalize all formulas to one height unless the spec explicitly states that layout choice. +**Formula images**: rows with `Acquire Via: formula` or `Type: Latex Formula` MUST be treated as no-crop even if a legacy `spec_lock.md` forgot the flag. Use the dimensions from `design_spec.md §VIII`, `analysis/image_analysis.csv`, or `images/formula_manifest.json`; do not normalize all formulas to one height unless the spec explicitly states that layout choice. ### 6.1 Inline Attribution for Sourced Images (web path) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md index d1c8c649a..798bca29d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md @@ -496,8 +496,9 @@ Precedence: Triggered automatically when `IMAGE_BACKEND` is not configured (or Path A fails) **and** the host provides a native image generation tool (Codex, Antigravity, Claude Code's image tool, and similar). No user prompting required — the agent detects the host capability and proceeds. The user may also explicitly name this path ("use Codex's image tool") to force it even when `IMAGE_BACKEND` is configured. - Agent invokes the host's native image tool directly; prompts come from `items[].prompt` +- **Batch for speed, mind the rate**: when the host can run independent tool calls in parallel (e.g. Claude Code issues independent calls concurrently), fire several generations together in modest groups — a few rows at a time (~3–4), not the whole manifest at once — so their latency overlaps without flooding the host's image quota. When the host only runs tools serially, generate one row at a time. This mirrors Path A's default concurrency of 3. - Outputs **must** land at `project/images/` with dimensions matching the Image Resource List -- After each placement, set the corresponding item's `status` to `Generated` in the manifest +- Mark each item's `status` `Generated` in the manifest the moment its file lands — as each completes, not in one pass at the end (so an interrupted batch leaves accurate state) - Executor downstream is path-agnostic — no spec change required between Path A and Path B ### Offline Manual Mode (C's third implementation mode) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-searcher.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-searcher.md index 5721dd2c2..0e8028844 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-searcher.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-searcher.md @@ -119,7 +119,37 @@ python3 scripts/image_search.py "" \ | `--strict-no-attribution` | no | off | Restrict to no-attribution licenses; refuse CC BY / CC BY-SA | | `--manifest` | no | (default) | Override manifest path | -**Pacing (mandatory)**: one search at a time. Wikimedia/Openverse expect identifying User-Agent and reasonable rate (~1 req/sec). Default pacing is fine. +### Batch mode (≥ 2 web rows) — preferred + +When more than one row is `Acquire Via: web`, do **not** call the CLI once per row. Write all rows into one `image_queries.json` and run a single concurrent batch — the web sister of `image_gen.py --manifest`: + +```bash +python3 scripts/image_search.py --batch /images/image_queries.json \ + -o /images +``` + +`image_queries.json` schema (one item per web row): + +```json +{ + "items": [ + { + "filename": "team.jpg", + "query": "executive boardroom meeting", + "slide": "03_team", + "purpose": "background", + "orientation": "landscape", + "status": "Pending" + } + ] +} +``` + +Required per item: `filename`, `query`, `status` (`Pending`). Optional per-item overrides: `slide`, `purpose`, `orientation`, `provider`, `strict_no_attribution`, `min_width`, `min_height`. + +The runner searches all `Pending` / `Failed` rows concurrently, appends each success to `image_sources.json` (the credit source of truth, idempotent on `filename`), and writes status back into `image_queries.json` — `Sourced` on success, `Needs-Manual` when the full provider/stage chain is exhausted. Status is saved after each completion, so an interrupted run preserves finished rows; re-running skips terminal rows. A single `web` row may still use single-query mode above. + +**Pacing**: free providers (Wikimedia/Openverse) are rate-sensitive, so batch concurrency defaults to a modest **3** (`--concurrency N`, or `IMAGE_SEARCH_CONCURRENCY` env). Use `--concurrency 1` to restore strict one-at-a-time pacing. Single-query mode is one request at a time by nature. --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md index 96c942325..6299ed94c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md @@ -555,7 +555,7 @@ Best for: slides needing strong visual brand identity. | Scenario | Recommended Technique | Avoid | |----------|-----------------------|-------| -| Card / panel shadow (only when floating over photo/colored panel) | Filter soft shadow (`flood-opacity` 0.06–0.12, single light source) | Hard black shadow, full-page abundance | +| Card / panel shadow (only when floating over photo/colored panel) | Filter soft shadow (`flood-opacity` 0.06–0.10, single light source) | Hard black shadow, full-page abundance | | Equal peer cards in a grid | All flat (no shadow) | Lifting every card uniformly | | Page-section background panel | Flat fill, no shadow | Treating panels as floating cards | | Accent / CTA button (one per page) | Colored shadow (same hue family, `flood-opacity` 0.12–0.20) | Generic gray shadow, applying to every button | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md index fa7885769..6d93791ea 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md @@ -28,7 +28,7 @@ As a top-tier AI presentation strategist, receive source documents, perform cont > > **One opt-in exception**: present the spec-refinement line alongside the split-mode note (SKILL.md Step 4). It is OFF by default — the above discipline holds unchanged. Only when the user *explicitly* asks to refine the spec do you hand off to the [refine-spec](../workflows/refine-spec.md) workflow, which produces the full spec first and stops for user review/revision of any part before generation. Never enter it unprompted. -> **Default presentation surface — Confirm UI.** Deliver the bundled package through the interactive page: write your recommendations to `/confirm_ui/recommendations.json`, then launch per [SKILL.md Step 4](../SKILL.md). You still author everything — enumerable fields name a recommended `id`; generative fields (color `palette`, CJK + Latin typography, generated-image style) each carry **≥3 distinct candidates** — creative recommendations always offer real choice, never a single silent option, same hard rule and thinking as h.5. Honest-shortfall exception (mirrors h.5): if the constraints genuinely cannot yield 3 non-conflicting options, present the smaller set and say why — never pad with duplicates or known-conflicting fillers. **Always also print the recommendations + URL in chat** as the always-valid fallback. On confirm, read `/confirm_ui/result.json` (`generation_mode: "split"` / `refine_spec: true` are explicit user choices). Skip the page if the user wants chat-only. Full launch flow, field rules, and JSON schema live in [SKILL.md Step 4](../SKILL.md) + [`scripts/docs/confirm_ui.md`](../scripts/docs/confirm_ui.md) — don't restate them here. The page is a confirmation surface only. +> **Default presentation surface — Confirm UI.** Deliver the bundled package through the interactive page: write your recommendations to `/confirm_ui/recommendations.json`, then launch per [SKILL.md Step 4](../SKILL.md). You still author everything — enumerable fields name a recommended `id`; generative fields (color `palette`, CJK + Latin typography, generated-image style) each carry **≥3 distinct candidates**, and the deck's **visual style** (§d Layer 2) carries a **≥3-style personality spectrum** (`visual_style_spectrum`, safe / shifted / bold) — creative recommendations always offer real choice, never a single silent option, same hard rule and thinking as h.5. Honest-shortfall exception (mirrors h.5): if the constraints genuinely cannot yield 3 non-conflicting options, present the smaller set and say why — never pad with duplicates or known-conflicting fillers. **Always also print the recommendations + URL in chat** as the always-valid fallback. On confirm, read `/confirm_ui/result.json` (`generation_mode: "split"` / `refine_spec: true` are explicit user choices). Skip the page if the user wants chat-only. Full launch flow, field rules, and JSON schema live in [SKILL.md Step 4](../SKILL.md) + [`scripts/docs/confirm_ui.md`](../scripts/docs/confirm_ui.md) — don't restate them here. The page is a confirmation surface only. ### a. Canvas Format Confirmation @@ -42,6 +42,14 @@ Provide specific page count recommendation based on source document content volu Confirm target audience, usage occasion, and core message; provide initial assessment based on document nature. +**Material divergence** — a **free-text** intent the user states beside audience (same content-strategy cluster): in their own words, how closely the deck should follow the source vs how freely it may reshape it. This is the user's own call — a free prose field (`content_divergence`), **not** a fixed set of options and **not** something you recommend from analyzing the source. Surface the question (in the confirm UI it is a text box under audience; in chat, ask it plainly); leave it for the user to fill. Blank = a balanced default. + +Read the user's prose as a point on a spectrum and apply judgment — from *stay close* (track the source's structure and wording, tune only for clarity, no substantive add / drop) through the default *balanced* (re-architect and distill into a narrative under the locked `mode`, keeping all substance) to *free* (regroup, reframe, expand terse points, draw out connections latent in the source, invent section structure and transitions). + +**Hard rule — facts stay sourced however free the user asks.** Divergence is freedom to *develop* what is in the source (reorganize / reframe / expand / connect), never licence to invent. Even the freest request must not introduce facts, figures, or claims from outside the source material — that is the `topic-research` job, not divergence. `mode` and divergence are orthogonal (e.g. a pyramid that hews to the source's own points vs. a pyramid built from freely synthesized themes). + +**Consumption — outline-authoring only.** Apply the user's stated intent when authoring the `§IX` outline. Record the prose (or "balanced default") in `design_spec.md §I` (Content Strategy). Do **NOT** write it to `spec_lock.md` — it is baked into `§IX` at authoring time and the Executor never reads it. It carries no page-count coupling — the §b page count stays the user's separate call. The beautify / template-fill workflows keep content verbatim, so they do not surface this field. + ### d. Style Objective Confirmation Two independent layers, each locks one catalog item. Output: `d. Mode: + Visual style: `. @@ -69,8 +77,8 @@ Write the locked value to `spec_lock.md` `- mode:` and record the rationale in ` The deck's **visual aesthetic** — shape language, decoration density, whitespace rhythm, typographic character, texture. Anchors the downstream confirmations e (Color), f (Icon), g (Typography), h (Image). Lock one preset from the catalog, or `custom`. **Source**: -- User named a style → map to the closest preset; if none fits, `custom` with a `visual_style_behavior` paragraph. -- No user description → recommend by the index's auto-selection table (content vibe / industry → style). Present as a recommendation; the user may override. +- User named a style (chat / template / beautify) → it is truth: map to the closest preset (or `custom` with a `visual_style_behavior` paragraph) and lock directly. **Skip the spectrum below** — do not re-offer choice they already made. +- No user description → **present a personality spectrum, not one safe pick** (this is the lever against "every deck looks the same" — the visual style is what most determines a deck's character, so it gets real choice, same hard rule and thinking as h.5). Author **≥3 distinct styles** from the index's auto-selection table spanning *safe* (the industry-norm recommendation) → *shifted* (an alternate one tick more expressive) → *bold* (a characterful style that challenges the default — `brutalist` / `zine` / `memphis` / `ink-wash` / `vintage-poster` etc., whenever the content can carry it). Give each a one-line **temperament tag + real-world analogy** (like h.5's "like an Economist feature"). Write the three to `recommendations.json` `visual_style_spectrum` (each `{id, tag_zh/en, note_zh/en}`) **and present the same three in chat** as the always-valid fallback; set `recommend.visual_style` to the *safe* pick as the pre-selected default. The user may pick any of the three, a style outside them, or Custom. Honest-shortfall exception (mirrors h.5): if the content genuinely supports fewer than 3 non-gimmicky directions, present the smaller set and say why — never pad with a style that fights the content. **Forbidden — a non-catalog name as `visual_style`**: the value MUST be an `id` from the visual-styles catalog (or genuine `custom` prose). A name that is **not** in that catalog is not a visual style — most often it is an image-rendering name from the `_index` "Paired rendering" column (`flat`, `vector-illustration`, `digital-dashboard`, `3d-isometric`, `corporate-photo`, …), which names the §h *illustration* family, not the deck's layout aesthetic. Do not borrow it. (Names that are intentionally **both** a style and its paired rendering — `glassmorphism`, `blueprint`, `editorial`, `dark-tech` — are valid styles because they *are* in the catalog.) Generic baseline words — `flat` / flat-design / 扁平 / modern / clean / simple / minimal — are **not** custom-worthy either: the whole system is flat by default (shadows discouraged), so map them to the closest preset (flat + grid → `swiss-minimal`; flat + rounded → `soft-rounded`; flat + dense → `brutalist`). Reserve `custom` for an aesthetic no preset covers. @@ -306,6 +314,14 @@ The script renders PNGs into `images/`, trying `codecogs`, `quicklatex`, `mathpa | **D** | Web-sourced | Real-world reference imagery, editorial support, stock-style needs (no API key required for default providers) | | **E** | Placeholders | Images to be added later | +> 🚧 **GATE — know your resources before recommending.** `images/` is a live working folder (source-extracted pictures, user drops, later replacements), so its facts are **re-derived on use, never trusted from a stale store**. Before recommending image usage, if `images/` is non-empty, regenerate the inventory from whatever is currently in it, then read it back: +> +> ```bash +> python3 scripts/analyze_images.py /images +> ``` +> +> Read `/analysis/image_analysis.csv` (size / ratio / category of every in-hand picture). The A–E choice is still your judgment, but it MUST be made with the current inventory in full view — never in ignorance of what is already on hand. `image_analysis.csv` is a regenerated view of the live folder, not a durable fact: re-run this whenever `images/` changes. + > **Confirmed value wins.** The `image_usage` in `result.json` (or the chat reply) **overrides the recommendation here** — map it to §VIII `Acquire Via` (`ai`→`ai`, `web`→`web`, `provided`→**`user`**, `placeholder`→`placeholder`, `none`→option A, no image rows). When it is not `ai` (and the plan has no AI part), skip h.5 entirely and write no `ai` rows. See SKILL.md Step 4 for the full mapping. **When recommending C** — surface its three implementation modes so the user knows "no API key" is a supported state: @@ -745,6 +761,9 @@ Content-outline and speaker-notes strategy follow the deck's locked **mode** — 4. **Generate execution lock**: read `templates/spec_lock_reference.md` and produce `projects/.../spec_lock.md` — a distilled, machine-readable short form of the color / typography / icon / image / **page_rhythm** / **page_layouts** / **page_charts** decisions above. This file is what the Executor re-reads before every page (see [executor-base.md](executor-base.md) §2.1). The values in `spec_lock.md` MUST exactly match the decisions recorded in `design_spec.md`; if they ever diverge, `spec_lock.md` wins and `design_spec.md` should be treated as historical narrative. - **page_rhythm is mandatory**: Based on the page list in §IX Content Outline, assign each page one of `anchor` / `dense` / `breathing` (see `spec_lock_reference.md` for the full vocabulary). This is what breaks the uniform "every page is a card grid" feel — without it the Executor defaults all pages to `dense`. - **Rhythm follows narrative, not quota**: `breathing` pages mark natural pauses — chapter transitions, standalone emphasis (hero quote / big number), SCQA bridges. Dense decks may legitimately be all `dense`. **Do NOT invent filler pages** ("Thank you", empty dividers) to pad rhythm — every `breathing` page must say something independent. + - **Cover impact is mandatory**: Page `P01` is the deck's first visual contract, not a generic title slide. In `design_spec.md §IX`, add a `Cover impact` line for `P01` that names one concrete hook and one concrete composition strategy. Use the source's strongest available signal: a provocative core claim, object / scene metaphor, hero number, founder / product / audience moment, or a distilled conflict. Pair it with one concrete composition strategy — such as `full-bleed image + floating title`, `typographic poster`, `hero object`, `data hook`, `editorial scene`, `high-contrast abstract geometry`, or a fresh composition the deck's subject suggests (these are starting points, not the allowed set). If no external or AI image is available, still specify a native-SVG visual hook; do not fall back to "title + subtitle + decorative background". (Beautify / template-fill keep the source cover verbatim — this rule does not apply on those preservation paths.) + - **Cover rhythm lock**: `P01` remains `anchor` in `spec_lock.md page_rhythm`, but its §IX `Cover impact` must prevent content-page patterns. Do not plan multi-card grids, agenda-like bullets, or equal-weight columns on the cover unless a template explicitly requires that structure, or a preservation path (beautify / template-fill) is transcribing the source cover verbatim. + - **Closing impact (only when the deck closes)**: the deck's last page is its final visual contract — the strongest impression after the cover. When the deck genuinely lands on a conclusion / call-to-action / final-takeaway page, give it a `Closing impact` line in §IX: name the one thing the audience should leave with (a distilled takeaway, a forward call, a memorable restatement of the core claim) + one composition that delivers it — never a generic "Thank you" / contact-only slide or a centered-title reprise of the cover. **Do NOT invent a closing page to satisfy this** — the filler-page ban above still holds; apply it only to the page where the deck actually resolves. Same exemptions as the cover: skip on template / beautify / template-fill preservation paths. - **page_layouts (write only when a template is in use)**: For each page that inherits a template SVG, add `P: ` (e.g., `P04: 03a_content_image_text`). Pages designed freely get **no entry** — Executor reads the absence as "free design, no inheritance". If zero pages use a template, omit the section entirely. - **page_charts (write only for chart pages that match a catalog template)**: For each page in `design_spec.md §VII` whose `reference template path` points to `templates/charts/.svg`, add `P: `. Pages with `no-template-match` in §VII MUST NOT appear here (Executor would look for a non-existent reference). If the deck has no data-visualization pages, omit the section. - **Hard rule**: Use both `page_layouts` and `page_charts` for the same page only when the layout template is a compatible shell for the chart. Do not pair chart pages with conflicting page layouts (e.g., `waterfall_chart` + timeline layout, KPI cards + circle-diagram layout). If no compatible layout exists, omit the page from `page_layouts`. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/chalkboard.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/chalkboard.md index 03abd8362..21a767c9f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/chalkboard.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/chalkboard.md @@ -6,7 +6,7 @@ Classroom chalkboard — a dark slate field, soft chalk-stroke line work, powder ## 1. Shape & decoration -- Shape language: chalk-stroke line work with slightly diffused, dry-medium edges; sketched boxes, brackets, arrows in chalk. Confident but never mechanical. +- Shape language: chalk-stroke line work with slightly diffused, dry-medium edges; sketched boxes, brackets, arrows in chalk. Confident but never mechanical — the sketched boxes and arrows are `` with non-aligned points; a primitive `` / `` snaps the chalk back to mechanical. - Decoration: underlines and emphasis marks; a few sprinkled chalk stars / dots. Blackboard pedagogy — organized sections, a clear central focus. - Whitespace: the dark board reads as room; let chalk marks breathe rather than crowd. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/dark-tech.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/dark-tech.md index cb1913cac..7327b17e1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/dark-tech.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/dark-tech.md @@ -22,6 +22,8 @@ Dark canvas, luminous accents, geometric precision. For tech, AI, dev tools, dat - Dark background; one or two luminous accents carry focus (glowing figures, active nodes); everything else low-key. - The accent does the work of attention — few points, high contrast. +> HEX values come from confirmation `e`; this style only governs the dark-field, luminous-accent discipline — it names no colors. + ## 4. Texture / elevation - Depth via glow and layering on dark, not drop shadows. Outer glow / light strokes mark elevation; gradients stay same-hue and subtle. (Dark-theme legibility — prefer light stroke / outer glow over black shadow: [`shared-standards.md §6`](../shared-standards.md).) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/editorial.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/editorial.md index 1ed537fb9..38e0d9eeb 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/editorial.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/editorial.md @@ -23,6 +23,8 @@ Magazine-grade hierarchy. Columns, hairline rules, a serif / sans interplay, str - Mostly monochrome text on a light field; one accent for emphasis (a rule, a highlighted figure, a kicker). - Restraint — color marks structure and emphasis, not decoration. +> HEX values come from confirmation `e`; this style only governs the monochrome-with-structural-accent discipline — it names no colors. + ## 4. Texture / elevation - Flat to barely-raised. Rules and whitespace separate content, not shadows. Shadow only on a genuine floating element. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/glassmorphism.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/glassmorphism.md index 8f081ca67..09fc08ba6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/glassmorphism.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/glassmorphism.md @@ -7,7 +7,7 @@ Frosted-glass SaaS — translucent layered panels, flowing gradient light, float ## 1. Shape & decoration - Shape language: rounded translucent glass panels (low fill-opacity over the dark field) with bright hairline edges; layered, floating cards that imply blur and frost; rounded corners (`rx` 12-20). -- Decoration: soft radial light blooms in the background; thin luminous edge highlights along panels; restrained — the glass material is the decoration, not added ornament. +- Decoration: soft radial light blooms in the background; thin luminous edge highlights along panels; restrained — the glass material is the decoration, not added ornament. Realize the radial bloom / glow halo as a `` / `` with a `` fill, never a `rect rx=w/2` standing in for it. - Whitespace: dark negative space reads as depth; let panels float on it with room to breathe. ## 2. Typography character diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-notes.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-notes.md index 3f0e36af7..caba3adb6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-notes.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-notes.md @@ -6,7 +6,7 @@ Whiteboard-ink minimalism — a pale field, confident black hand-ink line work, ## 1. Shape & decoration -- Shape language: hand-drawn line work with slight, intentional wobble — boxes, arrows, dividers and brackets sketched as if on a thoughtful whiteboard; never mechanically straight. Line defines structure; no filled cards. +- Shape language: hand-drawn line work with slight, intentional wobble — boxes, arrows, dividers and brackets sketched as if on a thoughtful whiteboard; never mechanically straight — realize it as `` / `` with off-grid points, not `` / `` primitives. Line defines structure; no filled cards. - Decoration: minimal — a few doodle marks (stars, dashes, dots, underlines) for emphasis. Restraint is the look; clutter breaks the "considered" feel. - Whitespace: generous and empty; the pale field carries most of the canvas, elements float with room around them. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-wash.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-wash.md index 419763f56..825dbb8bd 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-wash.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/ink-wash.md @@ -6,7 +6,7 @@ New-Chinese ink-wash — a rice-paper field, vast literati whitespace, restraine ## 1. Shape & decoration -- Shape language: minimal brush-stroke marks and hairline dividers; the occasional ink-dark block; a single seal-stamp (印章) square as a focal accent. No cards, no boxes — emptiness is the structure. +- Shape language: minimal brush-stroke marks and hairline dividers; the occasional ink-dark block; a single seal-stamp (印章) square as a focal accent. No cards, no boxes — emptiness is the structure. The brush-stroke and ink-bleed marks are irregular `` shapes with uneven control points — never an `` / `` standing in for a wash, which reads as fake ink. (The seal-stamp square is the one deliberate hard edge.) - Decoration: almost none; what little appears reads as brush and seal. Asymmetric, scroll-like composition with deliberate off-balance. - Whitespace: vast and intentional — the rice-paper field carries most of the page; a few elements float in great calm. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/paper-cut.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/paper-cut.md index 5b80d1ca3..147bf6001 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/paper-cut.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/paper-cut.md @@ -6,7 +6,7 @@ Layered paper-craft — scissor-cut shapes stacked in tactile layers, soft shado ## 1. Shape & decoration -- Shape language: forms defined by crisp, slightly-irregular cut edges (no outlines); simplified, stylized shapes that read as cut paper rather than illustration. +- Shape language: forms defined by crisp, slightly-irregular cut edges (no outlines); simplified, stylized shapes that read as cut paper rather than illustration. Those cut edges are irregular `` / `` outlines, not a clean `` / ``, which reads as a digital box rather than torn paper. - Decoration: layering itself is the device — each element is a "sheet" stacked over the one beneath; small cut-out accents on the top layer. - Whitespace: cozy, composed — the backing sheet shows through as breathing room. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/sketch-notes.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/sketch-notes.md index ccd9769a3..e293bab51 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/sketch-notes.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/sketch-notes.md @@ -6,7 +6,7 @@ Warm hand-drawn sketchnote — soft paper field, black ink doodle line work, gen ## 1. Shape & decoration -- Shape language: rounded shapes drawn with a slight wobble; pastel block fills that slightly overshoot their outlines (hand-painted feel); simple cartoon icons. +- Shape language: rounded shapes drawn with a slight wobble; pastel block fills that slightly overshoot their outlines (hand-painted feel); simple cartoon icons. Draw that wobble as a `` with non-aligned points — a `` is not wobble. - Decoration: small doodles — stars, sparkles, dots, underlines — sprinkled sparingly for warmth; wavy hand-drawn arrows connecting ideas with short inline labels. - Whitespace: airy and well-organized; generous gaps between elements keep it friendly, never dense. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/soft-rounded.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/soft-rounded.md index 4eaf9d3e1..c3e578e5e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/soft-rounded.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/visual-styles/soft-rounded.md @@ -22,6 +22,8 @@ Approachable and modern. Rounded cards, gentle elevation, friendly rhythm. For p - Theme color used confidently on covers / chapter backgrounds; same-hue tints for card backings; accent for key figures. - Warmer, more generous color use than swiss / editorial — still disciplined (60-30-10), never rainbow. +> HEX values come from confirmation `e`; this style only governs the confident-but-disciplined (60-30-10) color use — it names no colors. + ## 4. Texture / elevation - Gentle elevation: soft shadows on floating cards (resting tier), subtle tints, optional same-hue gradients. Two-tier elevation max; keep peer-grid cards flat. (Full shadow rules: [`shared-standards.md §6`](../shared-standards.md).) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md index a51ccee28..604910e2a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md @@ -40,7 +40,7 @@ python3 scripts/update_repo.py | Area | Primary scripts | Documentation | |------|-----------------|---------------| -| Conversion | `source_to_md/pdf_to_md.py`, `source_to_md/doc_to_md.py`, `source_to_md/excel_to_md.py`, `source_to_md/ppt_to_md.py`, `source_to_md/web_to_md.py` | [docs/conversion.md](./docs/conversion.md) | +| Conversion | `source_to_md/pdf_to_md.py`, `source_to_md/doc_to_md.py`, `source_to_md/excel_to_md.py`, `source_to_md/ppt_to_md.py`, `source_to_md/web_to_md.py`, `pptx_intake.py` | [docs/conversion.md](./docs/conversion.md) | | Project management | `project_manager.py`, `batch_validate.py`, `generate_examples_index.py`, `error_helper.py`, `pptx_template_import.py`, `template_fill_pptx.py` | [docs/project.md](./docs/project.md) | | SVG pipeline | `finalize_svg.py`, `svg_to_pptx.py`, `total_md_split.py`, `svg_quality_checker.py`, `extract_svg_assets.py`, `animation_config.py`, `notes_to_audio.py` | [docs/svg-pipeline.md](./docs/svg-pipeline.md) | | Spec maintenance | `update_spec.py` | [docs/update_spec.md](./docs/update_spec.md) | @@ -80,9 +80,9 @@ Template fill (direct PPTX, no SVG conversion): ```bash mkdir -p /sources /analysis /exports /validation -python3 scripts/template_fill_pptx.py analyze /sources/ -o /analysis/slide_library.json -python3 scripts/template_fill_pptx.py scaffold /analysis/slide_library.json -o /analysis/fill_plan.json --slides "1,3,4" -python3 scripts/template_fill_pptx.py check-plan /analysis/slide_library.json /analysis/fill_plan.json -o /analysis/check_report.json +python3 scripts/template_fill_pptx.py analyze /sources/ -o /analysis/.slide_library.json +python3 scripts/template_fill_pptx.py scaffold /analysis/.slide_library.json -o /analysis/fill_plan.json --slides "1,3,4" +python3 scripts/template_fill_pptx.py check-plan /analysis/.slide_library.json /analysis/fill_plan.json -o /analysis/check_report.json python3 scripts/template_fill_pptx.py apply /sources/ /analysis/fill_plan.json -o /exports/filled.pptx ``` diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/analyze_images.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/analyze_images.py index 548a9c6f1..4b083d359 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/analyze_images.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/analyze_images.py @@ -18,7 +18,8 @@ Usage: Output: - Analysis report displayed in console - - Generates image_analysis.csv in the parent directory of the images folder + - Generates image_analysis.csv under the project's analysis/ directory + (sibling of the images folder), alongside the PPTX intake bundle """ import argparse @@ -572,9 +573,12 @@ def main() -> None: print_results(results) generate_markdown(results, canvas_key) - # Save to CSV file (saved in the parent directory of the images folder) + # Save to CSV file (saved under the project's analysis/ directory, + # alongside the PPTX intake bundle) parent_dir = os.path.dirname(images_dir) - csv_path = os.path.join(parent_dir, "image_analysis.csv") + analysis_dir = os.path.join(parent_dir, "analysis") + os.makedirs(analysis_dir, exist_ok=True) + csv_path = os.path.join(analysis_dir, "image_analysis.csv") save_csv(results, csv_path) else: print("No image files found in the directory.") diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_identity.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_identity.py index 313327692..af9f3c256 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_identity.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_identity.py @@ -14,7 +14,7 @@ Usage: Examples: python3 scripts/beautify_identity.py projects/x/sources/deck.pptx - python3 scripts/beautify_identity.py deck.pptx -o projects/x/analysis/identity.json + python3 scripts/beautify_identity.py deck.pptx -o projects/x/analysis/deck.identity.json Dependencies: None beyond the standard library (reuses scripts/pptx_to_svg/). diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_inventory.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_inventory.py index f6eeb4de2..d9524cab9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_inventory.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/beautify_inventory.py @@ -13,7 +13,7 @@ Usage: python3 scripts/beautify_inventory.py [--images ] [-o inventory.json] Examples: - python3 scripts/beautify_inventory.py projects/x/analysis/slide_library.json \ + python3 scripts/beautify_inventory.py projects/x/analysis/.slide_library.json \ --images projects/x/images/image_manifest.json -o projects/x/analysis/beautify_inventory.json Dependencies: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js index e94b521c0..f0674036b 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js @@ -32,6 +32,8 @@ sec_refine: "Refine spec first", sub_mode: "Narrative mode", sub_visual: "Visual style", + sub_divergence: "Material divergence (how freely to reshape vs. stay close to the source)", + placeholder_divergence: "In your words — e.g. \"stick closely to the document\" / \"freely restructure and expand within the source\". Leave blank for a balanced default.", custom: "Custom", custom_placeholder: "Type your own…", recommended: "Recommended", @@ -100,6 +102,8 @@ sec_refine: "先精修设计规范", sub_mode: "叙事模式", sub_visual: "视觉风格", + sub_divergence: "材料发散度(多大程度重塑,还是贴近源材料)", + placeholder_divergence: "用你自己的话写,例如「严格贴着文档来」/「在源材料范围内自由重组并展开」。留空则按平衡处理。", custom: "自定义", custom_placeholder: "输入自定义内容…", recommended: "推荐", @@ -287,6 +291,14 @@ var grouped = list.length && list[0] && list[0].items; var flat = grouped ? list.reduce(function (a, g) { return a.concat(g.items || []); }, []) : list; var ids = flat.map(function (o) { return o.id; }); + // Optional personality spectrum: instead of a single ★ recommendation, + // the AI marks a few catalog ids (safe / shifted / bold) each with a + // temperament tag + a real-world analogy note. Replaces the single badge. + var spectrum = (opts2.spectrum && opts2.spectrum.length) ? opts2.spectrum : null; + var specById = {}; + if (spectrum) spectrum.forEach(function (s) { + if (s && s.id) specById[s.id] = { tag: localized(s, "tag"), note: localized(s, "note") }; + }); var allowCustom = opts2.allowCustom === true; // only for fields not fully enumerable var customSentinel = opts2.customSentinel || ""; var customInvalidValues = opts2.customInvalidValues || []; @@ -312,9 +324,15 @@ if (o.dim) label += " · " + o.dim; var desc = optionDesc(o); if (desc) label += (LANG === "zh" ? ":" : " — ") + desc; + var spec = specById[o.id]; + if (spec && spec.note) label += " · " + spec.note; var chip = el("div", "chip"); chip.appendChild(el("span", "chip-text", label)); - if (o.id === recommendedId) { + if (spec) { + // spectrum pick: badge shows its temperament tag, not the generic ★ + chip.classList.add("recommended"); + chip.appendChild(el("span", "rec-badge", "★ " + (spec.tag || t("recommended")))); + } else if (!spectrum && o.id === recommendedId) { chip.classList.add("recommended"); chip.appendChild(el("span", "rec-badge", "★ " + t("recommended"))); } @@ -494,6 +512,15 @@ var sec = section(3, "sec_audience"); textField(sec, function () { return STATE.audience; }, function (v) { STATE.audience = v; }, "placeholder_audience", false); + // Material divergence — a distinct, free-text sub-question inside §c, shown + // right under the audience box: the user states in their own words how + // closely to follow the source vs. how freely to reshape it. Free prose, not + // fixed options; no page-count coupling, no source-signal recommendation. + var subDiv = el("div", "subfield"); + subDiv.appendChild(el("div", "subfield-label", t("sub_divergence"))); + textField(subDiv, function () { return STATE.content_divergence; }, + function (v) { STATE.content_divergence = v; }, "placeholder_divergence", false); + sec.appendChild(subDiv); host.appendChild(sec); } @@ -505,7 +532,8 @@ var sub2 = el("div", "subfield"); sub2.appendChild(el("div", "subfield-label", t("sub_visual"))); enumField(sub2, CAT.visual_styles, recOrFirst("visual_style", CAT.visual_styles), - function () { return STATE.visual_style; }, function (v) { STATE.visual_style = v; }, { allowCustom: true }); + function () { return STATE.visual_style; }, function (v) { STATE.visual_style = v; }, + { allowCustom: true, spectrum: REC && REC.visual_style_spectrum }); sec.appendChild(sub2); host.appendChild(sec); } @@ -1031,6 +1059,7 @@ STATE.canvas = pick("canvas", CAT.canvas); STATE.page_count = (REC.page_count && REC.page_count.value != null) ? String(REC.page_count.value) : ""; STATE.audience = (REC.audience && REC.audience.value) || ""; + STATE.content_divergence = (REC.content_divergence && REC.content_divergence.value) || ""; // free text; blank = balanced default STATE.mode = pick("mode", CAT.modes); STATE.visual_style = pick("visual_style", CAT.visual_styles); diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md index 57de82a1b..cb38c1cb8 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md @@ -29,9 +29,9 @@ pip install flask ## Two kinds of field -- **Enumerable + custom** — canvas / mode / visual_style / icons / image usage. The page lists common options from `static/catalogs.json`, badges the AI's recommendation, and still offers a Custom box for edge cases (custom canvas size, bespoke narrative mode, mixed image plan, self-provided icon system, etc.). +- **Enumerable + custom** — canvas / mode / visual_style / icons / image usage. The page lists common options from `static/catalogs.json`, badges the AI's recommendation, and still offers a Custom box for edge cases (custom canvas size, bespoke narrative mode, mixed image plan, self-provided icon system, etc.). `visual_style` additionally honors an optional `visual_style_spectrum` that badges a 3-pick personality spectrum (safe / shifted / bold, each with a temperament tag + analogy) in place of the single recommendation — see the schema below. - **Closed enumerable** — formula policy / generation mode / refine spec, plus AI source only when image usage may include `ai`. These have no Custom box; out-of-catalog values snap back to the recommended option. Use pipeline vocabulary: icon ids are actual library ids such as `tabler-outline`, or `emoji` for system emoji; image usage labels mirror Strategist terminology: `ai` = AI-generated, `web` = Web-sourced, `provided` = User-provided, `placeholder` = Placeholder, `none` = No images. Use custom prose only when several sources are mixed. -- **Generative (open)** — color, typography, generated-image style. No finite catalog; the AI authors **≥3 candidates** the page renders as cards (never a single option — creative fields must offer real choice; fewer than 3 only on the honest-shortfall exception). `page_count` and `audience` are free inputs. +- **Generative (open)** — color, typography, generated-image style. No finite catalog; the AI authors **≥3 candidates** the page renders as cards (never a single option — creative fields must offer real choice; fewer than 3 only on the honest-shortfall exception). `page_count`, `audience`, and `content_divergence` are free inputs (`content_divergence` is a free-text intent shown under audience in §c, not a fixed-option field). **Custom box** appears only on fields whose universe is genuinely open — `canvas`, `mode`, `visual_style`, `icons`, and `image_usage`. Fully closed sets — `image_ai_path`, `formula_policy`, `generation_mode`, `refine_spec` — have **no** Custom box; an out-of-catalog value there is snapped back to the recommended option. @@ -59,8 +59,9 @@ Both files live under `/confirm_ui/`. "formula_policy": "mixed", "generation_mode": "continuous" }, - "page_count": { "value": "12-15" }, - "audience": { "value": "..." }, + "page_count": { "value": "12-15" }, + "audience": { "value": "..." }, + "content_divergence": { "value": "" }, "color": { "selected": 0, "candidates": [ @@ -97,6 +98,11 @@ Both files live under `/confirm_ui/`. } ] }, + "visual_style_spectrum": [ + { "id": "soft-rounded", "tag_zh": "稳妥专业", "tag_en": "Safe & professional", "note_zh": "像 Notion 官网", "note_en": "like the Notion site" }, + { "id": "editorial", "tag_zh": "编辑质感", "tag_en": "Editorial depth", "note_zh": "像经济学人专题", "note_en": "like an Economist feature" }, + { "id": "brutalist", "tag_zh": "硬核宣言", "tag_en": "Bold manifesto", "note_zh": "像研究机构年度宣言", "note_en": "like a research-house manifesto" } + ], "refine_spec": { "value": false } } ``` @@ -111,7 +117,9 @@ Both files live under `/confirm_ui/`. - **Typography candidates split CJK and Latin** for both `heading` and `body`; `css` is the fallback preview `font-family` stack. The page previews CJK sample text with `cjk + css` and Latin sample text with `latin + css`, so the two script choices are visible independently. Each candidate should also include `body_size` (the body baseline in px; common values are 18 and 24, but any reasonable integer is valid). The page exposes `body_size` as an editable numeric field whose hint shows a canvas-appropriate range (≈2.5–3.3% of the confirmed canvas height, parsed from the canvas `dim`), updated when the canvas changes — a hint only, never an override, so a 16:9 default is not silently kept on a tall canvas (Xiaohongshu / Story / A4). It also offers a custom typography text box so the user is not limited to the proposed candidates. - **Combined style preview** — a compact live "overall impression" strip sits just above the color section and is **sticky**: it pins under the topbar so it stays visible while the user scrolls through the color / icon / typography sections, keeping the picking controls and their combined effect on screen together. It applies the currently selected color palette **and** typography (heading sample in `primary` over `background`, body sample in `body_text`, an `accent` bar, a `secondary_bg` chip) and repaints on every color / HEX-override / font / `body_size` change. It does not replace the per-candidate swatches or font samples (those stay for picking); it is deliberately an abstract style chip, **not** a slide-layout preview — page layout preview remains the live-preview server's job (Step 6). No schema field; it derives entirely from the existing color + typography selections. - **Generated image style candidates** live in `image_strategy.candidates` and are shown only when `image_usage` is `ai` or a custom image plan may include generated images. Each candidate records `rendering`, `palette`, and short `visual` / `color` / `mood` lines from Strategist h.5. The chosen value is written to `result.json.image_strategy`; it is omitted when generated images are not part of the plan. +- **`visual_style_spectrum`** (optional) lets the AI surface the deck's aesthetic as a **personality spectrum** instead of one badged style. Each entry is `{ "id", "tag_zh"/"tag_en", "note_zh"/"note_en" }` where `id` is a real `visual_styles` catalog id; the page badges those chips with their temperament `tag` (replacing the single ★) and appends the `note` (a real-world analogy) inline. The full grouped style list and Custom box stay visible below, and `recommend.visual_style` is still the pre-selected default (it should equal the spectrum's safe pick). Author **≥3** spanning safe / shifted / bold (mirrors h.5; honest-shortfall exception applies — fewer only when the constraints genuinely cannot yield 3). The user's pick still writes back to `result.json.visual_style` as a plain id; the spectrum is presentation-only. Omit the field to fall back to the single-recommendation badge. - `recommend.generation_mode` and `refine_spec` mirror the two mandatory notes in SKILL.md Step 4. Confirmed `generation_mode: "split"` / `refine_spec: true` are explicit user choices, equivalent to opting in through chat. +- `content_divergence` is a **free-text** field shown right under the audience box in §c — the user states in their own words how closely to follow the source vs how freely to reshape it (e.g. "stick closely to the document" / "freely restructure and expand within the source"). It is **not** a fixed-option field; blank means a balanced default. Whatever the level, facts stay sourced — reshaping develops what is in the source, never imports facts from outside it. The Strategist consumes the prose when authoring the §IX outline and records it in `design_spec.md §I`; it is **not** written to `spec_lock.md` (the Executor never reads it). It carries no page-count coupling and no source-signal recommendation — it is purely the user's stated intent. Beautify / template-fill keep content verbatim and do not surface this field. - `lang` is a soft default; an explicit user language choice in the page (persisted to `localStorage`) wins. ### Output — `result.json` (written on submit, read by the AI) @@ -121,6 +129,7 @@ Both files live under `/confirm_ui/`. "canvas": "ppt169", "page_count": "12-15", "audience": "...", + "content_divergence": "freely restructure and expand within the source", "mode": "pyramid", "visual_style": "swiss-minimal", "color": { "name": "...", "palette": { "background": "#...", "secondary_bg": "#...", "primary": "#...", "accent": "#...", "secondary_accent": "#...", "body_text": "#..." } }, 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 acadc31fe..cbbc559b4 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 @@ -122,6 +122,7 @@ python3 scripts/source_to_md/ppt_to_md.py template.ppsx -o notes/template.md Behavior: - extracts slide text in reading order - converts PowerPoint tables to Markdown tables +- transcribes native chart data (type + categories × series values) into a Markdown table, so chart numbers are not lost in conversion - exports embedded pictures to a sibling `_files/` directory - appends speaker notes when present @@ -133,6 +134,28 @@ pip install python-pptx Legacy `.ppt` is not parsed directly. Resave it as `.pptx` or export it to PDF first. +## `pptx_intake.py` + +Standard enrichment layer for PPTX sources. It complements `ppt_to_md.py` rather +than replacing it: Markdown remains the normalized content source, while intake +artifacts provide source facts for Strategist and standalone PPTX workflows. + +```bash +python3 scripts/pptx_intake.py deck.pptx -o projects/demo/analysis +``` + +Outputs (per source deck, prefixed by file stem): +- `.identity.json` — canvas size/aspect, theme palette/fonts, observed colors/fonts +- `.slide_library.json` — text slots, geometry, native tables, native chart display caches +- `source_profile.json` — the single multi-deck index: a compact Strategist-facing digest per deck (over identity, tables, charts, and page types) under `decks[]`, with prefixed artifact pointers + +`project_manager.py import-sources` runs this automatically for PPTX/PPTM/PPSX/PPSM/POTX/POTM inputs and stores the bundle directly under `analysis/`. Multi-deck per project: importing several PPTX files gives each its own `.*` artifacts and a `decks[]` entry in the shared `source_profile.json` index (re-importing the same stem replaces its entry). The beautify / template-fill workflows stay single-deck and read one chosen deck's `.*` artifacts. + +Usage boundary: +- Standard generation uses these fields as facts and recommendation candidates; it does not inherit source slide coordinates or page order by default. +- Beautify promotes selected identity/content fields into locked constraints after confirmation. +- Template-fill uses the slide library as the native PPTX fill contract. + ## `source_to_md/web_to_md.py` Convert web pages to Markdown and download images locally. 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 20acfd660..7927a1f00 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 @@ -167,6 +167,8 @@ python3 scripts/image_search.py "offshore wind farm" \ --orientation landscape -o projects/demo/images ``` +For multiple web rows, `--batch images/image_queries.json` searches them concurrently (modest default, `--concurrency N` / `IMAGE_SEARCH_CONCURRENCY` to tune) instead of one call per row — the web sister of `image_gen.py --manifest`. Schema and status semantics: [`image-searcher.md`](../../references/image-searcher.md) §5. + Providers (Openverse and Wikimedia work with no key; configure Pexels / Pixabay for better stock-photo quality): | Provider | Config | Strength | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md index 6354d73cd..b31829d4d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md @@ -22,6 +22,11 @@ Notes: note), to avoid leaving unintended artifacts that could be committed by mistake. Pass `--copy` to force a copy for in-repo sources instead. - `--move` and `--copy` are mutually exclusive. +- PPTX-family inputs are enriched automatically under `analysis/` with + per-deck `.identity.json` / `.slide_library.json` plus the shared + multi-deck index `source_profile.json` (`decks[]`). + Multi-deck per project: several PPTX imports each get their own `.*` + artifacts and a `decks[]` entry; re-importing the same stem replaces its entry. Common formats: - `ppt169` diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_search.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_search.py index 8e07f3954..29217d8e7 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_search.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_search.py @@ -39,10 +39,13 @@ Examples: from __future__ import annotations import argparse +import concurrent.futures import importlib import json import os import sys +import tempfile +import threading from datetime import datetime, timezone from pathlib import Path from typing import Optional @@ -85,6 +88,29 @@ ALL_PROVIDERS: tuple[str, ...] = ZERO_CONFIG_PROVIDERS + KEYED_PROVIDERS ORIENTATION_CHOICES = ("any", "landscape", "portrait", "square") +# --- Batch mode (`--batch image_queries.json`) ----------------------------- +# Web providers are politeness-sensitive (Wikimedia/Openverse expect a modest +# rate), so the default concurrency is deliberately low. Sister-tool +# `image_gen.py` hits a paid API and defaults higher; here 3 keeps several +# rows in flight without hammering any single free provider. Set to 1 to +# restore strict one-at-a-time pacing. +DEFAULT_SEARCH_CONCURRENCY = 3 + +SEARCH_STATUS_PENDING = "Pending" +SEARCH_STATUS_SOURCED = "Sourced" +SEARCH_STATUS_FAILED = "Failed" +SEARCH_STATUS_NEEDS_MANUAL = "Needs-Manual" +SEARCH_VALID_STATUSES = { + SEARCH_STATUS_PENDING, + SEARCH_STATUS_SOURCED, + SEARCH_STATUS_FAILED, + SEARCH_STATUS_NEEDS_MANUAL, +} +# A row reaching `Needs-Manual` after the full provider/stage chain is terminal +# (see image-searcher.md §8); only Pending/Failed rows are retried on re-run. +SEARCH_RETRYABLE_STATUSES = {SEARCH_STATUS_PENDING, SEARCH_STATUS_FAILED} +SEARCH_REQUIRED_ITEM_FIELDS = ("filename", "query", "status") + # --------------------------------------------------------------------------- # .env loading @@ -531,6 +557,241 @@ def promote_candidate( return 0 +# --------------------------------------------------------------------------- +# Batch mode (`--batch image_queries.json`) +# --------------------------------------------------------------------------- + + +def load_search_manifest(path: str) -> dict: + """Load and validate an ``image_queries.json`` batch manifest. + + Schema (top level): ``{"items": [ ... ]}``. Each item requires + ``filename``, ``query``, ``status``. Optional per-item overrides: + ``slide``, ``purpose``, ``orientation``, ``provider``, + ``strict_no_attribution``, ``min_width``, ``min_height``, ``last_error``. + """ + try: + data = json.loads(Path(path).read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError( + f"Invalid JSON in {path}: {exc.msg} " + f"(line {exc.lineno}, col {exc.colno})" + ) from exc + + if not isinstance(data, dict): + raise ValueError( + f"{path}: top level must be a JSON object, got {type(data).__name__}" + ) + + items = data.get("items") + if not isinstance(items, list) or not items: + raise ValueError(f"{path}: 'items' must be a non-empty array") + + seen_filenames: set[str] = set() + for i, item in enumerate(items): + prefix = f"{path}: items[{i}]" + if not isinstance(item, dict): + raise ValueError(f"{prefix} must be an object") + for field in SEARCH_REQUIRED_ITEM_FIELDS: + if field not in item: + raise ValueError(f"{prefix} missing required field '{field}'") + if not isinstance(item[field], str) or not item[field].strip(): + raise ValueError( + f"{prefix} field '{field}' must be a non-empty string" + ) + if item["status"] not in SEARCH_VALID_STATUSES: + raise ValueError( + f"{prefix} status '{item['status']}' is invalid. " + f"Valid: {sorted(SEARCH_VALID_STATUSES)}" + ) + fname = item["filename"] + if fname in seen_filenames: + raise ValueError(f"{prefix} duplicate filename '{fname}'") + seen_filenames.add(fname) + + return data + + +def save_search_manifest(path: str, data: dict) -> None: + """Atomically write the batch manifest back (tmp file + rename).""" + target = Path(path) + fd, tmp_path = tempfile.mkstemp( + prefix=target.stem + ".", suffix=".tmp", dir=str(target.parent) + ) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.write("\n") + os.replace(tmp_path, target) + except Exception: + try: + os.unlink(tmp_path) + except OSError: + pass + raise + + +def _resolve_search_concurrency(cli_value: Optional[int]) -> int: + """CLI value wins over IMAGE_SEARCH_CONCURRENCY env; default 3.""" + if cli_value is not None: + return max(1, cli_value) + env_val = os.environ.get("IMAGE_SEARCH_CONCURRENCY", "").strip() + if env_val.isdigit(): + return max(1, int(env_val)) + return DEFAULT_SEARCH_CONCURRENCY + + +def _search_one_item( + item: dict, + *, + output_dir: Path, + save_candidates: bool, + max_candidates: int, + default_provider: Optional[str], + default_strict: bool, + default_min_width: int, + default_min_height: int, +) -> tuple[Optional[dict], Optional[str]]: + """Run the full search + download for one batch item (thread worker). + + Returns ``(manifest_item, error)``. Only the network/disk work happens + here; all manifest writes are serialized by the caller. + """ + orientation = item.get("orientation", "any") or "any" + strict = bool(item.get("strict_no_attribution", default_strict)) + request = ImageSearchRequest( + query=item["query"], + purpose=item.get("purpose", ""), + orientation="" if orientation == "any" else orientation, + filename=item["filename"], + slide=item.get("slide", ""), + min_width=int(item.get("min_width", default_min_width)), + min_height=int(item.get("min_height", default_min_height)), + ) + + pinned = item.get("provider") or default_provider + providers = [pinned] if pinned else _default_provider_chain() + output_path = output_dir / item["filename"] + + candidate, provider_name, stage = search_and_download( + providers, + request, + output_path=output_path, + strict_no_attribution=strict, + save_candidates=save_candidates, + max_candidates=max_candidates, + ) + if candidate is None: + return None, "no acceptable candidate across all providers/stages" + + actual_dimensions = _measure_actual_image(output_path) + item_args = argparse.Namespace( + filename=item["filename"], + slide=item.get("slide", ""), + purpose=item.get("purpose", ""), + query=item["query"], + orientation=orientation, + ) + manifest_item = _candidate_to_manifest_item( + candidate, + item_args, + provider_name=provider_name, + stage=stage, + actual_dimensions=actual_dimensions, + ) + return manifest_item, None + + +def run_search_manifest( + manifest: dict, + manifest_path: str, + *, + output_dir: Path, + sources_manifest_path: Path, + concurrency: int, + save_candidates: bool, + max_candidates: int, + default_provider: Optional[str], + default_strict: bool, + default_min_width: int, + default_min_height: int, +) -> tuple[int, int, int]: + """Process all Pending/Failed rows concurrently with a bounded pool. + + On success the rich provenance entry is appended to ``image_sources.json`` + (the credit source of truth) and the row's status flips to ``Sourced``. + A row that exhausts the provider/stage chain becomes ``Needs-Manual`` + (terminal). Status is written back after each completion, so an interrupt + preserves finished rows. Returns ``(sourced, needs_manual, skipped)``. + """ + items = manifest["items"] + pending_idx = [ + i for i, it in enumerate(items) + if it["status"] in SEARCH_RETRYABLE_STATUSES + ] + total = len(pending_idx) + skipped = len(items) - total + + if total == 0: + print( + f"[Batch] Nothing to do — all {len(items)} row(s) already in a " + "terminal state (Sourced / Needs-Manual)." + ) + return 0, 0, skipped + + print( + f"\n[Batch] {total} row(s) to search, {skipped} already done. " + f"concurrency={concurrency}\n" + ) + + sourced_count = 0 + needs_manual_count = 0 + write_lock = threading.Lock() + + def _one(idx: int): + try: + manifest_item, error = _search_one_item( + items[idx], + output_dir=output_dir, + save_candidates=save_candidates, + max_candidates=max_candidates, + default_provider=default_provider, + default_strict=default_strict, + default_min_width=default_min_width, + default_min_height=default_min_height, + ) + return idx, manifest_item, error + except Exception as exc: # noqa: BLE001 — provider code raises freely + return idx, None, str(exc)[:500] + + with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex: + futures = [ex.submit(_one, i) for i in pending_idx] + for fut in concurrent.futures.as_completed(futures): + idx, manifest_item, error = fut.result() + item = items[idx] + with write_lock: + if manifest_item is not None: + write_sources_manifest(sources_manifest_path, manifest_item) + item["status"] = SEARCH_STATUS_SOURCED + item["provider"] = manifest_item.get("provider", "") + item["license_tier"] = manifest_item.get("license_tier", "") + item.pop("last_error", None) + sourced_count += 1 + print(f" [OK] {item['filename']} ({item['provider']})") + else: + item["status"] = SEARCH_STATUS_NEEDS_MANUAL + item["last_error"] = error or "search failed" + needs_manual_count += 1 + print(f" [MANUAL] {item['filename']} — {item['last_error']}") + save_search_manifest(manifest_path, manifest) + + print( + f"\n[Batch] Done: {sourced_count} sourced / {needs_manual_count} " + f"needs-manual ({skipped} pre-skipped). Manifest: {manifest_path}" + ) + return sourced_count, needs_manual_count, skipped + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -544,11 +805,19 @@ def build_parser() -> argparse.ArgumentParser: ), formatter_class=argparse.RawDescriptionHelpFormatter, ) - parser.add_argument("query", help="Search query (2-5 keywords work best).") + parser.add_argument( + "query", + nargs="?", + default=None, + help="Search query (2-5 keywords work best). Omit in --batch mode.", + ) parser.add_argument( "--filename", - required=True, - help="Local filename for the chosen image (e.g. cover_bg.jpg).", + default=None, + help=( + "Local filename for the chosen image (e.g. cover_bg.jpg). " + "Required for single-query and --promote modes; ignored in --batch." + ), ) parser.add_argument( "-o", @@ -606,6 +875,27 @@ def build_parser() -> argparse.ArgumentParser: default=None, help="Override manifest path. Defaults to /image_sources.json.", ) + parser.add_argument( + "--batch", + default=None, + metavar="QUERIES_JSON", + help=( + "Process a batch of search requests from an image_queries.json " + "manifest concurrently, writing provenance into image_sources.json " + "and status back into the queries manifest." + ), + ) + parser.add_argument( + "--concurrency", + type=int, + default=None, + help=( + "Max concurrent searches in --batch mode. Defaults to " + f"IMAGE_SEARCH_CONCURRENCY env or {DEFAULT_SEARCH_CONCURRENCY}. " + "Keep modest — free providers are rate-sensitive; use 1 for " + "strict one-at-a-time pacing." + ), + ) parser.add_argument( "--no-candidates", action="store_true", @@ -651,6 +941,8 @@ def main(argv: Optional[list[str]] = None) -> int: # --- Promote mode --- if args.promote: + if not args.filename: + parser.error("--filename is required in --promote mode") return promote_candidate( output_dir, args.filename, @@ -658,7 +950,52 @@ def main(argv: Optional[list[str]] = None) -> int: manifest_path=Path(args.manifest) if args.manifest else None, ) - # --- Search mode --- + # --- Batch mode --- + if args.batch: + if not os.path.isfile(args.batch): + print(f"Error: queries manifest not found: {args.batch}", file=sys.stderr) + return 1 + try: + manifest = load_search_manifest(args.batch) + except ValueError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + batch_output_dir = ( + output_dir if args.output != "." else Path(args.batch).parent + ) + batch_output_dir.mkdir(parents=True, exist_ok=True) + sources_manifest_path = ( + Path(args.manifest) if args.manifest + else default_manifest_path(str(batch_output_dir)) + ) + try: + _, needs_manual, _ = run_search_manifest( + manifest, + args.batch, + output_dir=batch_output_dir, + sources_manifest_path=sources_manifest_path, + concurrency=_resolve_search_concurrency(args.concurrency), + save_candidates=not args.no_candidates, + max_candidates=args.max_candidates, + default_provider=args.provider, + default_strict=args.strict_no_attribution, + default_min_width=args.min_width, + default_min_height=args.min_height, + ) + except KeyboardInterrupt: + print("\n\nInterrupted by user. Partial progress preserved in manifest.") + return 130 + # Mirror image_gen.py: a non-zero code flags rows that need manual + # attention. It is a signal, not a halt — the workflow (image-base.md + # §6) surfaces Needs-Manual rows and continues regardless. + return 1 if needs_manual else 0 + + # --- Single-query search mode --- + if not args.query: + parser.error("query is required unless --batch or --promote is used") + if not args.filename: + parser.error("--filename is required in single-query mode") + request = ImageSearchRequest( query=args.query, purpose=args.purpose, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_intake.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_intake.py new file mode 100644 index 000000000..94f1ae572 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_intake.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +PPT Master - PPTX Intake Enrichment + +Extract reusable PPTX intake facts into a standard analysis bundle. This is a +read-only companion to `ppt_to_md.py`: Markdown remains the content source, +while this bundle provides canvas, visual identity, slide geometry, tables, and +native chart data for downstream workflows. + +Usage: + python3 scripts/pptx_intake.py -o + +Examples: + python3 scripts/pptx_intake.py deck.pptx -o projects/demo/analysis + +Dependencies: + None beyond the repository scripts used for PPTX parsing. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +_SCRIPTS_DIR = Path(__file__).resolve().parent +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + +from beautify_identity import extract_identity # noqa: E402 +from template_fill_pptx.analyzer import analyze_pptx # noqa: E402 + + +def _write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def _chart_summary(slide_library: dict[str, Any]) -> dict[str, Any]: + charts: list[dict[str, Any]] = [] + total_series = 0 + multi_plot_count = 0 + for slide in slide_library.get("slides", []): + for chart in slide.get("charts", []): + series_count = int(chart.get("series_count") or 0) + plot_types = chart.get("plot_types") or [] + if len(plot_types) > 1: + multi_plot_count += 1 + total_series += series_count + charts.append( + { + "slide_index": slide.get("slide_index"), + "chart_id": chart.get("chart_id"), + "chart_type": chart.get("chart_type"), + "plot_types": plot_types, + "category_count": chart.get("category_count", 0), + "series_count": series_count, + "series_names": [ + series.get("name") + for series in chart.get("series", []) + if series.get("name") + ], + } + ) + return { + "chart_count": len(charts), + "series_count": total_series, + "multi_plot_chart_count": multi_plot_count, + "charts": charts, + } + + +def _table_summary(slide_library: dict[str, Any]) -> dict[str, Any]: + tables: list[dict[str, Any]] = [] + for slide in slide_library.get("slides", []): + for table in slide.get("tables", []): + tables.append( + { + "slide_index": slide.get("slide_index"), + "table_id": table.get("table_id"), + "row_count": table.get("row_count", 0), + "column_count": table.get("column_count", 0), + } + ) + return {"table_count": len(tables), "tables": tables} + + +def build_source_profile( + pptx_path: Path, + identity: dict[str, Any], + slide_library: dict[str, Any], + stem: str | None = None, +) -> dict[str, Any]: + """Build the Strategist-facing per-deck digest over the raw intake artifacts. + + `stem` is the source-file stem used to prefix the per-deck artifact files so + several decks can coexist in one `analysis/` folder. Defaults to the pptx stem. + """ + stem = stem or pptx_path.stem + return { + "schema": "pptx_intake_profile.v1", + "stem": stem, + "source_pptx": str(pptx_path), + "slide_count": slide_library.get("slide_count", identity.get("slide_count", 0)), + "usage_contract": { + "standard_generation": ( + "Use identity and slide-library fields as source facts and recommendation " + "candidates only; do not preserve original page count, order, or coordinates " + "unless the user selected a beautify/template-fill workflow." + ), + "beautify": ( + "Promote source text, page order, page count, colors, and fonts into locked " + "constraints after user confirmation." + ), + "template_fill": ( + "Use slide slots, tables, charts, and geometry as the native PPTX fill contract." + ), + }, + "artifacts": { + "identity": f"{stem}.identity.json", + "slide_library": f"{stem}.slide_library.json", + }, + "canvas": identity.get("canvas", {}), + "identity": { + "theme_palette": (identity.get("theme") or {}).get("palette", {}), + "theme_fonts": (identity.get("theme") or {}).get("fonts", {}), + "observed_colors": (identity.get("observed") or {}).get("colors", []), + "observed_fonts": (identity.get("observed") or {}).get("fonts", {}), + }, + "structure": { + "canvas_px": slide_library.get("canvas_px", {}), + "page_types": [ + { + "slide_index": slide.get("slide_index"), + "page_type": slide.get("page_type"), + "slot_count": len(slide.get("slots", [])), + } + for slide in slide_library.get("slides", []) + ], + }, + "tables": _table_summary(slide_library), + "charts": _chart_summary(slide_library), + } + + +SOURCE_INDEX_NAME = "source_profile.json" + + +def upsert_source_index(output_dir: Path, digest: dict[str, Any]) -> Path: + """Merge one deck's digest into the single multi-deck index `source_profile.json`. + + The index stays the single must-read entry for the Strategist: it inlines every + deck's digest under `decks[]`, so a one-deck project is a one-entry index and a + multi-deck project lists each source deck self-containedly. Re-importing a deck + with the same stem replaces its entry in place. + """ + index_path = output_dir / SOURCE_INDEX_NAME + index: dict[str, Any] = {} + if index_path.is_file(): + try: + loaded = json.loads(index_path.read_text(encoding="utf-8")) + if isinstance(loaded, dict) and isinstance(loaded.get("decks"), list): + index = loaded + except (json.JSONDecodeError, OSError): + index = {} + stem = digest.get("stem") + decks = [d for d in index.get("decks", []) if d.get("stem") != stem] + decks.append(digest) + decks.sort(key=lambda d: str(d.get("stem", ""))) + index = { + "schema": "pptx_intake_index.v1", + "deck_count": len(decks), + "decks": decks, + } + _write_json(index_path, index) + return index_path + + +def run_intake(pptx_path: Path, output_dir: Path) -> dict[str, Path]: + """Write `.identity.json`, `.slide_library.json`, and merge the + deck's digest into the single multi-deck index `source_profile.json`.""" + output_dir.mkdir(parents=True, exist_ok=True) + stem = pptx_path.stem + identity = extract_identity(pptx_path) + slide_library = analyze_pptx(pptx_path) + digest = build_source_profile(pptx_path, identity, slide_library, stem) + + identity_path = output_dir / f"{stem}.identity.json" + slide_library_path = output_dir / f"{stem}.slide_library.json" + _write_json(identity_path, identity) + _write_json(slide_library_path, slide_library) + profile_path = upsert_source_index(output_dir, digest) + return { + "identity": identity_path, + "slide_library": slide_library_path, + "source_profile": profile_path, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Extract standard PPTX intake analysis artifacts.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("source", help="Source PPTX / PPTM / PPSX / PPSM / POTX / POTM file") + parser.add_argument("-o", "--output-dir", required=True, help="Output project analysis directory") + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + source = Path(args.source).expanduser().resolve() + if not source.is_file(): + print(f"Error: source not found: {source}", file=sys.stderr) + return 1 + try: + outputs = run_intake(source, Path(args.output_dir).expanduser().resolve()) + except (RuntimeError, KeyError, ValueError) as exc: + print(f"Error: PPTX intake failed: {exc}", file=sys.stderr) + return 1 + print(f"PPTX intake -> {Path(args.output_dir).expanduser().resolve()}", file=sys.stderr) + for name, path in outputs.items(): + print(f" {name}: {path}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/project_manager.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/project_manager.py index 4fb1abda2..ca6b7db2d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/project_manager.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/project_manager.py @@ -11,6 +11,7 @@ Usage: from __future__ import annotations import argparse +import filecmp import json import re import shutil @@ -147,6 +148,7 @@ class ProjectManager: "notes", "templates", SOURCE_DIRNAME, + "analysis", "exports", ): (project_path / rel_path).mkdir(parents=True, exist_ok=True) @@ -161,11 +163,12 @@ class ProjectManager: "## Directories\n\n" "- `svg_output/`: raw SVG output\n" "- `svg_final/`: finalized SVG output\n" - "- `images/`: presentation assets\n" + "- `images/`: runtime image pool; converter assets keep their original short filenames when possible\n" "- `icons/`: project icon set — selected library icons copied in (via icon_sync.py) plus any custom icons you add; embedded from here at export\n" "- `notes/`: speaker notes\n" "- `templates/`: project templates\n" "- `sources/`: source materials and normalized markdown\n" + "- `analysis/`: machine-extracted intermediate analysis (PPTX intake, image_analysis.csv) — the pipeline's canonical must-read source/asset facts\n" "- `exports/`: main native pptx (timestamped); `_svg.pptx` sibling added when exported with `--svg-snapshot`\n" "- `backup//`: svg_output/ archive (always written in default-flow mode; safe to delete old timestamps)\n" ), @@ -181,6 +184,11 @@ class ProjectManager: sources_dir.mkdir(parents=True, exist_ok=True) return sources_dir + def _analysis_dir(self, project_path: Path) -> Path: + analysis_dir = project_path / "analysis" + analysis_dir.mkdir(parents=True, exist_ok=True) + return analysis_dir + def _ensure_unique_path(self, path: Path) -> Path: if not path.exists(): return path @@ -275,6 +283,22 @@ class ProjectManager: ] ) + def _import_pptx_intake(self, presentation_path: Path, project_dir: Path) -> Path: + # Multi-deck intake: each PPTX writes its own `.identity.json` / + # `.slide_library.json` and is merged into the single multi-deck + # index `analysis/source_profile.json` (one entry per source deck). + analysis_dir = self._analysis_dir(project_dir) + self._run_tool( + [ + sys.executable, + str(TOOLS_DIR / "pptx_intake.py"), + str(presentation_path), + "-o", + str(analysis_dir), + ] + ) + return analysis_dir + def _import_excel(self, excel_path: Path, markdown_path: Path) -> None: self._run_tool( [ @@ -307,6 +331,15 @@ class ProjectManager: ] self._run_tool(command) + def _is_valid_imported_url_markdown(self, markdown_path: Path) -> bool: + """Return whether web_to_md produced a usable Markdown source.""" + if not markdown_path.is_file(): + return False + content = markdown_path.read_text(encoding="utf-8", errors="replace") + if "[Failed URLs]:" in content: + return False + return bool(content.strip()) + def _archive_url_record(self, sources_dir: Path, url: str) -> Path: file_path = self._ensure_unique_path(sources_dir / f"{derive_url_basename(url)}.url.txt") file_path.write_text( @@ -420,11 +453,53 @@ class ProjectManager: suffix = "_files" return name[:-len(suffix)] if name.endswith(suffix) else name + def _image_destination_name( + self, + images_dir: Path, + source_file: Path, + namespace: str, + existing_manifest: dict[str, dict], + ) -> str: + """Return a short unique image filename for the runtime image pool.""" + candidate = images_dir / source_file.name + if not candidate.exists(): + return source_file.name + try: + meta = existing_manifest.get(candidate.name, {}) + if ( + meta.get("source_namespace") == namespace + and candidate.is_file() + and filecmp.cmp(source_file, candidate, shallow=False) + ): + return candidate.name + except OSError: + pass + + stem = source_file.stem + suffix = source_file.suffix + counter = 2 + while True: + candidate = images_dir / f"{stem}_{counter}{suffix}" + if not candidate.exists(): + return candidate.name + try: + meta = existing_manifest.get(candidate.name, {}) + if ( + meta.get("source_namespace") == namespace + and candidate.is_file() + and filecmp.cmp(source_file, candidate, shallow=False) + ): + return candidate.name + except OSError: + pass + counter += 1 + def _propagate_image_assets(self, asset_dir: Path, project_dir: Path) -> None: """Copy converter-generated image assets and manifest into project images/. - Files are namespaced by source stem to avoid collisions when multiple - DOCX/PPTX sources contain identically-named internal media (image1.png, ...). + Filenames are preserved when possible because source Markdown commonly + uses short names that are meaningful in context. Only real collisions + receive a compact numeric suffix. """ manifest_path = asset_dir / "image_manifest.json" if not manifest_path.is_file(): @@ -443,6 +518,19 @@ class ProjectManager: images_dir.mkdir(parents=True, exist_ok=True) namespace = self._namespace_from_asset_dir(asset_dir) + existing_manifest: dict[str, dict] = {} + destination_manifest = images_dir / "image_manifest.json" + if destination_manifest.is_file(): + try: + data = json.loads(destination_manifest.read_text(encoding="utf-8")) + if isinstance(data, list): + existing_manifest = { + item["filename"]: item + for item in data + if isinstance(item, dict) and isinstance(item.get("filename"), str) + } + except (OSError, json.JSONDecodeError): + existing_manifest = {} rename_map: dict[str, str] = {} copied_count = 0 @@ -451,8 +539,15 @@ class ProjectManager: continue if source_file.suffix.lower() not in IMAGE_ASSET_SUFFIXES: continue - new_name = f"{namespace}__{source_file.name}" - shutil.copy2(source_file, images_dir / new_name) + new_name = self._image_destination_name( + images_dir, + source_file, + namespace, + existing_manifest, + ) + destination = images_dir / new_name + if source_file.resolve() != destination.resolve(): + shutil.copy2(source_file, destination) rename_map[source_file.name] = new_name copied_count += 1 @@ -464,7 +559,7 @@ class ProjectManager: if not isinstance(original, str): continue new_item = dict(item) - new_item["filename"] = rename_map.get(original, f"{namespace}__{original}") + new_item["filename"] = rename_map.get(original, original) new_item["source_namespace"] = namespace rebased_items.append(new_item) @@ -534,6 +629,7 @@ class ProjectManager: "archived": [], "markdown": [], "assets": [], + "analysis": [], "notes": [], "skipped": [], } @@ -548,17 +644,24 @@ class ProjectManager: for item in source_items: if is_url(item): - archived = self._archive_url_record(sources_dir, item) markdown_path = self._ensure_unique_path( sources_dir / f"{derive_url_basename(item)}.md" ) try: self._import_url(item, markdown_path) except Exception as exc: # pragma: no cover - summary path + archived = self._archive_url_record(sources_dir, item) + summary["archived"].append(str(archived)) summary["skipped"].append(f"{item}: {exc}") continue - summary["archived"].append(str(archived)) + if not self._is_valid_imported_url_markdown(markdown_path): + markdown_path.unlink(missing_ok=True) + archived = self._archive_url_record(sources_dir, item) + summary["archived"].append(str(archived)) + summary["skipped"].append(f"{item}: URL conversion produced no usable Markdown") + continue + summary["markdown"].append(str(markdown_path)) self._propagate_companion_image_assets(markdown_path, project_dir) continue @@ -640,6 +743,13 @@ class ProjectManager: summary["skipped"].append(f"{item}: PDF conversion failed ({exc})") elif suffix in PRESENTATION_SUFFIXES: canonical_markdown_path = sources_dir / f"{archived_path.stem}.md" + try: + intake_dir = self._import_pptx_intake(archived_path, project_dir) + intake_str = str(intake_dir) + if intake_str not in summary["analysis"]: + summary["analysis"].append(intake_str) + except Exception as exc: # pragma: no cover - summary path + summary["notes"].append(f"{item}: PPTX intake analysis failed ({exc})") if archived_path.stem in explicit_markdown_stems: summary["notes"].append( f"{item}: skipped presentation auto-conversion because a same-stem Markdown source was provided" @@ -825,6 +935,10 @@ def main(argv: list[str] | None = None) -> int: print("\nImported asset directories:") for item in summary["assets"]: print(f" - {item}") + if summary["analysis"]: + print("\nAnalysis artifacts:") + for item in summary["analysis"]: + print(f" - {item}") if summary["notes"]: print("\nNotes:") for item in summary["notes"]: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/doc_to_md.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/doc_to_md.py index a36b14af2..211ec8398 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/doc_to_md.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/doc_to_md.py @@ -55,6 +55,10 @@ PANDOC_FORMATS = { # Formats pandoc should extract embedded media from PANDOC_MEDIA_FORMATS = {".odt"} OFFICE_VECTOR_EXTENSIONS = {".emf", ".wmf"} +IMAGE_ASSET_SUFFIXES = { + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", + ".emf", ".wmf", ".svg", +} DOCX_NS = { "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", @@ -166,6 +170,59 @@ def _is_office_vector(ext: str) -> bool: return ext.lower() in OFFICE_VECTOR_EXTENSIONS +def _write_generic_image_manifest( + media_dir: Path, + rel_media_dir: str, + markdown: str, + source_kind: str, +) -> None: + """Write lightweight image metadata for non-DOCX converter paths.""" + if not media_dir.exists(): + return + + ref_pattern = re.compile(rf"{re.escape(rel_media_dir)}/([^)\s]+)") + refs = [Path(match.group(1)).name for match in ref_pattern.finditer(markdown)] + occurrence_map: dict[str, list[dict[str, object]]] = {} + for index, filename in enumerate(refs, 1): + occurrence_map.setdefault(filename, []).append({ + "occurrence_index": index, + "source_ref": f"{rel_media_dir}/{filename}", + }) + + manifest: list[dict[str, object]] = [] + for file_path in sorted(path for path in media_dir.iterdir() if path.is_file()): + ext = _normalize_ext(file_path.suffix) + if ext not in IMAGE_ASSET_SUFFIXES: + continue + width, height = _image_size(file_path) + ratio = width / height if width and height else None + asset_kind = "office_vector" if _is_office_vector(ext) else "bitmap" + occurrences = occurrence_map.get(file_path.name, []) + entry: dict[str, object] = { + "index": len(manifest) + 1, + "filename": file_path.name, + "original_filename": file_path.name, + "asset_kind": asset_kind, + "svg_renderable": asset_kind != "office_vector", + "pptx_native_supported": True, + "source_kind": source_kind, + "source_ext": ext, + "pixel_width": width, + "pixel_height": height, + "pixel_ratio": round(ratio, 6) if ratio else None, + "display_ratio": round(ratio, 6) if ratio else None, + "occurrences": occurrences, + "usage_count": len(occurrences) if occurrences else 1, + } + manifest.append(entry) + + if manifest: + (media_dir / "image_manifest.json").write_text( + json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + def _local_name(elem: ET.Element) -> str: """Return an XML element local name without its namespace.""" return elem.tag.rsplit("}", 1)[-1] @@ -829,6 +886,7 @@ def _convert_html(input_file: Path, out_file: Path) -> str: # Collapse 3+ blank lines to 2 for tidier output markdown = re.sub(r"\n{3,}", "\n\n", markdown).strip() + "\n" out_file.write_text(markdown, encoding="utf-8") + _write_generic_image_manifest(media_dir, rel_media_dir, markdown, "html_image") if not any(media_dir.iterdir()): media_dir.rmdir() @@ -1024,6 +1082,7 @@ def _convert_epub(input_file: Path, out_file: Path) -> str: markdown = markdownify(combined_html, heading_style="ATX", bullets="-") markdown = re.sub(r"\n{3,}", "\n\n", markdown).strip() + "\n" out_file.write_text(markdown, encoding="utf-8") + _write_generic_image_manifest(media_dir, rel_media_dir, markdown, "epub_image") if not any(media_dir.iterdir()): media_dir.rmdir() @@ -1089,6 +1148,7 @@ def _convert_ipynb(input_file: Path, out_file: Path) -> str: markdown = out_file.read_text(encoding="utf-8") if out_file.exists() else body media_dir = out_file.parent / rel_media_dir + _write_generic_image_manifest(media_dir, rel_media_dir, markdown, "ipynb_image") _report_result(out_file, media_dir if media_dir.exists() else None) return markdown @@ -1156,6 +1216,7 @@ def _convert_with_pandoc(input_file: Path, out_file: Path, suffix: str) -> str: markdown = _html_img_to_md(markdown) out_file.write_text(markdown, encoding="utf-8") + _write_generic_image_manifest(media_dir, rel_media_dir, markdown, "pandoc_image") _report_result(out_file, media_dir if media_dir.exists() else None) return markdown diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/pdf_to_md.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/pdf_to_md.py index c24377c4b..f3c6ef224 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/pdf_to_md.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/pdf_to_md.py @@ -7,6 +7,7 @@ Supports heading levels, bold, italic, and list detection. import argparse import hashlib +import json import os import re import sys @@ -712,6 +713,7 @@ def extract_pdf_to_markdown( img_dir = output_path.parent / rel_img_dir img_count = 0 + image_manifest: list[dict[str, object]] = [] for page_num, page in enumerate(doc, 1): if page_num > 1: @@ -951,6 +953,27 @@ def extract_pdf_to_markdown( if prev_was_list: markdown_content += "\n" markdown_content += f"![{image_name}]({rel_img_dir}/{image_name})\n\n" + width = int(block.get("width", 0) or 0) + height = int(block.get("height", 0) or 0) + ratio = width / height if width > 0 and height > 0 else None + image_manifest.append({ + "index": len(image_manifest) + 1, + "filename": image_name, + "original_filename": image_name, + "asset_kind": "bitmap", + "svg_renderable": True, + "pptx_native_supported": True, + "source_kind": "pdf_image", + "source_ext": f".{ext}", + "page_index": page_num, + "occurrence_index": img_count + 1, + "pixel_width": width or None, + "pixel_height": height or None, + "pixel_ratio": round(ratio, 6) if ratio else None, + "display_ratio": round(ratio, 6) if ratio else None, + "source_sha256": hashlib.sha256(image_data).hexdigest(), + "bbox": list(block.get("bbox", [])), + }) img_count += 1 prev_was_list = False print(f" [OK] Extracted image: {image_name}") @@ -980,6 +1003,29 @@ def extract_pdf_to_markdown( if prev_was_list: markdown_content += "\n" markdown_content += f"![{image_name}]({rel_img_dir}/{image_name})\n\n" + ratio = pix.width / pix.height if pix.width > 0 and pix.height > 0 else None + image_manifest.append({ + "index": len(image_manifest) + 1, + "filename": image_name, + "original_filename": image_name, + "asset_kind": "bitmap", + "svg_renderable": True, + "pptx_native_supported": True, + "source_kind": "pdf_vector_figure", + "source_ext": ".png", + "page_index": page_num, + "occurrence_index": img_count + 1, + "pixel_width": pix.width, + "pixel_height": pix.height, + "pixel_ratio": round(ratio, 6) if ratio else None, + "display_ratio": round(ratio, 6) if ratio else None, + "bbox": [ + figure_rect.x0, + figure_rect.y0, + figure_rect.x1, + figure_rect.y1, + ], + }) img_count += 1 prev_was_list = False print(f" [OK] Rendered vector figure: {image_name}") @@ -1000,6 +1046,11 @@ def extract_pdf_to_markdown( os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(markdown_content) + if img_dir and image_manifest: + (img_dir / "image_manifest.json").write_text( + json.dumps(image_manifest, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) print(f"[OK] Saved Markdown to: {output_path}") return markdown_content diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py index e68440018..64b1e276f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py @@ -359,6 +359,76 @@ def table_to_markdown(table: object) -> str: return "\n".join(lines) +def _format_chart_value(value: object) -> str: + """Render a chart data point, trimming whole-number floats.""" + if value is None: + return "" + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + return str(value) + + +def chart_to_markdown(chart: object, name: str) -> str: + """Render a chart's data as a Markdown table so the numbers survive conversion. + + A native PowerPoint chart stores its data in embedded XML, not in any text + frame — emitting only a `[Chart]` placeholder drops every value. The markdown + is the content contract for downstream generation, so transcribe categories × + series here. `scripts/pptx_intake.py` writes the same data in structured JSON + form for tooling; this is the human- and content-readable mirror. + """ + try: + chart_type = str(chart.chart_type) + except (ValueError, AttributeError): + chart_type = "" + + categories: list[str] = [] + try: + plots = list(chart.plots) + if plots: + categories = [ + escape_table_cell(str(cat)) if cat is not None else "" + for cat in plots[0].categories + ] + except (ValueError, IndexError, AttributeError): + categories = [] + + series_data: list[tuple[str, list[object]]] = [] + try: + for index, series in enumerate(chart.series, start=1): + try: + values = list(series.values) + except (ValueError, TypeError, AttributeError): + values = [] + label = str(series.name) if getattr(series, "name", None) else f"Series {index}" + series_data.append((escape_table_cell(label), values)) + except (ValueError, AttributeError): + series_data = [] + + header = f"> [Chart] {name}" + (f" — {chart_type}" if chart_type else "") + row_count = len(categories) if categories else max((len(v) for _, v in series_data), default=0) + if not series_data or row_count == 0: + return header + + table_header = (["Category"] if categories else ["#"]) + [sname for sname, _ in series_data] + lines = [ + header, + "", + "| " + " | ".join(table_header) + " |", + "| " + " | ".join(["---"] * len(table_header)) + " |", + ] + for row_index in range(row_count): + if categories: + label = categories[row_index] if row_index < len(categories) else "" + else: + label = str(row_index + 1) + cells = [label] + for _, values in series_data: + cells.append(_format_chart_value(values[row_index]) if row_index < len(values) else "") + lines.append("| " + " | ".join(cells) + " |") + return "\n".join(lines) + + def _image_part_for_shape(shape: object) -> object | None: """Return the first embedded image part referenced by a shape.""" element = getattr(shape, "element", None) @@ -710,7 +780,10 @@ def convert_presentation_to_markdown( continue if getattr(shape, "has_chart", False): - blocks.append(f"> [Chart] {getattr(shape, 'name', 'Chart')}") + try: + blocks.append(chart_to_markdown(shape.chart, getattr(shape, "name", "Chart"))) + except (ValueError, AttributeError, KeyError): + blocks.append(f"> [Chart] {getattr(shape, 'name', 'Chart')}") if blocks: lines.append("\n\n".join(blocks)) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/web_to_md.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/web_to_md.py index dba3a7f09..5cba9b25d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/web_to_md.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/web_to_md.py @@ -29,6 +29,7 @@ TLS fingerprint handling: import argparse import datetime import io +import json import os import re import sys @@ -219,6 +220,7 @@ def download_and_rewrite_images( os.makedirs(image_dir, exist_ok=True) downloaded = {} + manifest_by_filename: dict[str, dict[str, object]] = {} saved = 0 for idx, img in enumerate(images): @@ -243,6 +245,8 @@ def download_and_rewrite_images( img["src"] = src abs_url = urljoin(page_url, src) + content_type = "" + converted_from = "" if abs_url in downloaded: saved_name = downloaded[abs_url] else: @@ -269,6 +273,7 @@ def download_and_rewrite_images( pil_image = Image.open(img_data) # Update filename to .png + converted_from = filename filename = f"{stem}.png" local_path = os.path.join(image_dir, filename) @@ -313,6 +318,19 @@ def download_and_rewrite_images( f.write(resp.content) downloaded[abs_url] = filename saved_name = filename + manifest_by_filename[saved_name] = { + "index": len(manifest_by_filename) + 1, + "filename": saved_name, + "original_filename": converted_from or saved_name, + "asset_kind": "bitmap", + "svg_renderable": True, + "pptx_native_supported": True, + "source_kind": "web_image", + "source_url": abs_url, + "source_page_url": page_url, + "content_type": content_type.split(";")[0] if content_type else "", + "occurrences": [], + } saved += 1 except Exception as e: print(f" [WARN] Skip image {abs_url}: {e}") @@ -321,6 +339,27 @@ def download_and_rewrite_images( rel_path = os.path.join( rel_prefix, saved_name) if rel_prefix else saved_name img["src"] = rel_path + manifest_item = manifest_by_filename.get(saved_name) + if manifest_item is not None: + occurrences = manifest_item.setdefault("occurrences", []) + if isinstance(occurrences, list): + occurrences.append({ + "occurrence_index": idx + 1, + "source_url": abs_url, + "alt_text": img.get("alt", ""), + }) + manifest_item["usage_count"] = len(occurrences) + + if manifest_by_filename: + manifest_path = os.path.join(image_dir, "image_manifest.json") + with open(manifest_path, "w", encoding="utf-8") as f: + json.dump( + list(manifest_by_filename.values()), + f, + ensure_ascii=False, + indent=2, + ) + f.write("\n") return saved diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py index 31490e24e..a3a459ebf 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py @@ -18,6 +18,7 @@ from .drawingml_utils import ( rect_to_dml_xfrm, parse_hex_color, resolve_url_id, get_effective_filter_id, parse_font_family, is_cjk_char, estimate_text_width, + detect_text_lang, resolve_text_run_fonts, parse_transform_matrix, _xml_escape, ) from .drawingml_styles import ( @@ -1013,6 +1014,8 @@ def _build_run_xml( strike_attr = ' strike="sngStrike"' if 'line-through' in text_dec else '' fonts = parse_font_family(ff) if ff else default_fonts + run_fonts = resolve_text_run_fonts(text, fonts) + lang = detect_text_lang(text) fill_xml = _build_text_fill_xml(fill, fill_raw, opacity, ctx) outline_xml = _build_text_outline_xml(run) @@ -1020,13 +1023,13 @@ def _build_run_xml( space_attr = ' xml:space="preserve"' if text != text.strip() or ' ' in text else '' return f''' - + {outline_xml} {fill_xml} {effect_xml} - - - + + + {_xml_escape(text)} ''' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_utils.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_utils.py index 3919cb886..ceabd3a37 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_utils.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_utils.py @@ -441,6 +441,21 @@ def is_cjk_char(ch: str) -> bool: 0x20000 <= cp <= 0x2A6DF) +def detect_text_lang(text: str) -> str: + """Return a DrawingML language tag for a text run.""" + return 'zh-CN' if any(is_cjk_char(ch) for ch in text) else 'en-US' + + +def resolve_text_run_fonts(text: str, fonts: dict[str, str]) -> dict[str, str]: + """Return DrawingML latin/ea/cs typefaces for one text run.""" + latin = fonts['latin'] + if detect_text_lang(text) == 'zh-CN': + ea = fonts['ea'] + else: + ea = latin + return {'latin': latin, 'ea': ea, 'cs': latin} + + def estimate_text_width(text: str, font_size: float, font_weight: str = '400') -> float: """Estimate text width in SVG pixels.""" width = 0.0 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_notes.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_notes.py index 2ea290625..5ee0611a4 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_notes.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_notes.py @@ -4,6 +4,8 @@ from __future__ import annotations import re +from .drawingml_utils import detect_text_lang + def markdown_to_plain_text(md_content: str) -> str: """Convert Markdown notes to plain text for PPTX notes. @@ -70,19 +72,20 @@ def create_notes_slide_xml(slide_num: int, notes_text: str) -> str: paragraphs: list[str] = [] for para in notes_text.split('\n'): if para.strip(): + lang = detect_text_lang(para) paragraphs.append(f''' - + {para} ''') else: - paragraphs.append('') + paragraphs.append('') paragraphs_xml = ( '\n '.join(paragraphs) if paragraphs - else '' + else '' ) return f''' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx.py index df5690609..22bea99ac 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx.py @@ -4,9 +4,9 @@ Delegates to the template_fill_pptx package. Kept as the CLI entry point so the documented command paths keep working: - python3 scripts/template_fill_pptx.py analyze -o slide_library.json - python3 scripts/template_fill_pptx.py scaffold slide_library.json -o fill_plan.json - python3 scripts/template_fill_pptx.py check-plan slide_library.json fill_plan.json + python3 scripts/template_fill_pptx.py analyze -o .slide_library.json + python3 scripts/template_fill_pptx.py scaffold .slide_library.json -o fill_plan.json + python3 scripts/template_fill_pptx.py check-plan .slide_library.json fill_plan.json python3 scripts/template_fill_pptx.py apply fill_plan.json -o output.pptx Implementation lives in the template_fill_pptx/ package (ooxml, analyzer, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_fill.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_fill.py index 34467445f..9748391d1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_fill.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_fill.py @@ -77,9 +77,17 @@ def _chart_type_with_series(chart_root: ET.Element) -> ET.Element: plot_area = chart_root.find(".//c:plotArea", NS) if plot_area is None: raise RuntimeError("Chart XML has no plotArea") + chart_types: list[ET.Element] = [] for child in list(plot_area): if child.tag.endswith("Chart") and child.findall("c:ser", NS): - return child + chart_types.append(child) + if len(chart_types) > 1: + raise RuntimeError( + "template-fill chart edits do not support multi-plot / combination charts; " + "use beautify/main pipeline to redraw the chart, or leave the native chart untouched" + ) + if chart_types: + return chart_types[0] raise RuntimeError("Chart XML has no editable series") diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_read.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_read.py index ab930f20f..e3d9e65c3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_read.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/chart_read.py @@ -27,14 +27,15 @@ def _local_name(tag: str) -> str: return tag.rsplit("}", 1)[-1] -def _chart_type_with_series(chart_root: ET.Element) -> ET.Element | None: +def _chart_types_with_series(chart_root: ET.Element) -> list[ET.Element]: plot_area = chart_root.find(".//c:plotArea", NS) if plot_area is None: - return None + return [] + chart_types: list[ET.Element] = [] for child in list(plot_area): if _local_name(child.tag).endswith("Chart") and child.findall("c:ser", NS): - return child - return None + chart_types.append(child) + return chart_types def _coerce_number(value: str) -> int | float | str: @@ -75,23 +76,33 @@ def _series_name(series: ET.Element, fallback: str) -> str: def read_chart_data(chart_root: ET.Element) -> dict[str, Any]: """Return a compact summary of chart type, categories, series, and values.""" - chart_type = _chart_type_with_series(chart_root) - if chart_type is None: + chart_types = _chart_types_with_series(chart_root) + if not chart_types: return empty_chart_data() - series_nodes = chart_type.findall("c:ser", NS) - categories = _cache_values(series_nodes[0].find("c:cat", NS)) if series_nodes else [] + first_series = chart_types[0].find("c:ser", NS) + categories = _cache_values(first_series.find("c:cat", NS)) if first_series is not None else [] series_payload: list[dict[str, Any]] = [] - for index, series in enumerate(series_nodes, start=1): - series_payload.append( - { - "name": _series_name(series, f"系列{index}"), - "values": _cache_values(series.find("c:val", NS), numeric=True), - } - ) + plot_types: list[str] = [] + for chart_type in chart_types: + plot_name = _local_name(chart_type.tag) + plot_types.append(plot_name) + for series in chart_type.findall("c:ser", NS): + index = len(series_payload) + 1 + series_categories = _cache_values(series.find("c:cat", NS)) + if not categories and series_categories: + categories = series_categories + series_payload.append( + { + "name": _series_name(series, f"系列{index}"), + "values": _cache_values(series.find("c:val", NS), numeric=True), + "chart_type": plot_name, + } + ) return { - "chart_type": _local_name(chart_type.tag), + "chart_type": plot_types[0] if len(set(plot_types)) == 1 else "comboChart", + "plot_types": plot_types, "category_count": len(categories), "series_count": len(series_payload), "categories": categories, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/checker.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/checker.py index 5d452fe56..10b1fedf1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/checker.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/checker.py @@ -387,6 +387,22 @@ def check_plan(library: dict[str, Any], plan: dict[str, Any]) -> dict[str, Any]: ) summary["error"] += 1 continue + if len(chart.get("plot_types") or []) > 1: + results.append( + { + "status": "ERROR", + "plan_slide": slide_index, + "source_slide": source_slide, + "selector": selectors[0] if selectors else "", + "chart_id": chart.get("chart_id"), + "message": ( + "template-fill chart edits do not support multi-plot / combination charts; " + "use beautify/main pipeline to redraw the chart, or leave the native chart untouched" + ), + } + ) + summary["error"] += 1 + continue categories = chart_edit.get("categories", []) series = chart_edit.get("series", []) if not isinstance(categories, list) or not isinstance(series, list) or not series: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/cli.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/cli.py index 5bcbf161a..4ada454e0 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/cli.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/cli.py @@ -69,10 +69,10 @@ def build_parser() -> argparse.ArgumentParser: analyze = subparsers.add_parser("analyze", help="Extract slide library JSON from a PPTX") analyze.add_argument("pptx_file", help="Source PPTX file") - analyze.add_argument("-o", "--output", required=True, help="Output slide_library.json path") + analyze.add_argument("-o", "--output", required=True, help="Output .slide_library.json path") scaffold = subparsers.add_parser("scaffold", help="Create an editable fill plan skeleton") - scaffold.add_argument("library_json", help="slide_library.json from analyze") + scaffold.add_argument("library_json", help=".slide_library.json from analyze") scaffold.add_argument("-o", "--output", required=True, help="Output fill_plan.json path") scaffold.add_argument( "--slides", @@ -85,7 +85,7 @@ def build_parser() -> argparse.ArgumentParser: ) check = subparsers.add_parser("check-plan", help="Check a fill plan against source slot capacity") - check.add_argument("library_json", help="slide_library.json from analyze") + check.add_argument("library_json", help=".slide_library.json from analyze") check.add_argument("plan_json", help="Fill plan JSON") check.add_argument("-o", "--output", help="Optional JSON report output path") diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md index 6136765e1..49f1e71f0 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md @@ -14,6 +14,7 @@ | **Design Style** | {design_style} | | **Target Audience** | [Filled by Strategist] | | **Use Case** | [Filled by Strategist] | +| **Content Strategy** | [Material divergence — the user's free-text intent on how closely to follow the source vs how freely to reshape it (or "balanced default"); facts stay sourced however free. Confirmed at c; consumed when authoring §IX. Not in spec_lock.] | | **Created Date** | {date_str} | --- @@ -300,7 +301,8 @@ Catalog read: 71 templates #### Slide 01 - Cover -- **Layout**: Full-screen background image + centered title +- **Cover impact**: [MANDATORY — see strategist.md §6.2. Name one concrete hook (provocative core claim / hero number / object-scene metaphor / founder-product-audience moment / distilled conflict) + one composition strategy (full-bleed image + floating title / typographic poster / hero object / data hook / editorial scene / high-contrast abstract geometry / or a fresh one the subject suggests). This is the cover's spine — do NOT fall back to "title + subtitle + decorative background".] +- **Layout**: [realize the Cover impact above; choose the composition that delivers it — not a default centered title block] - **Title**: [Main title] - **Subtitle**: [Subtitle] - **Info**: [Author / Date / Organization] @@ -324,6 +326,14 @@ Catalog read: 71 templates --- +#### Slide NN - Closing *(only if the deck genuinely lands on a conclusion / CTA / final-takeaway page — do NOT invent one to fill this slot; see strategist.md §6.2)* + +- **Closing impact**: [MANDATORY for the closing page — name the one thing the audience leaves with (distilled takeaway / forward call / memorable restatement of the core claim) + one composition that lands it. Do NOT write a generic "Thank you" / contact-only / centered-title reprise of the cover.] +- **Layout**: [realize the Closing impact above — the deck's final impression, not a default sign-off] +- **Content**: [the takeaway / call-to-action itself, phrased to land] + +--- + ## X. Speaker Notes Requirements One speaker note file per page, saved to `notes/`: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/beautify-pptx.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/beautify-pptx.md index 4e4ea132f..a52663911 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/beautify-pptx.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/beautify-pptx.md @@ -26,7 +26,7 @@ Re-lays-out an existing `.pptx`: the text is preserved **verbatim**, the source **Distinct from mirror templates**: `replication_mode: mirror` (executor §1.1) keeps layout + visuals verbatim and edits text. Beautify is the inverse — content verbatim, layout redone, identity inherited. -**When this is the wrong route — re-architecture belongs to the main pipeline**: beautify preserves the source's page count and page order 1:1. It is for "keep this deck, just lay it out better". When the user instead wants the original page breakdown reconsidered — merge / split / reorder pages, re-outline the structure, build a *better deck* from the same content rather than a prettier version of the same pages — that is not beautify. Convert the deck with [`ppt_to_md`](../scripts/source_to_md/ppt_to_md.py) and run the main SKILL.md pipeline, where the Strategist re-architects the outline freely from the extracted content. The deciding question: is the source's page split information to preserve, or just the previous author's structure to improve? Preserve → beautify (here); improve → `ppt_to_md` + main pipeline. +**When this is the wrong route — re-architecture belongs to the main pipeline**: beautify preserves the source's page count and page order 1:1. It is for "keep this deck, just lay it out better". When the user instead wants the original page breakdown reconsidered — merge / split / reorder pages, re-outline the structure, build a *better deck* from the same content rather than a prettier version of the same pages — that is not beautify. This includes re-pagination for fit: "keep every word but split a crowded page so it reads better" changes page count, so it is the main pipeline, not beautify. Convert the deck with [`ppt_to_md`](../scripts/source_to_md/ppt_to_md.py) and run the main SKILL.md pipeline, where the Strategist re-architects the outline freely from the extracted content. The deciding question: is the source's page split information to preserve, or just the previous author's structure to improve? Preserve → beautify (here); improve → `ppt_to_md` + main pipeline. --- @@ -43,7 +43,7 @@ Re-lays-out an existing `.pptx`: the text is preserved **verbatim**, the source ## 3. Create the Project Workspace -Match the canvas to the source so 1:1 pages and paste-back align. Determine the source aspect first — before the project exists, run `beautify_identity.py ` to **stdout** and read `canvas.aspect` (the formal `analysis/identity.json` is written in Step 4, after `init`) — then `init` with the matching format: +Match the canvas to the source so 1:1 pages and paste-back align. Determine the source aspect first — before the project exists, run `beautify_identity.py ` to **stdout** and read `canvas.aspect` (the formal standard intake bundle is written in Step 4, after `init`) — then `init` with the matching format: | Source aspect | Format | |---|---| @@ -60,15 +60,15 @@ python3 ${SKILL_DIR}/scripts/project_manager.py import-sources /sources/ -o /analysis +``` **Content + images — already produced by Step 3.** `import-sources` ran `ppt_to_md` on the deck, so the **frozen content contract** is `sources/.md` (one source slide per block, in order). If the source deck contains pictures, they are already propagated to `images/` with per-slide binding in `images/image_manifest.json` (`occurrences[].slide_index`). Do **not** re-run `ppt_to_md` — it would duplicate the conversion and write images to `analysis/_files/` instead of `images/`. -**Visual identity (theme + observed sample + canvas)**: - -```bash -python3 ${SKILL_DIR}/scripts/beautify_identity.py /sources/ -o /analysis/identity.json -``` +**Visual identity (theme + observed sample + canvas)**: read `/analysis/.identity.json` (intake prefixes per-deck artifacts by source-file stem). | Field | Use | |---|---| @@ -79,20 +79,16 @@ python3 ${SKILL_DIR}/scripts/beautify_identity.py /sources/ Note: `theme` is what the deck declares; `observed` is a frequency sample of run-level overrides (not a complete style resolution — it misses `schemeClr` and master/layout inheritance, and counts chart/gradient fills). A hand-edited deck can diverge from `theme` — Step 5 recommends which to inherit and the user confirms. -**Chart + table data (for regeneration)**: read the source's chart and table *data* so they can be redrawn natively in the inherited style: +**Chart + table data (for regeneration)**: read `/analysis/.slide_library.json`. It contains the source chart and table *data* so they can be redrawn natively in the inherited style: -```bash -python3 ${SKILL_DIR}/scripts/template_fill_pptx.py analyze /sources/ -o /analysis/slide_library.json -``` - -| `slide_library.json` field | Use | +| `.slide_library.json` field | Use | |---|---| | `slides[].charts[]` (`chart_type` / `categories` / `series[].values`) | regenerate as a native SVG chart via the `§VII` `templates/charts/` path | | `slides[].tables[]` (`row_count` / `column_count` / cell text) | regenerate as a native SVG table | **Hard rule — regenerate visuals, do not carry them over**: charts / tables / images are rebuilt from their data in the inherited style, never spliced in byte-for-byte. This keeps the deck style-consistent and natively editable. **Data values are frozen** (categories / series / cell text / numbers unchanged); only their rendering is the deck's own. Pictures (`ppt_to_md`-extracted files) are reused but re-laid-out — position / crop / size follow the new layout, not the source slot. A user who wants an original element verbatim copies it across themselves. -**Optional source-SVG visual reference**: when the source deck has complex vector decoration, distinctive page chrome, or a visual language that cannot be captured by `identity.json` colors/fonts alone, create a read-only SVG reference package under `analysis/`. This is for understanding style only; it is not a carry-over asset path. +**Optional source-SVG visual reference**: when the source deck has complex vector decoration, distinctive page chrome, or a visual language that cannot be captured by `.identity.json` colors/fonts alone, create a read-only SVG reference package under `analysis/`. This is for understanding style only; it is not a carry-over asset path. ```bash python3 ${SKILL_DIR}/scripts/pptx_to_svg.py /sources/ -o /analysis/source_svg_import @@ -110,7 +106,7 @@ Optional reuse gate: if a candidate is a non-text brand/logo/motif/decorative as **Assemble the inventory** — the deterministic join into one per-slide ledger, `analysis/beautify_inventory.json`, the contract Step 5 confirms and Step 7 verifies against: ```bash -python3 ${SKILL_DIR}/scripts/beautify_inventory.py /analysis/slide_library.json \ +python3 ${SKILL_DIR}/scripts/beautify_inventory.py /analysis/.slide_library.json \ --images /images/image_manifest.json -o /analysis/beautify_inventory.json ``` @@ -125,8 +121,9 @@ If `images/image_manifest.json` does not exist because the source deck has no ex ## ✅ Extraction Complete - [x] `sources/.md` (from Step 3) holds every source slide's text, in order; extracted pictures, if any, are in `images/` + `images/image_manifest.json` -- [x] `analysis/identity.json` has theme + observed identity + canvas aspect -- [x] `analysis/slide_library.json` holds chart + table data for regeneration +- [x] `analysis/.identity.json` has theme + observed identity + canvas aspect +- [x] `analysis/.slide_library.json` holds chart + table data for regeneration +- [x] `analysis/source_profile.json` (multi-deck index) summarizes the source facts in its `decks[]` entry - [x] `analysis/beautify_inventory.json` ledgers per-slide text / images / data + ignored + needs-confirmation - [ ] **Next**: Step 5 — Beautify Plan (recommend & confirm) ``` @@ -138,12 +135,12 @@ If `images/image_manifest.json` does not exist because the source deck has no ex ⛔ **BLOCKING**: the scope is not hard-coded — same spirit as the Eight Confirmations. Recommend each item below from what the deck actually contains (the Step 4 inventory), present the plan, and **wait for the user to confirm or adjust** before writing any spec. Chat is the canonical channel; the confirm UI below is the visual convenience surface over it for the palette + typography review (its result is honored identically to a chat reply). This step has two halves: -- **Visual re-confirm via the confirm UI** — the **full** Step 4 confirm page (below), seeded from the source so every targeted-confirmation field (canvas, mode, visual style, palette, icons, typography incl. body baseline, image strategy, generation mode) is **pre-filled with the inherited / source-derived default and left editable**. Beautify *recommends* keeping the source's identity, but never removes the user's place to override any field — you may choose not to change a value, but you must not deny the place to change it. This is also where the deck's text size is set: `identity.json` carries fonts and palette but **no body font size** (source decks inherit sizes from master placeholders), so the body baseline is undetermined and must be chosen here, not silently defaulted to a small dense value. +- **Visual re-confirm via the confirm UI** — the **full** Step 4 confirm page (below), seeded from the source so every targeted-confirmation field (canvas, mode, visual style, palette, icons, typography incl. body baseline, image strategy, generation mode) is **pre-filled with the inherited / source-derived default and left editable**. Beautify *recommends* keeping the source's identity, but never removes the user's place to override any field — you may choose not to change a value, but you must not deny the place to change it. This is also where the deck's text size is set: `.identity.json` carries fonts and palette but **no body font size** (source decks inherit sizes from master placeholders), so the body baseline is undetermined and must be chosen here, not silently defaulted to a small dense value. - **Structural scope** — the inventory-driven list decisions below (ignored, reuse, needs-confirmation, verification level) stay in **chat**; they have no confirm-UI widget. | Plan item | Recommend from | Default lean | |---|---|---| -| Identity source | `identity.json` `theme` vs `observed` | present **both as color / typography candidates in the confirm UI** so the user picks the one that looks right (theme first when the deck is theme-driven; observed first when slides override heavily) — recommend a default ordering and say why | +| Identity source | `.identity.json` `theme` vs `observed` | present **both as color / typography candidates in the confirm UI** so the user picks the one that looks right (theme first when the deck is theme-driven; observed first when slides override heavily) — recommend a default ordering and say why | | Preserve scope | inventory `text_blocks` / `images` / `charts` / `tables` | all text verbatim; data values frozen; pictures reused | | Ignored | inventory `ignored` | name them so the user sees what drops (hidden / master-only text / image crop / rotation) | | Needs confirmation | inventory `needs_confirmation` | flag complex charts + overcrowded pages explicitly; ask how to handle | @@ -187,7 +184,7 @@ Write `/confirm_ui/recommendations.json` and launch the same confi } ``` -- **Recommend keep, allow override**: pre-fill canvas / mode / visual style / icons / image strategy with the source-faithful default (canvas = Step 3 format, mode = `briefing`, image_usage = `provided` since pictures are reused). Enumerable fields already list every catalog option with the source-faithful one badged, so the user can switch. Beautify's only true non-choices are the frozen text and the strict 1:1 page count (changing those means routing to the main pipeline instead — see CLAUDE.md). +- **Recommend keep, allow override**: pre-fill canvas / mode / visual style / icons / image strategy with the source-faithful default (canvas = Step 3 format, mode = `briefing`, image_usage = `provided` since pictures are reused). Enumerable fields already list every catalog option with the source-faithful one badged, so the user can switch. Beautify's only true non-choices are the frozen text and the strict 1:1 page count (changing those means routing to the main pipeline instead — see CLAUDE.md). The §c material-divergence field is therefore not surfaced here — beautify never reshapes content (text is verbatim). - **Our recommendation is the pre-selected default = the source replica**: for color and typography, author **several candidates** like the from-scratch flow. The pre-selected default (`selected: 0`, the first card) is what beautify recommends — the candidate that **best replicates the source deck's style** (the truest reading of `theme` / `observed`). Replicate-by-default. - **Judge the other alternatives exactly as the from-scratch flow does — fonts as much as colors**: don't invent a beautify-specific rule. Author each non-replica candidate with the **same content-driven judgment the Strategist uses when generating from scratch** (color §e, typography §g), applied to the material this project provides — the source document's content and subject, the company's own theme colors, and any brand signal. Pick the palette **and** the font pairing by what fits *this* deck's content; fonts are chosen by content fit, not just defaulted to a safe face. Reach **≥3 candidates total** (PPT-safe stacks; the same creative-choice rule used elsewhere) so a user who departs from the replica still lands on a considered, content-fitting direction — depart-by-choice. - **`body_size` is the load-bearing field**: seed it from a canvas-appropriate baseline (the page hints ≈2.5–3.3% of canvas height) — for a presentation / projection deck lean toward the relaxed end (e.g. `24` → 18pt), for a dense document deck the compact end (e.g. `18` → 13.5pt). The user sets the final value here; this is what prevents the deck from exporting at an unintentionally small size. @@ -227,7 +224,7 @@ python3 ${SKILL_DIR}/scripts/source_to_md/ppt_to_md.py /exports/.identity.json` colors + fonts | | Paste-back | copying a beautified element into the original deck looks native | ```markdown diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md index f425b5d5d..e8d9f2e49 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md @@ -60,15 +60,15 @@ Use this fixed layout: --- -## Step 3: Extract the Slide Library +## Step 3: Extract the PPTX Intake Bundle Run: ```bash -python3 skills/ppt-master/scripts/template_fill_pptx.py analyze "/sources/" -o "/analysis/slide_library.json" +python3 skills/ppt-master/scripts/pptx_intake.py "/sources/" -o "/analysis" ``` -Read `/analysis/slide_library.json` and identify: +Read `/analysis/.slide_library.json` (intake prefixes per-deck artifacts by the template deck's file stem) and identify: | Field | Use | |---|---| @@ -83,7 +83,7 @@ Read `/analysis/slide_library.json` and identify: A page's layout already encodes a rhetorical shape — a single hero statement, a lead-then-detail split, a 2×2 comparison, a stepwise progression, a metric row. Match the source material's own logic to a page whose structure expresses that same logic; do not pour unrelated content into a slot just because it is empty. When no selected page fits a piece of content well, drop that page or that content rather than forcing it — a forced fill reads as stiff. It is fine to use fewer pages than the source deck has. -**Layout-first planning**: Treat `slide_library.json` as a layout inventory, not as an ordered deck outline. Before writing `fill_plan.json`, infer each reusable source page's affordance from JSON fields: +**Layout-first planning**: Treat `.slide_library.json` as a layout inventory, not as an ordered deck outline. Before writing `fill_plan.json`, infer each reusable source page's affordance from JSON fields: | JSON signal | Layout planning use | |---|---| @@ -104,14 +104,14 @@ A page's layout already encodes a rhetorical shape — a single hero statement, Create a scaffold: ```bash -python3 skills/ppt-master/scripts/template_fill_pptx.py scaffold "/analysis/slide_library.json" -o "/analysis/fill_plan.json" --slides "1,3,4" +python3 skills/ppt-master/scripts/template_fill_pptx.py scaffold "/analysis/.slide_library.json" -o "/analysis/fill_plan.json" --slides "1,3,4" ``` Then edit `/analysis/fill_plan.json` by hand from the source material. The plan is the single execution contract. **Pages are reusable**: the output is the ordered `slides` list, not a one-to-one copy of the source deck. A source page is not single-use — list the same `source_slide` as many times as you need, each entry with its own `replacements`, to drive several output slides from one good layout (e.g., reuse a single content layout for five content pages). Likewise you may omit source pages entirely and put the selected ones in any order. -**Scaffold boundary**: `scaffold --slides` is only a convenience starter. If the final plan needs repeated source pages or a story order that differs from the template order, duplicate / reorder entries in `fill_plan.json` manually or generate the plan from `slide_library.json`; do not let scaffold output constrain the deck structure. +**Scaffold boundary**: `scaffold --slides` is only a convenience starter. If the final plan needs repeated source pages or a story order that differs from the template order, duplicate / reorder entries in `fill_plan.json` manually or generate the plan from `.slide_library.json`; do not let scaffold output constrain the deck structure. The plan structure: @@ -169,6 +169,7 @@ The plan structure: | Empty slots | Use `scaffold --include-empty` only when a real placeholder is empty in the source deck | | Native tables | Keep the original table row and column count; this workflow edits existing cells, not table structure | | Native charts | Each series `values` list must match the category count; this workflow edits chart data, not chart styling | +| Multi-plot / combo charts | Not supported for direct `chart_edits`; `check-plan` reports an error rather than silently writing the wrong plot. Use beautify / main pipeline to redraw them, or leave the native chart untouched. | | Facts | Every substantive claim must come from the user material | **Fit check before apply**: @@ -206,7 +207,7 @@ Example `notes` value for a Chinese content slide: Run the data-based capacity check before applying the plan: ```bash -python3 skills/ppt-master/scripts/template_fill_pptx.py check-plan "/analysis/slide_library.json" "/analysis/fill_plan.json" -o "/analysis/check_report.json" +python3 skills/ppt-master/scripts/template_fill_pptx.py check-plan "/analysis/.slide_library.json" "/analysis/fill_plan.json" -o "/analysis/check_report.json" ``` Interpret the report: @@ -278,7 +279,7 @@ If the extracted text is correct but visual overflow is likely, reduce the text ```markdown ## ✅ Template Fill Complete -- [x] `slide_library.json` extracted from the source PPTX +- [x] Standard PPTX intake extracted from the source deck, including `.slide_library.json` - [x] `fill_plan.json` selects only pages that fit the target story - [x] `check-plan` run and capacity warnings resolved or explicitly accepted - [x] Output PPTX generated through direct OOXML text replacement @@ -303,3 +304,4 @@ If the extracted text is correct but visual overflow is likely, reduce the text | Edit chart formatting / axes / legend layout | Not in v1 | | Edit SmartArt deeply | Not in v1 | | Automatic visual overflow detection | Not in v1; use text-capacity judgment from the library slots | +| Material-divergence reshaping (§c content strategy) | Not applicable — this workflow fills text into existing slots, it does not author an outline from a source, so the main pipeline's `content_divergence` free-text field has no role here |