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.
This commit is contained in:
KeyInfo Bot
2026-07-21 00:01:29 +08:00
parent 93eeb27a5c
commit db8558fa17
326 changed files with 43415 additions and 17908 deletions
+21 -21
View File
@@ -9,14 +9,23 @@
"commit": "d884ae04edebef577e82ff7c4e143debd0bbec99",
"syncedAt": "2026-07-03T16:00:00Z"
},
{
"id": "superpowers-zh",
"pluginName": "superpowers-zh",
"repo": "https://github.com/AreChen/superpowers-zh.git",
"ref": "main",
"adapter": "codex-plugin",
"commit": "c51f23adcd482fd908aa60928f2ece34d12f7768",
"syncedAt": "2026-07-14T02:27:30Z"
},
{
"id": "oh-my-codex",
"pluginName": "oh-my-codex",
"repo": "https://github.com/Yeachan-Heo/oh-my-codex.git",
"ref": "main",
"adapter": "codex-plugin",
"commit": "fce27bfd6c17c7665a6f1505b6b8384cc2c8edd5",
"syncedAt": "2026-07-12T16:00:00Z"
"commit": "435d4a9cc982ffaf83fabbfbb8711ae6c178ffca",
"syncedAt": "2026-07-19T16:00:01Z"
},
{
"id": "ui-ux-pro-max",
@@ -24,8 +33,8 @@
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "0468898265b1552e19f96e9da448e971b6d325fd",
"syncedAt": "2026-07-13T16:00:00Z"
"commit": "b484e8338c25b9cea3a25981a992d2817188971a",
"syncedAt": "2026-07-20T16:00:00Z"
},
{
"id": "caveman",
@@ -42,8 +51,8 @@
"repo": "https://github.com/Leonxlnx/taste-skill.git",
"ref": "main",
"adapter": "skill-collection",
"commit": "b17742737e796305d829b3ad39eda3add0d79060",
"syncedAt": "2026-07-07T02:43:19Z"
"commit": "7c397f22d3af6f2b3f1925eb147d8e8801086151",
"syncedAt": "2026-07-18T16:00:01Z"
},
{
"id": "shadcn",
@@ -51,8 +60,8 @@
"repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "bb8ba0daceb717eefc3a6ecb3457f28ca770d138",
"syncedAt": "2026-07-13T16:00:00Z"
"commit": "20442886c5cfb440441c35030462fbdf64838655",
"syncedAt": "2026-07-20T16:00:00Z"
},
{
"id": "frontend-slides",
@@ -87,8 +96,8 @@
"repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "465e3b4149b852d33ddd1cb94ac059401fe4e823",
"syncedAt": "2026-07-13T16:00:00Z"
"commit": "b05ac551468098ed7b4c1c21d9d0d413b4230c79",
"syncedAt": "2026-07-20T16:00:00Z"
},
{
"id": "next-skills",
@@ -96,17 +105,8 @@
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"adapter": "skill-collection",
"commit": "41b823880d688b3e9f9aae3323b08ddd457240c2",
"syncedAt": "2026-07-13T16:00:00Z"
},
{
"id": "superpowers-zh",
"pluginName": "superpowers-zh",
"repo": "https://github.com/AreChen/superpowers-zh.git",
"ref": "main",
"adapter": "codex-plugin",
"commit": "c51f23adcd482fd908aa60928f2ece34d12f7768",
"syncedAt": "2026-07-14T02:27:30Z"
"commit": "d3e41fb496aeddeb448ba454bd663c36ea8183a5",
"syncedAt": "2026-07-20T16:00:00Z"
}
]
}
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作",
"version": "20260605",
"keySource": "none",
"syncedAt": "2026-07-13T16:01:28Z"
"syncedAt": "2026-07-20T16:01:28Z"
}
@@ -2,8 +2,8 @@
"sourceId": "next-skills",
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"commit": "41b823880d688b3e9f9aae3323b08ddd457240c2",
"commit": "d3e41fb496aeddeb448ba454bd663c36ea8183a5",
"adapter": "skill-collection",
"sourcePath": "skills",
"syncedAt": "2026-07-13T16:00:00Z"
"syncedAt": "2026-07-20T16:00:00Z"
}
@@ -23,28 +23,30 @@ Enable Cache Components on an app and walk it to a passing build. This skill seq
- **No passing baseline before the flag.** If the app already uses `"use cache"`, the pre-flag build errors with `please enable the feature flag cacheComponents`. Enabling the flag is the first thing you do (in Incremental, before the codemod; in Direct, before fixing routes) — not a thing to do _after_ getting a passing build. Note this in your starting summary so it doesn't read as a regression.
- **Offline docs.** Offline copies of guide links live under `node_modules/next/dist/docs/`, with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/migrating-to-cache-components.md`). The trailing filename matches the slug. If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled. If offline docs are missing entirely, run `npx @next/codemod@latest agents-md` to write a version-matched index into `AGENTS.md` / `CLAUDE.md`.
- **Offline docs.** Guide links have offline copies under `node_modules/next/dist/docs/` (bundled since Next.js 16.2), with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/migrating-to-cache-components.md`). If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled.
- **Older versions without bundled docs.** Suggest `npx @next/codemod@latest agents-md` to the user before starting: it downloads a version-matched copy to `.next-docs/` and writes an index into `AGENTS.md` / `CLAUDE.md`. It touches files in their repo, so ask first and run it only if they want it.
## the shape of the work
There's one loop: walk the route tree top-down, one feature at a time, adopting each route against `next dev` + a browser. The build is a final check for each feature, not the working surface.
The choice in step 1 is whether to silence the validation errors first or fix them as you go. Either way the loop is the same:
The choice in step 1 is whether to opt every route out of validation first or fix routes as you go. Either way the loop is the same:
- **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. Picks the work apart into small reviewable PRs.
- **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. This splits the work into small, reviewable PRs.
- **Without (Direct).** Enable `cacheComponents` and start the loop on whatever the build flags first. Same loop, but every fix sits on one branch until adoption is complete.
In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature. Expect to spend most of the time in the loop, not in the pre-step.
## background
`cacheComponents: true` requires every route to be prerenderable. A route that reads request-time data outside `<Suspense>` is "blocking" and fails the build. `export const instant = false` marks a route as allowed to block, which clears it in both dev and build; on a layout it covers the whole subtree beneath it. Reads wrapped in a [`"use cache"`](https://nextjs.org/docs/app/api-reference/directives/use-cache) function count as cache boundaries, not blocking reads.
`cacheComponents: true` requires every route to be prerenderable. A route that reads request-time data outside `<Suspense>` is "blocking" and fails the build. `export const instant = false` marks a route as allowed to block, which clears it in both dev and build; on a layout it covers the whole subtree during the build, but client navigations still validate each descendant segment on its own. Reads wrapped in a [`"use cache"`](https://nextjs.org/docs/app/api-reference/directives/use-cache) function count as cache boundaries, not blocking reads.
Three classes of blocker bite agents in this order:
Three classes of blocker come up, usually in this order:
1. **Request-time reads** (`cookies()`, `headers()`, `await params`, `await searchParams`). All four block when awaited at the top of a page or layout. `params` and `searchParams` often get missed because they're not framed as "request data" the way cookies and headers are. The fix is to push the read into a `<Suspense>`-wrapped child — and for `params`/`searchParams`, forward the promise into the child and await it there; don't `await` at the page top.
2. **Sync-IO at module/render time** (`new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()`). These fail the build even with `instant = false` — the opt-out doesn't suppress them. If they're in a shared layout, they block every route under it. The codemod can't fix them; you have to translate each one (cache it with `"use cache"` if it's stable, or wrap it in `await connection()` + `<Suspense>` if it's per-request) before the build can pass. Grep the whole repo for these calls before running anything else.
3. **`"use cache"` files that read request data.** A file with a top-level `"use cache"` directive can't export `instant`; combining the two errors with `Only async functions are allowed to be exported in a "use cache" file.` and means the directive was wrong for that route. Remove it before running the codemod.
2. **Sync-IO at module/render time** (`new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()`). These fail the build even with `instant = false` — the opt-out doesn't suppress them. If they're in a shared layout, they block every route under it. The codemod can't fix them; you have to translate each one by hand before the build can pass (see the [incremental pre-step](#incremental)). Grep the whole repo for these calls before running anything else.
3. **`"use cache"` files that read request data.** A file with a top-level `"use cache"` directive can't export `instant`; combining the two errors with `Only async functions are allowed to be exported in a "use cache" file.`, which means the directive was wrong for that route. Remove it before running the codemod.
## working surfaces
@@ -52,8 +54,8 @@ Three classes of blocker bite agents in this order:
Prefer `next dev` over `next build` while you work.
- **`next dev`** — the working surface. Visit a route; its blocking errors surface in the dev overlay with full stack traces and fix cards linking the per-error docs. Work one route at a time — errors don't accumulate in one place. The route itself still returns HTTP 200, so read the overlay (or `.next-dev.log`), not status codes. A cleared overlay is one half of route-clean — the other half is browser verification (see [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time)) and a passing build for that route.
- **`next build`** — detection only. The build is `next dev`'s authoritative check, not its replacement. Use it as the last gate on each feature in the loop (a passing build is part of the per-route success bar) and as the final verification across the whole app. In Incremental, the build also confirms the pre-step (codemod opted every route out, no shared layout still has a sync-IO blocker) before you ship that PR. Don't reach for the build instead of the dev loop while you're working a route — a clean compile error doesn't tell you what ended up in the static shell vs streamed. By default the build stops at the first blocking route, so it's also poor for sizing the work. Two flags help when iterating: `--debug-build-paths` builds only the routes you name (comma-separated glob patterns of file paths relative to the project root, e.g. `--debug-build-paths="app/admin/**/page.tsx"` — not URL paths; `--debug-build-paths="app/(marketing)/about/page.tsx"` — not `/about`; `--debug-build-paths="app/admin"` matches nothing and silently builds zero routes), and `--debug-prerender` disables the early exit so the build continues past the first prerender failure, reports every blocking route, and prints a fuller stack trace that names the originating file and line.
- **`next dev`** — the working surface. Visit a route; its blocking errors surface in the dev overlay with full stack traces and fix cards linking the per-error docs. Work one route at a time — errors don't accumulate in one place. The route itself still returns HTTP 200, so read the overlay (or `.next-dev.log`), not status codes. A cleared overlay is one half of calling a route clean — the other half is browser verification (see [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time)) and a passing build for that route.
- **`next build`** — detection only. The build is `next dev`'s authoritative check, not its replacement. Use it as the last gate on each feature in the loop (a passing build is part of the per-route success bar) and as the final verification across the whole app. In Incremental, the build also confirms the pre-step (codemod opted every route out, no shared layout still has a sync-IO blocker) before you ship that PR. Don't reach for the build instead of the dev loop while you're working a route — a passing compile doesn't tell you what ended up in the static shell and what streamed. By default the build stops at the first blocking route, so it's also poor for sizing the work. Two flags help when iterating: `--debug-build-paths` builds only the routes you name (comma-separated glob patterns of file paths relative to the project root, e.g. `--debug-build-paths="app/admin/**/page.tsx"` — not URL paths; `--debug-build-paths="app/(marketing)/about/page.tsx"` — not `/about`; `--debug-build-paths="app/admin"` matches nothing and silently builds zero routes), and `--debug-prerender` disables the early exit so the build continues past the first prerender failure, reports every blocking route, and prints a fuller stack trace that names the originating file and line.
Every blocking error has a docs page — open it. Both the dev overlay and the build terminal print a `https://nextjs.org/docs/messages/<slug>` link with each error. That page is the canonical recipe for the fix; the inline message is a summary. Fetch the link for every distinct error you encounter, even if you think you know the pattern — the recipes evolve, and the same error class can have different correct fixes depending on what the route reads. Don't improvise from the inline message alone. (`/docs/messages/*` pages aren't bundled offline; if you have no network, fall back to the per-API guides under `node_modules/next/dist/docs/` and note the limitation when you report back.)
@@ -75,7 +77,7 @@ In preference order:
**Requires Turbopack.** If `package.json`'s `dev` script passes `--webpack`, flag it to the user and ask whether there's a reason to stay on webpack. If not, switch to Turbopack (the Next.js 16.3+ default). If they want to keep webpack, skip this install and use the [build-only loop](#the-loop-build-only-fallback) instead.
You don't need permission to install `next-dev-loop` itself. It's a tool, like installing a dev dependency. If a user is on the line, briefly tell them you're installing it for verification. In a non-interactive run (CI, dashboard, sandbox), install it without asking — "can't prompt the user" is not a reason to skip. The only legitimate skip is a real technical blocker: no network, no npm, read-only filesystem, a stated no-new-deps policy, or a webpack-only dev script. If you skip, name the specific blocker in your final report.
You don't need permission to install `next-dev-loop` itself. It's a tool, like installing a dev dependency. If a user is present, briefly tell them you're installing it for verification. In a non-interactive run (CI, dashboard, sandbox), install it without asking — "can't prompt the user" is not a reason to skip. The only legitimate skip is a real technical blocker: no network, no npm, read-only filesystem, a stated no-new-deps policy, or a webpack-only dev script. If you skip, name the specific blocker in your final report.
2. **A browser you can drive yourself.** Playwright, `agent-browser` directly, any browser-automation tool. Use only when `next-dev-loop` is genuinely blocked. You'll miss the framework-side checks (`/_next/mcp`), so DOM assertions alone don't catch every regression — be more cautious about what you call "verified."
@@ -85,7 +87,7 @@ In preference order:
## step 1: choose a strategy
Ask the user. Phrase it as a PR-shape question, not a sizing call. Never use the internal labels (Incremental, Direct, milestone A) when talking to the user — those are your own scaffolding. Ask in terms of PRs and features, e.g.: _"Do you want me to first open a PR that turns on Cache Components and opts every route out of validation, then handle the actual route adoptions feature-by-feature in follow-up PRs? Or do everything on one branch?"_ Even on a tiny app, the incremental path still has value (review-sized PR, revertible, the `// TODO: Cache Components adoption` markers double as your work queue for next session). Don't pick on their behalf.
Ask the user, in terms of the PRs they want, not the size of the job. Never use the internal labels (Incremental, Direct) when talking to the user — those are your own scaffolding. Ask in terms of PRs and features, e.g.: _"Do you want me to first open a PR that turns on Cache Components and opts every route out of validation, then handle the actual route adoptions feature-by-feature in follow-up PRs? Or do everything on one branch?"_ Even on a tiny app, the incremental path still has value (review-sized PR, revertible, the `// TODO: Cache Components adoption` markers double as your work queue for next session). Don't pick on their behalf.
If there's no user to ask, default to **Incremental** and document the choice.
@@ -96,7 +98,13 @@ If there's no user to ask, default to **Incremental** and document the choice.
Before invoking the codemod, fix the two classes of blocker it can't.
1. **Sync-IO at module/render time.** Grep the whole repo for `new Date()`, `Date.now()`, `Math.random()`, and `crypto.randomUUID()` (not only `app/**/layout.{js,jsx,ts,tsx}` — the read might live in any component imported by a layout). Translate each match using the recipe from its `blocking-prerender-*` error card.
1. **Sync-IO at module/render time.** Grep the whole repo for `new Date()`, `Date.now()`, `Math.random()`, and `crypto.randomUUID()` (not only `app/**/layout.{js,jsx,ts,tsx}` — the read might live in any component imported by a layout). Unblock each match with the `await connection()` + `<Suspense>` fix from its `blocking-prerender-*` error card: it defers the value to request time, exactly as it behaved before the migration, so it needs no product decision. Add this exact comment on the line above the `await connection()`:
```tsx
// TODO: Cache Components adoption. Added to unblock the build: remove this connection() to re-trigger the error and review the fix options.
```
It shares the `TODO: Cache Components adoption` prefix with the comments the codemod writes, so the check-in grep finds both. Removing the `await connection()` makes the error fire again with its fix cards — the same motion as removing an opt-out in the loop.
2. **Incompatible segment configs.** Grep for `^export const (revalidate|dynamic|fetchCache)` across `app/` and translate per the `requires` note above. The codemod does not touch them; leaving them in place fails the build after the codemod.
@@ -130,7 +138,7 @@ Synthetic routes like `/_not-found` have no user file — when they block, fix t
### end of the pre-step: check in
Incremental only. Stop here before starting step 2 — the pre-step is the shippable PR. Talk to the user in their language; don't say "milestone A" or "Incremental"; talk about adoption, PRs, and what the app does now. Tell them:
Incremental only. Stop here before starting step 2 — the pre-step is the shippable PR. Talk to the user in their language; don't say "Incremental" or other internal labels; talk about adoption, PRs, and what the app does now. Tell them:
- What you did: turned on Cache Components, ran the codemod that opts every page and layout out of the new validation (or did it by hand), fixed any blockers the codemod can't (list them), confirmed the build passes.
- What changed: every page and layout in `app/` now exports `instant = false` with a `// TODO: Cache Components adoption` comment, except client components and any that already had an `instant` export.
@@ -178,7 +186,7 @@ Per route:
### loop notes
- The [three blocker classes from background](#background) often get missed when fixing in place. Caching a downstream fetch (`getThing(id)`) doesn't clear an `await params` at the top of the page body — push the param promise into the `<Suspense>`-wrapped child.
- Ambiguous calls are user check-ins, not agent judgment. When you're not sure which fix fits, the blocking code looks security-sensitive, or the user might want to keep the route blocking on purpose — read [references/per-page-decisions.md](./references/per-page-decisions.md) before editing.
- Ambiguous calls are user check-ins, not agent judgment. When you're not sure which fix fits, the blocking code looks security-sensitive, or the user might want to keep the route blocking on purpose — read [references/per-page-decisions.md](./references/per-page-decisions.md) before editing. Show the route while you ask: the `next-dev-loop` session runs the browser headed, so drive to the page and leave it on screen so the user is looking at the thing they're deciding about, with a screenshot as the fallback when a headed browser isn't possible. "Should this stay blocking?" is much easier to answer while looking at the page than at a file path.
- Don't narrate the refactor with comments. The only comment the codemod (or you) should leave is `// TODO: Cache Components adoption` on opt-outs, and the user's existing comments. Don't annotate every `<Suspense>` boundary or `"use cache"` call with what it does — the code says that. Drop a comment only when the _why_ isn't clear from the code (e.g. a deliberate Block with a reason).
Keep a todo list of the feature's routes. When every route in the feature is clean, move to step 3.
@@ -188,14 +196,15 @@ Keep a todo list of the feature's routes. When every route in the feature is cle
Checklist before checking in with the user:
- `next build` completes without blocking-route errors.
- No bare `// TODO: Cache Components adoption` opt-outs in the feature (`grep` to confirm). Any `instant = false` left behind is a deliberate, documented Block — comment rewritten to a reason (see [references/per-page-decisions.md](./references/per-page-decisions.md) → "when to leave a Block in place").
- No bare TODOs in the feature: `grep -rn "TODO: Cache Components adoption"` finds both the codemod's opt-out comments and the sync-IO unblocks from the pre-step. Any `instant = false` left behind is a deliberate, documented Block — comment rewritten to a reason (see [references/per-page-decisions.md](./references/per-page-decisions.md) → "when to leave a Block in place"). Any `await connection()` left behind has been reviewed and kept on purpose, not left over from the pre-step.
- Each route visited in the browser: confirm the static shell renders first and every `<Suspense>` fallback resolves to its real content. Capture both states if you can — the fallback (mid-stream) and the final paint — so you have a streaming-experience demo to show the user. Throttle the network in the browser if streaming is too fast to observe.
Then check in with the user. Same rule as the pre-step: speak their language. Don't say "milestone B" or "featurebyfeature loop"; talk about the feature you adopted and what the user will see.
Then check in with the user. Same rule as the pre-step: speak their language. Don't say "feature-by-feature loop" or other internal labels; talk about the feature you adopted and what the user will see.
- What you did: which routes you touched, and the user-visible result per route (e.g. "the post page now streams the article body behind a skeleton while the layout stays static").
- What changed: opt-outs removed, fallbacks added, caching boundaries introduced.
- Show, don't tell. If the browser is running, drive the route live for the user so they see the static shell → fallback → final content sequence in real time. If you can't drive a live browser, attach the before/after screenshots you captured instead.
- Show, don't tell. The `next-dev-loop` session runs the browser headed, so drive the route live for the user so they see the static shell → fallback → final content sequence in real time. If you can't drive a live browser, attach the before/after screenshots you captured instead.
- Give them the click-through: a short table of the feature's routes — the URL to open and what to look for (what renders instantly, which fallbacks appear, what streams in) — so they can verify each one themselves.
- The question: "Want to open this feature as a PR and move on to the next, or stop here?" Wait for the answer.
**Trivial features can skip the check-in.** If adopting a feature only meant removing its `// TODO: Cache Components adoption` opt-out (no `<Suspense>` added, no `'use cache'` introduced, no render order change), the user sees nothing different. Move on to the next feature without stopping; mention it in passing the next time you do check in.
@@ -204,7 +213,7 @@ When the loop has run on every feature — every remaining `instant = false` sit
### route table glyphs
`ƒ` → `` is the adoption landing. `◐ (Partial Prerender)` means a static shell prerenders and the request-time content streams in — the goal state for any route that reads `cookies()`, `headers()`, `params`, or `searchParams`. Some routes legitimately stay `ƒ` when they do request-time work through a documented escape hatch (e.g. a layout that uses `await connection()`); the page is no longer _opted out_, it's genuinely dynamic. Don't rip the escape hatch back out chasing a ``. The inverse holds: `instant = false` does not force a route to be `ƒ`. The glyph reflects what the route does at prerender time, not which validation knobs it exports.
`ƒ` → `` is where adoption usually lands. `◐ (Partial Prerender)` means a static shell prerenders and the request-time content streams in — the goal state for any route that reads `cookies()`, `headers()`, `params`, or `searchParams`. Some routes legitimately stay `ƒ` when they do request-time work through a documented escape hatch (e.g. a layout that uses `await connection()`); the page is no longer _opted out_, it's genuinely dynamic. Don't remove the escape hatch only to chase a ``. The inverse holds: `instant = false` does not force a route to be `ƒ`. The glyph reflects what the route does at prerender time, not which validation knobs it exports.
`` tells you a shell exists, not what's in it. A `<Suspense>` boundary placed too high (e.g. wrapping the entire page body, or `<Suspense fallback={null}>` around the article content) pushes the visible content out of the static shell into the streamed payload; the build still reports `` because _some_ shell prerendered (often only `<html><body>` with framework markup). The route table can't tell you what's in the shell; a browser can. If the shell is empty and everything streams, pull the `<Suspense>` boundary down closer to the actual dynamic read.
@@ -213,7 +222,7 @@ When the loop has run on every feature — every remaining `instant = false` sit
The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill.
- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — dev-only validation warnings the overlay raises on client navigation. Same shape as the blocking-prerender errors you cleared in step 2; the guide covers the per-warning details. Recommend it next if the user wants navigations to actually be instant (a passing build doesn't guarantee that — a `<Suspense>` above the shared layout caught the page-load case but doesn't cover client navigation).
- [Adopting Partial Prefetching](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) — walks an audit of `<Link prefetch={true}>` calls driven by the dev overlay's `link-prefetch-partial` warning, then flips the `partialPrefetching` config. Walk the audit first, with the flag off — flipping it before the audit makes every route count as adopted, so the warnings never fire and the per-link signal is lost. The biggest payoff of Cache Components: `<Link>` prefetches only the static App Shell by default. Recommended after instant navigation, since its fixes feed directly into how much of each route the shell can prefetch.
- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it audits `<Link prefetch={true}>` calls (driven by the dev overlay's `link-prefetch-partial` warning) with the flag off first, then flips the `partialPrefetching` config. It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching).
- [Prefetching](https://nextjs.org/docs/app/guides/prefetching) and [Runtime prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) — broader prefetching reference. Runtime prefetching extends the static shell with per-session content; reach for it when a route's shell is too thin to be useful and Partial Prefetching alone doesn't cover the gap.
- [Locking the result in with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption.
@@ -0,0 +1,123 @@
---
name: next-partial-prefetching-adoption
description: >
Turn on Partial Prefetching in a Next.js app and work through the
insights it surfaces. Use when the user wants to enable or adopt
Partial Prefetching, flip the `partialPrefetching` flag, opt routes
in with `export const prefetch = 'partial'`, audit
`<Link prefetch={true}>` calls, or resolve the
link-prefetch-partial and instant-shell-url-data insights.
---
# next-partial-prefetching-adoption
Enable Partial Prefetching and walk the app until every link reuses a shared App Shell. This skill sequences the work; per-insight recipes live in the dev overlay fix cards and their docs pages. The [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) is the canonical reference for the concepts this skill applies.
The one thing that shapes everything below: **these insights surface only in `next dev`, in the dev overlay's Insights tab.** Nothing fails the build. There is no build-only fallback loop for this skill — the work is a sweep of the running app in the browser. If you can't drive a browser, stop and tell the user what you can't verify, or commit the milestone you've reached and hand off.
Talk to the user in terms of what they'll see — PRs, features, and how the app behaves after — never the insight slugs or step labels. Before you start, tell them briefly what Partial Prefetching changes: a `<Link>` loads a shared App Shell, and `prefetch={true}` no longer prefetches everything the old full prefetch did.
## requires
- **Cache Components already adopted.** `partialPrefetching` only works with `cacheComponents: true`, and the sweep below assumes the app has no blocking-route errors left: a route whose static shell fails validation surfaces the blocking-prerender error _instead of_ the prefetch insight, so unfinished adoption hides exactly the signal this skill works from. Run [`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption) to completion first — this skill is its follow-up.
- **Next.js 16.3 or later.** `partialPrefetching`, the `prefetch` route segment config, and the prefetch insights all land there.
- **A browser you can drive.** Install [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) before starting (`npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop`). Link prefetches fire when a link renders and enters the viewport, and shell validation fires on navigation — neither is reachable from `curl` or the build. If the app is webpack-pinned, drive a browser directly (`agent-browser`, Playwright) — you lose the framework cross-checks, not the insights; they're still in the overlay and the dev log.
### notes
- **Offline docs.** Guide links have offline copies under `node_modules/next/dist/docs/` (bundled since Next.js 16.2), with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/adopting-partial-prefetching.md`). If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled.
- **Older versions without bundled docs.** Suggest `npx @next/codemod@latest agents-md` to the user before starting: it downloads a version-matched copy to `.next-docs/` and writes an index into `AGENTS.md` / `CLAUDE.md`. It touches files in their repo, so ask first and run it only if they want it.
## background
Adopting Partial Prefetching means every route still delivers what its links prefetched before, now split between the App Shell (static and cached content) and the per-link runtime data behind `prefetch = 'allow-runtime'`. The [guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) is the canonical reference for what a prefetch contains and how to decide each case; this skill sequences that work against a running app.
## working surfaces
- **The dev server terminal — your primary record.** Each validated route's insights are logged as `Error: Route "...": Next.js encountered ...` lines with the `https://nextjs.org/docs/messages/<slug>` link. Tail the dev log during the sweep; it's the greppable record of what fired where, and it works the same on Turbopack and webpack.
- **The dev overlay Insights tab.** Insights are the amber, non-blocking tab. It appears only once an insight has fired, so a route that surfaces nothing shows no tab at all — that's the clean state, not a missing feature. Don't hunt for the tab on a quiet route; confirm clean from the dev log above, which is the reliable signal. The precondition is no blocking-prerender errors — those replace the insight on their route (see requires). An unrelated Issue (a hydration error, a console error) doesn't block the sweep; don't stall on it. When the tab is present, the overlay pill shows the count and each insight has fix cards linking its docs page. The overlay renders inside a shadow root (`nextjs-portal`), so accessibility-tree snapshots don't see it — evaluate into `shadowRoot` when you need to read or click it programmatically.
- **`next-dev-loop`** to drive navigations and read the overlay. Prefer it over hand-rolled browser automation for the same reasons as in the Cache Components skill (webpack apps: see requires).
Every insight has a docs page — open it. Fetch the linked page for every distinct insight you encounter; the inline message is a summary, the page is the recipe.
## step 1: audit `<Link prefetch={true}>` (before enabling)
If `partialPrefetching: true` is already set in `next.config.ts`, the app is adopted — skip to [step 3](#step-3-sweep-for-url-data-insights-after-enabling). Otherwise work the audit with the global flag **off**, adopting each destination with `export const prefetch = 'partial'` — enabling the flag first would mark every route adopted and silence the [`link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) insight this audit runs on. Ask the user how to ship it, in the language of PRs:
- **One branch** — the whole audit in one change, with the flag enabled and the codemod run at the end (step 2).
- **Route by route** — each adopted destination ships as its own PR. The insight still fires for the destinations you haven't reached, a live worklist, and step 2 comes after the last one.
The work is identical either way — only the commit boundaries differ. Default by app size: one branch for a handful of links, route by route when the audit is big enough that reviewers need smaller diffs. Note the choice in your report.
Enumerate the links across the whole source tree, not only `app/` — they often live in `src/components` or shared UI packages: `grep -rnE '\bprefetch\b' --include='*.tsx' --include='*.jsx' .`, then keep the `prefetch={true}` and bare `prefetch` prop matches (a bare prop is `true`) and drop `prefetch={false}` and other values. If nothing matches, check for a custom link wrapper before calling the audit empty — grep for `from 'next/link'`, and if a wrapper sets `prefetch={true}` internally or forwards it under another prop name, audit its call sites the same way. If there's still nothing, say so in your report and move on to [step 2](#step-2-enable-the-flag).
Then, for each one:
1. **Click it** in `next dev`. The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it.
2. **Adopt the destination.** Add `export const prefetch = 'partial'`. That clears the insight for every link pointing at it. If the route reads URL data (`params`, `searchParams`), it's a runtime-prefetch candidate for step 5 — keep `prefetch={true}` on its links and mark the route:
```tsx
// TODO(runtime-prefetch): assess with the user (prefetch = 'allow-runtime')
export const prefetch = 'partial'
```
Use that exact prefix so step 5 can grep them back. Don't cache or decide anything for these routes now.
3. **Preserve what that prefetch delivered.** The guide's [audit table](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#auditing-link-prefetchtrue-calls) is the canonical decision — fetch it and apply the matching row rather than re-deriving it. Caching uncached content is the judgment call in that table: trace where the data comes from and what freshness and revalidation it needs, per the [`use cache`](https://nextjs.org/docs/app/api-reference/directives/use-cache) docs, and ask the user when the answer isn't clear-cut. The URL-data routes you marked in the previous item wait for step 5.
## step 2: enable the flag
Once every audited destination has `prefetch = 'partial'`, finish in two moves.
1. **Enable the flag globally.** Set `partialPrefetching: true` in `next.config.ts` (alongside `cacheComponents: true`). Every route is adopted now, so every link is good.
2. **Strip the redundant `prefetch = 'partial'` exports.** Run the first-party `remove-partial-prefetch` codemod rather than a text find-and-replace. It removes only `export const prefetch = 'partial'` and leaves any other value (a deliberate `prefetch = 'allow-runtime'`) in place, along with your `TODO(runtime-prefetch)` markers, which wait for step 5.
Use the `@canary` channel, not `@latest`. The `remove-partial-prefetch` transform isn't in the stable `@next/codemod` release yet, and `@next/codemod@latest` errors with `Invalid transform choice`.
```bash
npx @next/codemod@canary remove-partial-prefetch ./app
```
The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand by removing `export const prefetch = 'partial'` from every `app/**/{page,layout}.{js,jsx,ts,tsx}` — leave any other `prefetch` value in place, and leave the `TODO(runtime-prefetch)` markers where they are. Don't hand-edit when the codemod can run.
## step 3: sweep for URL-data insights (after enabling)
This is a dev-only second pass. The shell check runs only with the flag on, fires at navigation time, and never blocks the build, so it can happen any time after step 2. The work is one loop — build a route queue from a concrete source (the last `next build` route table, or the `app/` tree), keep it as a todo list, and load every route in `next dev` until the queue is empty.
Watch the Insights tab and the dev log for `Next.js encountered … data` lines. The signal this step adds is [`URL data`](https://nextjs.org/docs/messages/instant-shell-url-data): a `params` or `searchParams` read outside `<Suspense>` ties the shared shell to one URL. It can surface even inside an existing `<Suspense>` when the boundary sits above the read. Open its docs page and follow the fix there.
If Cache Components adoption left gaps, loading routes can also re-surface the blocking-prerender errors from that step — [`runtime data`](https://nextjs.org/docs/messages/blocking-prerender-runtime) (`cookies()`/`headers()`) or [`uncached data`](https://nextjs.org/docs/messages/blocking-prerender-dynamic) (an uncached `fetch`/DB call). Those aren't Partial Prefetching insights; treat them as unfinished Cache Components work and fix them the same way.
These fixes rarely involve the user — each insight names the offending read and its docs page has the fix, so apply it and keep sweeping. Collect the rare exceptions for one batched question at the end: a page that is entirely one URL-dependent region (wrapping it all leaves an empty shell), or a route that should arguably stay opted out. Don't narrate the refactor with comments — the `<Suspense>` boundaries speak for themselves.
## step 4: verify
Checklist before checking in with the user:
- **An empty sweep is the expected outcome when Cache Components adoption finished cleanly** — the prereq already forced every `params`/`searchParams`/`cookies()` read behind `<Suspense>` (surfaced there as `blocking-prerender-*` errors), so a quiet log is success, not a missing signal. Any entry still in the Insights tab is a deliberate, documented decision. To confirm the signal can still fire, check `partialPrefetching` is on, the version is 16.3 or later, and the dev server was restarted after the config change — or move one URL read back outside `<Suspense>`, watch validation fire, then revert. Expect the probe to surface the Cache Components `blocking-prerender-runtime` error rather than the URL-data insight (the upstream check catches the read first) — either one proves the pipeline is alive.
- The App Shells are real: for each route you changed, confirm the first paint after a navigation shows the intended shared content, not an empty shell or a stuck fallback. A `<Suspense>` around the whole page body passes validation with an empty shell, which defeats the point.
- `next build` still passes.
Then check in with the user. Speak their language — no insight slugs or step labels.
- What you did: which links you audited, which destinations you adopted, and what each link now prefetches.
- What changed: dropped props, `use cache` boundaries added, and which routes carry a `TODO(runtime-prefetch)` marker for later.
- Demo against a production run. Prefetching is limited in development, so `next dev` won't show the result — run `next build` and `next start`, and hand the user that URL.
- Show, don't tell: drive one link live in the headed browser against the production server, so they see the shared App Shell paint instantly and the URL-specific region stream in. Attach before/after screenshots only when a live browser isn't possible.
- Give them the click-through: a table of each changed route — the link to click, and what to expect after the click (what paints instantly, what streams in) — so they can verify each result themselves.
- The question: "Want to commit this (or open the PR) before we look at which routes should also prefetch their URL-specific content?" Wait for the answer — adoption and runtime prefetching read best as their own changes.
## step 5: runtime prefetching (optional)
The audit marked the candidates instead of deciding them. Grep for `TODO(runtime-prefetch)` and walk the list with the user in one conversation. The question per route is whether they want the URL-dependent content prefetched ahead of the click, or streaming in after navigation is fine. A runtime prefetch costs a server invocation per prefetchable link — the guide's [per-link prefetching trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) section is the checklist. Don't make these calls alone.
Where the answer is yes, follow the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching) — add `export const prefetch = 'allow-runtime'` to the route (the codemod in step 2 already stripped the `'partial'` export) and cache the content behind the read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Where it's no, delete the marker and leave the route on the default. Either way no `TODO(runtime-prefetch)` marker survives this step. Confirm the opted-in routes against a production run (`next build` and `next start` — the runtime prefetch fires there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR.
## further reading
- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — the broader validation model and loading-state tooling.
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` `instant()` helper locks in what a navigation shows immediately; recommend it once the sweep is clean, since nothing else guards these in CI.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — grows each route's static shell so the App Shell carries more.
@@ -1,6 +1,6 @@
{
"name": "oh-my-codex",
"version": "0.20.1",
"version": "0.20.3",
"description": "oh-my-codex 是 Codex CLI 的多 Agent 编排、结构化工作流、插件级 hooks、MCP 和 HUD 扩展插件。",
"author": {
"name": "Yeachan Heo",
@@ -2,8 +2,8 @@
"sourceId": "oh-my-codex",
"repo": "https://github.com/Yeachan-Heo/oh-my-codex.git",
"ref": "main",
"commit": "fce27bfd6c17c7665a6f1505b6b8384cc2c8edd5",
"commit": "435d4a9cc982ffaf83fabbfbb8711ae6c178ffca",
"adapter": "codex-plugin",
"sourcePath": "plugins/oh-my-codex",
"syncedAt": "2026-07-12T16:00:00Z"
"syncedAt": "2026-07-19T16:00:01Z"
}
@@ -241,6 +241,25 @@ function writeOversizedStopNoop() {
process.exitCode = 0;
}
function writeOversizedToolHookOutput(eventName) {
const systemMessage = 'OMX native hook rejected oversized stdin JSON before parsing; maxBytes=1048576.';
const output = eventName === 'PreToolUse'
? {
systemMessage,
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: systemMessage,
},
}
: {
continue: false,
stopReason: 'native_hook_stdin_oversized',
systemMessage,
};
process.stdout.write(`${JSON.stringify(output)}\n`);
process.exitCode = 0;
}
function writeCompactFallback() {
process.exitCode = 0;
}
@@ -440,6 +459,11 @@ async function main() {
writeOversizedStopNoop();
return;
}
const eventName = extractTopLevelHookEventName(input.toString('utf8'));
if (eventName === 'PreToolUse' || eventName === 'PostToolUse') {
writeOversizedToolHookOutput(eventName);
return;
}
console.error(`[oh-my-codex] ${message}`);
process.exitCode = 1;
return;
@@ -10,14 +10,14 @@ Use this skill when users want to install or refresh oh-my-codex for the **curre
## Command
```bash
omx setup [--force] [--merge-agents] [--dry-run] [--verbose] [--scope <user|project>] [--plugin|--legacy|--install-mode <legacy|plugin>]
omx setup [--force] [--merge-agents|--no-merge-agents|--clear-merge-agents-policy] [--dry-run] [--verbose] [--scope <user|project>] [--plugin|--legacy|--install-mode <legacy|plugin>]
```
If you only want lightweight `AGENTS.md` scaffolding for an existing repo or subtree, use `omx agents-init [path]` instead of full setup.
Supported setup flags (current implementation):
- `--force`: overwrite/reinstall managed artifacts where applicable
- `--merge-agents`: when `AGENTS.md` already exists, preserve user-authored content and insert/refresh OMX-managed generated sections between explicit `<!-- OMX:AGENTS:START -->` / `<!-- OMX:AGENTS:END -->` markers
- `--force`: overwrite/reinstall managed artifacts where applicable; it is transient and is neither persisted nor replayed
- `--merge-agents`, `--no-merge-agents`, `--clear-merge-agents-policy`: exact bare policy selectors (equals/value spellings are rejected). Repeated identical selectors are idempotent; conflicting set/clear selectors fail before setup mutations. An explicit set overrides saved policy. `--merge-agents` selects the existing managed-section merge branch; `--no-merge-agents` only suppresses that branch; clear always removes the saved explicit policy and cannot combine with a set selector
- `--dry-run`: print actions without mutating files
- `--verbose`: print per-file/per-step details
- `--scope`: choose install scope (`user`, `project`)
@@ -41,7 +41,7 @@ Supported setup flags (current implementation):
- else discovered installed plugin cache under `${CODEX_HOME:-~/.codex}/plugins/cache/**/.codex-plugin/plugin.json` with `name: oh-my-codex` makes `plugin` the default for both `user` and `project` scope, so project setup does not duplicate plugin-provided skills/hooks with legacy `.codex/skills` and `.codex/hooks.json`
- else in `user` scope, interactive prompt on TTY (`legacy` by default, or `plugin` when a plugin cache is discovered)
- else default `legacy` unless a plugin cache is discovered
3. Create directories and persist effective scope/install mode
3. Create required directories. Preferences are atomically persisted only after all setup work succeeds.
4. In legacy mode, install prompts/native agents/skills and merge full config.toml. In plugin mode, archive/remove legacy OMX-managed prompts/skills, refresh installable native agent TOMLs for `agent_type` routing, clean up stale generated non-installable native agents, and keep native Codex hooks installed.
5. Verify Team CLI API interop markers exist in built `dist/cli/team.js`
6. Generate AGENTS.md defaults only when selected/allowed (or legacy behavior outside plugin mode)
@@ -55,6 +55,11 @@ Supported setup flags (current implementation):
- Local project orchestration file is `./AGENTS.md` (project root).
- If `AGENTS.md` exists and neither `--force` nor `--merge-agents` is used, interactive TTY runs ask whether to overwrite. Non-interactive runs preserve the file.
- Use `--merge-agents` to keep existing project guidance while allowing setup to refresh OMX-managed AGENTS sections and the generated model capability table idempotently.
- A successful explicit set persists `mergeAgents: true` or `false` in the invoking working root's `./.omx/setup-scope.json`, even for `--scope user`; it is never a global user preference and cannot leak to another root. A valid matching policy is replayed by both immediate and deferred `omx update` refreshes.
- `--no-merge-agents` is contextual, not a preserve, replace, or safety mode: the current prompt, skip, managed-refresh, plugin-default, and force behavior still controls the non-merge path. Absence keeps existing behavior exactly.
- TTY review preserves a matching policy while unrelated settings change. Reset or a scope change removes the inherited policy unless the same setup run explicitly sets true or false. `--clear-merge-agents-policy` always removes the policy and cannot combine with a set selector. Malformed, unknown, nonboolean, scopeless, or wrong-scope policy data is treated as absent.
- Explicit policy intent is atomically committed only after all setup work succeeds. If active-session or plugin-symlink safeguards skip the current AGENTS write but setup otherwise succeeds, explicit true, false, or clear still persists as future update intent; the safeguards themselves are unchanged.
- Merge is not the default, and this policy does not adopt the rejected #2892 merge-by-default behavior. Older OMX binaries ignore this additive field safely but can erase it when rewriting their known setup preferences.
- Scope targets:
- `user`: user directories (`~/.codex`, `~/.codex/skills`, `~/.omx/agents`)
- `project`: local directories (`./.codex`, `./.codex/skills`, `./.omx/agents`)
@@ -73,7 +78,7 @@ Use this map when reconciling setup behavior or debugging a confusing install:
| Surface | Owner | Notes |
| --- | --- | --- |
| `./.omx/setup-scope.json` | `omx setup` | Persists setup scope and install mode when needed. TTY reruns summarize it and offer keep/review/reset. |
| `./.omx/setup-scope.json` | `omx setup` | Persists setup scope, install mode, and an optional explicit AGENTS merge policy per working root. TTY reruns summarize choices and offer keep/review/reset; only valid matching-scope boolean policy replays on update. |
| `~/.codex/config.toml` / `./.codex/config.toml` | `omx setup` generated blocks + user edits | Setup refreshes OMX-managed blocks while preserving supported manual content; setup-owned runtime feature flags include `multi_agent`, `child_agents_md`, the Codex hook feature flag (`hooks` or legacy `codex_hooks`), and `goals`. |
| `~/.codex/hooks.json` / `./.codex/hooks.json` | `omx setup` shared ownership | Setup owns OMX native hook wrappers and preserves user-owned hooks. |
| prompts, skills, native agents | `omx setup` or Codex plugin delivery | Legacy mode installs local files; plugin mode relies on plugin discovery for bundled skills, archives/removes legacy OMX-managed prompt/skill copies, and refreshes setup-owned native agent TOMLs for `agent_type` routing while cleaning up stale generated/non-installable native agents. |
@@ -32,7 +32,9 @@ Complex tasks often fail silently: partial implementations get declared "done",
<Execution_Policy>
- Fire independent agent calls simultaneously -- never wait sequentially for independent work
- Use `run_in_background: true` for long operations (installs, builds, test suites)
- Always set `agent_type` when spawning native subagents; use `reasoning_effort` for per-dispatch intensity when needed
- Before substantive planning, reviewer delegation, HUD/runtime activation, or adapted role work, inspect the native task surface. Keyword routing state is not authority. When the surface exposes `agent_type`, use typed routing. When it reports `role_routing_unavailable`, run `omx ralplan preflight --json`; on `unsupported_documented_leader_proof`, stop before planner/reviewer work or adapted authority and use a Codex surface with documented root proof or a reviewed alternative workflow. Do not infer root authority from `session_id`, undocumented `thread_id`, session/pointer/transcript/cwd state, absent child data, or prompt labels.
- When the native surface exposes `agent_type` role routing, set `agent_type` to an installed OMX role and never omit it for OMX work; use `reasoning_effort` for per-dispatch intensity when needed.
- **OMX adapted role-pass protocol:** when native routing is `role_routing_unavailable`, do not fabricate `agent_type`. On documented Codex 0.144.5 the adapted path is unavailable: run `omx ralplan preflight --json`, stop on `unsupported_documented_leader_proof`, and do not use prompt labels, task-name carriers, pending intents, markers, or `omx ralplan role-intent write` as substitutes.
- Preserve legacy Ralph tier intent through native reasoning effort: LOW -> `low`, STANDARD -> `medium`, THOROUGH -> `xhigh`
- Deliver the full implementation: no scope reduction, no partial completion, no deleting tests to make them pass
- Apply the shared workflow guidance pattern: outcome-first framing, concise visible updates for multi-step execution, local overrides for the active workflow branch, validation proportional to risk, explicit stop rules, and automatic continuation for safe reversible steps. Ask only for material, destructive, credentialed, external-production, or preference-dependent branches.
@@ -52,6 +54,7 @@ Complex tasks often fail silently: partial implementations get declared "done",
- If an existing relevant snapshot is available, reuse it and record the path in Ralph state.
- If request ambiguity is high, gather brownfield facts first. `omx explore` is deprecated; use normal repository inspection tools/subagents for simple read-only repository lookups and `omx sparkshell` only for explicit shell-native read-only evidence. Then run `$deep-interview --quick <task>` to close critical gaps.
- Do not begin Ralph execution work (delegation, implementation, or verification loops) until snapshot grounding exists. If forced to proceed quickly, note explicit risk tradeoffs.
- When this is a Ralplan-originated handoff and native role routing is unavailable, complete `omx ralplan preflight --json` before intake. On `unsupported_documented_leader_proof`, record the reason in Execution Policy and stop; do not create the snapshot or begin the loop.
1. **Review progress**: Check TODO list and any prior iteration state
2. **Continue from where you left off**: Pick up incomplete tasks
3. **Delegate in parallel**: Route tasks to specialist native agents with explicit `agent_type` and appropriate `reasoning_effort`
@@ -77,6 +80,7 @@ Complex tasks often fail silently: partial implementations get declared "done",
- Standard changes: `task(agent_type="architect", reasoning_effort="medium", prompt="...")`
- >20 files or security/architectural changes: `task(agent_type="architect", reasoning_effort="xhigh", prompt="...")`
- Ralph floor: always run an explicit `architect` native subagent, even for small changes
- On `role_routing_unavailable`, do not invoke `omx ralplan role-intent write` or manufacture an Architect identity. On documented Codex 0.144.5, the adapted Architect path is unavailable; surface the leader-proof diagnostic/remediation from Execution Policy and stop. On a future or other surface, use an adapted route only after its documented positive root proof has been reviewed and implemented.
7.5 **Mandatory Deslop Pass**:
- After Step 7 passes, run `oh-my-codex:ai-slop-cleaner` on **all files changed during the Ralph session**.
- Scope the cleaner to **changed files only**; do not widen the pass beyond Ralph-owned edits.
@@ -200,7 +204,7 @@ Why bad: These are independent tasks that should run in parallel, not sequential
- [ ] Fresh test run output shows all tests pass
- [ ] Fresh build output shows success
- [ ] lsp_diagnostics shows 0 errors on affected files
- [ ] Architect verification passed through explicit `task(agent_type="architect", reasoning_effort="medium"...)` minimum
- [ ] Architect verification passed: on a routing-capable surface via explicit `task(agent_type="architect", reasoning_effort="medium"...)` minimum. On documented Codex 0.144.5 role-routing-unavailable surfaces, no adapted Architect pass is valid; Ralplan-originated work must have stopped with the leader-proof diagnostic.
- [ ] Codex goal-mode completion audit passed, and `update_goal({status: "complete"})` was called when an active goal exists
- [ ] ai-slop-cleaner pass completed on changed files (or --no-deslop specified)
- [ ] Post-deslop regression tests pass
@@ -49,8 +49,12 @@ The consensus workflow:
- If only one viable option remains, explicit invalidation rationale for alternatives
- Deliberate mode only: pre-mortem (3 scenarios) + expanded test plan (unit/integration/e2e/observability)
2. **User feedback** *(--interactive only)*: If `--interactive` is set, use the structured question UI (`omx question` in attached tmux; native structured input outside tmux when available) to present the draft plan **plus the Principles / Drivers / Options summary** before review (Proceed to review / Request changes / Skip review). Otherwise, automatically proceed to review.
3. **Architect** reviews for architectural soundness and must provide the strongest steelman antithesis, at least one real tradeoff tension, and (when possible) synthesis — **await completion before step 4**. Launch this as a subsequent `Architect` subagent (`agent_type: "architect"`) and pass the full task statement, context snapshot, PRD/test-spec paths, and relevant prior findings; do not use a default subagent with only a short improvised reviewer prompt. In deliberate mode, Architect should explicitly flag principle violations.
4. **Critic** evaluates against quality criteria — run only after step 3 completes. Launch this as a subsequent `Critic` subagent (`agent_type: "critic"`) with the full task statement, context snapshot, PRD/test-spec paths, and the completed Architect review; do not ask the Architect subagent to perform the Critic gate and do not substitute a default subagent fantasy prompt for the packaged Critic role. Critic must enforce principle-option consistency, fair alternatives, risk mitigation clarity, testable acceptance criteria, and concrete verification steps. In deliberate mode, Critic must reject missing/weak pre-mortem or expanded test plan.
**Native role-routing preflight:** Before substantive planning, reviewer delegation, HUD/runtime activation, or adapted-role work, inspect the native task surface. Keyword routing may already have selected Ralplan, but it is not authority. When the surface exposes `agent_type`, use typed native routing and do not run the adapted preflight. When it reports `role_routing_unavailable`, run `omx ralplan preflight --json`; on `unsupported_documented_leader_proof`, stop before planner/reviewer work or adapted authority and use a Codex surface with documented root proof or a reviewed alternative workflow. Do not infer root identity from `session_id`, undocumented `thread_id`, session/pointer/transcript/cwd state, absence of child data, or a prompt label.
**Native role-routing rule:** When the native surface exposes `agent_type` role routing, set `agent_type` to an installed OMX role and never omit it for OMX work. When it does not (`role_routing_unavailable`), do not fabricate `agent_type`. The formerly adapted `omx ralplan role-intent write` path is unavailable on documented Codex 0.144.5 because it lacks documented root proof; do not silently weaken routing with a prompt role label or inferred carrier. Use a Codex surface with documented root proof or a reviewed alternative workflow. A direct `omx ralplan role-intent write` attempt is denied with machine reason `unsupported_documented_leader_proof`.
3. **Architect** reviews for architectural soundness and must provide the strongest steelman antithesis, at least one real tradeoff tension, and (when possible) synthesis — **await completion before step 4**. Launch this as a subsequent role-specific `Architect` subagent and pass the full task statement, context snapshot, PRD/test-spec paths, and relevant prior findings; do not substitute an unvalidated reviewer identity or a short improvised reviewer prompt. In deliberate mode, Architect should explicitly flag principle violations.
4. **Critic** evaluates against quality criteria — run only after step 3 completes. Launch this as a subsequent role-specific `Critic` subagent with the full task statement, context snapshot, PRD/test-spec paths, and the completed Architect review; do not ask the Architect subagent to perform the Critic gate and do not substitute an unvalidated reviewer identity or a short improvised reviewer prompt. Critic must enforce principle-option consistency, fair alternatives, risk mitigation clarity, testable acceptance criteria, and concrete verification steps. In deliberate mode, Critic must reject missing/weak pre-mortem or expanded test plan.
5. **Re-review loop** (max 5 iterations): Any non-`APPROVE` Critic verdict (`ITERATE` or `REJECT`) MUST run the same full closed loop:
a. Collect Architect and Critic feedback
b. Revise the plan with Planner
@@ -203,17 +203,21 @@ Default-model rule:
- Use `OMX_DEFAULT_SPARK_MODEL` for spark/low-complexity worker-default guidance.
Thinking-level rule (critical):
- **No model-name heuristic mapping.**
- Team runtime must **not** infer `model_reasoning_effort` from model-name substrings (e.g., `spark`, `high-capability`, `mini`).
- When the leader assigns teammate roles/tasks, OMX allocates **per-worker reasoning effort dynamically** from the resolved worker role and `agentReasoning` overrides (`low`, `medium`, `high`, `xhigh`).
- Explicit launch args still win: if `OMX_TEAM_WORKER_LAUNCH_ARGS` already includes `-c model_reasoning_effort=...`, that explicit value overrides dynamic allocation for every worker.
- **No model-name heuristic mapping.** Team runtime must not infer `model_reasoning_effort` from model-name substrings (for example `spark`, `high-capability`, or `mini`).
- OMX-owned `agentReasoning` accepts exactly `low`, `medium`, `high`, `xhigh`, and `max`; configured values are case-normalized.
- Without explicit raw reasoning, Team selects the resolved role default or valid `agentReasoning` override for each worker.
- `max` is passed to Codex unchanged. Its support is capability-dependent on the selected Codex version, model, and provider; preserve authoritative downstream errors.
- `ultra` is unsupported in OMX-owned `agentReasoning` and is not an alias for configured `max`. Other invalid configured values retain the built-in role-default fallback.
- Explicit raw `-c model_reasoning_effort=...` is opaque and wins over configured and built-in role defaults, including `ultra` and future values. Preserve the selected raw token exactly.
- When both sources provide explicit raw reasoning, inherited Team reasoning wins over environment reasoning. Explicit raw reasoning always wins over the role default.
- Do not downgrade or retry `max` as `xhigh`; built-in role defaults remain unchanged.
Normalization requirements:
- Parse both `--model <value>` and `--model=<value>`
- Remove duplicate/conflicting model flags
- Emit exactly one final canonical flag: `--model <value>`
- Preserve unrelated args in worker launch config
- If explicit reasoning exists, preserve canonical `-c model_reasoning_effort="<level>"`; otherwise inject the worker role's default or `agentReasoning`-overridden reasoning level
- Parse both `--model <value>` and `--model=<value>`.
- Remove duplicate/conflicting model flags.
- Emit exactly one final canonical flag: `--model <value>`.
- Preserve unrelated args in worker launch config.
- Preserve the selected explicit raw `-c model_reasoning_effort=...` token exactly; otherwise inject the worker role's default or `agentReasoning`-overridden reasoning level.
## Required Lifecycle (Operator Contract)
+22 -32
View File
@@ -1,4 +1,4 @@
# PPT Master — AI generates natively editable PPTX from any document
# PPT Master — AI generates native PowerPoint from any document
[![Version](https://img.shields.io/github/v/release/hugohe3/ppt-master?label=version&color=blue)](https://github.com/hugohe3/ppt-master/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -46,7 +46,7 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this
</details>
> **AI generates your deck — it doesn't fill in a template.** PPT Master is a workflow that runs inside AI IDEs (Claude Code, Cursor, VS Code + Copilot, etc.): hand the AI your PDF / DOCX / web pages, and it produces a real PowerPoint on your machine — every element editable in PowerPoint, your data stays local, no platform or model lock-in. How it works and where the limits are → [Product Positioning](#product-positioning).
> **Editable is already table stakes — what sets PPT Master apart is native depth.** It hands you a real PowerPoint: slide masters, native shapes, data-backed charts and tables — not flat text boxes, and not a filled-in template. It also does more than lay slides out nicely — it reasons the argument into shape first, then designs; and that native depth keeps **converging with PowerPoint itself**, adding more of its native capabilities release after release. In form, it's a workflow that runs inside any agent-capable AI tool: hand the AI your topic or material, and it generates on your machine — your data stays local, no platform or model lock-in. How it works and where the limits are → [Product Positioning](#product-positioning).
<p align="center">
<a href="https://hugohe3.github.io/ppt-master/"><strong>Live Demo</strong></a> ·
@@ -100,30 +100,29 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this
---
Drop in your source material, and the deck you get back is **more than just editable**: it has native slide transitions plus opt-in entrance animations (off by default), speaker notes that can become audio narration, charts and tables that can ship as real data-backed PowerPoint objects, and it can follow your own PPT template — a complete deck you can present as-is and keep refining. How to use each capability → [Getting Started](./docs/getting-started.md).
Drop in your source material, and what you get back isn't a static layout you can edit — it's **a complete deck with real PowerPoint behavior**: native slide transitions, opt-in entrance animations (off by default), speaker notes that can become audio narration and even video, charts and tables that can ship as real data-backed PowerPoint objects, and it can follow your own PPT template — present it as-is, and keep refining. How to use each capability → [Getting Started](./docs/getting-started.md).
## Product Positioning
**If a file can't be opened and edited in PowerPoint, it shouldn't be called a PPT.** AI presentation tools roughly fall into four categories, and PPT Master only does the last one:
**Editable is now table stakes — the real question is how much of PowerPoint you actually get.** PPT Master delivers PowerPoint's native object model itself, and in depth: native shapes and connectors with working adjustment handles, data-backed charts and tables on demand, and the full text / picture / fill / effect model — click any element and keep editing it as a native PowerPoint object; and through the template / structured route, it can hand you a deck with real slide masters and layouts (`p:sldMaster` / `p:sldLayout` inheritance).
| Category | Output | Editable element-by-element in PowerPoint? |
|---|---|:---:|
| Template fill-in | PPTX built from a fixed template | Partially — limited by the template |
| Image-based | One large image per slide, packed into PPTX | ❌ each slide is a picture |
| HTML presentation | Web-based deck | ❌ not a PPTX |
| **Native editable (PPT Master)** | **Real DrawingML shapes, text boxes, charts** | ✅ click any element to edit |
And that depth is a **direction of travel, not a fixed checklist.** PPT Master's north star is to keep converging with PowerPoint itself: an ongoing effort to build and integrate more of PowerPoint's native capabilities, release after release, closing the gap between what an AI can generate for you and what you could build by hand in PowerPoint. The [PowerPoint ↔ SVG Mapping Guide](./docs/powerpoint-svg-mapping.md) is the honest, feature-by-feature record of how far that reaches today — and SmartArt is a deliberate omission, not a gap.
In form, it's not a website or an app but a workflow (a "skill") that runs inside AI IDEs like Claude Code, Cursor, VS Code + Copilot, or Codebuddy: you tell the AI in the IDE's chat — "make a deck from this PDF" — and it follows the workflow to produce a genuinely editable `.pptx` on your machine. No coding on your side; you do exactly three things — install Python, install an AI IDE, drop in your material.
In form, it's a workflow (a "skill") that runs inside any agent-capable AI tool: tell it in chat — "make a deck from this PDF" — and it runs the workflow on your machine and exports a natively editable `.pptx`. No coding on your side; you do exactly three things — install Python, install an AI tool, drop in your material.
This form buys three promises that other tools struggle to make at the same time:
Generating a new deck from source documents is the main pipeline, but not the only route. PPT Master can also distill reusable brand / layout / deck templates from your references, fill an existing `.pptx` with new content while preserving its design, and add native transitions, animations, and narration to a finished deck — each route with an explicit contract for what gets preserved.
- **Transparent, predictable cost** — the tool is free and open source; the only cost is your AI model usage. You pay exactly what you consume — no separate PPT subscription added on top
- **Data stays local** — your files shouldn't have to be uploaded to someone else's server just to make a presentation. Apart from AI model communication, the entire pipeline runs on your machine
- **No platform lock-in** — your workflow shouldn't be held hostage by any single company. Works with Claude Code, Cursor, VS Code Copilot, and more; supports Claude, GPT, Gemini, Kimi, and other models
On top of that native depth, this form comes with three promises:
- **Transparent, predictable cost** — free and open source; the only cost is your AI model usage, with no PPT subscription on top
- **Data stays local** — apart from AI model communication, the entire pipeline runs on your machine
- **No platform lock-in** — any agent-capable AI IDE can drive it; Claude, GPT, Gemini, Kimi, and other models all work
Why you'd choose it, and where it isn't the right fit → [Why PPT Master](./docs/why-ppt-master.md); the long-term capability boundaries behind these promises → [Project Positioning](./docs/project-positioning.md).
> [!IMPORTANT]
> ### This is a tool, not a wishing well
> `harness + model = agent` — PPT Master only owns the workflow; the model sets the ceiling. Recommended: **Claude with a large context window (~1M tokens) + AI image generation (`gpt-image-2`)**; other models can run the pipeline, with a quality gap.
> `harness + model = agent` — PPT Master only owns the workflow; the model sets the ceiling. Recommended: **Kimi K3 (or Claude) with a large context window (~1M tokens) + AI image generation (`gpt-image-2` or Google `gemini-3.1-flash-image`)**; other models can run the pipeline, with a quality gap.
>
> And don't expect a finished, perfect deck in one shot. The tool's value is taking most of the tedious work off your plate; the polishing that's left is yours — a natively editable deck exists precisely so you can keep working on it, not a flat image you can't touch. The cheaper the model, the more there is to do; if results disappoint, upgrade the model first, then check your usage against [Getting Started](./docs/getting-started.md) and the example projects.
@@ -234,7 +233,7 @@ Never used one of these? Don't worry — in this project they play exactly one r
| **IDE plugin / extension** | [Claude Code](https://claude.ai/code) (VS Code / JetBrains extension), [GitHub Copilot](https://github.com/features/copilot), [Cline](https://cline.bot/), etc. | Installed inside hosts like VS Code or JetBrains |
| **CLI agent** | [Claude Code](https://claude.ai/code) CLI, [Codex CLI](https://github.com/openai/codex), Gemini CLI, etc. | Runs in the terminal; suits scripting, remote, or server use |
> **Model recommendation**: for the best results, use **Claude Opus** with `gpt-image-2`; **Gemini 3.5 Flash** currently offers great overall value for money — notably fast and well worth a try.
> **Model recommendation**: for the best results, use **[Kimi K3](https://www.kimi.com/code/?aff=ppt-master)** (or Claude) to drive the pipeline, paired with AI image generation — **`gpt-image-2`** (OpenAI) or **`gemini-3.1-flash-image`** (Google). Kimi Code, the project sponsor, is a great pick for pay-as-you-go access.
**🔑 Want to use Claude / GPT / Gemini but don't have access yet?** Project sponsors **[PackyCode](https://www.packyapi.com/register?aff=ppt-master)**, **[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER)** and **[RunAPI](https://runapi.co/register?aff=WMLJ)** offer pay-as-you-go access to Claude, GPT, Gemini and more — no subscription required, with exclusive discounts for our users (details at the top of this page).
@@ -316,7 +315,7 @@ AI: Sure. Let's confirm the design spec:
The AI handles everything — content analysis, visual design, SVG generation, and PPTX export.
> **Output:** The SVG pipeline has one PPTX export route: PPT Master's converter reads `svg_output/` and writes a directly editable native DrawingML deck to `exports/<name>_<timestamp>.pptx`. Step 7 still always runs `finalize_svg.py`, producing self-contained files in `svg_final/` for visual inspection or manual insertion as SVG pictures; PowerPoint's manual **Convert to Shape** command is outside the supported contract. A copy of `svg_output/` is always snapshotted to `backup/<timestamp>/svg_output/` for re-export / archival. By default charts and tables export as SVG-derived shapes (pixel-consistent across PowerPoint / Keynote / WPS); pass `--native-objects` to instead emit them as **real editable PowerPoint chart / table objects backed by data** (rendering may vary across apps), saved as `exports/<name>_<timestamp>_native_charts.pptx`.
> **Output:** The SVG pipeline has one PPTX export route: PPT Master's converter reads `svg_output/` and writes a directly editable native DrawingML deck to `exports/<name>_<timestamp>.pptx`. Step 7 still always runs `finalize_svg.py`, producing self-contained files in `svg_final/` for visual inspection or manual insertion as SVG pictures; PowerPoint's manual **Convert to Shape** command is outside the supported contract. A copy of `svg_output/` is always snapshotted to `backup/<timestamp>/svg_output/` for re-export / archival. By default charts and tables export as individually editable SVG-derived DrawingML shapes, which prioritize cross-app visual consistency. Pass `--native-charts-and-tables` to replace eligible groups with PowerPoint-native Chart/Table objects backed by data, which provide **Edit Data** and object-specific controls but may render differently across apps; this variant is saved as `exports/<name>_<timestamp>_native_charts_tables.pptx`. Both routes are editable—the distinction is the PowerPoint object model, not editability itself.
> **Already have a `.pptx` you want to reuse?** Hand the AI that deck plus your material and ask it to "fill this deck with the new content" — it fills text, table, and chart data into your existing design and exports only the pages you pick, staying natively editable. See the [FAQ](./docs/faq.md) and [template-fill workflow](./skills/ppt-master/workflows/template-fill-pptx.md).
@@ -353,7 +352,8 @@ PPT Master reads the current process environment first, then the first `.env` fo
| | Document | Description |
|---|----------|-------------|
| 📘 | [Getting Started](./docs/getting-started.md) | First deck in 3 steps, plus how to use templates, live preview, animations, narration, voice cloning (**new users start here**) |
| 🆚 | [Why PPT Master](./docs/why-ppt-master.md) | How it compares to Gamma, Copilot, and other AI tools |
| 🆚 | [Why PPT Master](./docs/why-ppt-master.md) | Why choose it, and where it's not the right fit |
| 🧭 | [Project Positioning](./docs/project-positioning.md) | Long-term positioning, product promises, and capability boundaries |
| 🪟 | [Windows Installation](./docs/windows-installation.md) | Step-by-step setup guide for Windows users |
| 📖 | [SKILL.md](./skills/ppt-master/SKILL.md) | Core workflow and rules |
| 📐 | [Canvas Formats](./skills/ppt-master/references/canvas-formats.md) | PPT 16:9, Xiaohongshu, WeChat, and 10+ formats |
@@ -362,6 +362,8 @@ PPT Master reads the current process environment first, then the first `.env` fo
| 🏗️ | [Technical Design](./docs/technical-design.md) | Architecture, design philosophy, why SVG |
| ❓ | [FAQ](./docs/faq.md) | Model selection, cost, layout troubleshooting, custom templates |
<sub>Full documentation index → [`docs/`](./docs/README.md)</sub>
---
## Contributing
@@ -389,18 +391,6 @@ Looking to collaborate, integrate PPT Master into your workflow, or just have qu
---
## Star History
<a href="https://star-history.com/#hugohe3/ppt-master&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=hugohe3/ppt-master&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=hugohe3/ppt-master&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=hugohe3/ppt-master&type=Date" />
</picture>
</a>
---
## Sponsors & Support
PPT Master is currently built and maintained primarily by me. Every new template, bug fix, and documentation update takes ongoing resources — currently shared by the sponsors and individual supporters below.
@@ -433,4 +423,4 @@ Made with ❤️ by [Hugo He](https://www.hehugo.com/) — if this project helps
<sub>Official distribution: <a href="https://github.com/hugohe3/ppt-master">GitHub</a> (primary) · <a href="https://atomgit.com/hugohe3/ppt-master">AtomGit</a> (mirror). Redistributions on other platforms are unofficial. MIT licensed — attribution required.</sub>
[⬆ Back to Top](#ppt-master--ai-generates-natively-editable-pptx-from-any-document)
[⬆ Back to Top](#ppt-master--ai-generates-native-powerpoint-from-any-document)
@@ -2,8 +2,8 @@
"sourceId": "ppt-master",
"repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main",
"commit": "465e3b4149b852d33ddd1cb94ac059401fe4e823",
"commit": "b05ac551468098ed7b4c1c21d9d0d413b4230c79",
"adapter": "claude-skill",
"sourcePath": "skills/ppt-master",
"syncedAt": "2026-07-13T16:00:00Z"
"syncedAt": "2026-07-20T16:00:00Z"
}
@@ -170,12 +170,12 @@
# Use cloud providers only when you want high-quality or cloned voices —
# all four (ElevenLabs / MiniMax / Qwen / CosyVoice) accept a cloned voice_id.
# Clone the voice in the provider's console first, then pass --voice-id to
# notes_to_audio.py. See docs/audio-narration.md "Use a cloned voice".
# notes_to_audio.py. See workflows/stages/generate-audio.md.
# edge-tts 是默认旁白后端,无需 API Key。
# 需要高质量云端旁白或复刻音色时再配置云端提供商——
# ElevenLabs / MiniMax / Qwen / CosyVoice 四家都支持传入复刻 voice_id。
# 先在 provider 控制台复刻得到 voice_id,再用 --voice-id 传给 notes_to_audio.py。
# 详见 docs/zh/audio-narration.md "使用复刻音色"
# 详见 workflows/stages/generate-audio.md。
#
# ELEVENLABS_API_KEY=your-elevenlabs-api-key
# MINIMAX_API_KEY=your-minimax-key
@@ -5,902 +5,42 @@ description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶
# PPT Master Skill
> AI-driven multi-format SVG content generation system. Converts source documents into high-quality SVG pages through multi-role collaboration and exports to PPTX.
PPT Master is a routed presentation workflow. This entry owns global execution discipline and route selection only; each selected route owns its procedure.
**Core Pipeline**: `Source Document → Create Project → [Template] → Strategist Structured Plan → [Image_Generator] → Executor Live Preview → Quality Check → Post-processing → Export`
## Mandatory Load Order
### SVG Page-Design Boundary
1. Read this file.
2. Read [`workflows/routing.md`](workflows/routing.md).
3. Select exactly one top-level route from the routing authority.
4. Read only that route's authority and its explicitly triggered supporting documents.
| Scope | Contract |
| Selected route | Runtime authority |
|---|---|
| Any route that authors or regenerates slide visuals through SVG | `svg_output/` is the complete page-design source: every visible text, image, shape, chart/table fallback, and layout element that should appear on the exported slide is present in that page SVG or referenced by it. |
| Templates, `design_spec.md`, and `spec_lock.md` | Authoring/control inputs. They guide SVG creation but MUST NOT supply visible slide content that is absent from the completed SVG during export. |
| Semantic SVG markers | Minimal rendering-neutral compiler hints used only after existing Layout/Layer/Placeholder/Native metadata has been considered. They never replace native SVG geometry, text, styles, grouping, or asset references. |
| `svg_final/` | Mandatory derived, self-contained SVG visual preview. It may be opened directly or inserted into PowerPoint as an SVG picture, but it is not a supported PPTX source and carries no manual Convert-to-Shape compatibility contract. |
| SVG-to-PPTX export | The only supported generated-PPTX route reads `svg_output/` and maps its content through the project converter to DrawingML/native objects. It may reorganize represented content into Master/Layout/Slide structure but MUST NOT invent new visible page content. |
| Direct PPTX and presentation-behavior workflows | Remain separate. `template-fill-pptx`, `native-enhance-pptx`, animations, transitions, speaker notes, narration, and package relationships are not required to round-trip through SVG. |
| Generate PPTX | [`workflows/generate-pptx.md`](workflows/generate-pptx.md) |
| Create Template | [`workflows/create-template.md`](workflows/create-template.md) |
| Fill Native PPTX | [`workflows/template-fill-pptx.md`](workflows/template-fill-pptx.md) |
| Enhance Native PPTX | [`workflows/native-enhance-pptx.md`](workflows/native-enhance-pptx.md) |
**MUST — page-design closure**: For an SVG-authoring route, inspect the final page SVG to determine what the exported slide looks like. Do not reinterpret “SVG is the page-design language” as “SVG is the complete PPTX package description language.”
> [!CAUTION]
> ## 🚨 Global Execution Discipline (MANDATORY)
>
> **This workflow is a strict serial pipeline. The following rules have the highest priority — violating any one of them constitutes execution failure:**
>
> 1. **SERIAL EXECUTION** — Steps MUST be executed in order; the output of each step is the input for the next. Non-BLOCKING adjacent steps may proceed continuously once prerequisites are met, without waiting for the user to say "continue"
> 2. **BLOCKING = HARD STOP** — Steps marked ⛔ BLOCKING require a full stop; the AI MUST wait for an explicit user response before proceeding and MUST NOT make any decisions on behalf of the user
> 3. **NO CROSS-PHASE BUNDLING** — Cross-phase bundling is FORBIDDEN. (Note: the Strategist confirmation stage in Step 4 is ⛔ BLOCKING — the AI MUST present recommendations and wait for explicit user confirmation before proceeding. Once the user confirms, all subsequent non-BLOCKING steps — design spec output, SVG generation, speaker notes, and post-processing — may proceed automatically without further user confirmation)
> 4. **GATE BEFORE ENTRY** — Each Step has prerequisites (🚧 GATE) listed at the top; these MUST be verified before starting that Step
> 5. **NO SPECULATIVE EXECUTION** — "Pre-preparing" content for subsequent Steps is FORBIDDEN (e.g., writing SVG code during the Strategist phase)
> 6. **NO SUB-AGENT SVG GENERATION** — Executor Step 6 SVG generation is context-dependent and MUST be completed by the current main agent end-to-end. Delegating page SVG generation to sub-agents is FORBIDDEN
> 7. **SEQUENTIAL PAGE GENERATION ONLY** — In Executor Step 6, after the global design context is confirmed, SVG pages MUST be generated sequentially page by page in one continuous pass. Grouped page batches (for example, 5 pages at a time) are FORBIDDEN
> 8. **SPEC_LOCK RE-READ PER PAGE** — Before generating each SVG page, Executor MUST `read_file <project_path>/spec_lock.md`. All colors / fonts / icons / images MUST come from this file — no values from memory or invented on the fly. Executor MUST also read `pptx_structure.mode`, the current page's `page_rhythm` (`anchor` / `dense` / `breathing`), and `page_charts`. Only a deck/layout template route (`mode: structured`) looks up `page_layouts` (the input template SVG), `page_pptx_layouts` (the page assignment), `pptx_masters`, `pptx_layouts` (the unique reusable roster), and `template_adherence`; free-design and brand-only routes use `mode: flat` and omit those sections. This rule exists to resist context-compression drift on long decks and to break the uniform "every page is a card grid" default
> 9. **SVG MUST BE HAND-WRITTEN, NOT SCRIPT-GENERATED** — Every SVG page is written by the main agent directly, one page at a time (see rules 6 and 7). Writing or running a Python / Node / shell script that produces the SVG files in batch — looping over pages, templating from data, or emitting them via a generator — is FORBIDDEN, including under "save tokens", "quick draft", or "user is in a hurry" pretexts. The script-generation path was tried on a feature branch and abandoned: cross-page visual consistency depends on per-page authoring with full upstream context, which a generator script cannot reproduce. **Narrow exception**: `preset_shape_svg.py` may print one deterministic stock-shape fragment to stdout after the main agent has selected its semantic role, frame, and paint. It cannot write `svg_output/`, choose layout, batch shapes, or generate a page; the main agent reads the fragment and inserts it through the normal hand-authored page edit
> 10. **FOLLOW DETERMINISTIC ROUTING RULES** — Do not add blocking routing questions when this skill defines a route. If the user request violates a route precondition, state the required prerequisite and stop that route instead of asking the user to choose around the rule. Ordinary finite options, stylistic preferences, and recoverable details are surfaced with a recommended value plus alternatives at the next existing confirmation gate.
> [!IMPORTANT]
> ## 🌐 Language & Communication Rule
>
> - **Response language**: match the user's input and source materials. Explicit user override (e.g., "请用英文回答") takes precedence.
> - **User-facing option labels**: when presenting confirmations, brief proposals, choices, or finite option sets, use the user's language for labels and explanations. English enum IDs / file fields may appear in parentheses for precision, but never rely on English-only labels such as `deck`, `layout`, `mirror`, or `fidelity` without a localized explanation.
> - **Template format**: `design_spec.md` MUST follow its original English template structure (section headings, field names) regardless of conversation language. Content values may be in the user's language.
> [!IMPORTANT]
> ## 🔌 Compatibility With Generic Coding Skills
>
> - `ppt-master` is a repository-specific workflow, not a general application scaffold
> - Do NOT create `.worktrees/`, `tests/`, branch workflows, or generic engineering structure by default
> - On conflict with a generic coding skill, follow this skill unless the user explicitly says otherwise
## Rule Strength Labels
| Label | Meaning |
|---|---|
| `MUST` | Required behavior; violation is workflow failure |
| `MUST NOT` | Forbidden behavior |
| `DEFAULT` | Used when the user has not specified otherwise |
| `OPTIONAL` | Run only when explicitly triggered or when the route says so |
| `FALLBACK` | Recovery path after the primary path fails |
| `GATE` | Required checkpoint before entering the next step |
## Cross-Cutting Authorities
| Concern | Authority | Contract |
|---|---|---|
| Main pipeline sequencing | This `SKILL.md` | Owns Step 1-7 order, gates, role switching, and mandatory commands |
| Route selection | [`workflows/routing.md`](workflows/routing.md) | Owns deterministic route choice before the main pipeline or a standalone workflow |
| Workflow registry | [`workflows/index.md`](workflows/index.md) | Owns standalone workflow trigger/precondition/output inventory |
| Artifact ownership | [`references/artifact-ownership.md`](references/artifact-ownership.md) | Owns fact channels, source/derived artifact boundaries, and regeneration rules |
| Failure recovery | [`workflows/failure-recovery.md`](workflows/failure-recovery.md) | Owns stop/continue decisions for common failures |
| Confirm UI details | [`scripts/docs/confirm_ui.md`](scripts/docs/confirm_ui.md) | Owns schema, launcher behavior, port strategy, and chat fallback details |
## Main Pipeline Scripts
| Script | Purpose |
|--------|---------|
| `${SKILL_DIR}/scripts/source_to_md.py` | Unified source-to-Markdown dispatcher — default Step 1 entry for explicit file(s) or URL(s) |
| `${SKILL_DIR}/scripts/pptx_intake.py` | Standard PPTX intake enrichment — canvas / identity / slide geometry / tables / native chart data / SmartArt structure |
| `${SKILL_DIR}/scripts/project_manager.py` | Project init / validate / manage |
| `${SKILL_DIR}/scripts/icon_sync.py` | Copy chosen library icons into `<project>/icons/` at selection time; missing names reported + non-zero (re-pick gate) |
| `${SKILL_DIR}/scripts/analyze_images.py` | Image analysis |
| `${SKILL_DIR}/scripts/latex_render.py` | LaTeX formula rendering (manifest-driven PNG assets) |
| `${SKILL_DIR}/scripts/image_gen.py` | AI image generation (multi-provider) |
| `${SKILL_DIR}/scripts/slice_images.py` | Slice one AI illustration sheet into individual spot-illustration elements |
| `${SKILL_DIR}/scripts/svg_authoring_view.py` | Create a lightweight non-destructive inspection projection of PPTX-imported SVGs; never a release source |
| `${SKILL_DIR}/scripts/svg_quality_checker.py` | SVG quality check |
| `${SKILL_DIR}/scripts/preset_shape_svg.py` | Print one registry-backed native PowerPoint preset fragment to stdout for hand-authored SVG insertion |
| `${SKILL_DIR}/scripts/total_md_split.py` | Speaker notes splitting |
| `${SKILL_DIR}/scripts/finalize_svg.py` | SVG post-processing (unified entry) |
| `${SKILL_DIR}/scripts/svg_to_pptx.py` | Export to PPTX |
| `${SKILL_DIR}/scripts/native_enhance_pptx.py` | Existing PPTX enhancement project init / validation / direct OOXML patch export |
| `${SKILL_DIR}/scripts/native_narration_pptx.py` | Backward-compatible entrypoint for existing PPTX notes / narration enhancement |
| `${SKILL_DIR}/scripts/update_spec.py` | Propagate a `spec_lock.md` color / font_family change across all generated SVGs |
For complete tool documentation, see `${SKILL_DIR}/scripts/README.md`.
> **Windows note**: if a `python3 ...` command fails (common on python.org installs, which provide `python.exe` but not `python3.exe`), rerun the same command with `python` instead.
## Template Index
| Index | Path | Purpose |
|-------|------|---------|
| Layout templates | `${SKILL_DIR}/templates/layouts/layouts_index.json` | Query available page layout templates |
| Brand presets | `${SKILL_DIR}/templates/brands/brands_index.json` | Query available brand identity presets (color / typography / logo / voice) |
| Visualization templates | `${SKILL_DIR}/templates/charts/charts_index.json` | Query available visualization SVG templates (charts, infographics, diagrams, frameworks) |
| Icon library | `${SKILL_DIR}/templates/icons/` | See `${SKILL_DIR}/templates/icons/README.md`; search icons on demand with `ls templates/icons/<library>/ \| grep <keyword>` |
## Standalone Workflows
**Route authority**: Use [`workflows/routing.md`](workflows/routing.md) before entering the main pipeline or any standalone workflow.
**Registry**: Use [`workflows/index.md`](workflows/index.md) for the complete workflow list, triggers, preconditions, exclusions, outputs, and blocking points.
### PPTX Route Boundary
| User intent | Route |
|---|---|
| Raw PPTX template plus new material/topic, generate a PPTX | [`template-fill-pptx`](workflows/template-fill-pptx.md) |
| Existing PPTX, preserve page count/order and slide wording 1:1, improve layout | [`beautify-pptx`](workflows/beautify-pptx.md) |
| Existing PPTX as source material, rethink outline or change page count/order | Main pipeline via `source_to_md.py` plus PPTX intake |
| Build a reusable template package from a PPTX/design reference | [`create-template`](workflows/create-template.md), then return with the generated template workspace path |
| Finished PPTX, keep content/layout stable and add notes/audio/timing/transitions | [`native-enhance-pptx`](workflows/native-enhance-pptx.md) |
**MUST**: Raw `.pptx` template plus "generate PPTX" routes to `template-fill-pptx` by default. The SVG generation route consumes only an explicit template workspace path with a valid `templates/design_spec.md`, or a supported direct/legacy package root with `design_spec.md`.
**MUST**: Beautify is strictly 1:1. Any split, merge, drop, reorder, or page-count change routes to the main pipeline.
**MUST — reusable template mode boundary**: `create-template` has two distinct
contracts. `standard` and `fidelity` author new SVG prototypes and their own
Master/Layout/slot system; source visuals and assets are references, and source
Master/Layout topology is neither preserved nor distilled. `mirror` is a
restoration path: preserve the source slide roster/order, visual appearance,
Master/Layout parentage and identities, placeholder type/index/bounds, native
object ownership, and supported native-shape metadata. The lossless import is
the restoration authority; the lightweight authoring projection exists only to
keep model context small. Mechanical normalization may express the source facts
in the current explicit SVG contract and expand fixed-layer group wrappers into
direct atoms, but it MUST NOT merge, split, promote, demote, rename, or
re-parent source structure. Export compiles the selected contract and never
infers a different one. Mirror emits one complete page prototype per source
Slide plus one definition-only `layout_<layout_key>.svg` prototype for every
source Layout unused by those Slides. The independent Master/Layout roster then
registers the complete supported source graph without publishing synthetic
carrier pages. Stop only when required native evidence is missing or unsupported;
never silently drop or merge an identity.
**FALLBACK**: Ambiguous requests such as "make this PPT more professional" require exactly one discriminator question: preserve original page count/order and slide wording, or treat the deck as source material and restructure it?
**Hard rule — selected authority only**: Do not load another top-level route's procedure after routing. Profiles, stages, governance files, and child workflows refine the selected route; they never compete with it.
---
## Workflow
### Step 1: Source Content Processing
🚧 **GATE**: User has provided source material (PDF / DOCX / EPUB / URL / Markdown file / text description / conversation content — any form is acceptable).
> **No source content?** When the user supplies only a topic name or requirements without any file or substantive description, run the [`topic-research`](workflows/topic-research.md) workflow first, then return here with its products as input.
When the user provides non-Markdown content, convert immediately through the
unified dispatcher. It preserves the backend converters' existing behavior,
routes by source type, and writes the standard Markdown plus conversion profile.
| User Provides | Action |
|---------------|--------|
| PDF / DOCX / Office document / XLSX / XLSM / PPTX / EPUB / HTML / LaTeX / RST / web URL | `python3 ${SKILL_DIR}/scripts/source_to_md.py <file_or_URL_or_dir> [<file_or_URL_or_dir> ...]` |
| CSV / TSV | Read directly as plain-text table source |
| Markdown | Read directly |
For PPTX sources, Step 1 converts the deck to Markdown content; after Step 2
`import-sources`, standard PPTX intake is also written to `<project>/analysis/`.
Use `source_to_md.py -t <type>` only when extension detection is ambiguous.
Default local conversion writes Markdown/profile outputs beside each source file.
Use `-o` only when a specific output file/directory is required; with multiple
inputs or directory inputs, `-o` is an output directory. Backend converter details are documented in
[`scripts/docs/conversion.md`](scripts/docs/conversion.md).
> **Office vector assets (EMF/WMF) from DOCX/PPTX sources**:
> Source conversion extracts embedded Office vector images (.emf/.wmf)
> alongside bitmap images when the source format exposes them. After `import-sources`, these land in `images/`
> together with `image_manifest.json` and are first-class assets in §VIII Image Resource List.
>
> **Do NOT convert EMF/WMF to PNG.** The PPT Master pipeline preserves them as external
> references (`finalize_svg.py` skips them) and `svg_to_pptx.py` embeds them as
> PPTX-native media via `image/x-emf` / `image/x-wmf` MIME — PowerPoint renders them at full vector fidelity.
> Converting via LibreOffice/Inkscape introduces CJK font substitution drift and
> rasterization loss; the original EMF/WMF is always higher fidelity than the converted PNG.
>
> Browser-based live preview cannot render EMF (will show blank) — this is expected;
> the PPTX output is the source of truth.
**✅ Checkpoint — Confirm source content is ready, proceed to Step 2.**
---
### Step 2: Project Initialization
🚧 **GATE**: Step 1 complete; source content is ready (Markdown file, user-provided text, or requirements described in conversation are all valid).
```bash
python3 ${SKILL_DIR}/scripts/project_manager.py init <project_name> --format <format>
```
Format options must be named with concrete dimensions. Default: `ppt169` = `1280x720`, `viewBox="0 0 1280 720"`. Other examples: `ppt43` = `1024x768`, `story` = `1080x1920`, `banner` = `1920x1080`. For the full format list, see `references/canvas-formats.md`.
Import source content (choose based on the situation):
| Situation | Action |
|-----------|--------|
| Has source files (PDF/MD/etc.) | `python3 ${SKILL_DIR}/scripts/project_manager.py import-sources <project_path> <source_files_or_dirs...> --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 <project_path>/sources/<source.pptx> -o <project_path>/analysis
```
For each PPTX it writes `<stem>.identity.json` (canvas, theme palette/fonts, observed usage) and `<stem>.slide_library.json` (text slots, geometry, native tables, native chart caches, SmartArt nodes/connections), 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 `<stem>.*` 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 `<stem>.*` 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`. If Step 1 wrote Markdown beside the original sources, pass that source path/directory once. If Step 1 used `-o` to write Markdown elsewhere, pass both the original source path(s)/directory and the Markdown output path(s)/directory. 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.**
---
### Step 3: Template Option
🚧 **GATE**: Step 2 complete; project directory structure is ready.
**Default — free design.** Proceed directly to Step 4. Do NOT query any `*_index.json` unless triggered. Do NOT ask the user. Do NOT proactively suggest, hint at, or fuzzy-match any template based on content, slug-like words, or vague style descriptions.
**Hard boundary — raw PPTX template references are not Step 3 templates.** PPTX-as-source remains valid in Step 1 / Step 2, and raw PPTX template + generated PPTX routes to `template-fill`. But if the user wants the SVG/template-based generation route from that PPTX, stop before Step 3. The user must first run [`workflows/create-template.md`](workflows/create-template.md), then return with the generated template workspace path. Step 3 consumes an explicit workspace whose `templates/design_spec.md` declares `kind: brand` / `kind: layout` / `kind: deck`, or a compatible legacy-flat package whose root `design_spec.md` declares one of those kinds.
Do **not** reinterpret this boundary as 1:1 redesign or free SVG generation. Use `template-fill` for raw PPTX template + generated PPTX requests; use `beautify` only when the source deck's page count, order, and wording are preserved.
**Template flow triggers ONLY on explicit directory paths** supplied by the user in their initial message, plus one narrow workflow handoff: a project-scoped `create-template` run in the current conversation may pass its exact validated project workspace root directly into this Step. The trigger rule is mechanical, not interpretive:
| User input contains | Step 3 action |
|---|---|
| One or more explicit template workspace paths (each resolves to `templates/design_spec.md`, or to a compatible legacy-flat root `design_spec.md`, with `kind: brand` / `kind: layout` / `kind: deck` in YAML frontmatter) | Normalize each source directory, read its `kind`, dispatch per the kind matrix below, fuse if multiple |
| Current `create-template` workflow just completed project scope and validated its exact `<project>/` workspace | Consume that single workspace in place; it cannot join multi-path fusion |
| Anything else — bare template names ("用 presentation_core"), style descriptions ("麦肯锡风格"), brand mentions ("中国电信风格"), vague intent ("想用个模板"), or silence | Skip Step 3, free design |
There is no slug matching, no name lookup, no fuzzy resolution. A name without a path does not trigger — the user must give a path the AI can `cd` into.
**Structured-template preflight (before copy)**: For every deck/layout workspace, inspect all SVG roots and slots under its normalized template source. Every page must declare root Master/Layout key and picker names; Master/Layout visuals must be direct atoms rather than `<g>`; every slot must be a top-level `<g>` with positive bounds and exactly one compatible carrier, or an explicit composite `object` proxy. A zero-slot Layout is valid. If the SVG package uses a legacy semantic contract, run [`restore-pptx-structure`](workflows/restore-pptx-structure.md) first and return to Step 3 with the migrated workspace. A legacy flat directory shape alone is read compatibility and does not trigger restoration.
> Style descriptions ("麦肯锡风格" / "Keynote 风" / "极简风" / etc.) never trigger Step 3. They flow into the Strategist confirmation stage as a style brief (color / typography / tone in fields eg).
> Bare names ("presentation_core", "中国电信", "anthropic") do NOT trigger Step 3 even if a matching directory exists in the library. The user must give a path. AI must not "helpfully" resolve a name to a path.
> "What templates exist?" is out-of-band Q&A — answer by listing entries from `brands_index.json` / `layouts_index.json` / `decks_index.json` together with their paths. Listing alone does not advance the pipeline; the user must send a path back to trigger Step 3.
> To create a new layout or deck, read [`workflows/create-template.md`](workflows/create-template.md). To create a new brand, read [`workflows/create-brand.md`](workflows/create-brand.md).
#### Three template kinds
The architecture has three independent reference bundles. Full schema in [`docs/zh/templates-architecture.md`](../../docs/zh/templates-architecture.md). Summary:
| Kind | Physical dir | Contains | Frontmatter |
|---|---|---|---|
| **brand** | `templates/brands/<id>/templates/` inside a complete workspace | identity-only segment: color / typography / logo / voice / icon style | `kind: brand` |
| **layout** | `templates/layouts/<id>/templates/` inside a complete workspace | structure-only segment: canvas / page structure / page types / SVG roster | `kind: layout` |
| **deck** | `templates/decks/<id>/templates/` inside a complete workspace | full identity + structure reference with the middle (template overview) segment | `kind: deck` |
**Segment ownership** (governs fusion override priority):
| Segment | Sections | Owner kind on fusion |
|---|---|---|
| Identity | Color Scheme / Typography / Logo / Voice & Tone / Icon Style | brand |
| Structure | Canvas / Page Structure / Page Types / SVG Roster | layout |
| Middle | Template Overview (use cases / design intent) | deck (no other kind writes this) |
#### Single-path dispatch
| User path's `kind` | Step 3 action |
|---|---|
| `kind: brand` | Install `templates/` plus any existing `images/` and `icons/` into the matching project roots; ignore `exports/`. Strategist locks identity; structure stays free. |
| `kind: layout` | Install `templates/` plus any existing `images/` and `icons/` into the matching project roots; ignore `exports/`. Strategist locks structure; identity is decided in confirmation fields eg. |
| `kind: deck` | Install `templates/` plus any existing `images/` and `icons/` into the matching project roots; ignore `exports/`. Strategist locks all segments; confirmation narrows to deck-content fields. |
Normalize every explicit path before any write:
| Input shape | Spec / SVG source | Asset source | Install rule |
|---|---|---|---|
| Current workspace: `<root>/templates/design_spec.md` | `<root>/templates/` | Any existing `<root>/images/`, `<root>/icons/` | Map the existing portable roots to the target project's matching roots; ignore `<root>/exports/` |
| Compatible legacy-flat package: `<root>/design_spec.md` | `<root>/` | Package-local files | SVG/spec/non-bitmaps → project `templates/`; bitmaps → project `images/`; route declared icons to project `icons/` |
**Atomic install preflight (mandatory)**: Resolve source and destination paths, enumerate the complete file mapping, and reject every destination collision before copying any file. Equality between a current project workspace root and the target project root means in-place consumption and no copy. For an external single path, a collision stops Step 3 rather than overwriting. For multi-path fusion, do not copy packages sequentially: resolve segment conflicts and asset-name conflicts first, construct one final mapping, then write it once. Never use recursive copy as an implicit conflict policy.
Never infer that a flat directory has legacy Master/Layout semantics solely from packaging.
The same current-workspace routing applies to all three kinds: source/spec in `templates/`, visual assets in `images/`, runtime icons in `icons/`, and on-demand review artifacts in `exports/`. Empty optional roots are omitted rather than retained with placeholder files, so a normal workspace has no `exports/` until a review file is explicitly generated. The spec's `kind` tells Strategist how to read the installed source. Template SVGs are not export-time overlays: visible output still lives completely in `svg_output/`. Their complete visuals and explicit Master/Layout/placeholder metadata are nevertheless the authoring prototypes selected by `page_layouts`.
When `create-template` used project output scope, its workspace root is the target project itself and all core directories are already final. Resolve both roots before copying: equality means **in-place consumption**, so skip the installation. An in-place workspace cannot participate in multi-path fusion; use external workspaces for fusion. Never place the local source under a nested `templates/local_master/` directory because the confirmation and quality gates read the project `templates/` root.
A project-scoped workspace has the same portable routing as a library workspace. It may be copied or promoted across roots as one unit (`templates/` plus any existing `images/` and `icons/`); `exports/` stays review-only. Do not pass only another project's `templates/` subdirectory because that would omit sibling assets.
Legacy template packages may ship `native_structure.json` + `source_template.pptx`, omit root Master identity, use direct atomic placeholders, or carry old baseline/distillation metadata. Do not copy or consume those semantic contracts through Step 3. Run [`restore-pptx-structure`](workflows/restore-pptx-structure.md) on the package first, then return with the migrated workspace path. Old flat packaging remains readable when its SVG structure is already current.
The Strategist confirmation stage decides whether the selected deck/layout template is used `strict` or `adaptive`. Those template projects use `pptx_structure.mode: structured`, map every page to one input SVG in `page_layouts`, declare unique reusable identities in `pptx_masters` / `pptx_layouts`, and assign each page through `page_pptx_layouts` before SVG generation. A Layout definition may remain unused by generated pages when it names an installed template SVG as its prototype source. Brand-only projects remain on the free-design `mode: flat` route. Strict preserves the template's declared Master/Layout/slot contract. Adaptive keeps the template Master and may define and assign a new Layout key during authoring when the composition genuinely changes. Non-mirror paint and typography follow the project skin rules.
#### Multi-path fusion
When the user gives two or more paths of **different kinds**, Step 3 fuses them into a single `<project>/templates/design_spec.md`. **Default granularity is segment-level integer replacement** — entire identity / structure / middle segments are taken from the highest-priority source for that segment, no implicit field-level mixing.
Override priority by segment:
| Combination | Identity from | Structure from | Middle from |
|---|---|---|---|
| brand only | brand | (free design) | (none) |
| layout only | (free design) | layout | (none) |
| deck only | deck | deck | deck |
| brand + layout | brand | layout | (none) |
| brand + deck | brand (overrides deck) | deck | deck |
| layout + deck | deck | layout (overrides deck) | deck |
| brand + layout + deck | brand | layout | deck |
Field-level micro-adjustment (e.g. "use anthropic brand but primary changed to #FF0000") is **not** part of Step 3 fusion — it flows into Strategist confirmation stage eg as a normal user request.
#### Same-kind multiple paths — conflict resolution
When the user gives two paths of the **same kind** (e.g. `brands/anthropic` + `brands/google`), Step 3 surfaces a conflict prompt before fusing — like resolving a git merge conflict:
```
AI: 你给了两个 brand,检测到段级冲突:
- Color SchemeAnthropic 橙红 vs Google 多色)
- TypographyStyrene/AnthropicSans vs GoogleSans/Roboto
- LogoAnthropic 标 vs Google 标)
- Voice & Tonerestrained vs friendly
- Icon Stylestroke vs filled
要 (a) 全部按 Anthropic / (b) 全部按 Google / (c) 逐段挑?
```
Rules:
- Default: no implicit ordering — every cross-source segment difference is reported as a conflict
- Only when the user picks `(c)` does AI walk through each segment one by one
- Field-level conflicts are out of scope — segment-level only
- Three or more same-kind paths are not supported — ask the user to converge to at most two
#### Fused spec provenance
When fusion happens (any multi-path case), the resulting `<project>/templates/design_spec.md` carries a provenance block immediately under its H1:
```markdown
> **Fused from:**
> - deck: `templates/decks/中国电信/` base
> - brand: `templates/brands/anthropic/` identity override
> - layout: `templates/layouts/presentation_core/` structure override
> - conflicts resolved: Color Scheme from anthropicuser picked a
```
Single-path Step 3 does **not** add provenance (the source is self-evident from the copied files).
The fused frontmatter `kind` describes the resulting bundle: `deck` when both identity and structure are present, `layout` when only structure is present, and `brand` when only identity is present. Keep this field accurate; the Strategist confirmation server uses it to show template adherence only for bundles that actually own page structure.
**✅ Checkpoint — Default path proceeds to Step 4 without user interaction. If the user supplied one or more explicit template paths, those have been copied, staged in place, or fused into `<project_path>/templates/` before advancing.**
---
### Step 4: Strategist Phase (MANDATORY — cannot be skipped)
🚧 **GATE**: Step 3 complete; default free-design path taken, or (if triggered) template files copied or confirmed in place in the project.
First, read the role definition:
```
Read references/strategist.md
```
> ⚠️ **Mandatory gate**: before writing `design_spec.md`, Strategist MUST `read_file templates/design_spec_reference.md` and follow its full IX section structure. See `strategist.md` Section 1.
**Artifact ownership**: fact-channel and source/derived artifact boundaries are defined in [`references/artifact-ownership.md`](references/artifact-ownership.md). This Step uses those ownership rules; it does not redefine them.
**`<project_path>/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 `<stem>.identity.json` / `<stem>.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 Strategist confirmation stage, 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 / SmartArt entries per source deck), then open a specific deck's `<stem>.identity.json` / `<stem>.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 content-type files in `sources/`** — primarily `<stem>.md`, but also any user-supplied content the import archived there: `.md` / `.markdown` / `.txt` / `.csv` / `.tsv` / `.json` / `.jsonl` / `.yaml` / `.yml` (a `metrics.json` or `data.csv` may carry core content — judge by what the file holds). Text, tables, chart data values, and SmartArt node wording come from these (`ppt_to_md` transcribes native charts as Markdown tables and SmartArt nodes as hierarchical bullets). **Do NOT read pipeline sidecars in `sources/` as content**: `*.conversion_profile.json` (conversion audit) and `*_files/image_manifest.json` (asset index) are process metadata — open them only to audit a conversion or resolve assets, never as slide content. Converted-source originals archived in `sources/` (`.pdf` / `.pptx` / `.docx` / `.xlsx` / `.html` / `.epub` / `.tex` / `.rst` / `.ipynb` / `.typ`, etc.) are read via their converted `<stem>.md`, not scanned directly in the main pipeline. The `analysis/` chart / table / diagram entries are a **structural digest** for outline decisions (which slides carried charts, tables, or SmartArt; chart types / series names; SmartArt layout and hierarchy) — not a second copy of the content values; do NOT also pull chart values or SmartArt wording from `<stem>.slide_library.json` in the main pipeline. The `<stem>.slide_library.json` full structured data is owned by the direct-PPTX workflows: template-fill uses it as the native fill contract while preserving SmartArt unchanged; beautify uses it for native chart / table data and SmartArt relationships while keeping all wording from the Markdown.
**Strategist confirmation stage** (full template: `templates/design_spec_reference.md`):
**BLOCKING**: present the Strategist confirmation stage and **wait for explicit user confirmation or modification** before outputting Design Specification & Content Outline. This is the single core confirmation gate — once the final confirmation lands, all subsequent steps proceed automatically. The default Confirm UI delivers the gate in **three stages** (direction → design system → images / execution; see below); the chat fallback mirrors the same staged order.
1. Canvas format
2. Page count range
3. Target audience
4. Style objective
5. Template adherence — `strict` / `adaptive` (only when Step 3 loaded a deck/layout template)
6. Color scheme
7. Icon usage approach
8. Typography plan, including formula rendering policy
9. Image usage approach
**Confirm UI Auto-Launch (Mandatory — default visual confirmation surface)**: by default the Strategist confirmation stage is presented through an interactive local page in **three stages within one browser session** — Stage 1 confirms the direction anchors; the AI then re-derives the design-system layer from the **user's actual** anchors; Stage 2 confirms that layer; the AI then re-derives image and execution choices from the confirmed direction + design system; Stage 3 confirms the final operational layer. Color swatches, live font previews, icon samples, image-style reference previews, and candidate picks appear where they help judgment; the chat path is the always-valid fallback. [`scripts/docs/confirm_ui.md`](scripts/docs/confirm_ui.md) owns the schema, server lifecycle, port strategy, and fallback details; this section keeps the orchestration contract. The split:
| Stage | Confirms | Driven by |
|---|---|---|
| **1 — direction anchors** | canvas · audience + core message + `content_divergence` + `delivery_purpose` *(PPT only — omitted on non-PPT canvases)* (all §c key info) · `mode` + `visual_style` · `template_adherence` *(only when Step 3 loaded a deck/layout template)* | the source + user intent |
| **2 — design system** (re-derived from Stage 1) | page count · color · typography (font + size) · icons · formula policy | the confirmed Stage 1 |
| **3 — images / execution** (re-derived from Stage 1 + Stage 2) | image usage · generated-image style · AI-image generation path · generation mode · refine-spec toggle | the confirmed direction + design system |
> **Why three stages.** Design-system fields are anchored by the same few choices (`visual_style` anchors color / icon / typography; `delivery_purpose` sets the body size, page density, **and** the page-count recommendation). Image strategy depends on both the confirmed visual direction and the confirmed color system — its palette is color behavior only, while final HEX values follow Stage 2. Confirming direction first, then design system, then image / execution choices means each downstream stage fits the user's *real* choices instead of the AI's original assumptions. Page count is a **derived** field (content volume × `delivery_purpose`), which is why it lives in Stage 2, not up front.
Steps:
> ⛔ **Steps 2 → 3 → 4 are ONE uninterrupted run — do NOT yield to the user mid-flow.** When an intermediate `--wait` returns, the AI **immediately and autonomously** re-derives and writes the next stage in the **same turn**: do **not** summarize, ask a question, report progress, or end the turn in between. The browser is sitting on a "deriving…" spinner polling for the next stage you must write — stopping here strands the page and the user must prod you in chat to finish (a bug, not the intended flow). **Stage-1 and Stage-2 confirmations are intermediate machine handoffs, not stopping points.** The single ⛔ BLOCKING wait is the **final** confirmation at the end of step 4. (Chat-fallback path — only when the page never opened — is the exception: there you do present each stage in chat and wait for a reply.)
1. **Write Stage 1** to `<project_path>/confirm_ui/recommendations.json` with `"stage": "stage1"` and only the anchor fields. New recommendations MUST use the canonical `stage` selector. Enumerable anchors (`canvas` / `mode` / `visual_style` / `delivery_purpose`) name a recommended canonical `id` in a `recommend` block (the page lists common options from `confirm_ui/static/catalogs.json`); `visual_style` also carries the ≥3-style `visual_style_spectrum` (safe / shifted / bold — same hard rule as h.5). When Step 3 loaded a deck/layout template, also set `recommend.template_adherence` to `strict` or `adaptive`; omit the field entirely for free design and brand-only templates so the page does not display it. `audience` and `content_divergence` are plain `{ "value": "<free text>" }`. `content_divergence` is the **free-text** field shown under audience in §c — how closely to follow the source vs how freely to reshape it (blank = balanced; facts stay sourced at every level); 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 (`zh` / `en` / `ja`); visible text matches `lang`, or provide multilingual `name_zh` / `name_en` / `name_ja` + `note_zh` / `note_en` / `note_ja` — when the user's language is Japanese, set `lang: "ja"` and always include the `_ja` variants (labels resolve in the page language first — a `ja` page falls back ja → en → zh, so missing `_ja` labels silently render in English; zh/en pages keep their zh↔en fallback and only try `_ja` last).
2. **Launch + wait for Stage 1.** Background launch; the parent returns when the page writes the stage-1 `result.json`. **Long tool timeout — 600000 ms** (the `--wait` ≈590 s budget):
```bash
python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --daemon --wait
```
Page opens at the launch-log URL such as `http://127.0.0.1:5050` — the **same port as the Step 6 live preview** (they never run at once: this page shuts down at the end of Step 4). If 5050 is held, the launcher **auto-advances** (5051, …) — read the actual URL from the launch log and report it. The page does **not** close after Stage 1: it shows a "deriving…" state and polls for Stage 2. **Launch or wait failure is non-fatal**: if it fails or times out (flask missing, port blocked, no GUI / remote / web host), do **NOT** troubleshoot — **on any non-zero exit, re-check `result.json` once** for a fresh `status: stage1-confirmed` before dropping to the chat fallback. **On success (exit 0 with a stage-1 result), do not pause or report — go straight to step 3 in the same turn.**
3. **Re-derive Stage 2 from the confirmed anchors, write it, then wait for the design-system handoff — immediately, same turn (the page is polling for it).** Read the stage-1 `result.json` (`status: stage1-confirmed`). Using the user's **actual** confirmed anchors (not your originals), author the design-system candidates and **overwrite** `recommendations.json` with `"stage": "stage2"`: page count (content volume × `delivery_purpose`); color and typography as **generative ≥3-candidate** fields (creative recommendations always offer real choice; fewer than 3 only on the honest-shortfall exception, with a stated reason; color: core `palette` with background/secondary_bg/primary/accent/secondary_accent/body_text; typography: CJK + Latin for `heading` and `body` with `css` preview stacks + `body_size` as the body baseline in **px** (every canvas) — **one fixed value per confirmed `delivery_purpose`** (`text` 20 / `balanced` 24 / `presentation` 32), not a range; each typography candidate must include topic-matched `sample_heading` / `sample_heading_latin` / `sample_body` / `sample_body_latin` preview text, never a fixed unrelated industry sample); enumerable `icons` / `formula_policy` (recommended `id`). **Stage 2 is never skipped** — an active deck/layout template (`strict` adherence included) does not exempt it: the template skin becomes the recommended color / typography candidate and real alternatives fill the remaining cards. Never jump `recommendations.json` from `stage1` to `stage3`: the server refuses to render a skipped stage (the page stays on "deriving…") and the waits exit non-zero until the stages are written in order. The still-open page polls, renders Stage 2, and preserves the user's Stage 1 picks. Then attach to the already-running page; if Windows cleaned up the server, `--wait-only` auto-recovers it on the recorded/default port so the browser reconnects:
```bash
python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --wait-only --wait-stage stage2
```
This returns when the page writes the stage-2 `result.json` (`status: stage2-confirmed`). On a non-zero exit, re-check `result.json` once before falling back to chat — except a `stage skip detected` error, which is not a page failure: you wrote a stage out of order; rewrite `recommendations.json` with the stage the error names and re-attach.
4. **Re-derive Stage 3 from the confirmed anchors + design system, then wait for the final confirmation.** Read the stage-2 `result.json`. Author the image and execution recommendations and **overwrite** `recommendations.json` with `"stage": "stage3"`: `image_usage` as one or more source ids (`["ai"]`, `["ai","provided"]`, `["web","placeholder"]`, or `["none"]`; `none` is exclusive); `image_strategy.candidates` as **exactly three non-custom** rendering × palette recommendations from h.5 when `image_usage` includes `ai` (the page adds the fourth Custom card itself); enumerable `image_ai_path` / `generation_mode` and `refine_spec` (recommended `id` / boolean). If the recommendation involves several image sources, keep the source list structured in `recommend.image_usage` and write the usage rationale / page-role guidance into `image_notes` (for example, "封面和章节页用 AI 主视觉,产品页优先用户素材,行业背景页可用网络参考"). Write `image_ai_path` only when `image_usage` includes `ai`. Spot-illustration lean is **not** a candidate field here: it derives from the locked `visual_style`'s illustration propensity and is expressed only in the recommendation rationale / `image_notes`, never as a new confirmation field. Generated-image style palettes are **color behavior only**; final image colors follow the confirmed Stage-2 `color`. Custom image-strategy dimensions are handled by the built-in Custom card, are prose-only, and should not promise a gallery reference image. Then attach to the already-running page; `--wait-only` auto-recovers a dead server as above (same 600000 ms budget):
```bash
python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --wait-only
```
This is the ⛔ BLOCKING completion: returns when the page writes the final `result.json` (`status: confirmed`, `stage: final`, carrying Stage 1 + Stage 2 + Stage 3 fields). On a non-zero exit, re-check `result.json` once (a `stage skip detected` error means Stage 2 was never confirmed — go back to step 3, not the chat fallback). Confirmed sizes are **already px** (the system is px-only — no pt anywhere, no conversion): write `result.json` `typography.body_size` / `sizes` into `design_spec.md` / `spec_lock.md` / SVG verbatim. `generation_mode: "split"` / `refine_spec: true` are explicit user choices.
5. **Close the confirm page (Mandatory cleanup — every path).** Shut the server down before leaving Step 4 so it cannot keep holding port 5050 (which Step 6 live preview reuses):
```bash
python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --shutdown
```
**Idempotent and required regardless of whether Confirm was clicked**: clicking the final Confirm already shuts the page down (then a no-op); the chat-fallback path leaves it running. Run it after reading the confirmation, before Step 5.
**Always also print each stage's recommendations + URL in chat** as the always-valid fallback. **The chat fallback is staged too**: if the page never opens or a wait times out with no fresh result, present Stage 1 in chat → get confirmation → re-derive → present Stage 2 → get confirmation → re-derive → present Stage 3 → get confirmation → take those values. Either path converges.
**Honoring the confirmation (result.json is authoritative — Mandatory)**: the confirmed values **override your own recommendations** when you write `design_spec.md` / `spec_lock.md`. A user who changed any field changed it on purpose. In particular, map `image_usage` to §VIII `Acquire Via` (its value names differ from §h options — translate). `image_usage` may be either a legacy single string or a Confirm UI multi-select array; for arrays, apply every selected source. `image_notes`, when present, is a user-authored image intent note that Strategist must honor while assigning per-page §VIII rows:
| `result.json.image_usage` | §VIII `Acquire Via` | h.5 + Step 5 generation |
|---|---|---|
| `ai` | `ai` rows | Run h.5 (lock rendering + palette); Step 5 generates |
| `web` | `web` rows | None |
| `provided` | **`user`** rows | None — never generate |
| `placeholder` | `placeholder` rows | None |
| `none` | no image rows (§h option A) | None |
| Legacy custom prose | Infer the intended rows from the prose | Run h.5 only if the prose includes AI |
When the confirmed `image_usage` does not include `ai` (and no legacy custom prose includes AI), do **NOT** run h.5, do **NOT** write `ai` rows, and do **NOT** generate images in Step 5 — regardless of what you recommended. `none` is exclusive: if confirmed, write no §VIII image rows. The same "confirmed value wins" rule applies to every field (color → §III, typography → §IV, etc.).
**Small spot illustrations are a Strategist judgment, not a confirmation field.** The user chooses image *source* through `image_usage`; whether the deck leans into decorative illustrations is anchored by the locked `visual_style`'s **illustration propensity** (`core` / `supportive` / `sparse`), expressed only in the `image_notes` rationale — never a new confirmation control. An explicit user request to use or skip illustrations overrides that default either way; `image_usage: none` still wins (write no illustration rows); and source still comes from `image_usage` — a `core` style does not silently generate AI spots when the user did not pick AI. They are ordinary §VIII image rows (`Type: Illustration` / `Illustration Sheet`) using normal `Acquire Via` values. If the plan needs ≥3 same-family AI spot illustrations, use the `ai` Illustration Sheet + `slice` workflow by default; do not generate one AI image per spot. Full rule + precedence: [`references/strategist.md`](references/strategist.md) §h. Use them on suitable pages and omit them where they would weaken clarity.
**Upstream override → re-derive untouched downstream (Mandatory — chat-fallback / single-pass path).** On the **three-stage page path this is already handled** (Step 3 re-derives Stage 2 from the user's actual anchors; Step 4 re-derives Stage 3 from the confirmed anchors + design system). It still applies whenever anchors and downstream fields are confirmed **together** — the staged chat fallback collapsed into one bundle, or a legacy single-pass `result.json`. "Confirmed value wins" governs each field's *own* value — never recompute a value the user set (a size, canvas, or palette they edited stays verbatim). But a single-pass `result.json` can carry a changed **anchor** beside downstream fields still holding your original — now incoherent — recommendation (e.g. switched to `dark-tech` while the light palette you proposed is untouched). Before writing the spec, reconcile: when the user changed an anchor, re-derive the downstream fields the user did **not** themselves edit so they realize the new anchor; fields the user pinned stay as confirmed.
| Anchor the user changed | Re-derive (only the downstream fields the user left at your recommendation) |
|---|---|
| `visual_style` (§d Layer 2 — anchors eh) | color neutral tiers (§e), icon library / stroke (§f), typography character (§g), image rendering (§h.5) |
| `mode` (§d Layer 1) | outline structure + register (§IX) |
| `delivery_purpose` (§g) | body baseline + per-page density / rhythm (§6.1) |
| `audience` / core message (§c) | tone across eh, outline emphasis (§IX) |
| `color` HEX (§e) | h.5 palette (re-filter for the new HEX) |
Reconcile **without a new blocking wait** — fold the coherent values into `design_spec.md` / `spec_lock.md` and state the adjustment in the §8 next-step handoff (e.g. "you switched to `dark-tech`; the light palette you had left no longer fit, so background / accent were re-derived — tell me if you wanted the original"). Canvas is the explicit exception: font sizes are deliberately **not** rescaled on a canvas change (see strategist §g).
**Opt-out**: if the user has said they don't want the page (e.g. "不要网页" / "just confirm in chat" / "纯聊天确认"), skip the launch entirely (step 2) and present the Strategist confirmation stage in chat as before — steps 1, 3, 4 still apply (recommendations summary in chat; wait; take chat values).
The page is a **confirmation surface only** — Strategist still authors every recommendation; the page never generates content.
**Mandatory — split-mode note** (not a separate confirmation): after listing the Strategist confirmation stage details, you MUST append exactly one short line (rendered in the user's language, prefixed with 💡) about generation mode. Pick the variant by qualitative read of upstream-load signals — recommended page count, source-material bulk, whether `topic-research` ran with substantial web-fetch accumulation:
| Signal read | Line content |
|---|---|
| Heavy (long page count / bulky sources / heavy web-fetch accumulation) | State estimated page count and large source size; recommend switching to [split mode](workflows/resume-execute.md) after Step 5 — stop this chat, open a fresh window and input `继续生成 projects/<project_name>` to enter the execution session (SVG generation + export); no response or "continue" = default continuous mode. |
| Normal (default) | State scale is moderate, default continuous mode generates in one go; if mid-way window switch is desired, input `继续生成 projects/<project_name>` after Step 5 to switch to [split mode](workflows/resume-execute.md). |
This line is required output every run — the user must always see the mode choice exists. Whether to act on it is the user's call. When the Confirm UI is used, this choice also appears as the in-page generation-mode toggle and is captured in `result.json` (`generation_mode`); the chat-summary fallback still prints this line.
**Mandatory — spec-refinement note** (not a separate confirmation): after the split-mode line, you MUST append one short opt-in line (rendered in the user's language, prefixed with 💡) telling the user they may **refine the spec first** — Strategist will produce the full design spec, then stop for review/revision of any part of it before any generation, via the [refine-spec](workflows/refine-spec.md) workflow. Default is OFF: no request → the spec is written in one go and the pipeline auto-proceeds as usual. Only when the user explicitly asks in chat (e.g. "refine the spec first") or confirms `refine_spec: true` through Confirm UI does the [refine-spec](workflows/refine-spec.md) workflow take over after the Strategist confirmation stage. This line, like the split-mode line, is required output every run — the user must see the choice exists; whether to act on it is theirs. When the Confirm UI is used, this choice also appears as the in-page refine-spec toggle and is captured in `result.json` (`refine_spec`); the chat-summary fallback still prints this line.
**Formula rendering policy lives inside item 7 (Typography plan)**:
| Policy | Behavior |
|---|---|
| `mixed` (default) | Strategist renders complex formula-worthy expressions as PNG assets; simple inline expressions remain editable text / Unicode |
| `render-all` | Strategist renders every formula-worthy expression as PNG assets |
| `text-only` | No formula rendering; formulas remain editable text / Unicode |
After the Strategist confirmation stage is approved and **before outputting `design_spec.md` / `spec_lock.md`**, if the confirmed formula policy is `mixed` or `render-all` and the content contains formula-worthy expressions, Strategist MUST:
1. Identify explicit LaTeX and any source expressions that should be faithfully structured as formulas.
2. Write `<project_path>/images/formula_manifest.json` with only the formulas selected for rendering.
3. Run:
```bash
python3 ${SKILL_DIR}/scripts/latex_render.py <project_path>
```
4. Include the rendered formula PNGs as `Acquire Via: formula`, `Status: Rendered`, `Type: Latex Formula` rows in `design_spec.md §VIII Image Resource List`; also list them in `spec_lock.md images` with `| no-crop`.
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**. 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 <project_path>/images
```
> 🔁 **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 <project_path>/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**:
- `<project_path>/design_spec.md` — human-readable design narrative
- `<project_path>/spec_lock.md` — machine-readable execution contract (skeleton: `templates/spec_lock_reference.md`); Executor re-reads before every page
**✅ 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 Strategist confirmation stage
- [x] Strategist confirmation stage completed (user confirmed via Confirm UI `result.json` or chat fallback)
- [x] Split-mode note appended below the confirmation fields (heavy or normal variant)
- [x] Spec-refinement opt-in line appended (default OFF; only the user's explicit request enters the refine-spec workflow)
- [x] Design Specification & Content Outline generated
- [x] Execution lock (spec_lock.md) generated
- [ ] **Next**: Auto-proceed to [Image_Generator / Executor] phase
```
---
### Step 5: Image Acquisition Phase (Conditional)
🚧 **GATE**: Step 4 complete; Design Specification & Content Outline generated and user confirmed. Any formula rows already have `Acquire Via: formula` and `Status: Rendered`.
> **Trigger**: At least one row in the resource list has `Acquire Via: ai`, `web`, and/or `slice`. If every row is `user`, `formula`, or `placeholder`, skip to Step 6.
**Failure recovery**: stop/continue behavior for AI/web/slice/image-readiness failures is defined in [`workflows/failure-recovery.md`](workflows/failure-recovery.md). This Step keeps the acquisition procedure.
**Always load the common framework**:
```
Read references/image-base.md
```
Then **lazy-load the path-specific reference** for each row that actually needs it:
| Acquire Via | Load reference (only if any such row exists) | Run |
|---|---|---|
| `ai` | `references/image-generator.md` | write `<project_path>/images/image_prompts.json`, then follow `image-generator.md §7 Path Selection` (`image_gen.py --manifest` is **Path A only**) |
| `web` | `references/image-searcher.md` | `python3 ${SKILL_DIR}/scripts/image_search.py ...` (≥2 web rows → `--batch images/image_queries.json`) |
| `slice` | `references/image-generator.md` §4.3 | derived — **after** the parent `ai` sheet row is `Generated`, run `python3 ${SKILL_DIR}/scripts/slice_images.py <project_path>/images/<sheet>.png --grid RxC --names ... --trim --alpha` (see workflow step 2.5) |
| `user` / `formula` / `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 rows MUST use the manifest contract** — even when only 1 ai row exists. Always write `images/image_prompts.json` first and render `image_prompts.md` with `image_gen.py --render-md`. Then execute the confirmed path from `image-generator.md §7`: `image_gen.py --manifest` is **Path A only**; `host-native` is **Path B** and MUST skip `--manifest`; `manual` writes the prompts and stops for external generation. 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.
> 💡 **ai path — spot illustrations as one sheet**: when the §VIII image resource plan needs ≥3 same-family spot illustrations as decorative accessories, generate **one grid sheet** (a single `ai` sheet row) instead of one row per element, then slice it (workflow step 2.5 below). Choose sheet geometry from intended placement: `1xN` / `Nx1` are useful for extreme portrait / landscape cells, and a designed `MxN` grid is valid when its cell ratio fits the planned elements. The sheet row is generated but not placed; each cut **element row** (`Acquire Via: slice`) is placed and must appear in `spec_lock.md images`. One generation = one coherent style across all pieces. Resource contract + the geometry rules: [image-generator.md](references/image-generator.md) §4.3.
> ⚠️ **Honor the confirmed image source before running any generation command**: 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. Never run `image_gen.py --manifest` when the confirmed value is `host-native` or `manual`. Full selection rule: [image-generator.md](references/image-generator.md) §7 Path Selection.
Workflow:
1. Extract all resource rows from the design spec and group them by `Acquire Via`; rows with `Status: Pending` or `Status: Failed` and `Acquire Via ∈ {ai, web, slice}` must all reach a terminal state before Executor starts
2. Generate prompts (ai rows) and/or run search (web rows) per [image-base.md](references/image-base.md) §3 dispatch table
2.5. **Slice any spot-illustration sheets (only if `slice` rows exist).** For each generated `ai` **sheet** row, run `slice_images.py` (grid + the element `--names` matching the `slice` rows, `--trim --alpha`) so every element file lands in `images/`; mark each `slice` row `Generated`. A sheet still in `Needs-Manual` cannot be sliced — leave its `slice` rows `Needs-Manual` and surface them at the Step 7 readiness gate. Contract: [image-generator.md](references/image-generator.md) §4.3.
3. Verify every row reaches a terminal status: `Generated` (ai success / sliced element), `Sourced` (web success), or `Needs-Manual`. `Failed` is not a terminal status: it means the current run did not generate that item, but the item remains retryable. The agent must resolve every residual `Failed` item by rerunning the confirmed path or marking it `Needs-Manual` before Executor starts
4. Re-derive image facts now that web / AI / sliced files are in the folder — `python3 ${SKILL_DIR}/scripts/analyze_images.py <project_path>/images` — so `analysis/image_analysis.csv` reflects every acquired image **including the sliced elements** (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
## ✅ Image Acquisition Phase Complete
- [x] image_prompts.json created (when any ai rows processed)
- [x] image_prompts.md sidecar rendered (when any ai rows processed)
- [x] image_sources.json created (when any web rows processed)
- [x] Spot-illustration sheets sliced (when any `slice` rows exist); every element file present in `images/` and listed in `spec_lock.md images`
- [x] Each row: status is `Generated` / `Sourced` / `Needs-Manual` (no `Pending` or `Failed` remaining)
- [x] analyze_images.py re-run so image_analysis.csv covers the acquired web / AI / sliced 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 planning-session handoff below and stop this conversation:
```markdown
## ✅ Planning Session Complete
- [x] Spec: `design_spec.md`, `spec_lock.md`
- [x] Resources: `sources/`, `images/`, `templates/`
- [ ] **Next**: open a fresh chat window and input `继续生成 projects/<project_name>` to enter the execution session via the [`resume-execute`](workflows/resume-execute.md) workflow.
```
> On acquisition failure, do NOT halt — follow the Failure Handling rule in [image-base.md](references/image-base.md) §5: retry once, then mark the row `Needs-Manual`, report to user, and continue to the checkpoint above.
---
### Step 6: Executor Phase
🚧 **GATE**: Step 4 (and Step 5 if triggered) complete; all prerequisite deliverables are ready.
**Artifact ownership**: `svg_output/` is the author source, `svg_final/` is derived, and image facts come from the regenerated `analysis/image_analysis.csv`; see [`references/artifact-ownership.md`](references/artifact-ownership.md).
Read the execution references for this deck's locked `mode` + `visual_style` (from `spec_lock.md`):
```
Read references/executor-base.md # REQUIRED: common guidelines
Read references/shared-standards.md # REQUIRED: SVG/PPT technical constraints
Read references/native-shape-authoring.md # REQUIRED: stock-shape selection and fragment helper contract
Read references/modes/<locked-mode>.md # narrative skeleton (spec_lock.md `mode`)
Read references/visual-styles/<locked-style>.md # aesthetic (spec_lock.md `visual_style`)
```
> Read executor-base + shared-standards + native-shape-authoring + the one locked mode file + the one locked visual-style file. For `mode: custom` or `visual_style: custom`, skip that preset file and follow `mode_behavior` / `visual_style_behavior` from `spec_lock.md` instead. Never glob `modes/` or `visual-styles/`.
**Design Parameter Confirmation (Mandatory)**: before the first SVG, output key design parameters from the spec (canvas dimensions, color scheme, font plan, body font size). See executor-base.md §2.
**Live Preview Auto-Startup (Mandatory)**: before the first SVG, automatically start the browser editor in live mode and keep it running continuously through Executor + Step 7 export:
```bash
python3 ${SKILL_DIR}/scripts/svg_editor/server.py <project_path> --live --daemon
```
- Start it immediately when Executor begins; `svg_output/` may be empty. Editor opens at the launch-log URL such as `http://127.0.0.1:5050`; if another project already holds it, the launcher **auto-advances to the next free port** — read the actual URL from the launch log and report that.
- Treat the launch URL as a checkpoint value: before writing the first SVG, either report the actual URL from the launcher or state the launch failure explicitly. Do not silently continue while claiming preview is available.
- Run it as a long-running side process/session; do not wait for it to exit before generating SVG pages. Do not wait for user confirmation after startup.
- **Service must keep running** until one of: (a) the user clicks **Exit preview** in the browser, or (b) the user explicitly asks in chat to stop it. Generation continues even if the user closes the editor.
- **Do NOT read or apply submitted annotations during generation.** Users may annotate at any time, but Executor proceeds without touching them. The window to apply annotations opens only after Step 7 completes — see [`workflows/live-preview.md`](workflows/live-preview.md).
- The editor also supports **staged direct edits** (text content + SVG element attributes previewed immediately, then written to `svg_output/` only when the user clicks **Apply changes**; `Ctrl+Z` / Undo drops staged edits) alongside annotation; re-export stays chat-driven. Full scope and editor details: see [`workflows/live-preview.md`](workflows/live-preview.md) Notes.
**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 <project_path>/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 <project_path>/spec_lock.md` and use only its colors / fonts / icons / images, plus `pptx_structure.mode` and the per-page `page_rhythm` / `page_charts` lookups. Read `page_layouts` / `page_pptx_layouts` / `pptx_masters` / `pptx_layouts` only on a structured deck/layout template route; they are absent in flat free-design and brand-only projects. 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.
> ⚠️ **Generation rhythm**: generate pages sequentially, one at a time, in the same continuous context. Do NOT batch (e.g., 5 per group).
**Visual Construction Phase**: generate SVG pages sequentially, one at a time, in one continuous pass → `<project_path>/svg_output/`
Each completed SVG MUST be a standalone, complete representation of that slide's visible design. Template SVGs and locked planning artifacts may guide construction, but export must not reach back to them to add visible objects omitted from `svg_output/`. Speaker notes, animation, narration, transitions, and direct native-PPTX workflows remain separately owned artifacts/capabilities. Before drawing a literal stock shape, apply [`native-shape-authoring.md`](references/native-shape-authoring.md): use the stdout-only helper when one PowerPoint preset exactly matches, keep basic SVG primitives for rect/round-rect/ellipse, and keep free SVG for custom semantics. Never infer a preset from contour similarity.
Template pages MUST start from the complete `page_layouts` SVG, keep all inherited visible objects in `svg_output/`, and preserve the locked root Master/Layout identity plus stable atomic Master/Layout and slot ids. Strict keeps the prototype structure unchanged. Adaptive keeps its Master contract and, when Layout atoms or slot topology/bounds genuinely evolve, assigns a new key/name and updates `spec_lock.md` immediately. Non-mirror fill/stroke/effects/font sizes still follow `spec_lock`.
Free-design and brand-only pages use `pptx_structure.mode: flat`. Draw the complete page directly: keep backgrounds, repeated chrome, headings, text, images, and decoration as ordinary Slide-local SVG content. Do not plan `pptx_masters` / `pptx_layouts` / `page_pptx_layouts`, do not add root Master/Layout identity, and do not add `data-pptx-layer` or `data-pptx-placeholder` metadata. Group logical content normally with top-level `<g id>` elements. Export materializes one clean project-owned Master plus one Blank Layout, applies the locked theme colors/fonts/title-body defaults, removes stock content placeholders and unused built-in Layouts, and retains only the standard date/footer/slide-number capability hooks. It does not promote or deduplicate page content.
Do not duplicate specialized identity with `data-pptx-role`. Add it only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by `data-pptx-layer`, `data-pptx-placeholder`, or `data-pptx-native`; such an element needs a stable unique `id`. Do not add generic content roles to ordinary titles, body text, cards, KPIs, diagrams, charts, icons, or images. Full contract: [`references/semantic-svg.md`](references/semantic-svg.md).
**First-page gate (Mandatory)** — after the **first** SVG page, before drawing page 2:
```bash
python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path>/svg_output/<first_page>.svg
```
Fix every `error` on page 1 first — structural violations are systematic, and a first-page error repeated deck-wide costs a whole-deck rewrite.
**Quality Check Gate (Mandatory)** — after all SVGs, BEFORE annotation handling and speaker notes:
```bash
python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path>
```
- Any `error` (banned SVG features, viewBox mismatch, spec_lock drift, etc.) MUST be fixed before proceeding — return to Visual Construction, regenerate that page, re-run check.
- `warning` entries (low-res image, non-PPT-safe font tail, etc.): fix when straightforward, otherwise acknowledge and release.
- **Structured template routes only — PPTX-structure warnings are the exception.** For each empty-Layout / framing-only-Layout / bare-Master / duplicate-layout-key warning, output one disposition line: either the fix applied (merge keys in `spec_lock.md pptx_layouts` + SVG roots, mark the missing slots/layers) or why the flagged state is intended (e.g. "P01 cover is a fixed composition, zero-slot by design"). Flat free-design and brand-only routes have no positive Master/Layout checkpoint; the checker instead enforces a complete flat lock including colors/fonts/title-body defaults, plus the absence of Master/Layout/layer/placeholder metadata. "0 errors" alone does not pass a structured template gate when such warnings remain undispositioned.
- Run against `svg_output/` (not after `finalize_svg.py` — finalize rewrites SVG and masks violations).
**Logic Construction Phase**: generate speaker notes → `<project_path>/notes/total.md`
**✅ Checkpoint — Confirm all SVGs and notes are fully generated and quality-checked. Run the applicable conditional gates below, then proceed to Step 7**:
```markdown
## ✅ Executor Phase Complete
- [x] Live preview started before the first SVG and kept available at the reported URL
- [x] First-page gate run after page 1 (errors fixed before page 2)
- [x] All SVGs generated to svg_output/
- [x] svg_quality_checker.py passed (0 errors)
- [x] Structured-template PPTX warnings dispositioned one by one when applicable
- [x] Speaker notes generated at notes/total.md
```
> **Chart pages?** If this deck contains data charts (bar / line / pie / radar / etc.), run the standalone [`verify-charts`](workflows/verify-charts.md) workflow before Step 7 to calibrate coordinates. AI models routinely introduce 1050 px errors when mapping data to pixel positions; verify-charts eliminates that class of error. Skip if no chart pages.
> **Visual self-check (opt-in)?** If the user explicitly asked for a per-page visual re-pass on the SVGs ("跑一下视觉自检 / 视觉回看", "visual review", "check pages visually", etc.), run the standalone [`visual-review`](workflows/visual-review.md) workflow before Step 7. Do NOT run it by default and do NOT recommend it based on inferred model capability or deck size — trigger is user request only.
---
### Step 7: Post-processing & Export
🚧 **GATE**: Step 6 complete; all SVGs generated to `svg_output/`; speaker notes `notes/total.md` generated.
🚧 **Image readiness GATE** (when Step 5 left ai rows in `Needs-Manual`): every expected file must exist at `project/images/<filename>` before running 7.1.
**Failure recovery**: if a Step 7 command fails, fix the owning source artifact and resume from the failed sub-step per [`workflows/failure-recovery.md`](workflows/failure-recovery.md). Do not restart the planning session unless the owning source changed.
> If files are missing: PAUSE, list the missing filenames, point the user to `images/image_prompts.md` (each `### Image N:` block is paste-ready for ChatGPT / Gemini / Midjourney; auto-generated from `image_prompts.json`) and the required placement `project/images/<filename>`. Resume Step 7.1 only after all expected files are in place. `finalize_svg.py` and `svg_to_pptx.py` do not detect missing files at this layer — proceeding with gaps produces a deck with broken image references.
> **Spot-illustration sheets at this gate**: `slice` element files are **derived**, not placed by the user. If a sheet was `Needs-Manual` (offline), the element files do not exist yet — list the **sheet** filename (`images/<sheet>.png`) plus its element target names, and instruct: place the sheet, then run the Step 5 `slice_images.py` command for it, then re-run `analyze_images.py`, before resuming 7.1. Never tell the user to hand-place the individual element files — they only come from slicing the sheet.
> ⚠️ Run the three sub-steps **one at a time** — each must complete successfully before the next.
> ❌ **NEVER** combine them into a single code block or shell invocation.
Canonical three-command pipeline (this step is the workflow authority;
`references/shared-standards.md` §5 points here):
**Step 7.1** — Split speaker notes:
```bash
python3 ${SKILL_DIR}/scripts/total_md_split.py <project_path>
```
**Step 7.2** — SVG post-processing (icon embedding / image crop & embed / raster image optimization / text flattening):
```bash
python3 ${SKILL_DIR}/scripts/finalize_svg.py <project_path>
```
This mandatory step writes self-contained visual-preview SVGs to `svg_final/`. Those files may be opened directly or manually inserted into PowerPoint as SVG pictures. Default raster handling embeds images at the rendered SVG size budget (`--image-scale 2`, `--max-dimension 2560`); opaque PNG photos may be written as JPEG, and transparent assets remain PNG. The existing EMF/WMF exception still applies: Office vector assets stay externally referenced for lossless native-PPTX passthrough, so the native PPTX remains the source of truth for pages that use them. Use `--no-compress` or a higher `--max-dimension` only for diagnostic / high-fidelity SVG previews.
**Step 7.3** — Export PPTX (embeds speaker notes by default):
```bash
python3 ${SKILL_DIR}/scripts/svg_to_pptx.py <project_path>
# Output (default-flow mode):
# exports/<project_name>_<timestamp>.pptx ← native pptx (canonical output, reads svg_output/)
# backup/<timestamp>/svg_output/ ← Executor SVG source backup (always written)
# Add --native-objects to emit real editable chart/table objects instead of flattened shapes:
# exports/<project_name>_<timestamp>_native_charts.pptx ← native chart/table objects (data-pptx-native markers)
# Re-export with --recorded-narration audio (generate-audio workflow) embeds per-slide narration:
# exports/<project_name>_<timestamp>_narrated.pptx ← narrated pptx (embedded audio + auto-advance timings)
```
> The native pptx consumes `svg_output/` directly so the converter can preserve
> high-fidelity primitives (icon `<use>` placeholders, image `preserveAspectRatio`
> → native picture crop metadata, rounded rect `rx/ry` → `prstGeom roundRect`).
> Native raster images are optimized by default before writing `ppt/media`
> (`--image-sizing cap`, `--image-max-dimension 2560`, `--image-quality 85`).
> This optimization downscales only oversized full source images; it does not
> crop pixels out of embedded PPTX media, and it does not reduce a small
> placement image merely because it is currently displayed small. Display
> cropping remains editable PPT picture-crop metadata. Add `--no-image-optimize`
> only when the deck must retain original image bytes. Use
> `--image-sizing display --image-scale 2` only for aggressive size reduction.
> The `svg_output/`
> snapshot in `backup/<timestamp>/` is always written so the project can be
> re-exported from frozen SVG sources without re-running the LLM. The SVG-rendered
> preview remains the mandatory `svg_final/` artifact from Step 7.2; it is not
> packaged as a second PPTX. Use the default source selection for release
> exports. `-s final` is diagnostic-only
> when comparing conversion behavior against the post-processed SVGs; it does
> not change `svg_output/` ownership or establish a supported release route.
> **Supported PPTX boundary** — the only supported generated-PPTX path is
> `svg_output/` → the project SVG-to-DrawingML converter → native PPTX. The
> project does not emit an SVG-image PPTX and does not support PowerPoint's
> manual **Convert to Shape** operation on `svg_final/`. Inserted `svg_final/`
> pages remain ordinary SVG pictures unless the user independently accepts the
> results of an unsupported Office conversion.
> **PPTX structure mode** — release export reads the explicit route from
> `spec_lock.md`. Free-design and brand-only projects use
> `pptx_structure.mode: flat`, omit `pptx_masters` / `pptx_layouts` /
> `page_pptx_layouts` / `page_layouts`, and author no Master/Layout/layer/placeholder metadata in
> SVG. Export keeps every represented object Slide-local while materializing
> one clean project-owned Master and one Blank Layout from the current lock;
> stock content placeholders and unused built-in Layouts are removed; only the
> standard date/footer/slide-number capability hooks remain.
>
> Deck/layout template projects use `pptx_structure.mode: structured`, a
> complete `pptx_masters` roster, one unique `pptx_layouts` definition per
> reusable Layout, and exactly one `page_pptx_layouts` assignment per page.
> A Layout definition records Master key, PowerPoint picker name, and either a
> `P<NN>` or `template:<basename>` prototype source; an unused Layout uses a template prototype
> and is registered without a published carrier slide. Every SVG
> root repeats the Master/Layout keys and picker names. Master/Layout fixed
> visuals are direct root atoms; a `<g data-pptx-layer="master|layout">` is
> forbidden. Reusable slots are direct root `<g id>` elements with positive
> design-zone bounds and exactly one compatible carrier. A composite region
> uses only the explicit `object` + `proxy` fallback, and a Layout may
> intentionally have zero slots.
>
> Structured template export creates the declared Masters and Layouts,
> promotes the represented
> atoms, binds slot carriers, installs the locked theme/text defaults, and
> reopens the candidate package to verify Presentation → Master → Layout →
> Slide registration, picker names, static-object rosters, placeholder
> type/index/bounds, carrier bindings, hidden proxies, and zero-slot Layouts.
> It never selects pages, clusters visuals, promotes repeated chrome by
> heuristic, or invents missing structure. Legacy structured/template projects
> using `baseline`, `template`,
> `preserve`, `layout_strategy`, `data-pptx-layout-kind`,
> `distilled`/`utility`, direct atomic placeholders, or incomplete Master
> identity must run
> [`restore-pptx-structure`](workflows/restore-pptx-structure.md) before
> export.
> **Template structured export** — `page_layouts` records the complete
> input prototype per page, `pptx_masters` / `pptx_layouts` record the unique
> reusable output roster, and `page_pptx_layouts` records page assignment.
> Strict keeps the prototype
> Master/Layout/slot contract. Adaptive keeps its Master and may assign a new
> Layout identity during page authoring only when fixed Layout atoms or slot
> topology/bounds change; the lock is updated immediately. Non-mirror skin
> remains project-controlled, mirror preserves the reused visual identities,
> and the exporter never reads a template to add visible objects missing from
> `svg_output/`. Raw PPTX templates still route to `template-fill-pptx`;
> reusable template creation goes through `create-template`.
> **Paragraph editability vs line fidelity** — by default, mergeable dy-stacked
> paragraph blocks collapse into one editable PowerPoint text frame with multiple
> `<a:p>`, improving body-text editing and resize/reflow behavior. Add `--no-merge`
> only when the user explicitly asks for strict line-layout fidelity or when a
> layout-tight page must keep every dy-stacked line as its own text frame. The
> merge detector is conservative: adjacent lines with different effective font
> sizes retain a paragraph break, and mixed-layout text falls back to per-line frames.
> A multiline
> text carrier inside a slot must remain one native text frame; do not combine
> it with `--no-merge`. Strict-line text stays Slide-local rather than claiming
> one PowerPoint placeholder.
> **Native table/chart objects** — supported data charts and pure text-grid
> tables carry `data-pptx-native` markers by default (Executor transcribes
> them at draw time; see `references/executor-base.md` §3.2) and the markers
> stay dormant.
> Add `--native-objects` only when the user explicitly wants
> PowerPoint-editable native tables/charts and accepts that those objects may
> render differently across PowerPoint / Keynote / LibreOffice / WPS; marker-local
> details not represented by native metadata may be omitted. This is a lossy
> editable-first contract, not a reason to disable an otherwise supported marker. Without
> the flag, marked groups export through their SVG fallback children like
> ordinary SVG content. Imported objects that carry
> `data-pptx-native-status` are fallback-only; the quality checker and
> `--native-objects` export surface their reason as warnings rather than silently
> claiming editability. An imported chart with no baked preview is a different
> case: `data-pptx-visual-status="placeholder"` plus
> `data-pptx-route-status="reconstruction-only"` records a diagnostic route.
> Default export keeps that placeholder with a warning; when the same group has
> a valid active `data-pptx-native="chart"` payload, `--native-objects` may still
> reconstruct the editable chart. Invalid or contradictory status declarations
> remain export errors. For supported parsed classic families, the importer
> instead emits a deterministic visible fallback with
> `data-pptx-visual-status="normalized"`; this is readable reconstruction, not
> a claim of Office pixel parity. Active imported table/chart markers also carry
> `data-pptx-fallback-sha256`. If their fallback, reachable SVG fragment
> definition, local reference target, or marker transform changes later, default
> export keeps that SVG, the mandatory quality checker warns, and
> `--native-objects` fails rather than discard the edit. Legacy markers
> without a baseline remain native-compatible and only warn that stale detection
> is unavailable.
> Imported table markers may also cover the verified narrow P2 subset:
> exact physical row/grid topology, canonical rectangular merges with blank
> covered cells, safe per-side borders, plain multi-paragraph cells, and closed
> run-level rich text. A rich paragraph contains non-empty `runs`; each run
> requires `text` and may use only
> `bold` / `italic` / `underline` / `strike` / `color` / `font_size` /
> `font_family` / `lang` / `alt_lang`. Presentation-only source run XML
> normalizes, while relationship-bearing text, extensions, line breaks, fields,
> tabs, bullets, broken text topology, noncanonical merges, and unsafe direct
> formatting remain fallback-only. Imported classic charts
> additionally cover verified column/line/area combos, canonical OHLC stock,
> area date-axis cases, verified scatter/bubble axes, radar, safe `of_pie`
> `serLines`, and the closed axis/title/legend plus bar-gap/overlap normalization
> cases. The importer also accepts the seven closed ChartEx data models:
> treemap, sunburst, histogram, pareto, box-whisker, waterfall, and funnel.
> ChartEx data topology is retained for native read-back, but style, axis,
> labels, and binning details may normalize. These additions do not create a
> full `AxisSpec`, arbitrary ChartEx import, arbitrary rich OOXML, or new
> normalized-renderer coverage; unmodeled semantics continue to fail closed
> without reducing existing active-marker SVG-to-native conversion.
**Optional animation flags** (page transitions are on by default; per-element entrance is off by default — turn it on only when the user asks for it):
- `-t <effect>` — page transition. Default `fade`. Options: `fade` / `push` / `wipe` / `split` / `strips` / `cover` / `random` / `none`. `none` removes only the visual transition; an explicit automatic advance remains valid.
- `-a <effect>` — per-element entrance animation. **Default `none`** — pages appear as a whole, no auto-firing element builds (the unsolicited cascade reads as the "AI deck" tell). Opt in with `auto` (map effect from group id: chart→wipe, card-/step-/pillar-→fly, title/takeaway→fade; image-like ids `hero` / `figure-` / `image` / `img-` / `kpi` cycle a richer pool — zoom / dissolve / circle / box / diamond / wheel — so multiple images vary across the deck), a specific effect like `fade`, or `mixed` for the legacy 16-effect cycle. Requires top-level `<g id="...">` groups (already required by Executor).
- `--animation-trigger {on-click,with-previous,after-previous}` — Start mode (matches PowerPoint's animation-pane Start dropdown). Default `after-previous` (click-free cascade; pace via `--animation-stagger`). Use `on-click` for presenter-paced reveals, or `with-previous` for all-at-once.
- `--animation-config <path>` — optional object-level sidecar. Default: `<project_path>/animations.json` when present.
- `--auto-advance <seconds>` — kiosk-style auto-play. Click remains enabled, so click or timer may advance the slide.
**Animation compatibility gate**: the default element animation remains `none`.
When animation is enabled, unknown effects/modes/triggers, invalid numeric or
order values, missing slide/group references, and explicit structural layer,
static-role, or static-placeholder targets fail export; they never downgrade
or disappear silently. An explicit
sidecar group may override only the legacy chrome-name heuristic. `random`
resolution is stable for the same effective input; with `--conversion-trace`,
its resolved rows are written to the trace. Generated export performs per-slide semantic
read-back plus package timing/`p:cTn`/`p:spTgt` validation. Narration merges
audio timing into the existing DOM and preserves animation rows. Direct-PPTX
routes preserve source object animation, compare its object-animation fingerprint
before/after allowed edits, and validate structure; they do not author
animation effects. The exact 22 tuples and OOXML rules live in
[`scripts/docs/pptx-animations.md`](scripts/docs/pptx-animations.md).
**Optional custom animations** (only when the user asks to tune animation order/effects/timing for specific objects):
Run the standalone [`customize-animations`](workflows/customize-animations.md) workflow. Default export applies page transitions but no per-element entrance animation; create `animations.json` (or pass `-a auto`) only when the user asks for element animation or object-level customization.
**Optional recorded narration** (only when the user asks for narrated/video export):
Run the standalone [`generate-audio`](workflows/generate-audio.md) workflow. The AI picks a narration backend (`edge` by default, or a configured cloud provider such as ElevenLabs / MiniMax / Qwen / CosyVoice for high-quality or cloned voices), asks the user once (backend + voice + rate/settings + embed-or-not, all with recommended values), then executes `notes_to_audio.py` and (if chosen) re-exports the PPTX with `--recorded-narration audio`.
Do NOT call `notes_to_audio.py` directly without going through the workflow — `--voice` / `--voice-id` is required and the workflow produces the locale/provider-aware recommendation that makes the choice meaningful.
Full effect list, anchor logic, and limits: [`references/animations.md`](references/animations.md).
The compatibility contract covers PowerPoint OOXML; do not promise identical
animation playback in Keynote or other presentation applications.
> ❌ **NEVER** substitute `cp` for `finalize_svg.py` — finalize performs multiple critical processing steps
> ❌ **NEVER** use `-s final` for a release export. It is a diagnostic comparison only; the supported native route reads `svg_output/`.
> **Post-export annotation window**: the preview service from Step 6 typically remains running after export. If the user submitted annotations in the browser (during Executor or after export) and now asks to apply them — they may quote the browser prompt (`Changes saved to svg_output...` / `修改已保存到 svg_output...`), say "apply my annotations" / "应用注解" / equivalent — run [`live-preview`](workflows/live-preview.md) Step 2 to apply and re-export. Annotations submitted during generation are also handled here, not earlier.
> **Direct edits in the browser**: the user may also stage text / SVG attribute edits in the preview. These land in `svg_output/` only after the user clicks **Apply changes**. If they ask to "re-export" / "重新导出" after applying such edits, just re-run Step 7.27.3 (finalize + export); no annotation-application step is needed unless they also saved AI-needed annotations.
> **Preview not running?** Any time the user mentions "live preview", "preview", "看效果", or wants to select/click a slide element and the service is not running, run [`live-preview`](workflows/live-preview.md) Step 1 to start it. If the service is already running, just point them at the URL — do not restart.
---
## Role Switching Protocol
Before switching roles, **MUST first read** the corresponding reference file. Output marker:
## Global Execution Discipline
1. **Serial execution** — Follow the selected authority's steps in order. A completed non-blocking step may continue directly to the next eligible step.
2. **Blocking means stop** — At every `⛔ BLOCKING` gate, wait for explicit user confirmation. Do not decide on the user's behalf.
3. **No cross-phase bundling** — Do not combine work across an unclosed gate. Once the route's final user gate closes, later non-blocking steps may continue automatically.
4. **Gate before entry** — Verify every listed prerequisite before entering a step.
5. **No speculative execution** — Do not prepare later-phase artifacts before their owning step.
6. **Deterministic routing** — Do not add a route-choice question when [`routing.md`](workflows/routing.md) resolves the request. If a route prerequisite is missing, state it and stop that route.
7. **Owning-source recovery** — On failure, repair or regenerate the owning source artifact and resume from the route's declared pointer. Do not silently downgrade a required artifact.
## Global Communication Rules
- Match the user's language and source language unless the user explicitly overrides it.
- Localize user-facing option labels and explanations. Keep exact enum IDs or field names when needed for precision.
- Keep `design_spec.md` section headings and field names in the template's original English; content values may use the user's language.
- Before switching roles, read the corresponding role reference and output:
```markdown
## [Role Switch: <Role Name>]
@@ -910,22 +50,9 @@ Before switching roles, **MUST first read** the corresponding reference file. Ou
---
## Reference Resources
## Repository Compatibility
| Resource | Path |
|----------|------|
| Shared technical constraints | `references/shared-standards.md` |
| Native preset shape authoring | `references/native-shape-authoring.md` |
| Semantic SVG marker contract | `references/semantic-svg.md` |
| Canvas format specification | `references/canvas-formats.md` |
| Image-text layout patterns (Primary structures + Modifier layers — combine freely) | `references/image-layout-patterns.md` |
| Image layout sizing (math for side-by-side container dimensions) | `references/image-layout-spec.md` |
| SVG image embedding | `references/svg-image-embedding.md` |
| Icon library | `templates/icons/README.md` |
---
## Notes
- Local preview: `python3 -m http.server -d <project_path>/svg_final 8000`
- **Troubleshooting**: on generation issues (layout overflow, export errors, blank images, etc.), check `docs/faq.md` for known solutions
- This package is a workflow/skill, not a generic application scaffold. Do not create `.worktrees/`, `tests/`, branch workflows, or generic engineering structure by default.
- Keep required workflow, reference, script, and template documentation inside this Skill directory.
- Repository-level documents may point into the package; package runtime files must not depend on repository-level instructions.
- On Windows, if a documented `python3 ...` command is unavailable, rerun the same command with `python`.
@@ -1,9 +1,9 @@
# AI Image Comparison — Three-Dimension Reference Gallery
# AI Image Comparison — Reference Gallery
PPT Master's AI images are governed by three orthogonal dimensions: **rendering (visual style) × palette (color behavior) × type (internal composition)**.
This directory preserves rendering, legacy palette, and type comparison assets. The current Confirm UI displays **rendering only**; deck colors come directly from the selected PPT color system. The palette gallery remains for legacy diagnostics and must not become a second confirmation control.
This directory uses **controlled-variable comparison** — vary one dimension while holding the other two fixed — so you can see exactly what each dimension contributes.
> This is **not** an example project (see `examples/` for those). It is a dimension-selection reference for the Strategist role and end users when picking AI image parameters.
> This is **not** an example project. The rendering set remains a selection reference; the palette set is legacy diagnostic material, and the type set is an internal composition reference.
## The three comparison sets
@@ -58,8 +58,8 @@ Generated images land in the corresponding subdirectory. Each item's `status` in
| If you are deciding... | Look at |
|---|---|
| Which rendering to lock in Strategist h.5 | `rendering/` — scan all 20 side by side; pick the visual temperament that matches the deck |
| Which palette pairs best with your chosen rendering | `palette/` — see how color behavior shifts the same subject |
| Which rendering to lock in [`strategist-image.md`](../strategist-image.md) §2 | `rendering/` — scan all 20 side by side; pick the visual temperament that matches the deck |
| Diagnosing a legacy palette lock or comparison asset | `palette/` — see how historical color behaviors shift the same subject; do not expose it as a new-flow choice |
| Which type fits a specific image's purpose | `type/` — match the internal composition to the page's content shape |
> The three sets are intentionally independent. When deciding rendering, do not look at the palette set — its varying colors will distort your judgment of pure rendering style.
> The three sets are intentionally independent. When deciding rendering in the current flow, use only `rendering/`; the legacy palette set must not influence or reintroduce a separate color decision.
@@ -1,8 +1,8 @@
# Page Transitions & Per-Element Animations
PPT Master's exported PPTX supports **page transitions** and **per-element
entrance animations** as real PowerPoint OOXML. Other applications may
interpret timing differently; this contract makes no unconditional Keynote guarantee.
Execution contract for generated-PPTX **page transitions** and **per-element
entrance animations**. This file owns defaults, sidecar semantics, anchor
selection, validation, and package read-back.
## 1. Defaults
@@ -19,7 +19,7 @@ To regenerate a deck with different settings, rerun `svg_to_pptx.py` against the
Per-element animation is off by default. To enable it deck-wide, pass `-a auto` at export (no config needed). When a deck instead needs specific object timing — for example title first, chart second, annotation last — use the optional `animations.json` sidecar. The SVG remains static visual source; the sidecar only controls PPTX export behavior.
Run the standalone [`customize-animations`](../workflows/customize-animations.md) workflow when the user asks to tune animation order, effects, timing, or object-level reveals.
Run the [`customize-animations`](../workflows/stages/customize-animations.md) post-processing stage when the user asks to tune animation order, effects, timing, or object-level reveals.
```bash
# Build an editable scaffold from real top-level <g id> anchors
@@ -67,6 +67,12 @@ Rules:
- An explicit sidecar group may override the legacy chrome-name heuristic, but it cannot override `data-pptx-layer` or an explicit static role/placeholder marker.
- Unknown effects, modes, or triggers and invalid numeric/order fields fail validation; no fallback effect is substituted.
**Declared inheritance for omitted sidecar fields**:
- The whole `animations.json` artifact is optional. When absent, normal exporter CLI resolution applies.
- In any existing sparse sidecar, an omitted slide transition/animation property inherits the matching `defaults.transition` / `defaults.animation` property; when that defaults property is also absent, normal exporter CLI resolution applies. Explicit CLI overrides still win. Current authoring writes each slide's complete transition and animation blocks.
- A group override inherits `effect` and `duration` from its resolved slide animation; omitted `order` and `delay` use the exporter's sidecar resolution.
---
## 3. Page Transitions
@@ -150,7 +156,7 @@ Flags:
## 5. Anchor Logic — Top-Level `<g id="...">`
Per-element animations are anchored on **top-level `<g id="...">` content groups** in the SVG (e.g. `<g id="cover-title">`, `<g id="card-1">`). One group produces one animation-pane entrance row; whether that row needs a click depends on the selected Start mode.
Per-element animations are anchored on **top-level `<g id="...">` content groups** in the SVG (e.g. `<g id="cover-title">`, `<g id="card-1">`). IDs must be unique within the page. One group produces one animation-pane entrance row; whether that row needs a click depends on the selected Start mode. Nested implementation groups may remain anonymous because the sidecar does not target them.
Aim for **38 content groups per slide**. This is also the granularity PowerPoint uses for group-select / group-move, so it improves editing ergonomics regardless of animation.
@@ -161,7 +167,7 @@ Aim for **38 content groups per slide**. This is also the granularity PowerPo
- ≤ 8 visible top-level primitives → each becomes one anchor (capped to avoid 70+ atom cascades on dense pages).
- > 8 → animation is skipped on that slide. The slide still renders, just without entrance animation.
Executors should wrap logical sections in `<g id>` regardless of whether you plan to animate. The Executor reference (`skills/ppt-master/references/shared-standards.md`) requires it.
Executors should wrap logical sections in `<g id>` regardless of whether you plan to animate. [`shared-standards-core.md`](./shared-standards-core.md) requires it.
---
@@ -11,24 +11,37 @@ Global artifact ownership rules for PPT Master projects.
| Artifact | Owner | Role | Read/write contract |
|---|---|---|---|
| `sources/` content-type files | Content contract | Main pipeline source for text, tables, chart data values, and SmartArt node wording | Strategist reads content-type files (`.md` / `.markdown` / `.txt` / `.csv` / `.tsv` / `.json` / `.jsonl` / `.yaml` / `.yml`) and judges by content; do not replace values with PPTX geometry JSON in the main pipeline |
| `sources/*.facts.json` | Fact provenance contract | Stable external `fact_id` → claim/source mapping created by topic research | Strategist cites IDs in §IX; Executor resolves them for visible footnotes / natural notes attribution. Scenario data never enters this file. |
| `sources/` converted-source originals | Source archive | Imported source files that have a converted content contract (`.pdf` / `.pptx` / `.docx` / `.xlsx` / `.html` / `.epub` / `.tex` / `.rst` / `.ipynb` / `.typ`, etc.) and source-adjacent extracted assets | Read via the converted `<stem>.md` in the main pipeline; direct-PPTX workflows read the `.pptx` by route |
| `sources/*.conversion_profile.json`, `sources/*_files/image_manifest.json` | Pipeline sidecar | Conversion audit record / asset index | NOT read as slide content; open only to audit a conversion or resolve assets |
| `analysis/source_profile.json` | Machine fact index | Compact Strategist-facing PPTX intake digest | Main pipeline reads as factual context and recommendation candidates |
| `analysis/<stem>.identity.json` | Native deck identity facts | Canvas, theme palette/fonts, observed usage | Read selectively when detailed identity facts are needed |
| `analysis/<stem>.slide_library.json` | Native PPTX structure facts | Text slots, geometry, native tables, native chart caches, SmartArt nodes/connections | Direct PPTX workflows use as native fill/structure contract |
| `analysis/image_analysis.csv` | Regenerated image fact view | Measured facts about the current `images/` folder | Re-run `analyze_images.py` before reading image facts after changes |
| `design_spec.md` | Human design narrative | Explains design intent, outline, rationale, and resource plan | Strategist writes; humans and later roles read for intent |
| `spec_lock.md` | Execution contract | Literal colors, typography, icons, images, page rhythm, charts, and the route's PowerPoint structure mode; deck/layout template routes additionally own input prototypes, the Master roster, and the complete page-to-Master/Layout mapping | Strategist writes the route-specific contract; Executor re-reads it before every page and may add a new adaptive Layout identity only on a structured template route while authoring the page that first needs it; `restore-pptx-structure` owns legacy template migration |
| `design_spec.md` | Strategist design authority | Human-readable design intent, outline, rationale, and resource plan authored from the final confirmation plus source analysis | Strategist writes and audits it against every confirmed field before lock projection; humans and later roles read it for intent |
| `spec_lock.md` | Execution projection | Machine-readable colors, typography, icons, images, page rhythm, charts, `template_reuse_scope`, and the route's PowerPoint structure mode; mirror/layout template routes additionally own input prototypes, the Master roster, and the complete page-to-Master/Layout mapping | Strategist projects the route-specific contract from the audited Design Spec without making another design decision; `page-context` deliberately repeats its compact global projection per page as an anti-drift guard and adds current-page routing values. Executor may add a new adaptive Layout identity only on a structured mirror/layout route while authoring the page that first needs it. |
| `project_manager.py page-context` stdout | Derived per-page context | Read-only model-facing lock projection + current-page delta + fingerprints for large references | Generate immediately before each page without `--bundle`; never edit or persist it as a replacement source of truth. `global` is the bounded repeated lock guard. `reference_set` carries path/SHA/load policy for project/template Design Specs and selected prototype/chart SVGs, but never appends their payloads. |
| `analysis/page-context/P<NN>.usage.json` | Derived context telemetry | Actual compact page-context size plus hashes of owning inputs/references | `page-context --record-usage` deterministically replaces that page's snapshot; `page-context-report` summarizes current snapshots and unique references. Use token data to evaluate context cost, never as content or an execution contract. |
| `images/` | Runtime image pool | User, extracted, AI, web, formula, slice, EMF/WMF assets | Step 5 writes here; `analysis/image_analysis.csv` derives from current contents |
| `icons/` | Project icon inventory | Icons copied by `icon_sync.py` for this project | Executor uses locked project icons; exporter may fall back to global library only as documented |
| `templates/` | Project template reference | Step 3 imported specs, template SVGs, and non-image assets | Strategist/Executor read only when Step 3 is triggered |
| `templates/` | Project template reference | Step 3 imported specs, template SVGs, and non-image assets | Strategist reads the template Design Spec and actual SVG roster during planning. Continuous Executor reuses that context; fresh Executor reads the Design Spec once and each selected complete SVG only before first use or after its SHA changes. |
| `templates/template_execution_manifest.json` (`v1`) + `templates/template_execution/*.text-slots.json` (`v2-min`) | Derived template index | Compact prototype/source-import summary plus per-prototype text-slot diagnostics; the sidecar integrity hash is tool-only | Materialization may publish these deterministic records, but page-context does not inject or require them and models do not read them during page authoring. The complete prototype SVG is the sole visual/template authority; never author from either JSON artifact. |
| `<import_workspace>/svg/` | Imported native-payload backing | Complete PPTX-derived metadata, hidden carriers, fallback evidence, and source structure | Keep immutable; create-template materialization may resolve a validated source ref against these files, but models do not edit or bulk-read them |
| `<import_workspace>/svg-flat/` | Optional complete-page verification backing | Self-contained visual composition generated only by explicit `--inheritance-mode both` | Keep immutable when requested; never use as authoring or materialization input |
| `<import_workspace>/authoring-svg/` | Template-creation author source | Layered editable SVG IR for imported Master, Layout, and Slide objects | Template_Designer reads and edits this bundle; final template SVGs are materialized from it rather than copied from lossless backing |
| `<import_workspace>/authoring-svg/authoring_summary.json` | Model-readable authoring index | Current SVG roster plus compact per-file canvas, size, text, image, vector, placeholder, and source-ref counts | Models read this before authoring SVGs; regenerate after direct IR edits |
| `<import_workspace>/authoring-svg/authoring_manifest.json` | Tool-only authoring provenance contract | Per-document source/authoring hashes and document-local source-ref paths | Generated atomically with the IR; materialization validates it before reusing native payload; never load it into model context or duplicate raw payload here |
| `<import_workspace>/authoring-svg-flat/` | Optional complete-page verification IR | Self-contained page composition view with its own summary and provenance manifest | Generate only from an explicitly requested `svg-flat/`; use to verify composition, while layered `authoring-svg/` remains the canonical editable source |
| `<import_workspace>/icons/imported/` | Imported vector pool | One canonical copy of every factored vector subtree | Authoring SVGs reference `data-icon="imported/<name>"`; vector inventories retain source refs so expansion re-establishes IR identity |
| `confirm_ui/recommendations.json` | Confirmation proposal | Strategist-authored confirmation payload | Confirm UI reads; rewritten between Stage 1, Stage 2, and Stage 3 |
| `confirm_ui/result.json` | Confirmation result | User-confirmed values | Strategist treats final result as authoritative over recommendations |
| `svg_output/` | Page-design author source | Main-agent handwritten SVG pages containing the complete visible design | Quality checker and native PPTX export read this as the canonical visual/page-layout source; templates and locks do not add missing visible objects at export |
| `notes/total.md` | Speaker-note source | Complete notes before splitting | Step 6 writes; Step 7.1 splits |
| `notes/slide_*.md` | Split notes | Per-slide notes generated from `total.md` | Derived by `total_md_split.py` |
| `svg_final/` | Derived visual preview | Self-contained post-processed SVGs that may be opened directly or inserted as SVG pictures | Rebuild from `svg_output/` with `finalize_svg.py`; do not use as a supported PPTX source |
| `exports/` | Delivery artifacts | Native DrawingML PPTX and its explicit native-object/narration variants | Step 7.3 writes final outputs from `svg_output/` |
| `validation/svg_quality_report.json` | Quality provenance | Final SVG gate split into blocking / introduced / inherited / source-import categories, bound to the checked SVG bytes by SHA-256 | `svg_quality_checker.py --stage final --json` writes before export; the exporter reads it programmatically and links it only when the export-source fingerprint matches. Agents use successful command output and do not load the full JSON except for targeted failure/audit reads. |
| `validation/<output_stem>.report.json` | Published-package audit | PPTX package/resource postflight status, part counts, and quality-gate linkage | Step 7.3 writes after the PPTX passes package validation and emits a compact `[POSTFLIGHT]` receipt. Agents use the receipt on routine success and keep the full JSON cold unless targeted failure/audit evidence is required. |
| `exports/` | Delivery artifacts | Native DrawingML PPTX and explicit native-object/narration variants | Step 7.3 writes only final deliverables from `svg_output/`. |
| `backup/<timestamp>/svg_output/` | Frozen author-source archive | Re-export source without re-running LLM | `svg_to_pptx.py` writes a snapshot during export |
| `animations.json` | Optional animation config | Object-level animation sidecar | Created only by explicit animation workflow/request |
@@ -41,10 +54,12 @@ Global artifact ownership rules for PPT Master projects.
| Content values | Main pipeline text, tables, chart values, and SmartArt node wording come from content-type files in `sources/` (`.md` / `.markdown` / `.txt` / `.csv` / `.tsv` / `.json` / `.jsonl` / `.yaml` / `.yml`), not from `slide_library.json`. |
| Sources read policy | In `sources/`, read content-type files (`.md` / `.markdown` / `.txt` / `.csv` / `.tsv` / `.json` / `.jsonl` / `.yaml` / `.yml`) and judge by content — a `.json` / `.csv` may be core content or just data. Exclude known sidecars: `*.conversion_profile.json` and `*_files/image_manifest.json`. `analysis/` facts (`source_profile.json`, `<stem>.slide_library.json`) are read per Step 4 / direct-PPTX workflow, not in the `sources/` content scan. |
| PPTX structure | `slide_library.json` owns native geometry, slot facts, and SmartArt layout/relationships for direct PPTX workflows. |
| Design contract | `design_spec.md` explains; `spec_lock.md` executes. Executor must not infer execution values from prose. |
| Free-design / brand-only packaging authority | `spec_lock.md` declares `pptx_structure.mode: flat` and omits `pptx_masters`, `pptx_layouts`, `page_pptx_layouts`, and `page_layouts`. `svg_output/` owns the complete Slide-local visual design without root Master/Layout identity, fixed-layer ownership, or placeholder metadata. Export materializes one clean project-owned Master plus one Blank Layout, applies the locked theme defaults, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks. |
| Template structure authority | `page_layouts` owns each page's authoring-input prototype. `pptx_masters` / `pptx_layouts` own the unique reusable output definitions, while `page_pptx_layouts` owns page assignment. Strict keeps the prototype contract; adaptive may create a new Layout definition during page authoring and updates its assignment immediately. Unused definitions may register without a published Slide. Templates validate provenance but never add missing visible page objects during export. |
| Legacy structure migration | [`restore-pptx-structure`](../workflows/restore-pptx-structure.md) owns conversion of old unmapped/distilled/preserve structured projects and legacy template packages into the current template contract. An intentional free-design or brand-only `flat` project is not a migration input. The exporter does not migrate or visually cluster legacy template structure. |
| Design contract | Final confirmation → audited `design_spec.md` → projected `spec_lock.md`. Executor may apply projected `Template Application` prose to visible template content, but never infer lock values from it. On divergence, repair the lock from the Design Spec unless the Design Spec itself fails confirmation fidelity. |
| Flat packaging authority | Free-design, brand-only, and `template_reuse_scope: style` declare `pptx_structure.mode: flat` and omit `pptx_masters`, `pptx_layouts`, `page_pptx_layouts`, and `page_layouts`. `svg_output/` owns the complete Slide-local visual design without root Master/Layout identity, fixed-layer ownership, or placeholder metadata. Export materializes one clean project-owned Master plus one Blank Layout, applies the locked theme defaults, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks. |
| Template structure authority | `template_reuse_scope: mirror|layout` uses `page_layouts` for each page's authoring-input prototype. `pptx_masters` / `pptx_layouts` own the unique reusable output definitions, while `page_pptx_layouts` owns page assignment. Strict keeps the prototype contract; adaptive may create a new Layout definition during page authoring and updates its assignment immediately. Mirror additionally preserves literal visuals/text topology; layout allows project-controlled reflow/re-skinning. Unused definitions may register without a published Slide. Templates validate provenance but never add missing visible page objects during export. |
| Fact classes | External facts resolve through `sources/*.facts.json`; invented demo KPIs/targets/internal ratios are labeled `scenario` in `design_spec.md §IX` and visibly in the page. Never promote scenario data into the external fact registry. |
| Imported-template authoring | Editable SVGs under `authoring-svg/` own create-template edits, `authoring_summary.json` owns model-facing orientation, and `authoring_manifest.json` owns tool-only source-object identity. Lossless `svg/` owns immutable native payload and fallback evidence; optional `svg-flat/` owns only complete-page verification. Materialized `templates/*.svg` own the validated deliverable contract and contain no IR-only source refs. |
| Legacy template input | Old unmapped/distilled/preserve structured projects and incomplete template packages are not migrated in place. [`create-template`](../workflows/create-template.md) authors a new current workspace: original PPTX Type A may preserve existing native topology in mirror; legacy SVG-only Type B is visual reference for `standard` / `fidelity`. An intentional free-design or brand-only `flat` project is already current. The exporter does not migrate or visually cluster legacy structure. |
| Image facts | `images/` is live state; `analysis/image_analysis.csv` is a regenerated view, not a durable cache. |
| SVG source | `svg_output/` is the only author source for generated pages. |
| Page-design closure | On SVG-authoring routes, every visible exported-slide object exists in the corresponding page SVG or an explicitly referenced visual asset. |
@@ -52,9 +67,9 @@ Global artifact ownership rules for PPT Master projects.
| Post-processed SVG | `svg_final/` is disposable, must be rebuilt in Step 7.2, and serves only as a self-contained visual preview / manually insertable SVG picture. |
| Export source | The only supported generated-PPTX route reads `svg_output/` through the project SVG-to-DrawingML converter. A diagnostic `-s final` override does not change ownership or create a supported release route. |
| Shape-conversion boundary | PowerPoint's manual Convert-to-Shape operation on `svg_final/` is outside the project compatibility contract. |
| Confirmation | Final `confirm_ui/result.json` or chat confirmation overrides recommendations. |
| Confirmation | Final `confirm_ui/result.json` or chat confirmation overrides recommendations and is the mandatory input contract for `design_spec.md`; `spec_lock.md` is derived only after that Design Spec passes confirmation-fidelity review. |
**Forbidden - mixed ownership**: Do not copy chart values from Markdown into `analysis/` by hand, do not edit `svg_final/` as the source of a fix, and do not treat `design_spec.md` prose as a replacement for `spec_lock.md`.
**Forbidden - mixed ownership**: Do not copy chart values from Markdown into `analysis/` by hand, do not edit `svg_final/` as the source of a fix, do not edit imported lossless SVGs instead of their authoring IR, and do not treat `design_spec.md` prose as a replacement for `spec_lock.md`.
---
@@ -63,8 +78,10 @@ Global artifact ownership rules for PPT Master projects.
| Derived artifact | Regenerate from | Command / owner |
|---|---|---|
| `analysis/image_analysis.csv` | Current `images/` | `python3 ${SKILL_DIR}/scripts/analyze_images.py <project_path>/images` |
| `<import_workspace>/authoring-svg/authoring_summary.json` | Current authoring SVGs plus tool-only manifest roster | `python3 ${SKILL_DIR}/scripts/svg_authoring_view.py <import_workspace>/authoring-svg --refresh-summary`; in-place vector/picture extraction refreshes it automatically |
| `notes/slide_*.md` | `notes/total.md` | `python3 ${SKILL_DIR}/scripts/total_md_split.py <project_path>` |
| `svg_final/` | `svg_output/` plus project assets | `python3 ${SKILL_DIR}/scripts/finalize_svg.py <project_path>` |
| Native PPTX | `svg_output/` plus notes/assets | `python3 ${SKILL_DIR}/scripts/svg_to_pptx.py <project_path>` |
| `validation/svg_quality_report.json` | `svg_output/`, locks, template provenance | `python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage final --json` |
| Native PPTX + `validation/<output_stem>.report.json` | `svg_output/` plus notes/assets and final quality report | `python3 ${SKILL_DIR}/scripts/svg_to_pptx.py <project_path>` |
**Default - regenerate derived views**: When a source artifact changes, regenerate the derived artifact at the owning step instead of patching the derived file directly.
@@ -1,6 +1,7 @@
# Canvas Format Specification
> See shared-standards.md for SVG basic rules.
> See [`shared-standards-core.md`](./shared-standards-core.md) §4.1 for the normative root
> `viewBox` grammar, compatibility spellings, and fail-closed validation rules.
## Format Quick Reference
@@ -15,6 +16,13 @@
| `banner` | Landscape Banner | `1920x1080` | `0 0 1920 1080` | 16:9 | Web banners, digital screens |
| `a4` | A4 Print | `1240x1754` | `0 0 1240 1754` | 1:sqrt(2) | Print posters, flyers |
The table lists canonical root spellings. New custom canvases likewise use
`0 0 W H` with positive integer pixels. A fractional positive canvas is accepted
only as compatible input for an imported custom PowerPoint slide size; it is not
the default authoring form. All pages and internal Layout prototypes in one
export use the same numeric canvas and stay within PowerPoint's supported slide
range (914,40051,206,400 EMU per side, approximately 965,376 SVG px).
`ppt169` is the canonical PPT wide-screen canvas in this repo: `1280x720`, not any arbitrary 16:9 size. Same-ratio canvases such as `banner` (`1920x1080`) must be treated as different coordinate systems.
## Format Selection Decision Tree
@@ -63,7 +71,7 @@ Content purpose?
| Story | — | Middle 1500px | Top safe zone 120px, bottom 180px |
| WeChat Article Header | Center/left-aligned 48-72px | — | Image on right or as background |
> **Body font baseline scales with canvas and delivery purpose** — a PPT 16:9 baseline confirmed for read-close / business / projection cannot be carried onto tall canvases (Xiaohongshu / Story / A4). Pick the baseline from the confirmed canvas, not the recommended one; see the per-canvas px anchors in [`strategist.md`](strategist.md) §g "Font Size Ramp" (the system is px-only — all sizes are unitless px on every canvas).
> **Body font baseline scales with canvas and reading mode** — a PPT 16:9 baseline confirmed for read-close / business / projection cannot be carried onto tall canvases (Xiaohongshu / Story / A4). Pick the baseline from the confirmed canvas, not the recommended one; see the per-canvas px anchors in [`strategist.md`](strategist.md) §g "Typography Plan Confirmation" (the system is px-only — all sizes are unitless px on every canvas).
## ViewBox Examples
@@ -1,10 +1,23 @@
# Executor Common Guidelines
# Executor Flat and Shared Core
> Narrative skeleton and visual aesthetic come from this deck's locked files under [`modes/`](./modes/_index.md) and [`visual-styles/`](./visual-styles/_index.md). Technical constraints are in shared-standards.md.
Always-loaded Executor authority for flat SVG page authoring and behavior shared by every Generate route. Load conditional branches only when their trigger is present.
**Conditional branch routing**:
| Trigger | Load |
|---|---|
| `pptx_structure.mode: structured` | [`executor-structured.md`](./executor-structured.md) |
| Any data chart, chart catalog selection, or text-grid table | [`executor-chart.md`](./executor-chart.md) |
| A page will use a preset pattern fill or evaluate native chart/table replacement | [`native-data-interface.md`](./native-data-interface.md) before deciding eligibility or emitting metadata |
| Any image or formula resource, including template-bundled images | [`executor-image.md`](./executor-image.md) |
| Any `Status: Sourced` web image | [`executor-web-image.md`](./executor-web-image.md), after `executor-image.md` |
| Speaker notes generation after all SVG pages pass | [`executor-notes.md`](./executor-notes.md) |
> Narrative skeleton and visual aesthetic come from this deck's locked files under [`modes/`](./modes/_index.md) and [`visual-styles/`](./visual-styles/_index.md). Technical constraints are in [`shared-standards-core.md`](./shared-standards-core.md).
**Hard rule — complete page SVG**: Every visible object intended for the exported slide MUST exist in the final page SVG or be explicitly referenced by it. Templates and `spec_lock.md` guide construction; they are not export-time overlays for missing visible content.
**Hard rule — route-specific PowerPoint structure**: Free-design and brand-only projects use `pptx_structure.mode: flat`: write no root Master/Layout identity, `data-pptx-layer`, or `data-pptx-placeholder`; every visible object remains Slide-local. Export materializes one clean project-owned Master plus one Blank Layout from the current lock. Deck/layout template projects use `mode: structured`: every page reads its locked Master/Layout row and declares the four root identity attributes from the first draft. Do not add `data-pptx-layout-kind` or duplicate identity with `data-pptx-page-role`. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by specialized metadata; the marked element uses a stable unique `id`. See [`semantic-svg.md`](./semantic-svg.md).
**Hard rule — flat PowerPoint structure**: Free-design, brand-only, and `template_reuse_scope: style` projects use `pptx_structure.mode: flat`: write no root Master/Layout identity, `data-pptx-layer`, or `data-pptx-placeholder`; every visible object remains Slide-local, and the root declares exactly one canonical `data-pptx-page-role` (`cover` / `toc` / `section` / `content` / `ending`). A `style` template supplies colors, typography, decoration, and rhythm as style input without creating per-page prototype mappings. Export materializes one clean project-owned Master plus one Blank Layout from the current lock. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by specialized metadata; the marked element uses a stable unique `id`. See [`semantic-svg.md`](./semantic-svg.md).
**Hard rule — supported PPTX route**: The only supported generated-PPTX path is `svg_output/` through the project SVG-to-DrawingML converter. Step 7.2 still generates `svg_final/` as a mandatory self-contained visual preview that may be inserted as an SVG picture. Do not treat PowerPoint's manual Convert-to-Shape operation as an authoring target or compatibility requirement.
@@ -12,160 +25,63 @@
---
## 1. Template Adherence Rules
### 1.0 Pre-generation Batch Read
**Hard rule**: Before the first SVG page, batch-read every template SVG this deck will reference. Read once up front, never re-read during generation.
| Source list | Read path |
|---|---|
| Chosen template's `design_spec.md` (read frontmatter to detect `replication_mode`) | `templates/design_spec.md` |
| Every distinct `<basename>` in `spec_lock.md page_layouts` | `templates/<basename>.svg` |
| Every distinct chart name in `spec_lock.md page_charts` | `templates/charts/<chart_name>.svg` |
| Chart types in `design_spec.md §VII` not covered above | `templates/charts/<chart_name>.svg` |
**Default — read each template once; re-read only on the mid-deck exception below**:
- Layout SVG already loaded in this batch
- Chart SVG already loaded in this batch
`spec_lock.md` is the only file re-read per page (§2.1).
**Exception**: user mid-deck adds pages or swaps templates introducing a basename/chart absent from the original batch → read the new file once, continue.
> Note: batched prefix reads stay in the cached prompt prefix; per-page `spec_lock.md` re-reads append below and benefit from that cache. Scattered on-demand reads of layout/chart SVGs would invalidate downstream cache and sit in the compression-vulnerable mid-context region.
Resolve the per-page template SVG via `spec_lock.md page_layouts` (authoritative). There is no filename/page-type fallback.
**Resolution order (per page):**
1. **Mirror-mode template** (template's `design_spec.md` frontmatter has `replication_mode: mirror`) → see §1.1 below. The page is consumed as a **visual reference**, not as a placeholder shell.
2. `spec_lock.md page_layouts` has `P<NN>: <basename>` for this page → inherit the structure of `templates/<basename>.svg` (already in context from §1.0).
3. `template_adherence` is present but this page has no `page_layouts` entry → stop; the template contract is incomplete. Adaptive mode must still select a reference SVG.
4. No deck/layout template at all → flat free design: use the visual composition planned in §IX, but write no native Master/Layout mapping or SVG structure metadata.
> Note: `page_layouts` disambiguates the multiple content variants a template may ship; missing mappings are contract errors.
**Templates supply structure, not skin (non-mirror)**: a chart or layout template's gradients, drop-shadows, palette, **and font sizes** are placeholder. Inherit its geometry, label / legend placement, and series-encoding logic; re-skin every fill / stroke to the deck's `visual_style` + `spec_lock.colors` — flat styles strip the gradients and shadows, gradient / glass styles repaint their own. Forbidden — shipping a template's default `<linearGradient>` / `cardShadow` / Tailwind fills unchanged. Mirror templates are the exception: §1.1 preserves their visuals verbatim.
**Font size is skin, not geometry (non-mirror).** A chart / layout template's hardcoded `font-size` values (often 1116px, sized for the template's own dense placeholder text) are NOT inherited — classify each text into its `spec_lock.md` role and use that role's locked size, exactly as you re-skin color. **Structural roles (page title / body / subtitle / annotation / footnote) hold their one deck-wide size on every page** — the template's placeholder px never overrides it; same-role text drifting page to page is what makes a deck look unprofessional.
**Typography execution order (mandatory):**
1. Build a per-page text inventory from `design_spec.md §IX` + the current `notes/<NN>_*.md`.
2. Classify each text item before drawing. **Structural roles** (`title`, `subtitle` / `lead`, `body`, `annotation`, `footnote` / `page_number`) must map to their declared `spec_lock.typography` slot. A **one-off feature element** (a single hero number, an isolated emphasis label) may take an in-ramp intermediate value — the ramp is anchored on `body`, not a closed menu — but a feature size that **recurs** must be promoted to a declared slot. The failure mode this guards against is structural text silently inheriting the template's compact px, not legitimate feature sizing.
3. Copy the role's locked px value into `font-size` verbatim. Do this before placing the text; never start from a template `font-size` and then "adjust".
4. Layout from those locked sizes: compute line-height, wrapped line count, child `y` / `dy`, card padding, card height, column gaps, and available image/chart area from the chosen px values.
5. Only after this reflow may you inspect fit. If fit fails, move / resize containers or simplify local geometry first; do not reduce the role size merely because the inherited template slot was smaller.
**Geometry adapts to the type, never the reverse**: when the locked size is larger than the template's placeholder text, widen / heighten the card, open spacing, and recompute child `y` / `dy` to make room — do not shrink the font to fit the inherited container. A `font-size` change is a layout change: revise line-height and every downstream vertical coordinate that depends on it. For wrapped text, allocate at least the wrapped line count × line-height plus top / bottom padding; fixed `y` stacks copied from a smaller template are invalid once the locked role size is applied. The Executor renders the page it was given; page count and per-page density are the Strategist's call, fixed at confirmation — do **not** re-paginate, split the page, or drop authored content to cope with size here. Only when a single block still cannot fit after the geometry is fully reflowed may you shrink **that block** as a bounded last resort — and **only body text** is ever shrunk this way. Title, subtitle, annotation / caption, footnote and page number are **locked once set and never adjusted to fit** — their values hold across the whole deck. Step the overflowing body block's `font-size` down by `2`px at a time, and only if it still overflows step it down again, up to a cumulative floor of **`4`px below the locked body size** (e.g. `24` → no smaller than `20`). This is a **local, single-block** reduction — the deck-wide locked body size is unchanged on every other block and page. (The Executor works in **unitless px** throughout — spec_lock and SVG carry no `pt`.) If the block still overflows at the floor, surface a `warning:` rather than silently restructure the page. (Mirror templates are the exception: §1.1 preserves their sizes verbatim — there the source deck's typography *is* the spec.)
### 1.1 Mirror-mode templates — reference-style consumption
When the project's chosen template is a `mirror` template (`design_spec.md` frontmatter declares `replication_mode: mirror`), Executor switches to a **reference-style** consumption path that bypasses placeholder substitution:
1. **Per-page reference selection** — Strategist selects one mirror page per project page via `spec_lock.md page_layouts` (e.g., `P04: 015_content`). The basename is the mirror filename without extension; Strategist made this choice by reading `design_spec.md §V Page Roster` descriptions, not by guessing.
2. **Copy, don't fill** — open the referenced mirror SVG (already in context from §1.0). Copy it as the starting point, then edit slide-specific text in place. Preserve every non-text element and every `data-pptx-*` structure attribute verbatim unless adaptive mode intentionally assigns a new Layout contract.
3. **What you may edit** — the visible text content of `<text>` / `<tspan>` elements that express slide-specific content (title, body, captions, KPI labels, dates, page numbers). Replace the source deck's example text with the project's text for this page from `design_spec.md §IX` and `notes/<NN>_*.md`.
4. **What you must not touch** — element positions, sizes, fonts, colors, fills, strokes, gradients, **which image each `<image>` points at**, `<g>` grouping, sprite-sheet `<svg viewBox>` wrappers, decorative `<rect>` / `<path>` / `<circle>` / `<polygon>` shapes, `<use data-icon="...">` markers, embedded chart data structures. Mirror's value is preserving the source deck's visual identity — any geometric / decorative drift defeats the purpose. **The `href` path is not the image**: normalizing a bare `href="cover_bg.png"` to `href="../images/<name>"` (when Step 3 relocated the asset to `images/`) points at the *same* image and changes nothing visual — that is an allowed path fix, not a fidelity edit. Leaving the bare href as-is is also fine; the exporter and live preview resolve bare hrefs against `images/` either way.
5. **Content fit** — the mirror page was chosen by Strategist because its layout matches the content slot. If the project's content for `P<NN>` legitimately needs more / fewer items than the mirror page provides (e.g. mirror shows 3 KPI cards, project has 4 metrics), keep the mirror page's visual rhythm and either drop one metric to fit or split across two pages — do **not** restructure the mirror page's grid. If neither works, surface a `warning: P<NN> content does not fit mirror reference <basename>; suggest different reference page` and proceed with the closest-fit edit.
6. **Visible text editing** — mirror SVGs may keep literal source text rather than `{{...}}` authoring markers. Edit visible text in place, but retain any imported semantic `data-pptx-placeholder` identity.
7. **Output filename** — follow the standard project SVG naming convention (`<NN>_<page_name>.svg` where `<NN>` matches the project page index, not the mirror source index). The mirror filename is the *reference*, not the *output*.
**Detecting mirror mode**: read the chosen template's `design_spec.md` frontmatter once during §1.0 batch read. If `replication_mode: mirror`, every page follows §1.1 through its mandatory `page_layouts` reference.
**Mirror + chart pages**: chart structures inside a mirror SVG are already drawn (axis, series, labels). Treat them as visual references — replace the data labels and series text content to match the project's chart spec, but do not redraw the chart from a `templates/charts/<name>.svg` baseline. A mirror template's `page_charts` entries are normally absent for this reason.
**Legacy template boundary**: A template with missing root Master identity, direct atomic placeholders, `data-pptx-layout-kind`, unmapped `baseline`, `preserve`, or `layout_strategy: distill` is not a fallback input. Stop and run [`restore-pptx-structure`](../workflows/restore-pptx-structure.md) before generation.
### Page-Template Mapping Declaration (Required Output)
Before generating each page, output which template is used:
```
📝 **Template mapping**: `templates/03a_content_image_text.svg` (free-design routes may use "None")
🎯 **Adherence rules / layout strategy**: [specific description]
```
- **Content pages**: template defines only header/footer; content area is free
- **No template**: allowed only on free-design or brand-only routes
### 1.2 PowerPoint Master / Layout Mapping
This section applies only to deck/layout template routes. `page_layouts` selects the input SVG prototype, `pptx_masters` / `pptx_layouts` declare unique reusable output definitions, and `page_pptx_layouts` assigns every generated page before the first page is drawn. Free-design and brand-only routes use `pptx_structure.mode: flat`, omit all four sections, skip the rest of §1.2, and keep every SVG object Slide-local.
**Hard rule — template mode only**: A deck/layout template project uses `pptx_structure.mode: structured`. Missing mode or legacy values (`baseline`, `template`, `preserve`), `layout_strategy`, Layout-kind fields, partial mappings, and old direct placeholders must stop generation and route to [`restore-pptx-structure`](../workflows/restore-pptx-structure.md). `flat` is valid only when no deck/layout template is active.
**Hard rule — root identity**: A `page_pptx_layouts` row binds the page to one key in `pptx_layouts`; that unique definition supplies its Master key, Layout picker name, and prototype source. Put the declared Master key/name and Layout key/name on the root SVG. A Layout key belongs to exactly one Master and remains globally unique.
**Hard rule — atomic fixed layers**: Every `data-pptx-layer="master|layout"` visual is one direct root child that compiles to one DrawingML object. A marked `<g>` is forbidden. When reconstructing source PPTX groups, recursively push supported transforms, paint, opacity, and z-order into atomic children. Repeat the identical ordered Master atom contract on every page using that Master and the identical ordered Layout atom contract on every page sharing that `(master, layout)` pair.
**Hard rule — PowerPoint paint order**: Direct children appear in this order: Master background atoms, Layout background atoms, optional Slide background, remaining Master atoms, remaining Layout atoms, then slot groups and Slide-local content groups. Backgrounds are the inheritance plane beneath all shapes.
**Mandatory — slot authoring**: A reusable content slot is one direct root `<g id>` carrying `data-pptx-placeholder` and positive `data-pptx-placeholder-bounds`. A normal slot contains exactly one compatible direct drawable child marked `data-pptx-placeholder-carrier="true"`. Export unwraps that child into the real Slide placeholder binding. Decorations do not belong in the slot; move reusable decoration to a root Layout atom and keep page-specific labels/captions in another slot or Slide-local group.
**Mandatory — slot identity**: Preserve imported `data-pptx-placeholder-idx` values where available; otherwise omit the title index and assign unique indices only where repeated roles need disambiguation. Pages sharing one Layout key repeat the same slot ids/types/effective indices/default bounds/binding modes. Current text, crop, and Slide-local carrier geometry may differ.
**Composite proxy fallback**: A genuinely composite region may use a direct `<g data-pptx-placeholder="object" data-pptx-placeholder-binding="proxy">` with positive bounds. Its visible group remains Slide-local and export creates one hidden transparent matching placeholder proxy. This downgrade is valid only for `object`; do not use it for an ordinary title, body, picture, chart, table, or media slot.
**Zero-slot Layout**: A Layout may have no slot groups. Covers, posters, and fixed visual pages still declare their named Master/Layout and fixed atoms. Do not manufacture a full-page `object` slot or empty `utility` identity.
**Mandatory — per-page slot coverage**: On every mapped page, declare a slot for each standard role the page actually has: the page heading as `title`, a cover tagline as `subtitle`, the page number as `slide-number`, running footer text as `footer`, a hero / content image as `picture`, and a body block already authored as one merged text frame as `body`. A page shipping zero slots exports a Layout with no insertable placeholders — valid only for a genuinely fixed composition (see Zero-slot Layout above), never as the deck-wide default. Pages sharing one layout key ship the same slot set.
**Hard rule — variable slot content**: “Per-page headings never stay Slide-local by default” means authoring them as `title` / `subtitle` slots; it never permits page-varying text or images to become fixed Layout atoms. Any such value that varies across pages sharing one Layout key MUST be carried by a slot or remain Slide-local.
**Mandatory — master/layout layer coverage**: On every mapped page, mark the deck-wide background and every-page chrome (footer bar, running logo) `data-pptx-layer="master"`, and mark the static framing that defines this layout key's composition (header rule, divider band, zone panels — including chrome repeated on every content page but absent from the cover) `data-pptx-layer="layout"`. A mapped page with zero `data-pptx-layer` marks exports a bare Master and an empty Layout — the layer marks, not the slide content, give each Layout its visible design.
**Layout identity**: Different keys differ in fixed Layout atoms or slot topology/default bounds/binding modes. Identical contracts should share one key. Current wording, imagery, crop, and Slide-local geometry never define identity.
**Template adherence**: Strict copies the prototype Master/Layout/slot contract exactly. Adaptive keeps the prototype Master and may change reusable Layout atoms or slots only under a new explicit Layout key/name. When the completed composition genuinely needs that change, update `spec_lock.md pptx_layouts` immediately while authoring the first affected page; later pages may reuse the new key only by repeating its exact contract. Changing only a label is not a new Layout.
**Layout-content boundary**: Mark only genuinely reusable fixed framing as a Master/Layout atom. Concrete titles, body copy, metrics, chart marks, images, and page-specific groups remain inside slot groups or ordinary Slide-local content groups. The exporter never infers or clusters structure.
**Background ownership**:
| Scope | SVG authoring |
|---|---|
| Deck-wide default | Direct full-canvas solid `<rect data-pptx-layer="master">` repeated identically on every page |
| Page-type default | Direct full-canvas solid `<rect data-pptx-layer="layout">` repeated on every page sharing that layout key |
| One-page exception | Direct full-canvas solid `<rect data-pptx-layer="slide">` |
The exporter writes these solid fills as real Master/Layout/Slide `p:bg`, not selectable full-canvas shapes. Gradients, images, textures, and overlay panels stay explicit shapes unless the shared standard says otherwise.
---
## 2. Design Parameter Confirmation (Mandatory Step)
Before the first SVG page, output a confirmation listing: canvas dimensions, body font size, color scheme (primary/secondary/accent HEX), font plan, and the live-preview URL reported by the launcher. If the preview launch failed, state that failure before generating SVGs instead of silently proceeding. Prevents spec/execution drift.
Before the first SVG page, output a confirmation listing: the compact communication objective, canvas dimensions, body font size, color scheme (primary/secondary/accent HEX), font plan, and the live-preview URL reported by the launcher. If the preview launch failed, state that failure before generating SVGs instead of silently proceeding. Prevents purpose/spec/execution drift.
### 2.1 Per-page spec_lock re-read (Mandatory)
### 2.1 Per-page execution context (Mandatory)
> Long decks drift off the declared palette/icons mid-deck due to context compression. `spec_lock.md` is the canonical execution reference — re-read it per page to bypass model memory.
Before the first SVG, retain `design_spec.md`: continuous execution reuses planning context; fresh/resumed execution reads it once.
**Hard rule**: Before generating **each** SVG page, `read_file <project_path>/spec_lock.md`. Use only values from this file, not from memory. If context was auto-compacted, also `read_file <project_path>/design_spec.md` for the current page's §IX brief.
**Hard rule**: Before generating **each** SVG page, load its canonical current-page delta and record its model-facing size:
```bash
python3 skills/ppt-master/scripts/project_manager.py page-context <project_path> P<NN> --record-usage
```
`global` deliberately repeats the sub-1000-token lock projection as an anti-drift guard; `lock_source.sha256` binds its version. `page_context` is the current §IX/resource/template/chart delta. For every `reference_set` entry—project/template Design Spec or selected prototype/chart SVG—reuse an in-context path + SHA; read it once only when absent or changed.
Use lock values literally and optional `Template Application` from the retained Design Spec. The delta overrides neither facts nor constraints. After an approved change, rerun the command and reload only changed references. Deprecated `--bundle` is a compatibility no-op.
**Source facts**: The page delta carries page intent and routing facts, not the complete source corpus. Read the relevant `sources/` content and resolve listed `Fact IDs` from `sources/*.facts.json` when the page needs concrete claims, quotes, names, or data.
**Per-page communication trace**: Read `communication.objective`, `communication.core_message`, and the current §IX `Core message` + `Audience move` before choosing composition. The page must advance the compact objective and move the audience as authored in §IX; the global core message remains the deck-wide north star. A page that cannot state this movement is an upstream outline defect — surface `warning: P<NN> has no communication move` instead of compensating with decorative layout. Do not invent a new purpose, ask, or outcome at execution time. Structural pages may advance the contract by establishing relevance / tension / decision frame or by completing the final commitment; they are not exempt from having a reason to exist.
**Per-page reading-mode check**: Read `communication.consumption_mode` before choosing the page's composition. Apply it together with the authored §IX block texture and `page_rhythm`:
| `consumption_mode` | Page execution |
|---|---|
| `text` | Make the visible page independently understandable. Preserve complete prose, explicit labels / captions / sources, tables, and necessary detail; use bullets only for genuinely parallel or ordered items. |
| `balanced` | Keep the primary claim and its evidence on the page; let notes add interpretation and transitions. Mix prose, structured evidence, and necessary lists according to their semantic relationship. |
| `presentation` | Make one claim and one dominant visual expression legible at projection distance. Keep visible copy concise; put explanation and transitions in notes instead of creating paragraph dumps or compressed bullet prose. |
The §IX wording and sourced facts remain authoritative. Do not rewrite, drop, or invent content to force a mode at execution time. When the authored texture materially conflicts with the lock, render the least-destructive faithful composition and surface `warning: P<NN> content texture conflicts with consumption_mode <value>` as an upstream outline issue; do not encode this subjective judgment in the checker.
**Per-block expression**: render each `design_spec.md §IX Content` block in its written texture — a full-sentence block as wrapped prose, a fragment/label block as bullets/keywords. **Never split a full-sentence block into a bullet list** — splitting loses the information that the block was continuous reasoning, not a set of parallel points; not because a bullet lays out easier, and not because an inherited template slot is shaped as a list. If a block carries no clear texture, infer the mode from its wording and the page layout.
- **Prose render recipe**: one `<text>` per paragraph; wrap lines with sibling `<tspan>` where the first line uses `dy="0"` and every subsequent line repeats the parent `<text>`'s **exact `x`** and the **same positive relative `dy`** (the line-height). Equal relative `dy` + matching `x` + the same effective `font-size` lets lines flow inside one PowerPoint paragraph; a font-size change preserves a new paragraph inside the same text frame, while a growing/cumulative `dy`, an irregular gap, or a mismatched `x` (e.g. `x="0"` under `<text x="60">`) may split them into separate single-line boxes. Set the line-height `dy` from the font size × a line-height factor. **Default — line-height by density (may override per content fit)**: ~1.41.5× for dense / small-body blocks (CLReq comfortable minimum), 1.62.0× for large-type, sparse, or `breathing` blocks. Fit about width ÷ font-size CJK glyphs per line (Latin fits roughly twice that); the last line runs short. Use the body ramp size, not a new one.
- **Hard rule — one paragraph, one text frame**: use one `<text>` per prose paragraph, never one sibling `<text>` per visual line. Keep the first line as direct text; each later wrap is a direct `<tspan>` that repeats the parent `x`, keeps its effective font size, and uses one positive relative `dy`. An all-`<tspan>` form may start with `dy="0"`. Line height: 1.41.5× for dense/small body, 1.62.0× for large/breathing text.
- **Template precedence**: when an inherited template slot is a bullet list but the §IX block is prose, the prose wins — widen or reflow the container to hold the paragraph, or drop that card; do not pour the sentence back into the list slot.
- **Mode precedence**: the locked mode shapes voice / register, not §IX's authored titles or page order. When a `§IX` title is a user-authored topic label, keep it — do not upgrade it to an assertion just because the mode (e.g. `pyramid`) favors them; mode title-tendencies apply only to AI-drafted titles.
> Note: block-level phrasing, applied *within* the page's `page_rhythm` density (below), not against it.
**If `spec_lock.md` is missing**: emit `warning: spec_lock.md missing — generating without execution lock` once, then proceed using `design_spec.md` values. Expected only for legacy projects; new projects MUST have it (see [strategist.md](strategist.md) §6 step 4).
**Missing `spec_lock.md` or `design_spec.md`** → stop before drawing and report the missing gate artifact. Recover through [`failure-recovery.md`](../workflows/governance/failure-recovery.md) §3; do not bypass a failed page-context command or silently downgrade.
**Missing field in an existing lock**: follow [`failure-recovery.md`](../workflows/governance/failure-recovery.md) §2.
**Forbidden — values outside the lock**:
- Colors (fill / stroke / stop-color) MUST come from `colors`
- Icons MUST come from `icons.inventory`; library MUST equal `icons.library`
- Font family from `typography`: use role override (`title_family` / `body_family` / `emphasis_family` / `code_family`) if declared, else fall back to `font_family`
- Font sizes follow a **ramp anchored on `typography.body`**, not a closed menu. **Structural roles — page title, body, subtitle, annotation / caption, footnote / page number — render at one consistent size deck-wide, taken from their `spec_lock` slot; never re-pick a structural role's size page by page or carry a template's placeholder px.** This locks the **role**, not every glyph: a page may still carry deliberate typographic hierarchy — a lead-in sentence, an inline emphasis figure, a pull-quote, a kicker, a hero number — but each of those is its **own role / feature element** with its own size, **applied consistently deck-wide** (declare a recurring one as its own `spec_lock` slot). In-band intermediate sizes are for exactly these feature elements. What is banned is the *same* role drifting size to fit a container or by page whim — that scatter is what reads as unprofessional. Sizes outside every band require extending the lock first.
- **The page's core message is primary — render it`body`.** The one-idea / key-claim / key-takeaway line a page is built around is its most important text; map it to the locked `lead` or `subtitle` slot (≥ `body`), never to a sub-`body` size. Demoting it below body while data callouts or labels sit larger inverts the hierarchy — the failure this prevents. If no `lead` / `subtitle` slot is locked for a recurring core-message line, surface it (per below) instead of improvising a smaller one. A footnote / page number / source credit uses the locked `footnote` (or `annotation`) slot — never an invented sub-`annotation` size; and the body-shrink last resort (§1.0) bottoms out at `body 4`px, a hard floor never crossed.
- **Write the locked px verbatim; at most 2 decimals.** `font-size` MUST be the exact px from `spec_lock.typography` — if `body` is `24`, write `24`; never substitute a "rounder" or PowerPoint-familiar number (`20` / `18` / `36`). The system is px-only — there is no pt to convert, and a remembered pt-style value written as px renders the whole deck the wrong size. Prefer whole numbers (sizes are clean even px); keep a decimal only for a slot that genuinely carries one in `spec_lock`. Never emit long tails like `20.8026`: the exporter rounds the final size to 1 decimal pt, so extra px precision is wasted noise.
- Font sizes follow a ramp anchored on `typography.body`. Structural roles use their locked size deck-wide; recurring feature roles such as lead, pull quote, or hero number need their own lock slot. Never resize one role page by page or inherit a template placeholder size.
- **Core message`body`**: map the page's primary claim to locked `lead` / `subtitle`, never below body. Footnotes, page numbers, and credits use locked `footnote` / `annotation`; do not invent smaller sizes.
- **Write locked px verbatim, with at most two decimals.** Do not substitute familiar pt-style numbers or emit long precision tails.
- **Bounded body-fit last resort**: reflow geometry first; only an overflowing body block may step down by `2`px, never below `body 4`px. Other roles never shrink. At the floor, warn instead of dropping content or repaginating. Mirror pages preserve source typography.
- Images MUST reference files listed under `images`; no invented filenames
- Formula PNGs are images with `Acquire Via: formula` / `Status: Rendered`; place them only from the listed file path and never recreate the formula as text.
- Formula PNGs are images with `Acquire Via: formula`; place a `Rendered` file only from its listed path, use the normal placeholder for `Needs-Manual`, and never recreate the formula as text.
If a page needs a value not in `spec_lock.md`, surface it — do not silently invent one.
If a page needs a value not in `spec_lock.md`, surface it — do not silently invent one. When an intentional deck-wide or recurring color, type role/size, icon, or image is approved, extend `spec_lock.md` **before** drawing the first affected object, regenerate that page's context bundle, and only then author the page; do not draw with a temporary hardcoded value and retroactively silence drift warnings.
**Per-page layout rhythm — `page_rhythm` section**:
@@ -173,89 +89,35 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P<NN>`
| Tag | Layout discipline |
|-----|-------------------|
| `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. |
| `anchor` | Structural page (cover / chapter / TOC / ending). With `template_reuse_scope: mirror`, follow the selected prototype verbatim except visible text values. With `layout`, retain the selected structure system while realizing the page's §IX intent. With `style` or free design, realize §IX directly — for the cover deliver its `Cover impact` and for a closing page its `Closing impact`, never a default centered title + subtitle or 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. |
> Without rhythm variation, every page defaults to card grids (the "AI-generated" look). `page_rhythm` is the only narrative lever that survives context compression.
**Missing `page_rhythm` section** → emit `warning: spec_lock.md missing page_rhythm — defaulting all pages to dense` once, fall back to `dense` for all pages.
**Missing or empty `page_rhythm` section — fixed compatibility default** → emit `warning: spec_lock.md missing/empty page_rhythm — defaulting all pages to dense` once, fall back to `dense` for all pages.
**Tag not found for current page** → emit `warning: spec_lock.md page_rhythm tag not found for P<NN> — falling back to dense` once per deck (aggregate; do not repeat per page), fall back to `dense`. Do not invent a tag.
**Tag not found for current page — fixed compatibility default** → emit `warning: spec_lock.md page_rhythm tag not found for P<NN> — falling back to dense` once per deck (aggregate; do not repeat per page), fall back to `dense`. Do not invent a tag.
**Per-page template lookup — `page_layouts` section**:
Before drawing each page, look up its entry in `page_layouts` to decide which basename to inherit (the SVG itself was loaded in §1.0):
- Entry present (e.g., `P04: 03a_content_image_text`) → inherit the corresponding SVG already in context. The basename **must match** an actual file in the chosen template directory. If it does not, stop before drawing and report the invalid mapping; neither `strict` nor `adaptive` may fall back to free design inside a template deck.
- No entry for this page with `template_adherence: strict|adaptive` → stop before drawing and report the missing Strategist mapping. Adaptive mode still requires one selected complete template SVG; flexibility applies to the post-design output Layout, not to whether an input prototype exists.
- Whole section absent while `template_adherence` is present → stop before drawing; the current template contract is incomplete.
Do **not** invent a prototype entry, and do **not** assume a template just because `templates/` exists. For either template-adherence value, a missing or invalid `page_layouts` row is an upstream contract error. Free design is a separate deck route, never a per-page fallback.
**Per-page PowerPoint layout lookup — structured deck/layout templates only**:
- When `pptx_structure.mode` is `flat`, skip this lookup and the structured scaffold below. `pptx_masters`, `pptx_layouts`, `page_layouts`, and the corresponding SVG metadata must all be absent.
- When a deck/layout template is active, `pptx_structure.mode` must equal `structured`; any other or missing value routes to legacy restoration.
- Read the current page row as `<master_key> | <layout_key> | <layout name>` and resolve `master_key` in `pptx_masters`. Missing, malformed, or partial mappings stop before drawing.
- Write matching root Master/Layout key and picker names. Do not write `data-pptx-layout-kind` or `data-pptx-page-role`.
- On strict template use, the row and SVG contract match the selected prototype exactly.
- On adaptive template use, retain the prototype Master. If the final composition changes fixed Layout atoms or slot topology/bounds, allocate a new key/name and update this row before completing the page.
- A Layout key may repeat across non-adjacent pages only when its fixed atoms and slot contracts are identical.
**Structured template-page scaffold**:
```xml
<svg viewBox="…"
data-pptx-master="<master-key>" data-pptx-master-name="<master-name>"
data-pptx-layout="<layout-key>" data-pptx-layout-name="<layout-name>">
<rect id="master-bg" data-pptx-layer="master" /> <!-- one atomic Master object -->
<text id="master-footer" data-pptx-layer="master" ></text> <!-- no Master/Layout g -->
<path id="layout-rule" data-pptx-layer="layout" /> <!-- one atomic Layout object -->
<g id="title-slot" data-pptx-placeholder="title"
data-pptx-placeholder-bounds="60 36 1160 64">
<text id="title-carrier" data-pptx-placeholder-carrier="true" ></text>
</g>
<g id="body-slot" data-pptx-placeholder="body"
data-pptx-placeholder-idx="1"
data-pptx-placeholder-bounds="60 120 470 500">
<text id="body-carrier" data-pptx-placeholder-carrier="true" ></text>
</g>
<g id="picture-slot" data-pptx-placeholder="picture"
data-pptx-placeholder-idx="2"
data-pptx-placeholder-bounds="570 120 650 500">
<image id="picture-carrier" data-pptx-placeholder-carrier="true" />
</g>
<g id="content-block-1"></g> <!-- 38 content groups -->
<g id="content-block-2"></g>
</svg>
```
On structured template pages, Master/Layout atoms and slot groups are direct root children and precede ordinary content groups. Structural metadata nested inside an ordinary content group fails export. Flat pages use ordinary top-level semantic groups only.
**Per-page chart reference — `page_charts` section**:
Before drawing each page, look up its entry in `page_charts` to decide which chart structure applies (the SVG itself was loaded in §1.0):
- Entry present (e.g., `P09: timeline_horizontal`) → adapt the corresponding chart SVG already in context. Apply project colors/typography/density; do not copy verbatim. Cross-reference `templates/charts/charts_index.json` for the chart's purpose summary if needed.
- No entry for this page → either no chart on this page, or a chart that didn't match any catalog template (Strategist's `no-template-match` fallback). Design the visualization from scratch using `design_spec.md §VII` for guidance.
- Whole section absent → no chart pages in this deck.
---
## 3. Execution Guidelines
- **Proximity**: group related elements with tight spacing; separate unrelated groups
- **Element grouping (Mandatory)**: wrap every logical Slide-local content unit — title, core-message line, each content block, card, list item, and diagram — in a top-level `<g id="...">` with a descriptive id. Flat free-design/brand-only pages use ordinary semantic groups for every logical unit. On structured template pages, slot `<g>` elements are already semantic groups and direct Master/Layout atoms are the required exception to grouping. Authored native preset fragments (`preset_shape_svg.py`) already are one atomic `<g id>` each and count as one ordinary content group; keep their labels in a sibling parent `<g>`.
- **Element grouping (Mandatory)**: wrap each logical Slide-local body unit in a descriptive, page-unique top-level `<g id>`. Every visible direct root `<g>` declares root-coordinate `data-pptx-bounds="x y width height"`; frame/native coordinates do not replace it, and placeholder bounds also supply the slot frame. Nested groups need no bounds and any such values are ignored. Checker compares root bounds with the `viewBox` and recursively checks only estimable text against its root module: through `1px` is ignored, through `5%` warns, above `5%` fails per side. Images, shapes, paths, `<use>`, effects, and object frames remain geometrically free. Flat pages use ordinary groups; structured slots already qualify, while titles and direct Master/Layout atoms may remain root primitives.
- **Spec adherence**: follow color, layout, canvas format, and typography in the spec
- **Template structure**: if templates exist, inherit the visual framework
- **Template structure**: inherit the native visual framework only for `template_reuse_scope: mirror|layout`; `style` uses the flat route
- **Main-agent ownership**: SVG generation must run in the main agent (not sub-agents) — pages share upstream context for cross-page visual continuity
- **Generation rhythm**: lock global design context first, then generate pages sequentially in one continuous context. No batched groups (e.g., 5 at a time).
- **Generation rhythm**: P01 → first-page gate → uninterrupted remaining pages → final gate, in one context without batches or mid-run checker calls.
- **Fact provenance**: when a §IX page lists `Fact IDs`, resolve each ID from `sources/*.facts.json` and keep the claim/value unchanged. Render a compact source footnote using the source name and a short URL/domain when space permits; state the attribution naturally in speaker notes. When §IX says `Data class: scenario`, place a visible localized `Scenario data` / `情景数据` label adjacent to the affected KPI/chart and state naturally in notes that the number is illustrative. Never attach an external fact ID to scenario data or let an unlabeled invented KPI look factual.
- **Default — stage each page with the style's composition geometry (may override when the content genuinely calls for a plain grid)**: an SVG page is a canvas, not a DOM. Before defaulting to stacked rounded-rect cards or uniform equal columns, pick one page-scale move from the locked visual style's §1 `Composition geometry` (a bleed shape, diagonal split, oversized numeral, orbit rings, …) to stage the page's primary zone. Card grids are one option among many, not the house layout.
- **Reference — image-led promotional pages (not a constraint)**: for travel, venue, product-introduction, hospitality, event, real-estate, and brochure-style decks, let images define the page skeleton before placing text. Consult [`image-layout-patterns.md`](image-layout-patterns.md) §Imported Deck Patterns and prefer patterns such as `#74` TOC image-navigation cards, `#75` asymmetric chapter banners, `#77` photo mosaic with a text cell, `#78` ambient banner + evidence photo + text panel, `#79` ribbon-header image cards, and `#80` side hero image + staggered evidence cards before falling back to plain left/right image-text splits.
- **Phased batch generation** (recommended):
1. **Visual Construction Phase**: generate all SVG pages sequentially for visual consistency. Use layout judgment for chart marks during the draft. **MUST embed plot-area markers** per §3.1 below on every chart page — coordinate calibration is a post-generation step (see [`workflows/verify-charts.md`](../workflows/verify-charts.md)) that depends on these markers — and **native object metadata** per §3.2 on every eligible data-chart page. **Reach for native presets** per §3.0 as you draw each page: a block arrow, chevron, banner/ribbon, callout, standard flowchart node, or star is authored through `preset_shape_svg.py` at draw time — decided by the object's intent as you create it, never by scanning finished paths, and never committed to a bare `<path>`/`<polygon>` when a preset expresses it (a gradient fill/stroke or a pattern fill is the one paint exception — keep those ordinary SVG). **First-page gate (Mandatory)**: after completing the first page, run `python3 scripts/svg_quality_checker.py <project_path>/svg_output/<first_page>.svg` and fix every error before drawing page 2 — structural violations are systematic, and a first-page error repeated deck-wide costs a whole-deck rewrite.
2. **Quality Check Gate**: run `python3 scripts/svg_quality_checker.py <project_path>` on `svg_output/`. Any `error` (banned features, viewBox mismatch, spec_lock drift, non-PPT-safe font, etc.) MUST be fixed on the offending page before proceeding — regenerate and re-check. Address `warning`s when straightforward. On a structured deck/layout template route, PPTX-structure warnings (empty Layout, framing-only Layout, bare Master, duplicate layout keys) are never acknowledge-and-release: list each one and either fix the page/lock or state per warning why the flagged state is intended (e.g. a zero-slot cover) before proceeding. Flat free-design/brand-only routes have no Master/Layout checkpoint. Do NOT defer to after `finalize_svg.py` — finalize rewrites SVG and masks some violations.
- **Containers are structural**: cards and grids express grouping, hierarchy, or capacity, not a house style. Preserve meaningful template frames; restyle radius, fill, stroke, and depth from the active Design Spec and `spec_lock.md`. Chart-catalog adaptation is owned by [`executor-chart.md`](./executor-chart.md); preview effects never override project styling or structural roles.
- **Reference — prefer semantic geometry over preset stacks**: for relationships such as ascending, converging, breaking through, or stacking, consider one page-specific polygon/path that expresses the relationship before stacking generic arrows. This does not override §3.0 when one literal stock shape is the semantic object.
- **Reference — create depth with restraint**: use rhythm, spacing, typography, accent bars, and subtle tints before shadows. Reserve lift for a few genuinely floating elements; keep peer grids, dividers, and body containers flat.
- **Phased generation** (recommended):
1. **Visual Construction Phase**: generate all SVG pages sequentially for visual consistency. Use layout judgment for chart marks during the draft. **MUST embed plot-area markers** per [`executor-chart.md`](./executor-chart.md) §2.1 on every chart page — coordinate calibration is a post-generation step (see [`verify-charts`](../workflows/stages/verify-charts.md)) that depends on these markers — and **native object metadata** per [`executor-chart.md`](./executor-chart.md) §2.2 on every eligible data-chart page. **Reach for native presets** per §3.0 as you draw each page: a block arrow, chevron, banner/ribbon, callout, standard flowchart node, or star is authored through `preset_shape_svg.py` at draw time — decided by the object's intent as you create it, never by scanning finished paths, and never committed to a bare `<path>`/`<polygon>` when a preset expresses it (a gradient fill/stroke or a pattern fill is the one paint exception — keep those ordinary SVG). **First-page gate (Mandatory)**: after completing the first page, run `python3 scripts/svg_quality_checker.py <project_path> --stage first-page` and fix every error before drawing page 2. This mode checks P01 only. After it passes, draw P02 through the last page without checker calls.
2. **Quality Check Gate**: only after every planned SVG exists, run `python3 scripts/svg_quality_checker.py <project_path> --stage final --json` on `svg_output/`. Any `error` (banned/unsupported features, invalid values, unresolved references, viewBox mismatch, etc.) MUST be fixed on the offending page before proceeding — regenerate and re-check. Every `warning` is advisory: it never sends the page back for required modification, never authorizes automatic rewriting of compatible user syntax, and needs no acknowledgement/disposition line. Recommendation warnings describe the generated-SVG default; fidelity/quality warnings may be surfaced when material, while the existing input remains releasable. Prototype-identical diagnostics are recorded as `inherited`, source conversion losses as `source-import`, changed/new advisories as `introduced`, and release failures as `blocking` in `validation/svg_quality_report.json`. If release truly depends on a condition, it belongs in `errors`. On success, use the exit status and terminal summary; do not open or `cat` the complete JSON into model context. Read only targeted fields for failure investigation or an explicit audit request. Do NOT defer error handling to after `finalize_svg.py` — finalize rewrites SVG and masks some violations.
3. **Logic Construction Phase**: after SVGs pass the quality check, batch-generate speaker notes for narrative continuity.
### 3.0 Native Preset Shape Selection
@@ -282,89 +144,25 @@ This automatic decision applies only before drawing a new object. Do not scan
existing SVG, classify path contours, or upgrade ordinary SVG during export.
**Hard rule**: do not hand-write `data-pptx-authoring`, `data-pptx-prst`,
`data-pptx-frame`, adjustment, carrier, preview, or fingerprint metadata. The
helper generates them atomically from the shared 187-shape registry. Rerun the
helper when geometry or paint changes.
`data-pptx-frame`, adjustment metadata, or registry paths. The helper generates
one compact atomic `<g>` from the shared 187-shape registry, with semantic
metadata and base paint written once. Rerun the helper when geometry or paint
changes; never edit one of its direct paths.
Connector-family presets require `--object-kind connector`, `fill="none"`, and
a visible stroke. They export as unconnected `p:cxnSp`; do not hand-add
endpoint/site metadata. `actionButton*` presets provide visual geometry only,
not actions or hyperlinks.
For chart-template and diagram authoring, thin relationships use ordinary
`<line>` / supported open `<path>` geometry with registered arrow markers;
solid directional blocks use ordinary `shape` presets such as `rightArrow` or
`chevron`. Do not select a connector-family preset merely because two nodes are
related, and never hand-add endpoint/site metadata. Connector-family presets
remain available only for an explicit request for a standalone unconnected
`p:cxnSp`; imported Connector topology stays under the preserve/mirror contract.
`actionButton*` presets provide visual geometry only, not actions or hyperlinks.
**Hard rule — narrow helper scope**: the helper prints one shape fragment to
stdout. It does not write a page or choose layout. Read the fragment and insert
it through the normal `apply_patch` page edit; never redirect, loop, or batch it
into `svg_output/`.
### 3.1 Chart Plot-Area Marker (MANDATORY on every chart page)
> The [`verify-charts`](../workflows/verify-charts.md) workflow enumerates chart pages from `design_spec.md §VII`, then reads each page's plot-area marker to feed `svg_position_calculator.py`. Missing marker → verify-charts has to re-derive the plot area from axis lines, paying the cost on every run.
**Hard rule**: every SVG page that contains a data visualization chart includes a plot-area marker inside `<g id="chartArea">`, placed **after axis lines** and **before the first data element** (bar, line, area, point).
**Rectangular plot area** (bar / horizontal_bar / grouped_bar / stacked_bar / line / area / stacked_area / scatter / waterfall / pareto / butterfly):
```xml
<!-- chart-plot-area: x_min,y_min,x_max,y_max -->
```
**Radial charts** (pie / donut / radar):
```xml
<!-- chart-plot-area: pie | center: cx,cy | radius: r -->
<!-- chart-plot-area: donut | center: cx,cy | outer-radius: r1 | inner-radius: r2 -->
<!-- chart-plot-area: radar | center: cx,cy | radius: r -->
```
**How to determine coordinate values**:
| Value | Derivation |
|-------|------------|
| `x_min` | X coordinate of the Y-axis line (leftmost data boundary) |
| `y_min` | Y coordinate of the topmost grid line (highest data boundary) |
| `x_max` | X coordinate of the rightmost axis endpoint or grid line |
| `y_max` | Y coordinate of the X-axis baseline |
| `cx, cy` | Center point of pie/donut/radar (accounting for `transform="translate()"`) |
| `r` | Outer radius of the chart |
**Per-page verification** — after writing each chart SVG, confirm the marker exists:
```bash
grep "chart-plot-area" <project_path>/svg_output/<current_page>.svg
```
> All chart templates in `templates/charts/` include this marker as a reference. If you are drawing a chart and the marker is absent, you have a bug.
- **Technical specs**: see [shared-standards.md](shared-standards.md) for SVG/PPT constraints
- **Card containers — use the documented patterns**: when a content page needs section cards (4 quadrants, parallel aspects, capability blocks, info cards), use the patterns codified in [`templates/charts/CHART_STYLE_GUIDE.md`](../templates/charts/CHART_STYLE_GUIDE.md) §11 — half-rounded section tab (§11.1), nested card border without stroke (§11.2), card-grid skeletons (§11.3), diagonal dashed connector for cross-quadrant relationships (§11.5), ground-anchor ellipse as a non-filter depth marker (§11.6), bidirectional interaction arrows for paired protocols (§11.7). Do not reinvent the "tinted full-rounded rect + white cover-rect to hide the bottom corners" hack; it survives in older templates but breaks SVG→PPTX color editing. Reference templates: [`labeled_card.svg`](../templates/charts/labeled_card.svg), [`quadrant_text_bullets.svg`](../templates/charts/quadrant_text_bullets.svg), [`kpi_cards.svg`](../templates/charts/kpi_cards.svg), [`matrix_2x2.svg`](../templates/charts/matrix_2x2.svg), [`team_roster.svg`](../templates/charts/team_roster.svg), [`client_server_flow.svg`](../templates/charts/client_server_flow.svg).
- **Reference — prefer semantic shapes over preset stacks (not a constraint)**: when a slide needs to express "ascending / converging / breaking through / stacking" — i.e., a relationship that goes beyond a generic arrow — prefer a single custom `<polygon>` or `<path>` that encodes the semantics geometrically, rather than stacking multiple preset arrows. A converging-tip path or a podium polygon reads faster than three arrows pointing at a label. Examples of this technique appear in many imported corporate decks; see `projects/01_template_import/svg_output/slide_01.svg` shape-158 for a reference (gradient-filled inward-pointing arrow). Do not codify these as templates — they are page-specific; the rule is just "consider polygon before stacking presets."
- **Reference — visual depth through restraint (not a constraint)**: layered depth comes from rhythm (flat vs lifted, dense vs spacious), not from shadows everywhere. Shadow typically suits 2-3 genuinely floating elements per page (cards on photos, primary CTA, overlays); keep peer-grid cards, dividers, body containers flat. Reach for typography weight, spacing, accent bars, subtle tints **before** shadow.
### 3.2 Native Object Metadata Marker (MANDATORY on eligible data-chart and text-grid table pages)
> `svg_to_pptx.py --native-objects` converts marked groups into real PowerPoint chart/table objects (charts get an embedded Excel workbook). Markers stay dormant in the default export — pages render from their SVG children — but a deck without markers can never form native objects. Write the marker at draw time: the data is already in hand, and recovering it later costs a full re-read pass.
**Hard rule**: every data chart whose type appears in the **Supported chart types** list of [shared-standards.md](shared-standards.md) "Native PPTX Table / Chart Markers" (the single authority for the eligible set, marker contract, and JSON schemas) gets `data-pptx-native="chart"` plus a `<metadata data-pptx-native="chart">` JSON child on its top-level `<g>`, transcribing the same data just plotted. Every pure text-grid data table gets `data-pptx-native="table"` the same way, transcribing all visible cell text into `columns` / `rows`.
- Chart types absent from that list and conceptual/diagrammatic graphics (process flows, cycles, quadrant cards, timelines, KPI cards) get **no marker**`svg_quality_checker.py` rejects unsupported marker types.
- Canonical rectangular merged text cells may carry a table marker by putting anchor-only `row_span` / `col_span` in metadata and leaving covered cells blank. Nonrectangular/overlapping merges, nonblank covered cells, and graphical cells (icons, harvey balls, rating dots) get **no table marker** and stay on the SVG fallback route.
- Transcribe, don't restyle: `categories` / `series[].values` are the numbers just plotted; `style.colors` carries the series HEX values already used on the page (from `spec_lock.colors`).
- Data-point color: when a single column/bar series uses data-point colors in the fallback, copy those fills into `series[].point_colors` in category order.
- Data labels: when visible point values are part of the fallback chart, write `data_labels` instead of companion text; use `data_labels.points` for selected labels, and use `number_format`, `font_size`, `font_family`, and per-point `colors` / `color` when the fallback labels carry suffixes or color-coded text.
- Line markers: when the fallback line chart draws visible point nodes, set `line_style: "lineMarker"`; leave the default `line` only for line charts without nodes.
- Area-under-line: when a combo plot is drawn as a filled area under a line, keep `type: "line"`, add `area_fill: true`, and copy the area transparency into `series[].fill_opacity`; copy visible line `stroke-width` into `series[].line_width` for line/area series.
- Native chrome: write `title`, `subtitle`, axis titles, or `show_legend: true` only when the fallback visibly renders the same chrome inside the native chart's replacement scope. `title` is the PowerPoint chart title, not an object name; use `name` for page-semantic object naming (e.g. `p03-revenue-chart`). Write explicit `x`/`y`/`width`/`height` read from the drawn plot area; omission is the fallback — the exporter then infers the frame from the drawn fallback geometry.
- Value-axis labels: when the fallback keeps category labels but intentionally omits numeric value-axis tick labels, set `show_value_axis_labels: false`.
- Freeform chart text: transcribe center labels, source notes, and other in-chart annotations as companion `caption` / `note` / `notes` entries with explicit slide-coordinate bounds; do not rely on fallback `<text>` children to survive native export.
- Native chart typography mirrors the SVG fallback. Copy the fallback's shared chart font into `style.font_family` and visible chart text sizes into the matching metadata fields (`title_font_size`, `subtitle_font_size`, `axis_font_size`, `note_font_size`, etc.) only when role sizes differ; otherwise let the exporter infer them from visible fallback text. When a visible chart title, subtitle, or axis title needs its own size/color/font, write that field as an object with `text`, `font_size`, `font_family`, and `color`. Use `axis_title_font_size`, `legend_font_size`, or companion per-entry `font_size` only when the fallback visibly uses a separate size.
- Native table typography mirrors the SVG fallback. Write `style.font_family` and `style.font_size` from the visible table text; use `header_font_size` or per-cell `font_size` only when the fallback visibly does so. If the fallback has no explicit table font, fall back to the deck body family and locked body size from `spec_lock.md typography`.
- The marker group's transform stays translate/scale only (no rotate / matrix / skew).
- Visual parity is not a goal: the SVG drawing remains the designed visual; the native object is a data-editable counterpart with PowerPoint-default styling that users restyle by hand after export. Never simplify the SVG design to match what a native object could show.
**Per-page verification** — after writing each eligible data-chart or text-grid table page, confirm the marker exists:
```bash
grep "data-pptx-native" <project_path>/svg_output/<current_page>.svg
```
### SVG File Naming Convention
@@ -380,6 +178,8 @@ Strategist chooses the library and inventory; Executor only implements. Library
> **Resolution is project-first.** Strategist copied the chosen icons into `<project_path>/icons/<lib>/` (via `icon_sync.py`); `finalize_svg.py embed-icons` embeds from there, falling back to the global library per-icon. **Custom icons**: drop an `.svg` into `<project_path>/icons/<lib>/` (any `<lib>`, e.g. `custom/`) and reference it as `data-icon="<lib>/<name>"` — it embeds like any other. Reference only icons in the `spec_lock.md` inventory.
> **Icon identifiers are case-sensitive filenames.** For bundled libraries, copy the verified lowercase basename exactly (`tabler-outline/award`, never `tabler-outline/Award`) into `spec_lock.md` and every `data-icon` value. Custom icon identifiers preserve the custom file's exact case; the pipeline never silently lowercases names.
**Built-in icons — Placeholder method (recommended)**:
```xml
@@ -405,7 +205,9 @@ Strategist chooses the library and inventory; Executor only implements. Library
> ⚠️ **Color**: ALWAYS use `fill="#HEX"` on `<use data-icon="...">`. NEVER use `stroke` or `fill="none"`, even for stroke-style libraries.
>
> **stroke-width** (stroke-style libraries only, currently `tabler-outline`): allowed values `{1.5, 2, 3}`. If `spec_lock.md icons.stroke_width` is declared, all placeholders MUST use that value deck-wide. Default `2` if absent (legacy). Ignored on non-stroke libraries.
> **stroke-width** (stroke-style libraries only, currently `tabler-outline`): allowed values `{1.5, 2, 3}`. If `spec_lock.md icons.stroke_width` is declared, all placeholders MUST use that value deck-wide. Ignored on non-stroke libraries.
>
> **Missing `icons.stroke_width` in an existing stroke-library lock — fixed compatibility default**: use `2`, emit one warning, and continue. New authoring must still declare the field.
>
> Icons are auto-embedded by `finalize_svg.py` — no need to run `embed_icons.py` manually.
@@ -449,147 +251,27 @@ ls skills/ppt-master/templates/icons/simple-icons/ | grep github
---
## 5. Visualization Reference
Chart SVGs referenced in **VII. Visualization Reference List** are loaded once via the §1.0 batch read. This section governs adaptation only.
**Hard rule**: adapt the loaded chart SVG; do not improvise from memory and do not replicate verbatim. Apply project colors, typography, content; preserve visualization type.
**Adaptation rules**:
- **Preserve**: visualization type (bar/line/pie/timeline/process/framework…) as specified
- **Adapt**: data, labels, colors (project scheme), dimensions
- **Freely adjust**: composition, axis ranges, grid, legend, spacing, decoration — as long as the chart stays accurate and readable
- **Forbidden**: changing visualization type without spec justification; omitting data points or structural elements from the outline
> Templates: `templates/charts/` (76 types). Index: `templates/charts/charts_index.json`
### 5.1 Chart Coordinate Calibration
Coordinate calibration runs as a **standalone post-generation workflow**, not inside the executor pipeline. After SVG generation completes, if the deck contains data charts, run [`workflows/verify-charts.md`](../workflows/verify-charts.md) before post-processing.
The executor's only obligation here is upstream: embed the `<!-- chart-plot-area ... -->` marker on every chart page during initial draft (§3.1). Verify-charts enumerates chart pages from `design_spec.md §VII` (authoritative deck plan) and uses the marker to feed `svg_position_calculator.py`.
> Do NOT run `svg_position_calculator.py` during the initial draft. The calculator calibrates already-generated SVGs against their declared plot areas; running it before the SVG exists has nothing to compare against.
---
## 6. Image Handling
Handle images by their status in the Design Spec's Image Resource List. Status enum and lifecycle: [`svg-image-embedding.md`](svg-image-embedding.md).
| Status | Source | Handling |
|--------|--------|----------|
| **Existing** | User-provided | Reference images directly from `../images/` directory |
| **Generated** | Generated by Image_Generator | Reference images directly from `../images/` directory |
| **Sourced** | Web-acquired by Image_Searcher | Reference from `../images/`. **Read [`image_sources.json`](image-searcher.md) to decide attribution** — see §6.1 below. |
| **Rendered** | Deterministic formula PNG | Reference from `../images/`; use `preserveAspectRatio="xMidYMid meet"` |
| **Needs-Manual** | Acquisition failed and file is absent | Use dashed border placeholder unless the expected file exists |
| **Placeholder** | Not yet prepared | Use dashed border placeholder |
**Reference syntax**: see [`svg-image-embedding.md`](svg-image-embedding.md).
**Template-bundled images**: when a template (deck / layout / brand) is applied, its bitmaps are copied into the project's `images/` alongside every other runtime image (SKILL.md Step 3). Reference them the same way — `../images/<name>` — and do **not** reproduce a template SVG's bare sibling href (e.g. `href="cover_bg.png"`): the template SVG is reference material, the rendered page lives in `svg_output/` and must point at `../images/`. Mirror templates (§1.1) are the one exception — they copy hrefs verbatim, and the exporter resolves those bare hrefs against `images/`.
**Placeholder**: Dashed border `<rect stroke-dasharray="8,4" .../>` + description text
**`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`, `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)
Whenever the slide uses an image with `Status: Sourced`, look up the corresponding entry in `project/images/image_sources.json` and act on `license_tier`:
| `license_tier` | Action on this slide |
|---|---|
| `no-attribution` | Embed the `<image>` element only. **No credit element needed.** |
| `attribution-required` | Embed the `<image>` element **plus** a small inline `<text>` credit element per the visual spec in [image-searcher.md §7](./image-searcher.md). |
| `manual` | Embed the `<image>` element only. **No credit element** — a user-supplied `--from-url` replacement; verifying usage rights / any required credit is the user's responsibility. |
The credit text is **not** rendered by post-processing or export — it must be present in the SVG you produce. The shape of the credit element (size, position, color, multi-image source line, hero gradient overlay) is specified in [image-searcher.md §7](./image-searcher.md). Do not invent a different style.
Use `attribution_text` from the manifest entry as the **starting point**, then compress for the small-text constraint (drop URL, drop filename, keep "via Provider / License"). For CC0/PD images that landed in the `attribution-required` tier only because of upstream metadata quirks (rare), credits are still safe to render.
`svg_quality_checker.py` treats missing CC BY / CC BY-SA inline attribution as an **error**. Fix the offending SVG before post-processing.
**The manifest is the single source of truth for credits.** Do not duplicate license info into speaker notes or any other artifact.
---
## 7. Font Usage
## 5. Font Usage
Source of truth: `spec_lock.md typography`. Use `font_family` as default; override per role with `title_family` / `body_family` / `emphasis_family` / `code_family` if declared. LaTeX formulas that Strategist rendered are PNG images, not a `code_family` text role.
If `spec_lock.md` is absent, consult [`strategist.md`](strategist.md) §g — do not invent a stack.
**Missing required field — `typography.font_family`** → stop and return to Generate Step 4 / [`strategist.md`](strategist.md) §6.2 to repair `spec_lock.md`; do not infer a stack from `design_spec.md`.
**Hard rule**: every SVG `font-family` stack MUST resolve to pre-installed exported Latin / EA typefaces (Microsoft YaHei / SimHei / SimSun / Arial / Calibri / Segoe UI / Times New Roman / Georgia / Consolas / Courier New / Impact / Arial Black). PPTX has no runtime fallback — missing fonts degrade to Calibri.
---
## 8. Speaker Notes Generation Framework
## 6. Completion Routing
### Task 1. Generate Complete Speaker Notes Document
After every SVG page passes the final quality check, load [`executor-notes.md`](./executor-notes.md) and complete its notes contract before entering the route's Step 7.
After all SVG pages are finalized, enter Logic Construction Phase and write the full notes to `notes/total.md`. Batch-writing (not per-page) lets transitions plan coherently.
**Pure spoken narration**: notes are read aloud verbatim by `notes_to_audio.py` (TTS). Write only what should be spoken. No visible markers, no labeled meta-lines, no enumerated key-point lists, no duration annotations — anything you write outside the heading will be vocalized.
**Per-page structure**: `# <number>_<page_title>` heading (the `#` heading line is the only thing stripped before TTS), pages separated by `---`. Body is 25 natural sentences carrying the page's core message. Page-to-page transitions live inside the opening sentence as natural prose ("接下来……" / "Having framed X, let's turn to Y") — no bracketed `[过渡]` / `[Transition]` tags.
**Concrete examples** — same shape applies to any language; just write naturally in that language.
中文 deck
```
# 02_市场格局
在明确了行业背景之后,我们来看具体的市场格局。当前线上零售集中度持续上升,前三大平台合计份额已经达到百分之六十八,腰部玩家正在被快速挤压,留给新进入者的窗口期不超过十八个月。这意味着我们的策略必须聚焦,而不是铺开。
```
英文 deck
```
# 02_market_landscape
Having framed the industry backdrop, let's look at the actual market landscape. Online retail concentration keeps rising — the top three platforms now hold sixty-eight percent of combined share, mid-tier players are being squeezed fast, and the window for new entrants is under eighteen months. This means our strategy has to focus, not spread.
```
> 日本語 / 한국어 / 其他语言:照搬同样的结构,用对应语言自然书写即可。
**Number readability**: TTS reads digits and symbols literally. Prefer fully-spelled forms in the language being spoken when literal pronunciation would be awkward (e.g. Chinese "百分之六十八" reads better than "68%"; "1-2分钟" reads as "一减二分钟"). Plain integers and percentages in English are fine as-is.
**Common mistakes to avoid**:
- Leaving any bracketed stage marker (`[过渡]` / `[Transition]` / `[Pause]` / `[Data]` / `[Scan Room]` / `[Interactive]` / `[Benchmark]` etc.) in the text — they will be read aloud literally.
- Adding `要点:① …` / `Key points: (1) …` / `时长:2分钟` / `Duration: 2 minutes` / `Flex: …` lines — TTS will speak "要点 一 …".
- Mixing languages within one deck's notes.
### Task 2. Split Into Per-Page Note Files
Auto-split `notes/total.md` into per-page files in `notes/`.
**Naming**: match SVG names (`01_cover.svg``notes/01_cover.md`); `slide01.md` also supported (legacy).
---
## 9. Next Steps After Completion
## 7. Next Steps After Completion
> **Auto-continuation**: After Visual Construction Phase (all SVG pages) and Logic Construction Phase (all notes) are complete, the Executor proceeds directly to the post-processing pipeline.
**Post-processing & Export** (canonical workflow: [`SKILL.md` Step 7](../SKILL.md)):
```bash
# 1. Split speaker notes
python3 scripts/total_md_split.py <project_path>
# 2. SVG post-processing (auto-embed icons/images and flatten positioned text)
python3 scripts/finalize_svg.py <project_path>
# Output: svg_final/ self-contained SVG visual previews
# 3. Export PPTX
python3 scripts/svg_to_pptx.py <project_path>
# Output (default-flow mode):
# exports/<project_name>_<timestamp>.pptx ← native pptx (canonical output)
# backup/<timestamp>/svg_output/ ← Executor SVG source backup (always written)
```
**Post-processing & Export**: Follow [`generate-pptx.md`](../workflows/generate-pptx.md)
Step 7. That workflow owns the serial commands, gates, success criteria, and
published artifacts; [`svg-pipeline.md`](../scripts/docs/svg-pipeline.md) owns
tool-specific flags and behavior.
`svg_final/` may be opened directly or manually inserted into PowerPoint as an SVG picture. It is not a second PPTX route. Use `-s final` only for converter diagnostics; release exports use the default `svg_output/` source. Manual Convert-to-Shape behavior is unsupported.
@@ -0,0 +1,132 @@
> See [`executor-base.md`](./executor-base.md) for the always-loaded Executor core and [`native-data-interface.md`](./native-data-interface.md) for native chart/table metadata schemas.
# Executor Chart and Table Branch
Conditional Executor authority for data charts, chart-catalog adaptations, chart verification markers, and eligible native chart/table replacement metadata.
**Trigger**: load when `design_spec.md §VII` contains a chart/table visualization, `spec_lock.md page_charts` contains any row, or the current page carries any data-encoded chart or text-grid table. Mini charts, sparklines, inset charts, and small multiples count even when they are absent from `page_charts` or the chart catalog.
## 1. Reference Loading and Per-page Selection
For each selected `templates/charts/<key>.svg`, use its Skill-relative `reference_set` path + SHA: read once before first use or after change, otherwise reuse it. Never load the full catalog.
**Per-page chart reference — `page_charts` section**:
Before drawing each page, look up its entry in `page_charts` to decide which chart structure applies (the SVG itself was loaded in §1):
- Entry present (e.g., `P09: timeline_horizontal`) → adapt the corresponding chart SVG already in context under §3; do not copy it verbatim. Use the selected §VII row and SVG; do not load the full chart catalog during execution.
- No entry for this page → either no chart on this page, or a chart that didn't match any catalog template (Strategist's `no-template-match` fallback). Design the visualization from scratch using `design_spec.md §VII` for guidance.
- Whole section absent → no chart pages in this deck.
---
## 2. Chart and Native-data Authoring
### 2.1 Chart Plot-Area Marker (MANDATORY on every chart page)
> The [`verify-charts`](../workflows/stages/verify-charts.md) stage enumerates chart pages from `design_spec.md §VII`, then reads each page's plot-area marker to feed `svg_position_calculator.py`. A missing marker invokes that stage's declared fallback and adds avoidable derivation work.
**Hard rule**: every SVG page that contains a data visualization chart includes a plot-area marker inside `<g id="chartArea">`, placed **after axis lines** and **before the first data element** (bar, line, area, point).
**Rectangular plot area** (bar / horizontal_bar / grouped_bar / stacked_bar / line / area / stacked_area / scatter / waterfall / pareto / butterfly):
```xml
<!-- chart-plot-area: x_min,y_min,x_max,y_max -->
```
**Radial charts** (pie / donut / radar):
```xml
<!-- chart-plot-area: pie | center: cx,cy | radius: r -->
<!-- chart-plot-area: donut | center: cx,cy | outer-radius: r1 | inner-radius: r2 -->
<!-- chart-plot-area: radar | center: cx,cy | radius: r -->
```
**How to determine coordinate values**:
| Value | Derivation |
|-------|------------|
| `x_min` | X coordinate of the Y-axis line (leftmost data boundary) |
| `y_min` | Y coordinate of the topmost grid line (highest data boundary) |
| `x_max` | X coordinate of the rightmost axis endpoint or grid line |
| `y_max` | Y coordinate of the X-axis baseline |
| `cx, cy` | Center point of pie/donut/radar (accounting for `transform="translate()"`) |
| `r` | Outer radius of the chart |
**Per-page verification** — after writing each chart SVG, confirm the marker exists:
```bash
rg -n "chart-plot-area" <project_path>/svg_output/<current_page>.svg
```
> Calculator-supported data-chart templates in `templates/charts/` include this
> marker as a reference. If a data chart covered by §2.1 lacks it, that is a
> bug. Conceptual diagrams, frameworks, and other non-data visualizations in
> the same library do not use a plot-area marker.
Technical SVG/PPT constraints remain in [`shared-standards-core.md`](./shared-standards-core.md).
### 2.2 PowerPoint-Native Chart/Table Replacement Marker (MANDATORY on eligible data-chart and text-grid table pages)
> `svg_to_pptx.py --native-charts-and-tables` replaces marked groups with PowerPoint-native Chart/Table objects (charts get an embedded Excel workbook). Markers stay dormant in the default export, whose SVG children become independently editable DrawingML shapes, but a deck without markers can never form data-backed native Chart/Table objects. Write the marker at draw time: the data is already in hand, and recovering it later costs a full re-read pass.
**Hard rule**: before deciding whether a chart or table is eligible for native replacement, load [`native-data-interface.md`](./native-data-interface.md). Every data chart whose type appears in that authority's **Supported chart types** list gets `data-pptx-replace-with="chart"` plus one `<metadata type="application/json">` JSON child on its top-level `<g>`, transcribing the same data just plotted. Every pure text-grid data table gets `data-pptx-replace-with="table"` the same way, transcribing all visible cell text into `columns` / `rows`. The parent marker determines the JSON schema; do not duplicate a chart/table kind on the metadata child.
**MUST — atomic authoring**: Treat the visible SVG fallback, the parent `data-pptx-replace-with` marker, and its JSON `<metadata>` child as one object. Write all three in the same SVG edit while the plotted data is in context. A supported chart or eligible table is unfinished if either the marker or metadata is missing; do not defer either one to `verify-charts`, the final quality gate, or export.
**Hard rule — eligibility follows data semantics**: Size, point count, visual prominence, page role, catalog match, and the current export mode do not change eligibility. A two-point mini line chart, sparkline, chart inset, KPI-card trend, or small multiple that encodes recoverable categories/values still gets its own marked top-level `<g>` and metadata payload when its chart type is supported. Decorative strokes and arrows without recoverable chart data remain ordinary SVG geometry.
Generated authoring MUST omit `data-pptx-import-source` and
`data-pptx-fallback-sha256`: those attributes record imported-PPTX provenance
and its sealed fallback baseline. Never copy a static baseline from a chart
catalog or reusable template; normal content edits would make it stale.
`data-pptx-replace-with` is a **data-backed replacement claim**, not a generic label for a group that contains numbers and not a marker for ordinary PowerPoint shapes or connectors. Add it only when the matching JSON payload can be written in the same edit; if the object is meant to remain SVG geometry, do not add the marker.
- Chart types absent from that list and conceptual/diagrammatic graphics (process flows, cycles, quadrant cards, timelines, or a KPI card container) get **no marker**`svg_quality_checker.py` rejects unsupported marker types. A supported data chart nested visually inside one of those compositions still gets its own marker.
- Canonical rectangular merged text cells may carry a table marker by putting anchor-only `row_span` / `col_span` in metadata and leaving covered cells blank. Nonrectangular/overlapping merges, nonblank covered cells, and graphical cells (icons, harvey balls, rating dots) get **no table marker** and stay on the SVG fallback route.
- Transcribe, don't restyle: `categories` / `series[].values` are the numbers just plotted; `style.colors` carries the series HEX values already used on the page (from `spec_lock.colors`).
- Data-point color: when a single column/bar series uses data-point colors in the fallback, copy those fills into `series[].point_colors` in category order.
- Data labels: when visible point values are part of the fallback chart, write `data_labels` instead of companion text; use `data_labels.points` for selected labels, and use `number_format`, `font_size`, `font_family`, and per-point `colors` / `color` when the fallback labels carry suffixes or color-coded text.
- Line markers: when the fallback line chart draws visible point nodes, set `line_style: "lineMarker"`; leave the default `line` only for line charts without nodes.
- Area-under-line: when a combo plot is drawn as a filled area under a line, keep `type: "line"`, add `area_fill: true`, and copy the area transparency into `series[].fill_opacity`; copy visible line `stroke-width` into `series[].line_width` for line/area series.
- Native chrome: write `title`, `subtitle`, axis titles, or `show_legend: true` only when the fallback visibly renders the same chrome inside the native chart's replacement scope. `title` is the PowerPoint chart title, not an object name; use `name` for page-semantic object naming (e.g. `p03-revenue-chart`). Write explicit `x`/`y`/`width`/`height` read from the drawn plot area; omission is the fallback — the exporter then infers the frame from the drawn fallback geometry.
- Value-axis labels: when the fallback keeps category labels but intentionally omits numeric value-axis tick labels, set `show_value_axis_labels: false`.
- Freeform chart text: transcribe center labels, source notes, and other in-chart annotations as companion `caption` / `note` / `notes` entries with explicit slide-coordinate bounds; do not rely on fallback `<text>` children to survive native export.
- Native chart typography mirrors the SVG fallback. Copy the fallback's shared chart font into `style.font_family` and visible chart text sizes into the matching metadata fields (`title_font_size`, `subtitle_font_size`, `axis_font_size`, `note_font_size`, etc.) only when role sizes differ; otherwise let the exporter infer them from visible fallback text. When a visible chart title, subtitle, or axis title needs its own size/color/font, write that field as an object with `text`, `font_size`, `font_family`, and `color`. Use `axis_title_font_size`, `legend_font_size`, or companion per-entry `font_size` only when the fallback visibly uses a separate size.
- Native table typography mirrors the SVG fallback. Write `style.font_family` and `style.font_size` from the visible table text; use `header_font_size` or per-cell `font_size` only when the fallback visibly does so. If the fallback has no explicit table font, fall back to the deck body family and locked body size from `spec_lock.md typography`.
- The marker group's transform stays translate/scale only (no rotate / matrix / skew).
- Visual parity is not a goal: the SVG drawing remains the designed visual and exports as editable DrawingML shapes; the native object is the data-backed counterpart with PowerPoint's chart/table-specific model. Never simplify the SVG design to match what a native object could show.
**Per-page verification** — after writing each eligible data-chart or text-grid table page, enumerate the eligible objects and confirm a one-to-one match: every object has one parent marker and exactly one JSON metadata child. Finding one marker somewhere on a page is insufficient when the page contains multiple eligible objects.
```bash
rg -n 'data-pptx-replace-with="(chart|table)"|<metadata type="application/json">' <project_path>/svg_output/<current_page>.svg
```
---
## 3. Visualization Reference
Chart SVGs referenced in **VII. Visualization Reference List** are loaded once through §1. This section governs adaptation only.
**Hard rule**: adapt the loaded chart SVG; do not improvise from memory and do not replicate verbatim. Apply the active Design Spec and `spec_lock.md`; preserve the visualization type and data semantics.
**Adaptation rules**:
- **Preserve**: visualization type (bar/line/pie/timeline/process/framework…), information relationships, data encoding, structural grouping, and capacity
- **Carry forward**: every planned label, value, unit, status, source, and explanatory block; never shorten or drop content to imitate a lighter catalog preview
- **Adapt**: project data and labels, dimensions, axes, legend, and spacing as the authored content requires
- **Project-owned**: palette, typography, container treatment, effects, background, and page chrome; catalog preview values are fallbacks, never defaults
- **Bound final body modules**: add or revise root-coordinate `data-pptx-bounds` on every visible direct root `<g>` copied into the final page; nested groups need none, chart geometry and local references are not content-boundary inputs, and catalog reference warnings never waive the final-page contract
- **Adjust with fidelity**: composition, axis ranges, and grid may change within the project contract only when no content, relationship, hierarchy, or capacity is lost
- **Forbidden**: changing visualization type without spec justification; omitting data points or structural elements from the outline
> Templates: `templates/charts/`. The Strategist's selected key is already locked in `page_charts`; execution opens only that key's SVG.
### 3.1 Chart Coordinate Calibration
Coordinate calibration runs as a **conditional post-generation stage**, not inside the Executor authoring loop. After SVG generation completes, if the deck contains data charts, run [`verify-charts`](../workflows/stages/verify-charts.md) before post-processing.
The executor's only obligation here is upstream: embed the `<!-- chart-plot-area ... -->` marker on every chart page during initial draft (§2.1). Verify-charts enumerates chart pages from `design_spec.md §VII` (authoritative deck plan) and uses the marker to feed `svg_position_calculator.py`.
> Do NOT run `svg_position_calculator.py` during the initial draft. The calculator calibrates already-generated SVGs against their declared plot areas; running it before the SVG exists has nothing to compare against.
@@ -0,0 +1,32 @@
> See [`executor-base.md`](./executor-base.md) for the always-loaded Executor core.
# Executor Image Branch
Conditional Executor authority for image status handling, placement, crop behavior, formula images, and template-bundled images.
**Trigger**: load when `design_spec.md §VIII` or `spec_lock.md images` contains any image/formula row, or when a selected template carries bitmap assets.
## 1. Image Handling
Handle images by their status in the Design Spec's Image Resource List. Status enum and lifecycle: [`svg-image-embedding.md`](svg-image-embedding.md).
| Status | Source | Handling |
|--------|--------|----------|
| **Existing** | User-provided | Reference images directly from `../images/` directory |
| **Generated** | Generated by Image_Generator | Reference images directly from `../images/` directory |
| **Sourced** | Web-acquired by Image_Searcher | Reference from `../images/`. **Read [`image_sources.json`](image-searcher.md) to decide attribution** — load [`executor-web-image.md`](./executor-web-image.md). |
| **Rendered** | Deterministic formula PNG | Reference from `../images/`; use `preserveAspectRatio="xMidYMid meet"` |
| **Needs-Manual** | Acquisition failed and file is absent | Use dashed border placeholder unless the expected file exists; the Step 7 readiness gate swaps placeholders for real files before export |
| **Placeholder** | Not yet prepared | Use dashed border placeholder |
**Reference syntax**: see [`svg-image-embedding.md`](svg-image-embedding.md).
**Template-bundled images**: when a template (deck / layout / brand) is applied, its bitmaps are copied into the project's `images/` alongside every other runtime image ([`apply-template-workspace.md`](../workflows/stages/apply-template-workspace.md)). Reference them the same way — `../images/<name>` — and do **not** reproduce a template SVG's bare sibling href (e.g. `href="cover_bg.png"`): the template SVG is reference material, the rendered page lives in `svg_output/` and must point at `../images/`. `template_reuse_scope: mirror` ([`executor-structured.md`](./executor-structured.md) §1.1) is the one exception — it copies hrefs verbatim, and the exporter resolves those bare hrefs against `images/`.
Use each resource row's locked layout pattern; do not collapse image-led pages into a generic left/right split.
**Placeholder**: Dashed border `<rect stroke-dasharray="8,4" .../>` + description text
**`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 — declared-inference fallback for a missing `no-crop` flag**: rows with `Acquire Via: formula` or `Type: Latex Formula` MUST be treated as no-crop. For a rendered file, use dimensions in this order: current `analysis/image_analysis.csv`, `design_spec.md §VIII`, then `images/formula_manifest.json`. For a `Needs-Manual` row, size the dashed placeholder from the planned dimensions in §VIII, then the manifest; the Step 7 readiness gate re-analyzes the supplied file and reconciles the container before export. Do not normalize all formulas to one height unless the spec explicitly states that layout choice.
@@ -0,0 +1,54 @@
> See [`executor-base.md`](./executor-base.md) for the always-loaded Executor core.
# Executor Speaker-notes Branch
Conditional late-stage authority for generating the complete speaker-notes document.
**Trigger**: load only after all SVG pages pass the final quality check.
## 1. Speaker Notes Generation Framework
### 1.1 Generate Complete Speaker Notes Document
After all SVG pages are finalized, enter Logic Construction Phase and write the full notes to `notes/total.md`. Batch-writing (not per-page) lets transitions plan coherently.
**Pure spoken narration**: notes are read aloud verbatim by `notes_to_audio.py` (TTS). Write only what should be spoken. No visible markers, no labeled meta-lines, no enumerated key-point lists, no duration annotations — anything you write outside the heading will be vocalized.
**Per-page structure**: `# <number>_<page_title>` heading (the `#` heading line is the only thing stripped before TTS), pages separated by `---`. Body is 25 natural sentences carrying the page's core message. Page-to-page transitions live inside the opening sentence as natural prose ("接下来……" / "Having framed X, let's turn to Y") — no bracketed `[过渡]` / `[Transition]` tags.
**Reading-mode note burden**:
| `consumption_mode` | Notes emphasis |
|---|---|
| `text` | Add interpretation or transition without reading the already self-contained page aloud. |
| `balanced` | Connect the visible claim and evidence, explain the trade-off, and bridge to the next page. |
| `presentation` | Carry the reasoning, context, and supporting detail intentionally omitted from the sparse projected surface. |
**Concrete examples** — same shape applies to any language; just write naturally in that language.
中文 deck
```
# 02_市场格局
在明确了行业背景之后,我们来看具体的市场格局。当前线上零售集中度持续上升,前三大平台合计份额已经达到百分之六十八,腰部玩家正在被快速挤压,留给新进入者的窗口期不超过十八个月。这意味着我们的策略必须聚焦,而不是铺开。
```
英文 deck
```
# 02_market_landscape
Having framed the industry backdrop, let's look at the actual market landscape. Online retail concentration keeps rising — the top three platforms now hold sixty-eight percent of combined share, mid-tier players are being squeezed fast, and the window for new entrants is under eighteen months. This means our strategy has to focus, not spread.
```
> 日本語 / 한국어 / 其他语言:照搬同样的结构,用对应语言自然书写即可。
**Number readability**: TTS reads digits and symbols literally. Prefer fully-spelled forms in the language being spoken when literal pronunciation would be awkward (e.g. Chinese "百分之六十八" reads better than "68%"; "1-2分钟" reads as "一减二分钟"). Plain integers and percentages in English are fine as-is.
**Common mistakes to avoid**:
- Leaving any bracketed stage marker (`[过渡]` / `[Transition]` / `[Pause]` / `[Data]` / `[Scan Room]` / `[Interactive]` / `[Benchmark]` etc.) in the text — they will be read aloud literally.
- Adding `要点:① …` / `Key points: (1) …` / `时长:2分钟` / `Duration: 2 minutes` / `Flex: …` lines — TTS will speak "要点 一 …".
- Mixing languages within one deck's notes.
After `notes/total.md` is complete, return to Generate Step 7.1. The route authority owns the serial split command and its success criterion.
@@ -0,0 +1,176 @@
> See [`executor-base.md`](./executor-base.md) for the always-loaded Executor core and [`pptx-structure-interface.md`](./pptx-structure-interface.md) for the SVG metadata contract.
# Executor Structured Template Branch
Conditional Executor authority for `template_reuse_scope: mirror|layout` with `pptx_structure.mode: structured`.
**Trigger**: load only when the lock selects structured template reuse.
## 1. Template Reuse Rules
### 1.0 Template Context Load
| Context | Load policy |
|---|---|
| `templates/design_spec.md` | Strategist reads once; continuous Executor reuses it, fresh Executor reads it once |
| Current page delta | Run [`executor-base.md`](./executor-base.md) §2.1 immediately before that page |
| Selected prototype SVG | Read once on an absent/changed `reference_set` path + SHA; otherwise reuse it |
**Hard rule**: Page-context carries no prototype payload. `reference_set` identifies the authoritative complete SVG; never author from a roster, manifest, sidecar, filename, or summary alone.
Manifest/text-slot files are derived tool metadata, not model inputs. Missing metadata neither invalidates a legacy workspace nor permits text-topology changes.
**Mapping change**: update the owning plan and regenerate that page's delta; load only a new/changed prototype fingerprint.
Resolve the per-page template SVG from `page_context.template.prototype`; the owning `spec_lock.md page_layouts` row remains authoritative. There is no filename/page-type fallback.
**Resolution order (per page):**
1. `template_reuse_scope: mirror` → see §1.1. The installed workspace must support `replication_mode: mirror`.
2. `template_reuse_scope: layout` → resolve `P<NN>: <basename>` from `page_layouts`, retain the structure system, and apply the non-mirror skin/reflow rules below.
3. `mirror` / `layout` with no current-page `page_layouts` row → stop; adaptive mode still requires one selected input prototype.
> Note: `page_layouts` disambiguates the multiple content variants a template may ship; missing mappings are contract errors.
**Default — re-skin `layout` (may override when the application plan keeps template visuals and the lock reflects them)**: inherit geometry, label/legend placement, and series encoding; otherwise repaint template gradients, shadows, fills, and strokes from the current style/lock. Template font sizes remain placeholders. `mirror` preserves visuals under §1.1.
**Font size is skin, not geometry (non-mirror).** A chart / layout template's hardcoded `font-size` values (often 1116px, sized for the template's own dense placeholder text) are NOT inherited — classify each text into its `spec_lock.md` role and use that role's locked size, exactly as you re-skin color. **Structural roles (page title / body / subtitle / annotation / footnote) hold their one deck-wide size on every page** — the template's placeholder px never overrides it; same-role text drifting page to page is what makes a deck look unprofessional.
**Typography execution order (mandatory):**
1. Build a per-page text inventory from `design_spec.md §IX` + the current `notes/<NN>_*.md`.
2. Classify each text item before drawing. **Structural roles** (`title`, `subtitle` / `lead`, `body`, `annotation`, `footnote` / `page_number`) must map to their declared `spec_lock.typography` slot. A **one-off feature element** (a single hero number, an isolated emphasis label) may take an in-ramp intermediate value — the ramp is anchored on `body`, not a closed menu — but a feature size that **recurs** must be promoted to a declared slot. The failure mode this guards against is structural text silently inheriting the template's compact px, not legitimate feature sizing.
3. Copy the role's locked px value into `font-size` verbatim. Do this before placing the text; never start from a template `font-size` and then "adjust".
4. Layout from those locked sizes: compute line-height, wrapped line count, child `y` / `dy`, card padding, card height, column gaps, and available image/chart area from the chosen px values.
5. Only after this reflow may you inspect fit. If fit fails, move / resize containers or simplify local geometry first; do not reduce the role size merely because the inherited template slot was smaller.
**Geometry adapts to the type, never the reverse**: when the locked size is larger than the template's placeholder text, widen or heighten the card, open spacing, and recompute child `y` / `dy`; do not shrink text to inherit a smaller container. Recompute line-height and downstream coordinates, and allocate wrapped-line height plus padding. Page count and density remain the confirmed Strategist decision: do not repaginate, split, or drop content. If a fully reflowed block still fails, apply the single bounded body-fit exception in [`executor-base.md`](./executor-base.md) §2.1. Mirror instead preserves source typography under §1.1.
### 1.1 Mirror reuse — literal page replacement
When `spec_lock.md` records the AI-derived `template_reuse_scope: mirror`, Executor switches to a literal replacement path. The workspace capability `replication_mode: mirror` is a prerequisite, not the trigger by itself:
1. **Per-page reference selection** — Strategist selects one mirror page per project page via `spec_lock.md page_layouts` (e.g., `P04: 015_content`). The basename is the mirror filename without extension; Strategist made this choice by reading `design_spec.md §V Page Roster` descriptions, not by guessing.
2. **Copy, don't fill** — use the retained full mirror SVG as the starting point, then edit slide-specific text in place. Preserve every non-text element and every `data-pptx-*` structure attribute verbatim. Do not reopen the same path + SHA merely because another page selects it.
3. **What you may edit** — decide the semantic slot mapping and replacement text only. Change only visible string values already carried by `<text>` and `<tspan>` nodes that express slide-specific content (title, body, captions, KPI labels, dates, page numbers). Keep the number, order, nesting relationship, and **all attributes** of every `<text>` / `<tspan>` node unchanged. Never merge or split nodes, move a string between nodes, add a new tspan, or delete an empty carrier. `svg_quality_checker.py` and export validate attributes, topology, and prototype hashes against the complete prototype internally.
4. **What you must not touch** — element positions, sizes, fonts, colors, fills, strokes, gradients, **which image each `<image>` points at**, `<g>` grouping, sprite-sheet `<svg viewBox>` wrappers, decorative `<rect>` / `<path>` / `<circle>` / `<polygon>` shapes, `<use data-icon="...">` markers, embedded chart data structures. Mirror's value is preserving the source deck's visual identity — any geometric / decorative drift defeats the purpose. **The `href` path is not the image**: normalizing a bare `href="cover_bg.png"` to `href="../images/<name>"` (when Step 3 relocated the asset to `images/`) points at the *same* image and changes nothing visual — that is an allowed path fix, not a fidelity edit. Leaving the bare href as-is is also fine; the exporter and live preview resolve bare hrefs against `images/` either way.
5. **Content fit** — if the replacement needs a different number of text segments/items, do not merge/split nodes, drop sourced content, or restructure the grid. Select a better mirror prototype and update the planning mappings, or report `warning: P<NN> content does not fit mirror reference <basename>; choose another prototype or change template_reuse_scope to layout/style`.
6. **Visible text editing** — mirror SVGs may keep literal source text rather than `{{...}}` authoring markers. Edit values in place while retaining imported semantic `data-pptx-placeholder` identity and exact text topology.
7. **Output filename** — follow the standard project SVG naming convention (`<NN>_<page_name>.svg` where `<NN>` matches the project page index, not the mirror source index). The mirror filename is the *reference*, not the *output*.
**Detecting mirror mode**: read `page_context.template.reuse_scope` from the current page delta. `replication_mode: mirror` in the installed template only determines whether that derived scope is legal; it must never force mirror behavior when the lock records `layout` or `style`.
**Mirror + chart pages**: chart structures inside a mirror SVG are already drawn (axis, series, labels). Treat them as visual references — replace the data labels and series text content to match the project's chart spec, but do not redraw the chart from a `templates/charts/<name>.svg` baseline. A mirror template's `page_charts` entries are normally absent for this reason.
**Legacy template boundary**: A template with missing root Master identity, direct atomic placeholders, `data-pptx-layout-kind`, unmapped `baseline`, `preserve`, or `layout_strategy: distill` is not a fallback input. Stop and create a new current workspace through [`create-template`](../workflows/create-template.md) before generation.
### Page-Template Mapping Declaration (Required Output)
Before generating each page, output which template is used:
```
📝 **Template mapping**: `templates/03a_content_image_text.svg` (free-design routes may use "None")
🎯 **Adherence rules / layout strategy**: [specific description]
```
- **Content pages**: template defines only header/footer; content area is free
- **No template**: allowed only on free-design or brand-only routes
### 1.2 PowerPoint Master / Layout Mapping
This section applies only when a deck/layout template's AI-derived lock records `template_reuse_scope: mirror|layout`. `page_layouts` selects the input SVG prototype, `pptx_masters` / `pptx_layouts` declare unique reusable output definitions, and `page_pptx_layouts` assigns every generated page before the first page is drawn. `template_reuse_scope: style`, free-design, and brand-only routes use `pptx_structure.mode: flat`, omit all four sections, skip the rest of §1.2, and keep every SVG object Slide-local.
**Hard rule — reuse-scope route**: `template_reuse_scope: mirror|layout` requires `pptx_structure.mode: structured`. `template_reuse_scope: style` requires `mode: flat` even though a template supplied its visual vocabulary. Missing mode or legacy values (`baseline`, `template`, `preserve`), `layout_strategy`, Layout-kind fields, partial mappings, and old direct placeholders stop generation. Create a new template workspace through [`create-template`](../workflows/create-template.md); do not upgrade the active SVG project in place.
**Hard rule — root identity**: A `page_pptx_layouts` row binds the page to one key in `pptx_layouts`; that unique definition supplies its Master key, Layout picker name, and prototype source. Put the declared Master key/name and Layout key/name on the root SVG. A Layout key belongs to exactly one Master and remains globally unique.
**Hard rule — atomic fixed layers**: Every `data-pptx-layer="master|layout"` visual is one direct root semantic atom that compiles to one DrawingML object. An ordinary marked `<g>` is forbidden; one validated compact authored-preset `<g>` emitted by `preset_shape_svg.py` is the sole group exception because it compiles to one native shape. When reconstructing source PPTX groups, recursively push supported transforms, paint, opacity, and z-order into atomic children. Repeat the identical ordered Master atom contract on every page using that Master and the identical ordered Layout atom contract on every page sharing that `(master, layout)` pair.
**Hard rule — PowerPoint paint order**: Direct children appear in this order: Master background atoms, Layout background atoms, optional Slide background, remaining Master atoms, remaining Layout atoms, then slot groups and Slide-local content groups. Backgrounds are the inheritance plane beneath all shapes.
**Mandatory — slot authoring**: A reusable content slot is one direct root `<g id>` carrying `data-pptx-placeholder` and one positive `data-pptx-bounds`; the same design zone is both the reusable Layout default and the slot module boundary. A normal slot contains exactly one compatible direct drawable child marked `data-pptx-carrier="true"`. Export unwraps that child into the real Slide placeholder binding. Decorations do not belong in the slot; move reusable decoration to a root Layout atom and keep page-specific labels/captions in another bounded slot or Slide-local group.
**Mandatory — slot identity**: Preserve imported `data-pptx-idx` values where available; otherwise omit the title index and assign unique indices only where repeated roles need disambiguation. Pages sharing one Layout key repeat the same slot ids/types/effective indices/default bounds/binding modes. Current text, crop, and Slide-local carrier geometry may differ.
**Composite proxy fallback**: A genuinely composite region may use a direct `<g data-pptx-placeholder="object" data-pptx-binding="proxy">` with positive bounds. Its visible group remains Slide-local and export creates one hidden transparent matching placeholder proxy. This downgrade is valid only for `object`; do not use it for an ordinary title, body, picture, chart, table, or media slot.
**Forbidden — dummy carriers**: Never satisfy a carrier slot with tiny text, near-transparent glyphs, background-colored punctuation, or other fake content. Leave an intentionally blank text carrier empty/whitespace-only—the exporter emits a legal invisible U+200B run—or use the composite `object` proxy contract. If `strict` prototype binding cannot represent the completed composition, surface the mismatch; select a compatible prototype or create an explicit adaptive Layout instead of hiding the conflict.
**Zero-slot Layout**: A Layout may have no slot groups. Covers, posters, and fixed visual pages still declare their named Master/Layout and fixed atoms. Do not manufacture a full-page `object` slot or empty `utility` identity.
**Mandatory — per-page slot coverage**: On every mapped page, declare a slot for each standard role the page actually has: the page heading as `title`, a cover tagline as `subtitle`, the page number as `slide-number`, running footer text as `footer`, a hero / content image as `picture`, and a body block already authored as one merged text frame as `body`. A page shipping zero slots exports a Layout with no insertable placeholders — valid only for a genuinely fixed composition (see Zero-slot Layout above), never as the deck-wide default. Pages sharing one layout key ship the same slot set.
**Hard rule — variable slot content**: “Per-page headings never stay Slide-local by default” means authoring them as `title` / `subtitle` slots; it never permits page-varying text or images to become fixed Layout atoms. Any such value that varies across pages sharing one Layout key MUST be carried by a slot or remain Slide-local.
**Mandatory — master/layout layer coverage**: On every mapped page, mark the deck-wide background and every-page chrome (footer bar, running logo) `data-pptx-layer="master"`, and mark the static framing that defines this layout key's composition (header rule, divider band, zone panels — including chrome repeated on every content page but absent from the cover) `data-pptx-layer="layout"`. A mapped page with zero `data-pptx-layer` marks exports a bare Master and an empty Layout — the layer marks, not the slide content, give each Layout its visible design.
**Layout identity**: Different keys differ in fixed Layout atoms or slot topology/default bounds/binding modes. Identical contracts should share one key. Current wording, imagery, crop, and Slide-local geometry never define identity.
**Template adherence**: Strict preserves reusable Master/Layout atoms and slot ids/types/indices/default bounds/bindings. Under `layout`, the application plan may still change current text/tspans, line height, crop, and carrier-local geometry inside those bounds; `mirror` remains topology-frozen. Adaptive keeps the prototype Master and changes reusable atoms or slots only under a new explicit Layout key/name, written to `spec_lock.md pptx_layouts` while authoring the first affected page. Changing only content is not a new Layout.
**Layout-content boundary**: Mark only genuinely reusable fixed framing as a Master/Layout atom. Concrete titles, body copy, metrics, chart marks, images, and page-specific groups remain inside slot groups or ordinary Slide-local content groups. The exporter never infers or clusters structure.
**Background ownership**:
| Scope | SVG authoring |
|---|---|
| Deck-wide default | Direct full-canvas solid `<rect data-pptx-layer="master">` repeated identically on every page |
| Page-type default | Direct full-canvas solid `<rect data-pptx-layer="layout">` repeated on every page sharing that layout key |
| One-page exception | Direct full-canvas solid `<rect data-pptx-layer="slide">` |
The exporter writes these solid fills as real Master/Layout/Slide `p:bg`, not selectable full-canvas shapes. In structured mode, gradients, preset patterns, images, textures, and overlay panels remain explicit shapes or pictures; the generic background-promotion rule outside structured mode does not expand this ownership contract.
---
## 2. Per-page Structured Lookup
**Per-page template lookup — `page_layouts` section (`mirror` / `layout` only)**:
Before drawing each page, use `page_context.template.prototype` to identify the inherited basename. Its matching `reference_set` entry supplies the complete SVG's path and SHA; §1.0 owns whether that file must be read or can be reused from the active context:
- Entry present (e.g., `P04: 03a_content_image_text`) → inherit the corresponding full SVG. The basename **must match** an actual file in the chosen template directory. If it does not, stop before drawing and report the invalid mapping; neither `strict` nor `adaptive` may fall back to free design inside a structured template deck.
- No entry for this page with `template_reuse_scope: mirror|layout` → stop before drawing and report the missing Strategist mapping. Adaptive mode still requires one selected complete template SVG; flexibility applies to the post-design output Layout, not to whether an input prototype exists.
- Whole section absent while `template_reuse_scope: mirror|layout` is present → stop before drawing; the current template contract is incomplete.
- `template_reuse_scope: style` → the whole section must be absent; do not perform per-page prototype lookup.
Do **not** invent a prototype entry, and do **not** assume a structured template just because `templates/` exists. For `mirror` / `layout`, a missing or invalid `page_layouts` row is an upstream contract error. `style` is a separate flat deck route, never a per-page fallback.
**Per-page PowerPoint layout lookup — `template_reuse_scope: mirror|layout` only**:
- When `pptx_structure.mode` is `flat` (including `template_reuse_scope: style`), skip this lookup and the structured scaffold below. `pptx_masters`, `pptx_layouts`, `page_layouts`, and the corresponding SVG metadata must all be absent; each root still declares its canonical `data-pptx-page-role`.
- With `template_reuse_scope: mirror|layout`, `pptx_structure.mode` must equal `structured`; any other or missing value is rejected. Do not migrate an invalid structured contract in place: create a new current-contract workspace through Create Template before generation resumes.
- Read the current page assignment as `P<NN>: <layout_key>`. Resolve the assigned Layout key in `pptx_layouts`, then resolve its Master key in `pptx_masters`. Missing, malformed, or partial mappings stop before drawing.
- Write matching root Master/Layout key and picker names. Do not write `data-pptx-layout-kind` or `data-pptx-page-role`.
- On strict template use, the row and SVG contract match the selected prototype exactly.
- On adaptive template use, retain the prototype Master. If the final composition changes fixed Layout atoms or slot topology/bounds, allocate a new key/name and update this row before completing the page.
- A Layout key may repeat across non-adjacent pages only when its fixed atoms and slot contracts are identical.
**Structured template-page scaffold**:
```xml
<svg viewBox="…"
data-pptx-master="<master-key>" data-pptx-master-name="<master-name>"
data-pptx-layout="<layout-key>" data-pptx-layout-name="<layout-name>">
<rect id="master-bg" data-pptx-layer="master" …/> <!-- one atomic Master object -->
<text id="master-footer" data-pptx-layer="master" …>…</text> <!-- no Master/Layout g -->
<path id="layout-rule" data-pptx-layer="layout" …/> <!-- one atomic Layout object -->
<g id="title-slot" data-pptx-placeholder="title"
data-pptx-bounds="60 36 1160 64">
<text id="title-carrier" data-pptx-carrier="true" …>…</text>
</g>
<g id="body-slot" data-pptx-placeholder="body"
data-pptx-idx="1"
data-pptx-bounds="60 120 470 500">
<text id="body-carrier" data-pptx-carrier="true" …>…</text>
</g>
<g id="picture-slot" data-pptx-placeholder="picture"
data-pptx-idx="2"
data-pptx-bounds="570 120 650 500">
<image id="picture-carrier" data-pptx-carrier="true" …/>
</g>
<g id="content-block-1" data-pptx-bounds="60 120 470 500">…</g> <!-- 38 content groups -->
<g id="content-block-2" data-pptx-bounds="570 120 650 500">…</g>
</svg>
```
On structured template pages, Master/Layout atoms and slot groups are direct root children and precede ordinary content groups. Structural metadata nested inside an ordinary content group fails export. Flat pages use ordinary top-level semantic groups only.
@@ -0,0 +1,25 @@
> Load after [`executor-image.md`](./executor-image.md).
# Executor Web-image Attribution Branch
Conditional Executor authority for inline attribution on web-sourced images.
**Trigger**: load when at least one placed image has `Status: Sourced`.
## 1. Inline Attribution for Sourced Images
Whenever the slide uses an image with `Status: Sourced`, look up the corresponding entry in `project/images/image_sources.json` and act on `license_tier`:
| `license_tier` | Action on this slide |
|---|---|
| `no-attribution` | Embed the `<image>` element only. **No credit element needed.** |
| `attribution-required` | Embed the `<image>` element **plus** a small inline `<text>` credit element per the visual spec in [image-searcher.md §7](./image-searcher.md). |
| `manual` | Embed the `<image>` element only. **No credit element** — a user-supplied `--from-url` replacement; verifying usage rights / any required credit is the user's responsibility. |
The credit text is **not** rendered by post-processing or export — it must be present in the SVG you produce. The shape of the credit element (size, position, color, multi-image source line, hero gradient overlay) is specified in [image-searcher.md §7](./image-searcher.md). Do not invent a different style.
Use `attribution_text` from the manifest entry as the **starting point**, then compress for the small-text constraint (drop URL, drop filename, keep "via Provider / License"). For CC0/PD images that landed in the `attribution-required` tier only because of upstream metadata quirks (rare), credits are still safe to render.
`svg_quality_checker.py` treats missing CC BY / CC BY-SA inline attribution as an **error**. Fix the offending SVG before post-processing.
**The manifest is the single source of truth for credits.** Do not duplicate license info into speaker notes or any other artifact.
@@ -28,7 +28,7 @@ Defined in `design_spec.md §VIII`. Status enum: see [`svg-image-embedding.md`](
| formula_001.png | 736x168 | Block equation on P03 | Latex Formula | `formula` | Rendered | `E = mc^2` |
| spot_team.png | TBD after slicing | Team spot illustration | Illustration | `slice` | Pending | From `spot_sheet.png` cell 1,1 |
**Required per non-skipped row**: `Acquire Via`, `Status`, `Reference`.
**Required per non-skipped row**: `Acquire Via` and `Status`. `Reference` is required for every `web` / `slice` row and every newly authored `ai` row. An existing `ai` row whose `Reference` is omitted or blank may continue only through the declared inference in [`image-generator.md`](./image-generator.md) §8; no other path may infer it.
---
@@ -99,7 +99,7 @@ License / attribution data lives **only** in `project/images/image_sources.json`
- SVG `<title>` / `<desc>` (stripped by `svg_to_pptx.py`)
- A separate "Image Credits" appendix slide (lost on single-page sharing)
Executor reads the manifest per slide and renders inline credits when needed — see [`executor-base.md`](./executor-base.md) §6.1 and [`image-searcher.md`](./image-searcher.md) §7.
Executor reads the manifest per slide and renders inline credits when needed — see [`executor-web-image.md`](./executor-web-image.md) §1 and [`image-searcher.md`](./image-searcher.md) §7.
---
@@ -28,7 +28,7 @@ AI images exist to serve the deck's communication goal. Pick whatever combinatio
**Hard rule — only what's actually hard**:
- Same `deck_rendering` + same `deck_palette` for every image in the deck
- Same `deck_rendering` + same locked deck color roles for every image in the deck
- HEX codes and color names are rendering guidance — never visible text in the image
- Long body copy / data points / bulleted lists / long quotes stay in SVG (improving them later means regenerating the image, which is expensive)
- **In-image text is only for words that will not need editing later** — visual keywords, decorative lettering, mood words. Editable text (titles that may be reworded, subtitles, dates, authors, captions, body) belongs in SVG. Changing one in-image word costs an image regeneration; one SVG word costs a keystroke.
@@ -38,33 +38,31 @@ Everything else is the AI's judgment per page. No mandated padding, no type-lock
---
## 2. Three Dimensions
## 2. Style and Composition Inputs
Every AI image is described by three orthogonal dimensions. Lock them in this order: **Rendering** (deck-wide) → **Palette** (deck-wide) → **Type** (per image).
Every AI image uses one deck-wide rendering, the deck's already-locked color roles, and a per-image type / composition. Only rendering is a separate image-direction decision.
| Dimension | Decides | When fixed |
|---|---|---|
| **Rendering** | Visual style family (vector / sketch-notes / 3d-isometric / corporate-photo / …) | Once per deck — every AI image in the deck shares one rendering |
| **Palette** | How the deck's HEX colors are *used* (proportion + role + temperament). HEX values come from `design_spec.colors`, not from the palette | Once per deck |
| **Deck colors** | The exact background / primary / accent / secondary-accent / text roles from `spec_lock.md colors`; these are consumed directly, not reconfirmed | Already locked in Stage 2 |
| **Type** | What the image's internal composition skeleton looks like — geometric layout of a local infographic block (infographic / flowchart / framework / matrix / cycle / funnel / pyramid / comparison / timeline / map / scene). Only applies to `page_role: local`; for `page_role: hero_page`, describe composition with §4.1 primitives instead of picking a type. | Per image |
> **What rendering vs palette means**: rendering is *how the image is drawn* (line quality, texture, depth). Palette is *how colors are distributed and behave* (which color dominates, which is accent, what proportion). The HEX values come from Strategist; palette is the **usage contract** for those HEX values.
> Rendering decides *how the image is drawn* (line quality, texture, depth). Color instructions come from the deck roles: background / secondary background usually dominate, primary carries main forms, and accents stay scarce. Adjust those proportions to the page role, but never invent or substitute HEX.
### 2.1 Where to find each dimension
| Reference | Loaded |
|---|---|
| [`image-renderings/_index.md`](./image-renderings/_index.md) — rendering catalog + auto-selection table | Always (Step 1 below) |
| [`image-palettes/_index.md`](./image-palettes/_index.md) — palette catalog + auto-selection table | Always (Step 1 below) |
| [`image-type-templates/_index.md`](./image-type-templates/_index.md) — type catalog + auto-selection table | Always (Step 1 below) |
| `image-renderings/<chosen>.md` | After Step 2 picks the rendering — only the chosen one |
| `image-palettes/<chosen>.md` | After Step 2 picks the palette — only the chosen one |
| `image-type-templates/<chosen>.md` | After Step 3 picks the type per image — only the types actually used |
**Hard rule — on-demand loading**:
- Read the three `_index.md` files once at role entry.
- After locking dimensions, read **only** the specific rendering / palette / type files you selected.
- Read the rendering and type `_index.md` files once at role entry.
- After locking inputs, read **only** the specific rendering / type files selected.
- **Never** glob-read an entire subdirectory (`image-renderings/*.md` is forbidden). Token cost balloons and the AI loses focus.
---
@@ -73,56 +71,62 @@ Every AI image is described by three orthogonal dimensions. Lock them in this or
### Step 1 — Load the dimension indices
Read all three index files. They are short (~50 lines each) and contain auto-selection tables that let you map `design_spec` signals → dimension values without reading every detail file.
Read the two index files that own user-visible image direction and per-image composition.
```
read_file references/image-renderings/_index.md
read_file references/image-palettes/_index.md
read_file references/image-type-templates/_index.md
```
### Step 2 — Resolve deck-wide rendering + palette
### Step 2 — Resolve deck-wide rendering + deck colors
**Primary path — Strategist already locked these in `spec_lock.md colors`**:
**Primary path — Strategist already locked rendering and ordinary deck colors in `spec_lock.md colors`**:
```
image_rendering: vector-illustration
image_palette: cool-corporate
background: #F8F9FA
primary: #1E3A5F
accent: #D4AF37
```
If both fields are present, use them directly — Strategist made the decision in h.5 with full d-e-f-g-h linkage context. Do NOT re-decide.
Use them directly. Do not create another image-color choice and do not change HEX to suit a rendering.
**Hard rule — `custom` escape hatch**: when either value is the literal string `custom`, do NOT `read_file` the preset library for that dimension. Read the sibling `*_behavior` line from `spec_lock.md colors` and splice that prose into the prompt in place of the preset's fewshot snippet. The behavior prose owns the style paragraph (for `custom` rendering) or the proportion/role rules (for `custom` palette). See [`image-renderings/_index.md`](./image-renderings/_index.md) §1.5 / [`image-palettes/_index.md`](./image-palettes/_index.md) §2 for the invocation rules.
**Hard rule — `custom` escape hatch**: when `image_rendering` is `custom`, do not read a preset rendering file. Splice `image_rendering_behavior` into the prompt. The deck color-role rows remain authoritative.
**Fallback path — when `spec_lock.md` lacks both fields** (legacy decks or pipelines that skipped h.5):
**Declared-inference fallback — when an existing `spec_lock.md` omits the `image_rendering` key** (see [`failure-recovery.md`](../workflows/governance/failure-recovery.md) §2):
This fallback covers a missing key only. An empty or invalid value stops for lock repair. If `spec_lock.md` itself is absent, stop at [`generate-pptx.md`](../workflows/generate-pptx.md) Step 5 before prompt assembly or image generation; do not use `design_spec.md` as a substitute.
| Signal | Maps to |
|---|---|
| `design_spec.md d. Style` mode + descriptor | Rendering (consult renderings `_index.md` auto-selection table) |
| `design_spec.md e. Color Scheme` (HEX) + content vibe | Palette (consult palettes `_index.md` auto-selection table) |
| `design_spec.md f. Icon library` | Sanity check: chosen rendering should be compatible with the icon library's visual weight |
| Existing `spec_lock.md colors` rows | Deck color-role source; never replace them from `design_spec.md` |
| Existing `spec_lock.md icons.library` | Sanity check: chosen rendering should be compatible with the icon library's visual weight |
If the auto-selection table surfaces multiple candidates, pick the first; do not present a choice to the user.
If rendering inference surfaces multiple candidates, pick the first; do not present another choice after confirmation.
> **Tell the user**: when falling back, print one line "spec_lock.md missing `image_rendering`/`image_palette` — inferring `<X>` / `<Y>` from design_spec. For optimal deck consistency, lock these in Strategist h.5." Then proceed.
If the table returns `custom`, stop and repair the lock: authoring `image_rendering_behavior` is a planning decision this fallback cannot make, and the deck's SVG style prose is not an image-rendering description.
Then `read_file` the **single resolved** rendering file and the **single resolved** palette file. These two files give you:
> **Tell the user**: when falling back, print one line "spec_lock.md has no `image_rendering`—inferring `<X>` from design_spec; image colors still use the locked deck roles." Then proceed.
Then read the **single resolved** rendering file. It gives you:
- The 80-120 word style paragraph (rendering)
- The proportion / role / temperament rules for the deck's three HEX values (palette)
- Two ready-to-paste prompt snippets per file (fewshot)
- Two ready-to-paste rendering snippets (fewshot)
Derive color behavior directly from the available roles: background / secondary background carry roughly 5570% of the image field, primary carries main forms, and accent / secondary accent together usually stay below 10%. A rendering may justify a different balance, but all colors still come from the lock and decorative text colors must remain readable.
### Step 3 — Per-image type + assembly
For each `Acquire Via: ai` row in `design_spec.md §VIII`:
1. **Determine type** — only when `page_role: local` (the image sits as a region block on an SVG page). Match the row's `Purpose` against the `_index.md` auto-selection table (methodology visualization → `framework`; process steps → `flowchart`; SWOT/Eisenhower → `matrix`; PDCA / flywheel → `cycle`; etc.). `Purpose` is authoritative for picking among the 11 internal-composition types. **When `page_role: hero_page`, skip type selection** and describe composition directly using §4.1 primitives (single-subject / portrait / typographic / atmospheric).
2. **Determine `text_policy`** — Strategist's value wins when set. Otherwise pick `none` or `embedded` based on whether in-image text serves the page. Long body / data / lists stay in SVG.
3. **Determine `page_role`** — Strategist's value wins when set. Otherwise pick `local` or `hero_page` based on whether the image carries the page or sits inside one.
1. **Determine `page_role`** — Strategist's explicit value wins; a blank or omitted value resolves to `local`. `hero_page` must be explicit.
2. **Determine `text_policy`** — Strategist's value wins when set. **Declared-inference fallback for a blank or omitted value**: pick `none` or `embedded` from the row's `Purpose`, `Reference`, and page intent based on whether in-image text serves the page. Long body / data / lists stay in SVG.
3. **Determine type** — only when the resolved `page_role` is `local` (the image sits as a region block on an SVG page). Match the row's `Purpose` against the `_index.md` auto-selection table (methodology visualization → `framework`; process steps → `flowchart`; SWOT/Eisenhower → `matrix`; PDCA / flywheel → `cycle`; etc.). `Purpose` is authoritative for picking among the 11 internal-composition types. **When the resolved `page_role` is `hero_page`, skip type selection** and describe composition directly using §4.1 primitives (single-subject / portrait / typographic / atmospheric).
4. `read_file references/image-type-templates/<type>.md` (only if not already read — types are commonly reused across images in one deck)
5. **Assemble the prompt** by combining:
- The rendering's style paragraph (from Step 2)
- The palette's proportion + role rules applied to the deck's HEX values (from Step 2)
- Color-role instructions derived directly from the locked deck HEX values (from Step 2)
- The type's structural layout (from Step 3)
- The image's specific `Reference` intent (from `design_spec.md §VIII`)
- The container sizing guidance from the type file (so the model knows it's painting a local block, not a full canvas)
@@ -142,7 +146,7 @@ Every assembled prompt follows this paragraph structure. **Write prose, not tag
```
[Rendering style paragraph — 80-120 words from the chosen rendering file].
[Palette behavior — apply the chosen palette's proportion + role rules to the deck's HEX values, e.g. "primary #1E3A5F dominates as the main shape, secondary #F8F9FA provides 60% breathing space, accent #D4AF37 appears in one or two emphasis points only"].
[Deck color behavior — apply the locked color roles directly, e.g. "secondary background #F8F9FA provides 60% breathing space, primary #1E3A5F carries main forms, accent #D4AF37 appears in one or two emphasis points only"].
[Type-specific composition — from the chosen type file, e.g. "central hub node with four radiating satellite nodes connected by clean lines"].
[Image-specific subject — translated from the row's Reference intent into concrete visual nouns].
[Container note — "composed as a {W}x{H}px image for {page_role} use"; add composition cues only when the page actually needs them. SVG-overlay-reservation cues ("leave the lower band calm — SVG title overlays it", "keep the right third calmer for SVG text") are valid **only** when `page_role: hero_page` (SVG sits on top of the image). For `page_role: local`, the image sits inside a region block and the SVG layer never overlays its interior — never reserve overlay space in a local prompt].
@@ -242,7 +246,7 @@ Example opening for a triptych hero:
### 4.3 Illustration sheets — one generation, many spot elements
When a deck wants several small **spot illustrations** scattered as decorative accessories across pages (the illustration counterpart to icons), do **not** generate them one image per slot — that multiplies generation cost and lets the style drift between calls. Generate **one sheet** that lays out all the elements in a grid, then slice it. One call buys a set of elements with an identical style, palette, and line qualitythe same cross-page consistency the deck-wide `deck_rendering` / `deck_palette` lock exists to protect.
When a deck wants several small **spot illustrations** scattered as decorative accessories across pages, do **not** generate them one image per slot. Generate one sheet and slice it. One call buys identical rendering, deck-color treatment, and line qualitythe same consistency protected by `deck_rendering` + `color_scheme`.
**When to use**: the §VIII image resource plan needs ≥3 small spot illustrations from the same family across the deck. For a single hero/local image, stay with the normal one-row-per-image flow (§4.1). Use sheets only where decorative illustration genuinely lifts the page; an unused element costs nothing, but a deck papered in decoration reads cheap.
@@ -254,7 +258,7 @@ When a deck wants several small **spot illustrations** scattered as decorative a
- Lay the elements out in an explicit **R×C grid, evenly spaced with clear gutters**, each element **centered in its own cell** and isolated (no element bleeds into a neighbor).
- State the intended cell shape in the prompt: compact square object, tall portrait element, or wide landscape vignette. Do not let the model shrink every subject into a centered square sticker.
- One **flat single-color background** across the whole sheet, set to the deck's background/secondary HEX — this is what lets the slicer key it out cleanly and lets the cut element sit on the slide without a visible box.
- Shared `deck_rendering` + `deck_palette` as always. NO text, labels, or numbers anywhere (§5.1, §5.3).
- Shared `deck_rendering` + `color_scheme` as always. NO text, labels, or numbers anywhere (§5.1, §5.3).
**Cell geometry is designed, not assumed.** `slice_images.py --grid RxC` cuts rows first and columns second. The cell ratio is:
@@ -270,9 +274,9 @@ Use that deliberately. On a wide sheet (`16:9`, `21:9`, `4:1`, `8:1`), `1xN` mak
| Tall side accents / upright objects | wide or square sheet | `1xN`, or any `MxN` whose cells are portrait |
| Wide banners / horizontal vignettes | wide sheet | `Nx1`, or any `MxN` whose cells are landscape |
If one deck needs mixed shapes, create separate sheets per shape family unless one carefully designed grid gives every element enough room in its own cell. Keep the visual family consistent through the same `deck_rendering` and `deck_palette`, not by forcing all cells into one square sheet.
If one deck needs mixed shapes, create separate sheets per shape family unless one carefully designed grid gives every element enough room. Keep the visual family consistent through the same `deck_rendering` and `color_scheme`, not by forcing all cells into one square sheet.
**Resource contract — the sheet and its elements are different row kinds.** A sliced element can only be placed if it exists as a resource the Executor is allowed to reference (`spec_lock.md images`). So §VIII carries two row kinds (full rules: [`design_spec_reference.md`](../templates/design_spec_reference.md) §VIII):
**Resource contract — the sheet and its elements are different row kinds.** A sliced element can only be placed if it exists as a resource the Executor is allowed to reference (`spec_lock.md images`). So §VIII carries two row kinds (planning authority: [`strategist-image.md`](./strategist-image.md)):
- **Sheet row**`Acquire Via: ai`, `Type: Illustration Sheet`, the intent prompt, named as the slice source with its intended cell shape and placement purpose (`Reference: landscape footer-vignette spot set`). It is generated in Step 5 but **never placed on a slide** — keep it **out of** `spec_lock.md images`. Image_Generator resolves the exact `aspect_ratio`, grid, and slice command from this intent.
- **Element rows** — one per used element, `Acquire Via: slice`, filename matching a `--names` output, `Reference` naming the parent sheet + cell/element. These **are** placed — list every one in `spec_lock.md images`, usually with ` | no-crop` (a tight-trimmed transparent spot should be fit, not cover-cropped). Their dimensions are filled in after slicing (Step 5 re-runs `analyze_images.py`). **Set each element row's Layout pattern from the decorative-cutout family, never a boxed container** — see Placement below.
@@ -292,9 +296,9 @@ python3 scripts/slice_images.py <project>/images/illus_sheet.png --grid 2x3 \
2. **Clean grid, or it cuts ugly.** The model will not place every element perfectly; force a clear grid with gutters, and generate **a few sheets** (re-roll the same prompt) to pick the cleanest-laid-out one before slicing. State the exact row/column structure and cell shape so the model does not invent a square matrix. `--trim` absorbs the rest.
3. **Generate only as large as needed.** Each cell is a fraction of the sheet. Pick the smallest sheet size that keeps each sliced cell at least **1.5-2x** the intended display size. `1K` is usually enough for small 80-160px decorative spots; use `2K` for medium 180-320px placements; reserve `4K` for large, cropped, or potentially enlarged elements.
**Placement — these are decorative accessories, not boxed pictures.** A transparent spot wasted in a centered rectangle looks cheaper than no spot at all. Each element row's Layout pattern comes from the decorative-cutout family in [`image-layout-patterns.md`](./image-layout-patterns.md): `#63` sticker/cutout, `#4` bleed off the canvas edge, `#58` corner fragment, `#66` fade into the background, `#69` slight editorial rotation, `#49` asymmetric cluster. Push spots to the margins, let them run off-edge or sit behind/beside text, vary size and angle across pages, and overlap the content rather than reserving a tidy tile for them. Anchor most pages on one primary element and let the rest stay small ([primary-per-page](./strategist.md) §h) — scattered same-weight tiles are exactly the generic look to avoid.
**Placement — these are decorative accessories, not boxed pictures.** A transparent spot wasted in a centered rectangle looks cheaper than no spot at all. Each element row's Layout pattern comes from the decorative-cutout family in [`image-layout-patterns.md`](./image-layout-patterns.md): `#63` sticker/cutout, `#4` bleed off the canvas edge, `#58` corner fragment, `#66` fade into the background, `#69` slight editorial rotation, `#49` asymmetric cluster. Push spots to the margins, let them run off-edge or sit behind/beside text, vary size and angle across pages, and overlap the content rather than reserving a tidy tile for them. Anchor most pages on one primary element and let the rest stay small ([primary-per-page](./strategist-image.md)) — scattered same-weight tiles are exactly the generic look to avoid.
**Through-line — one family, many roles.** A spot sheet pays off more when the same motif family also drives the deck's cover and section dividers, so the deck reads as one designed system rather than a hero plus unrelated doodles. Because the slicer cuts a **uniform grid**, a large cover / divider anchor is **not** a giant cell in the spot sheet — generate it as its own `page_role: hero_page` image (§4.1 primitives) that shares this sheet's `deck_rendering`, `deck_palette`, and subject world. In §VIII the hero_page anchor row(s) and the `slice` spot rows then belong to one visual family (name the shared subject world in each `Reference`), differing only in scale and role. Plan this only when the deck leans into illustration never a per-section quota; the planning rule lives in [strategist.md](./strategist.md) (deck illustration motif).
**Through-line — one family, many roles.** A spot sheet pays off more when the same motif family also drives the cover and section dividers. A large cover / divider anchor is not a giant sheet cell—generate it as its own `hero_page` image sharing the sheet's `deck_rendering`, `color_scheme`, and subject world. Plan this only when the deck leans into illustration, never as a quota.
---
@@ -369,7 +373,7 @@ The table below is **a reference for the one case where you want the in-image le
- Decorative / background lettering, posters, large mood words → describe the artistic treatment freely
- Cover hero title that wants its own visual identity (blackletter, retro chrome, art-deco display, brushed script) → describe freely
- Sketch-notes / ink-notes / hand-drawn renderings where the lettering is part of the rendering itself → describe freely
- Any case where rendering + palette already imply a font character (e.g. `vintage-poster` rendering implies period display lettering) → trust the rendering, no need to echo SVG body
- Any case where rendering already implies a font character (e.g. `vintage-poster` implies period display lettering) → trust the rendering, no need to echo SVG body
**When to use the table**: a designed title (cover main title, chapter heading) on a deck whose visual identity is grounded in the SVG body typography, and where a surprise font choice would feel out of place.
@@ -403,11 +407,13 @@ Write `project/images/image_prompts.json` with this shape:
"project": "{project_name}",
"generated_at": "{ISO-8601 date}",
"deck_rendering": "vector-illustration",
"deck_palette": "cool-corporate",
"color_scheme": {
"background": "#FFFFFF",
"secondary_bg": "#F8F9FA",
"primary": "#1E3A5F",
"secondary": "#F8F9FA",
"accent": "#D4AF37"
"accent": "#D4AF37",
"secondary_accent": "#4A7BB5",
"body_text": "#1D2430"
},
"items": [
{
@@ -441,8 +447,7 @@ Write `project/images/image_prompts.json` with this shape:
| Field | Required | Source | Description |
|---|---|---|---|
| `deck_rendering` | yes | Step 2 lock | Single rendering name shared by all items in this deck |
| `deck_palette` | yes | Step 2 lock | Single palette name shared by all items |
| `color_scheme` | yes | `design_spec.md §III` | HEX triplet from Strategist |
| `color_scheme` | yes | `spec_lock.md colors` | Exact deck color roles used by every item; no separate image palette |
| `items[].filename` | yes | `§VIII` resource list | Output filename with extension |
| `items[].type` | conditional | Step 3 per-image (only when `page_role: local`) | One of 11 internal-composition types: `infographic`, `flowchart`, `framework`, `matrix`, `cycle`, `funnel`, `pyramid`, `comparison`, `timeline`, `map`, `scene`. **Omit `type` entirely when `page_role: hero_page`** — the composition comes from §4.1 primitives written directly into the prompt, not from a type file. |
| `items[].page_role` | yes | Step 3 per-image | `local` (default — region block on SVG page) or `hero_page` (image is page's main voice; SVG overlay minimal or empty) |
@@ -457,7 +462,11 @@ Write `project/images/image_prompts.json` with this shape:
> **Back-compat for legacy `type` values**: existing manifests using `background` / `hero` / `portrait` / `typography` (the four removed pseudo-types) remain readable. Read them as: `background``page_role: hero_page` + no type; `hero``page_role: hero_page` + no type (use §4.1 Primitive A in prompt); `portrait``page_role: local` + no type (use §4.1 Primitive B); `typography``page_role: hero_page` + `text_policy: embedded` + no type (use §4.1 Primitive C). New manifests should follow the rule above (omit `type` when `page_role: hero_page`).
>
> Existing manifests without `deck_rendering` / `deck_palette` / `type` / `page_role` / `text_policy` remain valid — older items default to `page_role: local`, `text_policy: none`. Legacy `page_role: full_page` (pre-2026-05-15) is read as `hero_page`.
> **Existing manifest compatibility**:
>
> - **Fixed compatibility defaults**: a missing `page_role` resolves to `local`; a missing `text_policy` resolves to `none`. Emit one aggregate legacy-compatibility warning per manifest.
> - **Declared replay procedure**: an existing manifest may lack `deck_rendering`, or an existing local item may lack `type`, because `items[].prompt` is already assembled. Leave that metadata absent, execute the existing prompt verbatim, and do not reconstruct either value. This exception applies only to replaying an existing manifest; new manifests must satisfy the field table above. A `hero_page` item still omits `type` intentionally.
> - A legacy `deck_palette` field may remain but cannot override `color_scheme`. Read legacy `page_role: full_page` as `hero_page`.
---
@@ -475,13 +484,13 @@ C (AI-generated) supports three implementation modes sharing one `image_prompts.
| `IMAGE_BACKEND` not configured (or Path A fails) AND host has a native image tool | **Path B**: Host-native tool | Agent invokes the host's image capability; outputs land at `project/images/<filename>` |
| **Both Path A and Path B fail/unavailable** | **Offline Manual Mode** | Manifest stays on disk; user generates externally from `items[].prompt` and places files at `project/images/<filename>` |
**Selection logic**the confirmed user choice wins; absent one, fall back to the automatic A → B → C chain:
**Selection logic — declared-procedure fallback when no path is confirmed**: the confirmed user choice wins. When neither channel confirmed a specific path — the effective choice is `auto` (explicitly confirmed or defaulted) or absent — use the automatic A → B → C chain:
0. **Confirmed override (wins)** — honor the confirmed image source. The **chat choice is canonical**; the Confirm UI is only a convenience surface that, when used, records the same choice to `<project>/confirm_ui/result.json` as `image_ai_path` (so there is no `result.json` on the chat path — read the choice from the conversation). From either channel, if the choice is set and not `auto`, honor it directly, **even when it contradicts `IMAGE_BACKEND`**:
0. **Confirmed override (wins)** — honor the confirmed image source **from whichever channel the confirmation actually happened in**: when the Confirm UI was used, it records the choice to `<project>/confirm_ui/result.json` as `image_ai_path`; a chat confirmation is equally binding and leaves no `result.json` — read the choice from the conversation. From either channel, if the choice is set and not `auto`, honor it directly, **even when it contradicts `IMAGE_BACKEND`**:
- `api`**Path A** (`image_gen.py --manifest`).
- `host-native`**Path B** (host's native image tool) — skip A and do **not** run `image_gen.py --manifest`, *even if `IMAGE_BACKEND` is configured*.
- `manual`**Offline Manual** (write prompts, render the Markdown sidecar, hand off; do **not** run `image_gen.py --manifest`).
("use Codex's image tool" / "走接口生成" in chat = `host-native` / `api`.) If the chosen path turns out unavailable (e.g. `host-native` but the host has no image tool), fall through along the chain below from that point. Only when no source named a path (chat silent, and `image_ai_path` `auto` / absent) does the automatic chain decide.
("use Codex's image tool" / "走接口生成" in chat = `host-native` / `api`.) If an explicitly chosen path is unavailable or still fails after its retry, mark the affected row `Needs-Manual`; do not switch to another automated provider. Only when no channel named a specific path (the effective value is `auto` — explicitly confirmed or defaulted — or absent) does the automatic chain decide.
1. **Try Path A** — if `IMAGE_BACKEND` is configured (env or `.env`), run `image_gen.py --manifest`. If it fails twice in a row, fall to Path B.
2. **Try Path B** — if `IMAGE_BACKEND` was not configured (A skipped), or A failed, and the host has a native image tool (Codex / Antigravity / Claude Code / similar), the agent invokes the host's image capability directly.
3. **Fall to C (Offline Manual)** — if B is also unavailable (no host-native tool) or fails, write prompts to `images/image_prompts.json` and hand off to the user.
@@ -557,19 +566,19 @@ Triggered automatically when `IMAGE_BACKEND` is not configured (or Path A fails)
- Do **not** run `image_gen.py --manifest` in Path B. That command is Path A and may use configured API/proxy backends even when the user confirmed host-native.
- Still run `python3 scripts/image_gen.py --render-md project/images/image_prompts.json` so the human-readable sidecar exists without touching any backend.
- **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 (~34), 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/<filename-from-resource-list>`. Match the Image Resource List dimensions when the host supports arbitrary sizes. Hosts with **fixed native resolutions** (common — e.g. ~1672x941 landscape / ~1086x1448 portrait) generate at the closest native size and backfill the actual pixels into the resource list `Dimensions` column — same convention as formula rows ("actual dimensions from formula manifest") and slice rows ("dimensions filled after slicing"). Do **not** upscale the file to fake the requested size (interpolation adds no detail); minor display-side upscaling (up to ~1.3x in practice) surfaces as a quality-checker warning — acknowledge and release per the warning policy.
- Outputs **must** land at `project/images/<filename-from-resource-list>`. Match the Image Resource List dimensions when the host supports arbitrary sizes. Hosts with **fixed native resolutions** (common — e.g. ~1672x941 landscape / ~1086x1448 portrait) generate at the closest native size and backfill the actual pixels into the resource list `Dimensions` column — same convention as formula rows ("actual dimensions from formula manifest") and slice rows ("dimensions filled after slicing"). Do **not** upscale the file to fake the requested size (interpolation adds no detail); minor display-side upscaling (up to ~1.3x in practice) may surface as a non-blocking quality-checker warning and requires no acknowledgement.
- 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)
**Trigger**: Both Path A and Path B fail or are unavailable.
**Trigger**: the automatic chain reaches this point after both Path A and Path B fail or are unavailable, the user explicitly confirmed `manual`, or an explicitly confirmed automated path still fails after its own retry.
**Workflow** (no user prompting; system enters this mode automatically):
1. Verify `images/image_prompts.json` was written
2. Set `status: "Needs-Manual"` on every affected item per [`image-base.md`](./image-base.md) §6
3. Continue to Step 6 — SVG references `images/<filename>` optimistically; Step 7 entry verifies presence
3. Continue to Step 6 — Executor draws a dashed placeholder for each `Needs-Manual` row; the Step 7 image readiness gate verifies the supplied files and swaps them in
4. Print one consolidated handoff to the user:
- Filenames awaiting manual generation
- Pointer to `images/image_prompts.md` (paste-ready `### Image N:` block per item) or `image_prompts.json` (`items[].prompt`)
@@ -582,13 +591,15 @@ Triggered automatically when `IMAGE_BACKEND` is not configured (or Path A fails)
#### AI-specific Failure Handling (extends image-base.md §6)
If Path A's backend fails twice in a row:
When the path is `auto` and Path A's backend fails twice in a row:
1. Do not halt. Automatically attempt to fall back to **Path B (Host-Native Tool)**.
2. If Path B also fails or is unavailable, mark the row `Needs-Manual`.
3. Report to user: filename, prompt used, error message.
4. Fall through to **Offline Manual Mode** above.
When `api` or `host-native` was explicitly confirmed, failure or unavailability does not authorize an automated provider switch. Retry the confirmed path once; if it still fails, mark the row `Needs-Manual`, report the filename/prompt/error, and use the manual handoff above.
> If the alternate platform watermarks outputs (e.g. Gemini web), the repository includes `scripts/gemini_watermark_remover.py`.
#### Guardrails (All Modes)
@@ -596,16 +607,16 @@ If Path A's backend fails twice in a row:
**Hard rule**:
- Do not claim an image is generated without an actual file at the expected path
- `Needs-Manual` is set after a failed attempt OR on entering Offline Manual Mode — not as a way to skip work that automation could have done
- Status transitions are evidence-driven: `Pending``Generated` (file exists) or `Pending``Needs-Manual` (no automation, or attempt failed once)
- `Needs-Manual` is set only when `manual` was confirmed or the selected automated recovery path was attempted and failed — not as a way to skip work that automation could have done
- Status transitions are evidence-driven: a file at the expected path permits `Generated`; an exhausted recovery path permits `Needs-Manual`
---
## 8. Common Issues & Variant Workflow
### Reference field is blank — quick examples
### Reference field is omitted or blank — declared-inference fallback for existing AI rows
When the Resource List row has no `Reference`, infer a reasonable image from `Purpose`. Examples (not prescriptions):
When an existing AI Resource List row omits `Reference` or contains a blank `Reference`, infer a reasonable image from its non-empty `Purpose`. If `Purpose` is also omitted or blank, stop and repair the row. Examples (not prescriptions):
| Purpose | A reasonable starting point |
|---------|-----------------------------|
@@ -627,7 +638,7 @@ Diagnose the failure category, adjust the **one specific dimension** responsible
|---|---|---|
| Image looks generic, model-average | Tag-soup prompt | Rewrite as one coherent paragraph per §4 |
| Wrong style family (looks photorealistic when flat was intended) | Rendering mismatch or rendering paragraph diluted | Reaffirm chosen rendering's style paragraph at the top of the prompt |
| Colors don't match deck | HEX not echoed in prompt, or palette proportion rule omitted | Repeat HEX values 2-3 times in the prompt; restate palette proportion rule |
| Colors don't match deck | Locked role HEX not echoed, or their role / proportion instructions were diluted | Repeat the locked HEX values 2-3 times; restate which deck role owns the field, main forms, and sparse accents |
| Hex code or color name visible as text in image | Missing §5.1 closing sentence | Append the §5.1 hard rule verbatim |
| Garbled letters in supposedly text-free image | `text_policy: none` rule too weak | Strengthen with explicit list: "no letters, no numbers, no words, no signs, no labels, no captions, no watermarks" |
| SVG text overlay clashes with busy image area | Page design needs negative space the prompt didn't request | Add a composition cue like "leave the {center / left third / lower band} relatively calm for text overlay" — only when the page actually overlays text on top of the image |
@@ -646,9 +657,9 @@ Diagnose the failure category, adjust the **one specific dimension** responsible
- Generating prompts for `web` rows — those go through [`image-searcher.md`](./image-searcher.md)
- Brand names or HEX codes inside the subject description (degrades output)
- Mixing renderings or palettes across images in the same deck
- Mixing renderings or inventing image-only colors across images in the same deck
- Tag-soup prompts (keyword lists separated by commas without a coherent visual scene)
- Globbing `image-renderings/*.md` or any subdirectory — read only the chosen file
- Placing an image without updating its `image_prompts.json` `status` and the resource list status
- Switching rendering or palette for a single image`hero_page` is not an exception to deck-wide coherence
- Switching rendering or deck-color roles for a single image`hero_page` is not an exception to deck-wide coherence
- Embedding body copy, data points, bullet lists, or long quotes inside an image — those route to SVG
@@ -148,7 +148,7 @@ Stack any of these freely on top of a Primary structure. Multiple Modifiers per
24. **Custom path crop (blob, arrow, leaf, silhouette)**`<clipPath><path d="…"/></clipPath>`; allows any curved or organic shape. PowerPoint export translates this to `custGeom` and survives roundtrip.
25. **Layered paper-cut stack** — clip each image layer under the image-only contract in [`shared-standards.md`](shared-standards.md) §1.2; draw vector layers directly in their final geometry. A small conditional shadow on each layer can create physical separation.
25. **Layered paper-cut stack** — clip each image layer under the image-only contract in [`shared-standards-core.md`](./shared-standards-core.md) §1.2; draw vector layers directly in their final geometry. A small conditional shadow on each layer can create physical separation.
26. **Triptych baked into a single wide image** — one wide `<image width=1160 height=334>` whose internal composition already contains 23 scenes. Generate the triptych as one image (not three separate calls) when scene-to-scene consistency matters — the model preserves character identity, lighting continuity, and color grading far more reliably when panels are produced together.
@@ -174,9 +174,9 @@ Stack any of these freely on top of a Primary structure. Multiple Modifiers per
35. **Duotone treatment** — two-color mapping of a photograph (e.g. deep navy shadows + warm cream highlights). Bake it into the source image; the native PPT route does not support a runtime duotone filter chain.
36. **Drop shadow under image panel**`<filter><feDropShadow dx dy stdDeviation flood-color flood-opacity/></filter>` applied to the image's container `<rect>` (or to the `<image>` itself). Standard depth lift.
36. **Drop shadow under image panel**`<filter><feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000000" flood-opacity="0.10"/></filter>` applied to the image panel's backing `<rect>`. Standard depth lift; filters do not apply directly to `<image>` under the project contract.
37. **Inner / outer glow on overlay shape**`<filter><feGaussianBlur/><feMerge/></filter>` on a shape, or simply a slightly larger blurred `<rect>` underneath the target.
37. **Inner / outer glow on overlay shape**`<filter><feGaussianBlur stdDeviation="6"/><feMerge/></filter>` on a shape, or simply a slightly larger blurred `<rect>` underneath the target.
## Image as Texture / Atmosphere
@@ -204,7 +204,7 @@ Stack any of these freely on top of a Primary structure. Multiple Modifiers per
67. **Image with knock-out / cut-out shape** — overlay a shape filled with the background color or another image, creating the impression of a hole punched through the underlying image.
68. **Text-as-mask over image** — letterforms revealing image through them. Under the canonical SVG compatibility boundary in [`shared-standards.md`](shared-standards.md), realize this pattern as a pre-rendered image rather than a runtime effect. Prompt for "large lettering revealing the underlying scene through letterforms" and treat the result as a fixed artistic choice.
68. **Text-as-mask over image** — letterforms revealing image through them. Under the canonical SVG compatibility boundary in [`shared-standards-core.md`](./shared-standards-core.md), realize this pattern as a pre-rendered image rather than a runtime effect. Prompt for "large lettering revealing the underlying scene through letterforms" and treat the result as a fixed artistic choice.
69. **Image rotated at a slight angle for editorial feel**`transform="rotate(angle cx cy)"` on the `<image>` or its container `<g>`; 26 degrees typical. Adds dynamism without breaking layout.
@@ -247,13 +247,13 @@ Combine freely. The "AI-default" failure mode is the opposite: defaulting to bar
**Skip-detection signal** — if every page's `Layout pattern` column resolves to bare #2 / #3 / #5 / #6 with no Modifier ids, the catalog was not consulted. Re-read and reconsider.
**Cross-page through-line (recurring motif).** The patterns above are per-page, but a deck reads as *designed* when one illustration motif family recurs across pagesa cover anchor, section dividers repeating the motif for chapter identity (`#75`), and small `#63` spots from the same family threaded through the body. Keep them one family (shared rendering / palette / subject world), vary scale and placement, and never let the recurrence harden into a per-section quota. Planning lives in [strategist.md](./strategist.md) (deck illustration motif); generation mechanics split by role — hero / divider anchors: [image-generator.md](./image-generator.md) §4.1 primitives; body spot sheets: §4.3.
**Cross-page through-line (recurring motif).** The patterns above are per-page, but a deck reads as *designed* when one illustration motif family recurs across pagesa cover anchor, section dividers repeating the motif (`#75`), and small `#63` spots threaded through the body. Keep one family (shared rendering / locked deck colors / subject world), vary scale and placement, and never turn recurrence into a quota.
## Hard Constraints
- Long body copy, data points, numeric labels, and Chinese text always go in the SVG layer — never baked into the image.
- All project-wide SVG compatibility exceptions and conditional mappings are
owned by [`shared-standards.md`](shared-standards.md). This catalog neither
- Project-wide SVG compatibility rules start at [`shared-standards-core.md`](./shared-standards-core.md),
whose routing table names each conditional owner. This catalog neither
restates nor relaxes that contract; each pattern records only its
scenario-specific rendering choice.
@@ -1,4 +1,4 @@
> See shared-standards.md for common technical constraints.
> See [`shared-standards-core.md`](./shared-standards-core.md) for common technical constraints.
# Image Layout Specification
@@ -6,14 +6,14 @@ Layout rules for pages where the image is placed **side-by-side with body text**
**Core principle (side-by-side)**: compute container layout from the image's original aspect ratio so the image displays completely — no excess whitespace, no cropping.
> **Scope**: this spec applies to *side-by-side* intent only. Other intents (hero / full-bleed, atmosphere / background, accent / inline) use full-bleed placement where ratio alignment is not a constraint and cropping is expected — the ratio→split table below does NOT apply. See `references/strategist.md` §h for intent selection.
> **Scope**: this spec applies to *side-by-side* intent only. Other intents (hero / full-bleed, atmosphere / background, accent / inline) use full-bleed placement where ratio alignment is not a constraint and cropping is expected — the ratio→split table below does NOT apply. See `references/strategist-image.md` for intent selection.
---
## Layout Decision Flow
```
1. Decide narrative intent (hero / atmosphere / side-by-side / accent) — see strategist.md §h
1. Decide narrative intent (hero / atmosphere / side-by-side / accent) — see strategist-image.md
2. If intent = side-by-side: continue below. Otherwise: compose per narrative; this spec does not apply.
3. Get image original dimensions → Calculate ratio (width/height)
4. Select layout type based on ratio
@@ -1,127 +1,36 @@
# Palettes — Index
# Image Palettes Legacy Reference
A **palette** is the deck's **color behavior** — proportion, role, temperament. It does **not** supply HEX values; those come from `design_spec.colors`. The palette tells the model how to use the HEX values: which dominates, which carries accent, what proportion the background occupies, what the overall temperament feels like.
Compatibility tombstone for retired `image_palette` fields and historical palette comparison assets.
> Why this split: SVG renders the HEX precisely from `design_spec`. The AI image must use the **same HEX values** so the image visually belongs in the deck — but the image needs more than a HEX list; it needs a **usage rule**. That's the palette.
## 1. Current Generation Contract
---
**Hard rule**: Current deck generation never presents, selects, authors, or consumes `image_palette` or `image_palette_behavior`.
## 1. Catalog (14 palettes)
Each palette has its own file with: rendering compatibility matrix and a fewshot prompt snippet.
| Palette | Temperament | Best for |
|---|---|---|
| [`cool-corporate`](./cool-corporate.md) | Stable, professional, restrained | Consulting / B2B / finance |
| [`warm-earth`](./warm-earth.md) | Friendly, grounded, human | Brand / lifestyle / education |
| [`tech-neon`](./tech-neon.md) | Energetic, futuristic, high-contrast | AI / SaaS / product launch |
| [`editorial-classic`](./editorial-classic.md) | Refined, magazine, balanced | Journalism / opinion / culture |
| [`macaron`](./macaron.md) | Soft pastel, gentle, approachable | Education / children / onboarding |
| [`mono-ink`](./mono-ink.md) | High-contrast monochrome with sparse accents | Methodology / Before-After / manifesto |
| [`vivid-launch`](./vivid-launch.md) | Bold, saturated, attention-grabbing | Product launch / marketing / event |
| [`dark-cinematic`](./dark-cinematic.md) | Premium, atmospheric, low-light | Premium product / film / entertainment |
| [`duotone`](./duotone.md) | Two-color limited, poster-like | Cultural / cover hero / cinematic |
| [`nature-organic`](./nature-organic.md) | Earthy, natural, wellness | Environment / wellness / outdoor |
| [`jewel-tone`](./jewel-tone.md) | Deep saturated gemstone — emerald/sapphire/ruby + gold | Luxury / fashion / premium product / heritage |
| [`frost-ice`](./frost-ice.md) | Near-white field with pale cool accents | Health / medical / beauty / premium SaaS |
| [`sunset-gradient`](./sunset-gradient.md) | Warm gradient flow (pink → orange → purple) | Lifestyle / creative / travel / event |
| [`earthy-dusty`](./earthy-dusty.md) | Muted desaturated earth tones, Morandi-adjacent | Interior / wellness / mindfulness / slow living |
---
## 2. Escape hatch — `custom`
When no preset temperament matches (brand HEX outside preset ranges, ceremonial / cultural / niche aesthetic), set `image_palette: custom` and supply a one-paragraph `image_palette_behavior`.
**Trigger** — all of:
| Condition | Check against |
| Input | Current behavior |
|---|---|
| No preset temperament fits | `design_spec.e Color Scheme` |
| Brand / template / chat names no preset | truth-precedence inputs |
| Not expressible as "preset X + small HEX swap" | Strategist confirmation chat |
| `spec_lock.md colors` | Sole source of generated-image color roles and exact HEX values |
| `image_rendering` | Controls rendering treatment only; it does not create a second color decision |
| Legacy `image_palette` row | Ignore it; it cannot override the deck color lock |
| No palette row | Expected; do not synthesize a preset or `custom` fallback |
**Hard rule — `palette_behavior` prose**:
**Forbidden — legacy activation**:
| Rule | Value |
- Do not load sibling palette preset files while planning or generating a current deck.
- Do not use the historical auto-selection table, compatibility matrix, or prompt snippets.
- Do not write `image_palette: custom` or `image_palette_behavior`.
---
## 2. Legacy Interpretation and Maintenance
The sibling preset files remain archived in place for diagnosing historical locks and maintaining the legacy palette comparison assets documented by [`README.md`](../ai-image-comparison/README.md). They are not a runtime catalog.
| Legacy row | Historical meaning |
|---|---|
| Length | One paragraph, 2-5 sentences |
| Per-HEX content | role + approximate area share (proportion follows information weight; no fixed % menu) |
| HEX source | Quote `design_spec.colors` values verbatim with backticks; never invent HEX |
| Forbidden | Naming a competing preset ("like macaron but darker") |
| `image_palette: <preset>` | Selects the named sibling preset as the archived color-behavior definition. |
| `image_palette: custom` | Declares that no preset owns the behavior; the required sibling `image_palette_behavior` row is the complete definition. |
| `image_palette_behavior: <prose>` | With `custom`, records a 25 sentence mapping from the lock's HEX roles to intended proportion and temperament. It must not name a competing preset or invent another HEX value. |
```yaml
- image_palette: custom
- image_palette_behavior: "Primary deep aubergine `#4C1D95` anchors the dominant ~35% of canvas; secondary warm cream `#FEF3C7` carries ~55% as breathing field; accent burnished gold `#D4AF37` appears only in 5-10% as small ceremonial accents. Restrained, ceremonial gravitas — no fourth color."
```
A historical `custom` row without a non-empty `image_palette_behavior` is incomplete. Report the missing legacy definition; do not reconstruct it.
> Note: §4's rendering × palette matrix only covers the 14 presets. When `palette: custom`, Strategist owns the compatibility judgment in h.5.
**Hard rule**: `custom` is a tail-case, not a default. See [`strategist.md`](../strategist.md) h.5 for the one-`custom`-per-dimension limit.
---
## 3. Auto-selection table — `design_spec` → palette
Match `design_spec.md d. Style` + `e. Color Scheme` content vibe. First match wins. **No row matches** → use `custom` per §2 rather than force-fitting `cool-corporate`.
| Content vibe / industry | Recommended palette | Alternates |
|---|---|---|
| Consulting / finance / B2B / corporate | `cool-corporate` | `editorial-classic`, `frost-ice` |
| Tech / SaaS / AI | `tech-neon` | `cool-corporate`, `dark-cinematic` |
| Modern SaaS / fintech / health-tech | `frost-ice` | `cool-corporate`, `tech-neon` |
| Health / medical / beauty / skincare | `frost-ice` | `nature-organic`, `earthy-dusty` |
| Education / training / onboarding | `macaron` | `warm-earth` |
| Methodology / Before-After / mindset shift | `mono-ink` | `editorial-classic` |
| Personal / lifestyle / brand story | `warm-earth` | `nature-organic`, `earthy-dusty` |
| Interior / wellness / mindfulness / slow living | `earthy-dusty` | `warm-earth`, `nature-organic` |
| Product launch / marketing / event | `vivid-launch` | `tech-neon`, `sunset-gradient` |
| Creative agency / travel / music / lifestyle | `sunset-gradient` | `vivid-launch`, `warm-earth` |
| Luxury / fashion / jewelry / premium / heritage | `jewel-tone` | `dark-cinematic`, `editorial-classic` |
| Children / storybook | `macaron` | `warm-earth` |
| Premium / entertainment / film | `dark-cinematic` | `jewel-tone`, `duotone` |
| Cultural / media / cover-art | `duotone` | `editorial-classic` |
| Environment / wellness / outdoor | `nature-organic` | `warm-earth`, `earthy-dusty` |
| Finance / journalism / explainer | `editorial-classic` | `cool-corporate` |
| Government / formal | `cool-corporate` | `editorial-classic` |
---
## 4. Rendering × Palette compatibility
Some combinations clash. Use this matrix as a sanity check after auto-selection.
| | cool-corp | warm-earth | tech-neon | editorial | macaron | mono-ink | vivid-launch | dark-cinem | duotone | nature-org | jewel-tone | frost-ice | sunset-grad | earthy-dusty |
|---|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| vector-illustration | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓✓ |
| flat | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
| minimalist-swiss | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ |
| glassmorphism | ✓✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✗ | ✓ | ✓ | ✓✓ | ✓ | ✓ |
| 3d-isometric | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✗ | ✓✓ | ✓✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
| digital-dashboard | ✓✓ | ✗ | ✓✓ | ✓✓ | ✗ | ✓ | ✓ | ✓✓ | ✗ | ✗ | ✗ | ✓✓ | ✗ | ✗ |
| corporate-photo | ✓✓ | ✓✓ | ✓ | ✓✓ | ✗ | ✗ | ✓ | ✓✓ | ✗ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
| blueprint | ✓✓ | ✗ | ✓✓ | ✓ | ✗ | ✓✓ | ✗ | ✓✓ | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| editorial | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ |
| sketch-notes | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✓ | ✗ | ✗ | ✓✓ | ✗ | ✗ | ✗ | ✓ |
| ink-notes | ✓ | ✓ | ✗ | ✓✓ | ✗ | ✓✓ | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | ✗ | ✓ |
| chalkboard | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
| paper-cut | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✗ | ✓ | ✗ | ✓ | ✓✓ | ✗ | ✓ | ✗ | ✓✓ |
| watercolor | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✗ | ✓ | ✓ | ✗ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
| warm-scene | ✓ | ✓✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ |
| screen-print | ✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| vintage-poster | ✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✗ | ✓ | ✓✓ |
| fantasy-animation | ✗ | ✓✓ | ✗ | ✗ | ✓✓ | ✗ | ✓ | ✗ | ✗ | ✓✓ | ✗ | ✗ | ✓ | ✗ |
| pixel-art | ✗ | ✓ | ✓✓ | ✗ | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| nature | ✓ | ✓✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ |
✓✓ recommended | ✓ acceptable | ✗ avoid
---
## 5. How to use
1. After picking rendering, look up your candidate palette in the auto-selection table.
2. Cross-check the compatibility matrix — if `✗`, pick the alternate.
3. `read_file image-palettes/<chosen>.md` and apply its proportion + role rules to the deck's HEX values when assembling prompts. (For `custom`, this step is replaced by the consumption branch in [`image-generator.md`](../image-generator.md) Step 2 — no preset file to read.)
**Lock for the whole deck.**
When that maintenance is explicitly requested, read only the named historical asset or preset. Keep its palette behavior inside the historical fixture; do not copy it into current recommendations, `design_spec.md`, `spec_lock.md`, or generated-image prompts.
@@ -2,7 +2,9 @@
A **rendering** is a visual style family: line quality, texture, depth, material, mood. Lock one rendering per deck — every AI image in the deck shares it.
> **HEX values are not in renderings**. The deck's HEX triplet comes from `design_spec.colors`. Rendering describes *how* the image is drawn; palette describes *how* the HEX values are distributed. See [`image-generator.md`](../image-generator.md) §2.
> **HEX values are not in renderings.** Rendering describes how the image is drawn. The new flow reads exact deck color roles directly from `spec_lock.md colors`; it does not ask for or author a separate image palette. See [`image-generator.md`](../image-generator.md) §2.
> **Deck HEX has hard precedence.** Any color name or sample HEX inside an individual rendering file is illustrative legacy prose and MUST be replaced by the current deck-role values when assembling a prompt. A rendering may change texture, lighting, opacity, and role proportions, but it may not tint, warm-grade, cool-grade, replace, or invent HEX. If its material language requires colors the selected deck roles cannot support, do not offer that rendering in Stage 2.
---
@@ -52,15 +54,7 @@ Each rendering has its own file with: style paragraph, line / texture / depth no
### 1.5 Escape hatch — `custom`
When no preset carries the deck's temperament, set `image_rendering: custom` and supply a one-paragraph `image_rendering_behavior`.
**Trigger** — all of:
| Condition | Check against |
|---|---|
| No preset style fits | `design_spec.d Style` |
| Brand / template / chat names no preset | truth-precedence inputs |
| Not expressible as "preset X + small adjustment" | Strategist confirmation chat |
Whenever proposed image usage includes `ai`, Stage 2 authors one separate, visible custom rendering proposal in addition to the preset cards. It uses `rendering: custom` plus a complete behavior paragraph, remains initially unselected, and enters the confirmed contract only when the user chooses it. A template-backed proposal must honor inherited identity and the confirmed template-application plan.
**Hard rule — `rendering_behavior` prose**:
@@ -75,7 +69,7 @@ When no preset carries the deck's temperament, set `image_rendering: custom` and
- image_rendering_behavior: "Hand-screened poster aesthetic — slightly misregistered halftone overlays, 3 flat ink colors with visible dot pattern at 12% opacity, no gradients, no anti-aliased edges; reads as silkscreen print."
```
**Hard rule**: `custom` is a tail-case, not a default. See [`strategist.md`](../strategist.md) h.5 for the one-`custom`-per-dimension limit.
**Hard rule**: the custom candidate is mandatory when AI images are proposed; selecting `custom` is a tail-case, not the default. See [`strategist-image.md`](../strategist-image.md) for the Stage-2 carrier and downstream lock behavior.
---
@@ -20,11 +20,11 @@ Chalk on a dark green or black board — classroom aesthetic. Used in teaching-f
## 3. Using the deck's HEX values
chalkboard has a strong **fixed-palette tendency**: dark slate background + white chalk + pastel accents.
chalkboard has a strong **material-color tendency**: dark field + light chalk + sparse accents. It never replaces deck colors. Offer it only when the confirmed background / body-text roles can support that contrast.
- Background: dark slate green `#1F3A2E` or near-black `#1A1A1A` (does not use the deck's secondary)
- Primary chalk: off-white `#F5F5F0` (the chalk itself); the deck's primary HEX may appear as a tinted pastel accent
- Accent: the deck's accent HEX rendered as a soft pastel chalk shade
- Board: use the deck's exact `background` or `secondary_bg` role
- Chalk strokes: use the exact `body_text` role; dry texture supplies the chalk effect without changing its HEX
- Accent marks: use exact `accent` / `secondary_accent` values sparingly; do not pastel-tint them
---
@@ -32,4 +32,4 @@ chalkboard has a strong **fixed-palette tendency**: dark slate background + whit
**Snippet A — half-page lesson visual, text_policy: embedded**
> Classroom chalkboard aesthetic. Dark slate green background `#1F3A2E` with visible chalk-dust texture at 20% opacity. Three rounded boxes drawn in soft white chalk strokes arranged in a row, each containing a simple chalk-drawn icon (a tree, a lightbulb, a paper plane). Above the boxes, a hand-lettered chalk title in confident white lettering reads "GROWTH" in English block caps. Hand-drawn chalk arrows connect the boxes left-to-right. Small chalk star and dot decorations sprinkled sparingly. One soft pastel-yellow chalk underline below the title, drawn with deliberate dry-medium quality. Composed as a 600×500 half-page block with 14% inner padding. Chalk text kept to short keywords. Color values are rendering guidance only.
> Classroom chalkboard aesthetic. Use the deck's locked background role as the board, with visible chalk-dust texture at 20% opacity. Three rounded boxes drawn in the locked body-text color as soft chalk strokes sit in a row, each containing a simple chalk-drawn icon (a tree, a lightbulb, a paper plane). Above the boxes, a hand-lettered title reads "GROWTH" in English block caps. Hand-drawn chalk arrows connect the boxes left-to-right. Small chalk star and dot decorations appear sparingly. One underline uses the locked accent color with deliberate dry-medium texture. Composed as a 600×500 half-page block with 14% inner padding. Chalk text stays to short keywords.
@@ -4,7 +4,7 @@ Ghibli/Disney-inspired hand-drawn animation warmth. Soft painterly forms, gentle
## 1. Style paragraph (paste-ready, 110 words)
> Hand-drawn animation style inspired by Ghibli and classic Disney storybook aesthetics. Forms are softly rendered with painterly fills and gentle outlines — never harsh, always warm. Color palette is warm and slightly desaturated, with pastel skies, soft greens, warm earth tones, occasional accents of magical color. Lighting is dreamy and atmospheric — soft window light, golden-hour glow, gentle morning haze. Characters (when present) are stylized in a friendly cartoon manner — large expressive eyes, simplified anatomy, warm body language. Subtle painterly brush texture across the image. Atmospheric perspective creates depth — foreground richer, background softer. Overall feel is dreamy, magical, comforting — children's storybook quality.
> Hand-drawn animation style inspired by Ghibli and classic Disney storybook aesthetics. Forms are softly rendered with painterly fills and gentle outlines — never harsh. Exact colors come only from the deck's locked roles; dreamy warmth comes from soft window light, gentle glow, morning haze, brush texture, and atmospheric perspective rather than invented pastel or earth-tone HEX. Characters (when present) are stylized in a friendly cartoon manner — large expressive eyes, simplified anatomy, warm body language. Foreground forms are richer and the background softer. Overall feel is dreamy, magical, comforting, and storybook-like.
---
@@ -20,11 +20,13 @@ Ghibli/Disney-inspired hand-drawn animation warmth. Soft painterly forms, gentle
## 3. Using the deck's HEX values
fantasy-animation reads HEX values through a **warm storybook filter**:
fantasy-animation uses the exact deck roles with a storybook treatment:
- Primary HEX: dominant warm scene color (sky, foliage, environment)
- Secondary HEX: lighter atmospheric tone (cream, soft sky, morning haze)
- Accent HEX: a small magical pop — perhaps the glow of a window, a flower, a key character element
- Primary HEX: dominant environment or character color, unchanged
- Secondary HEX: atmospheric field or haze, unchanged
- Accent HEX: a small magical focal point — perhaps a window, flower, or key character element, unchanged
If those roles cannot support the intended storybook mood without recoloring, choose another rendering in Stage 2.
---
@@ -32,4 +34,4 @@ fantasy-animation reads HEX values through a **warm storybook filter**:
**Snippet A — half-page storybook scene, text_policy: none**
> Hand-drawn animation style inspired by Ghibli storybook aesthetics. A soft warm scene of a small cottage on a hillside with a winding path leading down to the foreground. Sky in warm cream-amber `#FEF3C7` transitions to a soft accent peach `#FED7AA` at the horizon. Hillside in gentle warm green-amber blending the deck's primary tone, with painterly brush texture. A simplified storybook tree in the foreground left, hand-drawn cartoon-warm style. A small warm-glowing window in the cottage adds a magical accent color. Atmospheric perspective distant hills are paler and softer, foreground has more saturated color. Subtle painterly brush quality throughout. Composed as a 600×800 half-page block with 10% inner padding. Simplified storybook style — any figures are gentle cartoon silhouettes, no realistic faces. NO text or labels. Color values are rendering guidance only.
> Hand-drawn storybook animation. A small cottage sits on a hillside with a winding path leading to the foreground. The sky and haze use the deck's locked secondary-background role, the hillside and tree use the locked primary, and one small glowing cottage window uses the locked accent. Do not tint or replace those values. Painterly brush texture, soft light, and atmospheric perspective create the warmth: distant hills are softer, while foreground forms carry more visual weight. A simplified storybook tree stands at foreground left. Composed as a 600×800 half-page block with 10% inner padding. Any figures are gentle cartoon silhouettes with no realistic faces. No text or labels.
@@ -4,7 +4,7 @@ Pure white paper, black ink, sparse semantic color accents — the Mike Rohde sk
## 1. Style paragraph (paste-ready, 105 words)
> Professional hand-drawn visual-note style on pure white paper. All line work is black ink with slight wobble — confident, intentional, with the human-hand quality of a thoughtful whiteboard session. Hand-lettered titles appear bold and slightly oversized (when text policy allows). Color is intentionally sparse — black ink dominates ~85% of the visible content, with one or two semantic accent colors (coral red for risk/emphasis, muted teal for positive/solution, dusty lavender for neutral categories) covering less than 10% of canvas combined. Backgrounds and shape fills are mostly empty white. Small doodle decorations — stars, dashes, dots — are minimal. Overall feel is professional, considered, manifesto-quality.
> Professional hand-drawn visual-note style on a clean paper field. All line work uses the deck's body-text color with slight wobble — confident, intentional, with the human-hand quality of a thoughtful whiteboard session. Hand-lettered titles appear bold and slightly oversized (when text policy allows). Color is intentionally sparse: line work dominates ~85% of the visible content, while one or two semantic accents drawn only from the deck's locked accent roles cover less than 10% combined. Backgrounds and shape fills remain mostly empty. Small doodle decorations — stars, dashes, dots — are minimal. Overall feel is professional, considered, manifesto-quality.
---
@@ -20,13 +20,13 @@ Pure white paper, black ink, sparse semantic color accents — the Mike Rohde sk
## 3. Using the deck's HEX values
ink-notes has a near-fixed visual language: **black ink + white background + 1-2 semantic accents**. This palette tendency may override `design_spec.colors`:
ink-notes has a near-fixed material language: **dark ink + light background + 1-2 semantic accents**. It never overrides `design_spec.colors`; offer it only when the confirmed roles can support that contrast:
- Background: pure white `#FFFFFF` (do not replace with the deck's secondary unless it's already near-white)
- Lines and text: near-black `#1A1A1A` (do not replace with the deck's primary)
- Semantic accents: ink-notes traditionally uses coral red, muted teal, dusty lavender. The deck's `accent` HEX can serve as the emphasis color, but consider whether it carries the right semantic weight (red=risk, teal=positive, gold=highlight)
- Background: use the deck's `background` / `secondary_bg`
- Lines and text: use the deck's `body_text`
- Semantic accents: use the deck's `accent` / `secondary_accent` roles and their established meaning; do not add traditional ink-notes colors outside the lock
This makes ink-notes **the rendering most likely to deviate from the deck's HEX**. Reaffirm in the prompt that the ink-notes natural palette is intentional and the deck's primary HEX is reserved for the accent role.
This makes ink-notes the rendering most likely to fight a deck's HEX. Offer it only when the locked background / text / accent roles can support the treatment; never invent traditional coral / teal / lavender after confirmation.
---
@@ -34,4 +34,4 @@ This makes ink-notes **the rendering most likely to deviate from the deck's HEX*
**Snippet A — Before/After methodology (comparison type), text_policy: embedded**
> Professional hand-drawn visual-note style on pure white background. Composition is a Before/After split — vertical hand-drawn divider down the center. Both sides use black ink line work with slight wobble. Left side ("Before") shows a simple stick-figure character with a frustrated posture, a speech bubble with hand-lettered "OLD WAY" in English block caps, and a small list of three hand-drawn dashes with brief 1-2 word annotations (e.g. "manual", "slow", "fragile"). Right side ("After") shows a confident stick-figure character with a clean checkmark above, hand-lettered "NEW WAY" in English block caps, and three checkbox-style annotations (e.g. "automated", "fast", "reliable"). A curved hand-drawn "mindset shift" arrow bridges left to right with a small hand-lettered label "the shift". Sparse semantic color: coral red `#E8655A` (the deck's accent) appears only on the left side's pain points; muted teal `#5FA8A8` appears only on the right side's positives. Total color accent area under 10% of canvas. All hand-lettered text is short keywords. Composed as a 1200×500 hero banner with 14% inner padding.
> Professional hand-drawn visual-note style on the deck's locked background color. Composition is a Before/After split — vertical hand-drawn divider down the center. Both sides use the locked body-text color as ink line work with slight wobble. Left side ("Before") shows a simple stick-figure character with a frustrated posture, a speech bubble with hand-lettered "OLD WAY" in English block caps, and a small list of three hand-drawn dashes with brief 1-2 word annotations (e.g. "manual", "slow", "fragile"). Right side ("After") shows a confident stick-figure character with a clean checkmark above, hand-lettered "NEW WAY" in English block caps, and three checkbox-style annotations (e.g. "automated", "fast", "reliable"). A curved hand-drawn "mindset shift" arrow bridges left to right with a small hand-lettered label "the shift". Sparse semantic color: the locked accent marks the left-side pain points and the locked secondary accent marks the right-side positives. Total accent area stays under 10% of canvas. All hand-lettered text is short keywords. Composed as a 1200×500 hero banner with 14% inner padding.
@@ -4,7 +4,7 @@ Warm cream paper with black hand-drawn lines and soft pastel color blocks. The m
## 1. Style paragraph (paste-ready, 110 words)
> Warm hand-drawn sketchnote style on cream paper background. All lines are drawn with deliberate slight wobble — black ink on cream, never perfectly straight, with the human-hand quality that makes the image feel like a thoughtful teacher's whiteboard. Color blocks are soft pastels — light blue, mint, lavender, peach — filled into rounded shapes that don't quite reach their outlines (a deliberate "hand-painted overshoot" feel). Simple cartoon icons and small doodle decorations (stars, sparkles, dots, underlines) appear sparingly to add warmth. Composition is airy and well-organized, with generous white space between elements. Overall feel is warm, instructional, friendly — the visual language of educational explainer videos and knowledge cards.
> Warm hand-drawn sketchnote style on the deck's light background role. All lines use the locked body-text color with deliberate slight wobble, never perfectly straight, giving the human-hand quality of a thoughtful teacher's whiteboard. Soft color blocks derive only from the locked primary, secondary-accent, and accent roles and fill rounded shapes without quite reaching their outlines (a deliberate "hand-painted overshoot" feel). Simple cartoon icons and small doodle decorations (stars, sparkles, dots, underlines) appear sparingly to add warmth. Composition is airy and well organized, with generous whitespace. Overall feel is instructional, friendly, and approachable.
---
@@ -20,11 +20,11 @@ Warm cream paper with black hand-drawn lines and soft pastel color blocks. The m
## 3. Using the deck's HEX values
sketch-notes has a strong **built-in palette tendency** toward warm cream + black ink + soft pastels. When the deck's `design_spec.colors` align (warm-earth or macaron palette family), use them directly. When they don't align (cool-corporate primary), sketch-notes may be the wrong rendering — consult the compatibility matrix in `image-palettes/_index.md`.
sketch-notes has a strong built-in tendency toward warm paper, dark ink, and soft color blocks. Offer it only when the locked deck roles can carry that tendency; otherwise choose another rendering in Stage 2. Once confirmed, use only the locked deck colors.
- Cream paper background: keep close to the rendering's natural `#F5F0E8` even if `design_spec.secondary` differs slightly (the cream **is** part of sketch-notes)
- Black ink lines: keep at `#1A1A1A` or near-black — do not replace with the deck's primary
- Pastel color blocks: the deck's primary / secondary / accent HEX values, **rendered as soft pastel tints** rather than full saturation
- Paper background: use the deck's `background` or `secondary_bg`; do not introduce a natural cream outside the lock
- Ink lines: use the deck's `body_text` role
- Color blocks: use the deck's primary / secondary-accent / accent roles with restrained coverage; do not invent pastel HEX
- Single emphasis accent: the deck's accent HEX, used in 1-2 strong sparing places (a key arrow, an emphasized doodle)
---
@@ -33,4 +33,4 @@ sketch-notes has a strong **built-in palette tendency** toward warm cream + blac
**Snippet A — half-page educational concept, text_policy: embedded**
> Warm hand-drawn sketchnote on warm cream paper background. Black ink lines with slight wobble define three rounded rectangle info boxes arranged in a soft triangle layout. Each box is filled with a soft pastel block color: top box in light blue (a pastel tint of the deck's primary `#1E3A5F`), bottom-left box in mint, bottom-right box in lavender. Color fills don't completely reach the outlines (slight hand-painted overshoot). Hand-drawn wavy arrows connect the boxes each arrow with a small inline hand-lettered keyword like "leads to", "becomes", "supports" (≤2 words per arrow). Each box contains one simple hand-drawn cartoon icon — a lightbulb, a plant, a gear — in black ink. Small doodle decorations (a few stars, dots, sparkles) sparingly around the composition. Composed as a 600×600 half-page block with 14% inner padding. Generous white space. Color values are rendering guidance only — do not display HEX codes or color names as text.
> Warm hand-drawn sketchnote on the deck's locked background color. Lines use the locked body-text color with slight wobble and define three rounded rectangle info boxes arranged in a soft triangle. The top box uses the locked primary, the bottom-left uses the locked secondary accent, and the bottom-right uses the locked accent, each with restrained coverage and no invented tint. Color fills do not completely reach their outlines (slight hand-painted overshoot). Hand-drawn wavy arrows connect the boxes, each with a brief inline keyword such as "leads to", "becomes", or "supports" (≤2 words). Each box contains one simple hand-drawn cartoon icon — a lightbulb, a plant, a gear. Small doodle decorations appear sparingly. Composed as a 600×600 half-page block with 14% inner padding and generous whitespace.
@@ -4,7 +4,7 @@ Golden-hour cinematic warmth — illustrated scenes with intentional warm lighti
## 1. Style paragraph (paste-ready, 100 words)
> Atmospheric scene illustration with golden-hour cinematic lighting. Forms are softly rendered — recognizable but not photo-realistic, with soft edges and intentional light direction. The scene has a clear primary light source (warm low sun, lamplight, window light) casting long soft shadows. Color palette leans warm and slightly desaturated — ambers, terracottas, cream highlights against deeper warm shadows. A subtle bloom or halo effect around the light source adds cinematic warmth. Composition follows narrative principles — foreground subject, middle ground context, atmospheric background. Overall feel is cinematic, contemplative, emotionally warm well suited to brand story and personal narrative content.
> Atmospheric scene illustration with golden-hour cinematic lighting. Forms are softly rendered — recognizable but not photorealistic, with soft edges and intentional light direction. The scene has a clear primary light source (low sun, lamplight, window light) casting long soft shadows. Exact colors come only from the deck's locked roles; emotional warmth comes from light placement, bloom, contrast, and atmosphere rather than hue substitution. Composition follows narrative principles — foreground subject, middle-ground context, atmospheric background. Overall feel is cinematic, contemplative, and emotionally warm, well suited to brand story and personal narrative content.
---
@@ -20,13 +20,13 @@ Golden-hour cinematic warmth — illustrated scenes with intentional warm lighti
## 3. Using the deck's HEX values
warm-scene reads HEX values through a **warm-graded filter**:
warm-scene uses the deck's exact HEX roles and creates warmth through light, not recoloring:
- Primary HEX: dominant warm tone in shadows and midground (warm-graded version of the deck's primary)
- Secondary HEX: highlight tone in the lit areas (warm cream, soft amber)
- Accent HEX: a small bright concentrated zone — sun bloom, lamp glow, key warm reflection
- Primary HEX: dominant tone in shadows and middle ground, unchanged
- Secondary HEX: lit field or atmospheric separation, unchanged
- Accent HEX: a small concentrated zone — sun bloom, lamp glow, or key reflection, unchanged
If the deck's primary is cool (navy, teal), warm-scene will subtly warm-grade it; if the deck's palette is already warm, the rendering reinforces it directly.
If the locked roles cannot support an emotionally warm scene without hue shifts, do not offer `warm-scene` in Stage 2. Never warm-grade a cool primary after confirmation.
---
@@ -34,4 +34,4 @@ If the deck's primary is cool (navy, teal), warm-scene will subtly warm-grade it
**Snippet A — half-page personal story, text_policy: none**
> Atmospheric scene illustration with golden-hour cinematic lighting. A softly rendered figure (simplified silhouette, no detailed face) walks along a path in a warm landscape — the figure in the foreground left, the path leading to a small distant cabin in the middleground, soft warm hills in the atmospheric background. Strong golden warm light comes from the upper right (the sun near the horizon), casting long soft shadows toward the left. Sky transitions from amber `#D97706` at the horizon to soft cream `#FEF3C7` at the top. Foreground shadows in deeper warm primary `#7C2D12`. Subtle bloom around the sun. No hard outlines — forms emerge from light and shadow. Subtle film grain at 8% opacity. Composed as a 600×800 half-page block with 10% inner padding. Simplified silhouette figures only no realistic faces. NO text or labels. Color values are rendering guidance only.
> Atmospheric scene illustration with golden-hour cinematic lighting. A softly rendered figure (simplified silhouette, no detailed face) walks along a path — foreground left, a small distant cabin in the middle ground, soft hills in the atmospheric background. Strong low-angle light comes from the upper right and casts long soft shadows. The sky uses the deck's locked secondary-background role, main forms use the locked primary, and a small sun bloom uses the locked accent; none is hue-shifted. No hard outlines — forms emerge from light and shadow. Add subtle bloom and film grain at 8% opacity. Composed as a 600×800 half-page block with 10% inner padding. Simplified silhouette figures only; no realistic faces, text, or labels.
@@ -1,4 +1,4 @@
> See [`image-base.md`](./image-base.md) for the common framework. Technical SVG/PPT constraints are in [`shared-standards.md`](./shared-standards.md).
> See [`image-base.md`](./image-base.md) for the common framework. Technical SVG/PPT constraints are in [`shared-standards-core.md`](./shared-standards-core.md).
# Image_Searcher Reference Manual
@@ -196,7 +196,7 @@ Never treat a generic `required_terms` pass as acceptance. For example, matching
```bash
python3 scripts/image_search.py --from-url <image-url> --filename <name>.jpg -o <project_path>/images
```
Recorded with `license_tier: manual` — verifying usage rights is the user's call. Human replacement is a legitimate outcome, not a failure. It updates the image and `image_sources.json` but does **not** rewrite `image_queries.json`, so a row fixed this way may still read `Needs-Manual` in the batch manifest — harmless: the file is present, so export proceeds (executor-base §6.1);
Recorded with `license_tier: manual` — verifying usage rights is the user's call. Human replacement is a legitimate outcome, not a failure. It updates the image and `image_sources.json` but does **not** rewrite `image_queries.json`, so a row fixed this way may still read `Needs-Manual` in the batch manifest — harmless: the file is present, so export proceeds ([`executor-web-image.md`](./executor-web-image.md) §1);
3. (opt-in) `--save-candidates` to pull auto-alternatives with their own `source_page_url`s, then `--promote` the best (below);
4. if nothing fits, mark the row `Needs-Manual`.
@@ -281,7 +281,7 @@ Applied by Executor when an image's `license_tier == "attribution-required"`. Th
- **Position**: bottom-right of the image's container, hugging the image edge (within ~8 px)
- **Font size**: 68pt equivalent (≈ 0.71 % of canvas short edge)
- **Color**: `#999` on light/photo backgrounds; `rgba(255,255,255,0.6)` on dark/photo
- **Color**: `fill="#999999"` on light/photo backgrounds; `fill="#FFFFFF" fill-opacity="0.6"` on dark/photo
- **Content**: `© {author} / {provider_short} / {license_short}`
- `provider_short`: `Openverse` / `Wikimedia` / `Pexels` / `Pixabay`
- `license_short`: `CC BY 4.0` / `CC BY-SA 4.0` / `Public Domain`
@@ -301,8 +301,8 @@ Use single-letter labels (a/b/c) only when needed for disambiguation.
### 7.3 Hero / full-bleed image
- Bottom 1.5 cm gradient overlay: transparent → `rgba(0,0,0,0.5)`
- 7pt white semi-transparent text inside the overlay band, right-aligned ~24 px from edge
- Bottom 1.5 cm gradient overlay: `stop-color="#000000" stop-opacity="0"``stop-color="#000000" stop-opacity="0.5"`
- 7pt text with `fill="#FFFFFF" fill-opacity="0.6"` inside the overlay band, right-aligned ~24 px from edge
### 7.4 Source for the credit text
@@ -86,6 +86,6 @@ For `page_role: hero_page` images, default container is the slide canvas (e.g. 1
1. For each `page_role: local` row in the Image Resource List, pick the type using the auto-selection table above.
2. For each `page_role: hero_page` row, **skip type selection** — go straight to [`image-generator.md`](../image-generator.md) §4.1 primitives.
3. `read_file image-type-templates/<type>.md` — only the types actually used in this deck. Most decks use 2-4 types; load each at most once.
4. Apply the type's composition skeleton when assembling the prompt, alongside the locked deck-wide rendering and palette.
4. Apply the type's composition skeleton alongside the locked deck-wide rendering and deck color roles.
**Multiple types per deck is normal.** Locking is at rendering + palette level, not type level.
**Multiple types per deck is normal.** Rendering and deck colors stay fixed; type varies per image.
@@ -18,23 +18,23 @@ Each mode has its own file with: narrative skeleton, page-structure tendencies,
| [`showcase`](./showcase.md) | Visual-led impact; big imagery / numbers; emotional rhythm | Launches, brand reveals, event / promo decks |
| [`briefing`](./briefing.md) | Neutral, complete, scannable; topic titles, even weight, no thesis | Status updates, reference decks, catalogs, meeting packs, FAQs |
> The five partition presentation *intent*, not aesthetics: persuade (`pyramid`) · tell a story (`narrative`) · teach (`instructional`) · impress (`showcase`) · simply inform (`briefing`).
> The five are **argument strategies, not a taxonomy of communication purposes**. A presentation may inform + align + request a decision at once; that composite intent stays as open prose in the Stage-1 communication contract. Stage 2 chooses the mode that best carries the dominant body-page spine, or one concrete `custom` act sequence when no preset can serve the stated priority / sequence.
>
> **A mode is a lens, not a mandate over the user's own structure.** When the user brings their own outline, it is authoritative: transcribe it into `design_spec.md §IX` as given — page order and titles preserved — and let the mode govern only voice / register and page-internal treatment. A mode never reorders a user's pages or rewrites their given titles (mode is Reference-strength; a user-authored outline is exactly the override). When the user gives no structure, the mode does the structural lifting. To lay an outline out with the least reshaping, `briefing` imposes the lightest skeleton.
---
## 2. Auto-selection — content / audience signal → mode
## 2. Auto-selection — communication contract + source signal → mode
| Signal | Recommended mode | Alternates |
| Contract / source signal | Recommended mode | Alternates |
|---|---|---|
| Strategic decision / analysis / board / investor | `pyramid` | `narrative` |
| Pitch / case study / origin story / campaign arc | `narrative` | `showcase` |
| Course / onboarding / how-to / science explainer | `instructional` | `pyramid` |
| Product launch / brand reveal / event opener / keynote / 发布会 / TED | `showcase` | `narrative` |
| Status update / reference / catalog / FAQ / meeting pack / 周报 / 参考 | `briefing` | `pyramid` |
| Decision / recommendation outcome; analysis, board, investor; criteria and trade-offs must land | `pyramid` | `narrative` |
| Persuasion or mobilization lands through a case, tension, transformation, or origin arc | `narrative` | `showcase`, `pyramid` |
| Understanding or capability must build step by step; course, onboarding, how-to, explainer | `instructional` | `pyramid`, `briefing` |
| Attention / emotion / launch moment is primary; sparse presenter-led delivery | `showcase` | `narrative` |
| Complete reference, status, record, hand-off, FAQ, meeting pack; no thesis dominates | `briefing` | `pyramid`, `instructional` |
> No single signal dominates — read the deck's actual purpose from `c. Key Information`. When two modes fit, follow the **primary** intent of the body pages, not the cover. A data review legitimately runs almost entirely `pyramid`; do not force variety.
> No keyword decides the mode. Read `communication_intent`, `audience_outcome`, `core_message`, delivery context / afterlife, source texture, and any user-authored outline together. When several purposes coexist, follow the dominant **argument movement of the body pages**, not the cover and not the first purpose word. A data review can legitimately run almost entirely `pyramid`; a progress report whose durable hand-off matters more than persuasion may stay `briefing`.
**Close calls** — the genuinely adjacent pairs; every other pair is far enough apart that the auto-selection signal decides.
@@ -52,8 +52,8 @@ Each mode has its own file with: narrative skeleton, page-structure tendencies,
## 3. How to use
1. Strategist reads this index at confirmation `d. Layer 1`.
2. Pick one mode from the auto-selection table + the deck's stated purpose.
3. Lock it: write `- mode: <name>` into `spec_lock.md`, record the rationale in `design_spec.md`.
2. Preselect one mode from the auto-selection table + the confirmed communication contract and source structure; separately author the visible AI custom candidate required by §4.
3. Record the confirmed mode and rationale in `design_spec.md`, then project `- mode: <name>` into `spec_lock.md`.
4. Executor reads **only** `modes/<locked-mode>.md` at generation entry — never globs this directory.
**Lock scope**: deck-wide (one mode per deck). The five are the catalog you select from; if the structure is genuinely mixed, pick the mode of the body pages and let pages vary within it, or recommend a `custom` blend (§4). Recommend the best fit; the user confirms.
@@ -64,10 +64,10 @@ Each mode has its own file with: narrative skeleton, page-structure tendencies,
`custom` holds **any bespoke narrative direction the five don't give as-is** — and what *kind* of thing it is doesn't matter. It might be a nameable cadence (dialectic 正反合, myth-vs-reality, countdown / Top-N, Socratic), a deliberate multi-act fusion of several modes, or the user's own feel for how the deck should carry (confrontational here, detached there). Don't try to taxonomize it.
**Either side may originate it.** The user can ask for it directly; or the Strategist — as the deck's strategist — may **recommend** `custom` when a bespoke direction (often a fusion of two modes) genuinely serves the deck better than any single preset. Like every confirmation, it's a recommendation the user confirms or overrides — and the recommendation must **spell the custom out in plain language** (what the cadence / fusion / posture actually is), never present the bare token `custom`, so the user confirms something legible. Either way, the Strategist **crystallizes the intent into a `- mode_behavior:` paragraph** — concrete enough that the Executor can follow it per page (the act sequence or posture shifts, the title voice, the page rhythm, the notes register). Set `- mode: custom` in `spec_lock.md` with that sibling line; the Executor follows the prose in place of a preset file. (This records the intent so it survives 20 pages of generation — the Executor only ever reads `spec_lock.md`, never the chat.)
**Always author the candidate; select it only when warranted.** Stage 2 includes one visible, non-empty AI custom proposal beside the five presets, spelling out the cadence / fusion / posture in plain language. It is initially unselected and does not replace the best-fit preset recommendation unless the user already supplied that exact custom direction; with a template, it must fit available prototype capacity. When the user selects it, the editable prose is saved as `mode: custom` plus `mode_behavior`; otherwise it remains recommendation-only. The Strategist crystallizes a selected custom direction in the Design Spec first, then projects the same pair to `spec_lock.md`. The Executor follows that prose in place of a preset file. (This records the intent so it survives 20 pages of generation — the Executor only ever reads `spec_lock.md`, never the chat.)
> **One value per deck — fusion is *one* `custom`, not several modes.** A deck always locks a single `mode`. A multi-mode blend is expressed as **one** `mode: custom` whose `mode_behavior` paragraph describes the acts — never by locking several modes.
>
> **First ask whether it's really fusion.** A locked mode is a *tendency*, not a cage: a `narrative` deck can still carry one analytical (pyramid-style) page, an `instructional` deck one showcase reveal — that is leaning within a dominant mode, and needs **no** `custom`. Reach for `custom` only when there is genuinely no single dominant spine.
**The one thing to avoid**: reaching for `custom` as a *dodge* — defaulting to it because picking among the five takes judgment. When a preset genuinely fits, lock the preset; propose `custom` when a bespoke direction earns its place, not to avoid choosing. (And a user-stated direction is authoritative the same way a user-supplied outline is — see the lens-not-mandate note in §1.)
**The one thing to avoid**: selecting `custom` as a *dodge* — defaulting to it because picking among the five takes judgment. The custom candidate is mandatory; a custom lock is not. When a preset genuinely fits, keep that preset selected. A user-stated direction remains authoritative the same way a user-supplied outline is — see the lens-not-mandate note in §1.
@@ -30,7 +30,7 @@ Neutral information delivery. Lay the facts out plainly and completely, organize
## 3. Speaker-notes register
Even, factual, plain. State what the page shows without building tension or pressing a "so what". No rhetorical questions, no suspense — a clear read-out the listener can follow or skim. Numbers stated plainly. (Common framework: [`executor-base.md §8`](../executor-base.md).)
Even, factual, plain. State what the page shows without building tension or pressing a "so what". No rhetorical questions, no suspense — a clear read-out the listener can follow or skim. Numbers stated plainly. (Common framework: [`executor-notes.md`](../executor-notes.md) §1.)
## 4. Page skeleton example
@@ -32,7 +32,7 @@ Titles state what the page teaches ("How attention weights are computed") — cl
## 3. Speaker-notes register
Patient, explanatory. Define before using; analogy then principle. Anticipate the learner's question and answer it. Steady pace; signpost transitions ("now that we have X, we can ask Y"). Conversational data. (Common framework: [`executor-base.md §8`](../executor-base.md).)
Patient, explanatory. Define before using; analogy then principle. Anticipate the learner's question and answer it. Steady pace; signpost transitions ("now that we have X, we can ask Y"). Conversational data. (Common framework: [`executor-notes.md`](../executor-notes.md) §1.)
---
@@ -30,7 +30,7 @@ Titles read as beats that advance the arc ("Then the numbers stopped adding up")
## 3. Speaker-notes register
Conversational narration — like talking with the audience, not reading a report. Scenario-conflict-resolution per page. Metaphors make the abstract tangible ("like adding a turbocharger"). Plain rhetorical questions create suspense; bridge each page from the prior one. Conversational data ("nearly a third", "more than doubled"). (Common framework: [`executor-base.md §8`](../executor-base.md).)
Conversational narration — like talking with the audience, not reading a report. Scenario-conflict-resolution per page. Metaphors make the abstract tangible ("like adding a turbocharger"). Plain rhetorical questions create suspense; bridge each page from the prior one. Conversational data ("nearly a third", "more than doubled"). (Common framework: [`executor-notes.md`](../executor-notes.md) §1.)
---
@@ -44,7 +44,7 @@ SCQA opening, pyramid body:
## 3. Speaker-notes register
Conclusion-driven: the first sentence of each page's notes is the takeaway, then 2-3 supporting facts in flowing prose. Composed, authoritative. Every number paired with its comparison in the same sentence ("23% — nearly double the industry's 12%"). Spell percentages as words where the spoken form reads more naturally. (Common framework: [`executor-base.md §8`](../executor-base.md).)
Conclusion-driven: the first sentence of each page's notes is the takeaway, then 2-3 supporting facts in flowing prose. Composed, authoritative. Every number paired with its comparison in the same sentence ("23% — nearly double the industry's 12%"). Spell percentages as words where the spoken form reads more naturally. (Common framework: [`executor-notes.md`](../executor-notes.md) §1.)
---
@@ -30,7 +30,7 @@ Titles are short and evocative — a phrase, not a sentence.
## 3. Speaker-notes register
Energetic, evocative — sets mood and builds anticipation. Short, punchy sentences. Lets the visual do the work and narrates the feeling around it. (Common framework: [`executor-base.md §8`](../executor-base.md).)
Energetic, evocative — sets mood and builds anticipation. Short, punchy sentences. Lets the visual do the work and narrates the feeling around it. (Common framework: [`executor-notes.md`](../executor-notes.md) §1.)
---
@@ -0,0 +1,368 @@
> See [`shared-standards-core.md`](./shared-standards-core.md) for the mandatory SVG foundation.
# Native Data Interface
Conditional interface for preset pattern fills and PowerPoint-native chart/table replacement metadata. Load when either feature appears in the authored SVG.
## 1. Pattern Fill — `<pattern>` with PPTX preset annotation
`<pattern>` requests one fixed DrawingML preset; the converter does not render
the tile's arbitrary geometry. Use this interface only when that preset mapping
is intended.
`data-pptx-pattern="<preset>"` is the generated default for selecting the
intended preset from the enum below. The converter retains an `ltUpDiag`
fallback when the annotation is absent; the checker reports that fallback as a
non-blocking fidelity warning. Invalid explicit preset names remain errors
because they violate the closed OOXML enum.
Pattern colors may come from importer metadata (`data-pptx-fg` /
`data-pptx-bg`) or from the pattern's child paint. Without metadata, the first
child `<rect>` fill becomes the background and the first stroke (or other fill)
becomes the foreground. A missing background defaults to white; a missing
foreground means no native pattern fill can be emitted. The child geometry
itself is never used as a repeatable tile.
**Valid `data-pptx-pattern` values** (OOXML `ST_PresetPatternVal` — closed enum, anything outside makes PowerPoint open with "needs to be repaired"):
| Category | Values |
|---|---|
| Grids | `smGrid` · `lgGrid` · `dotGrid` *(no `ltGrid` — common typo)* |
| Diagonal lines | `ltUpDiag` · `ltDnDiag` · `dkUpDiag` · `dkDnDiag` · `wdUpDiag` · `wdDnDiag` · `dashUpDiag` · `dashDnDiag` · `diagCross` |
| Horizontal / vertical lines | `horz` · `vert` · `ltHorz` · `ltVert` · `dkHorz` · `dkVert` · `narHorz` · `narVert` · `dashHorz` · `dashVert` · `cross` |
| Percent fills | `pct5` · `pct10` · `pct20` · `pct25` · `pct30` · `pct40` · `pct50` · `pct60` · `pct70` · `pct75` · `pct80` · `pct90` |
| Checks & confetti | `smCheck` · `lgCheck` · `smConfetti` · `lgConfetti` |
| Decorative | `horzBrick` · `diagBrick` · `weave` · `plaid` · `trellis` · `zigZag` · `wave` · `sphere` · `divot` · `shingle` · `solidDmnd` · `openDmnd` · `dotDmnd` |
`svg_quality_checker.py` warns when a referenced pattern lacks the annotation;
it errors when the pattern uses `patternTransform` or names a preset outside
this enum.
## 2. PowerPoint-Native Chart / Table Replacement Markers (Authoring Mandatory; Export Opt-in)
Native PowerPoint tables and Excel-backed charts activate at export time only. Metadata authoring is not opt-in: the default chart/table route still writes dormant replacement metadata while keeping hand-authored SVG geometry pixel-stable across PowerPoint / Keynote / LibreOffice / WPS.
**Hard rule — authoring is mandatory**: Executor writes the marker and JSON metadata in the same edit as every supported data chart and pure text-grid data table ([`executor-chart.md`](./executor-chart.md) §2.2). Mini charts, sparklines, insets, KPI-card trends, and small multiples are included when they encode recoverable data in a supported chart type. Canonical rectangular merged text cells may use the narrow `row_span` / `col_span` contract below; graphical cells stay unmarked on the SVG fallback route. The marker group supplies both visible SVG fallback children for browser/live-preview rendering and JSON metadata for `svg_to_pptx` native export.
**Hard rule — activation is the opt-in, dormant unless exported with `--native-charts-and-tables`**: A marker only declares that a group is eligible for PowerPoint-native Chart/Table replacement. Normal `svg_to_pptx.py` runs keep the fallback SVG children and convert them into independently editable DrawingML shapes. Pass `--native-charts-and-tables` only when the data source and chart/table-specific object model matter more than cross-renderer layout fidelity: it emits the PowerPoint Chart/Table object and skips the fallback children to avoid duplicates. Native styling preserves the core palette, text, axis, grid, and background colors where possible, but it is still a PowerPoint Chart/Table object rather than a pixel-identical SVG drawing.
The native route is deliberately data-object-first and may be lossy: marker-local labels, callouts, KPIs, guide lines, custom split/bin semantics, or styling that is absent from the payload may disappear or normalize. Export warns about this route-level risk and any narrower issue it can detect. Loss of visual parity is not grounds to remove an active marker that the emitter can otherwise convert; use the default SVG-fallback export when exact authored artwork matters more than a native data source and object-specific controls.
| Replacement marker | Native output | Required metadata |
|---|---|---|
| `<g data-pptx-replace-with="table">` | `<p:graphicFrame>` with `<a:tbl>` | bounds + `columns` or `rows` |
| `<g data-pptx-replace-with="chart">` | `<p:graphicFrame>` with `c:chart` / `cx:chart` + chart part + embedded workbook | bounds + `type`, plus chart data |
**Metadata placement**: Put JSON in one child
`<metadata type="application/json">`. The parent group's
`data-pptx-replace-with` value selects the table or chart schema, so the
metadata child does not repeat an object-kind attribute. Attribute JSON
(`data-pptx-json="..."`) remains read-compatible but is harder to XML-escape
correctly and is not canonical authoring.
**Bounds**: Provide `x`, `y`, `width`, and `height` in metadata, or as
`data-pptx-x` / `data-pptx-y` / `data-pptx-width` / `data-pptx-height` on the
marker group. If any bound is omitted, the exporter infers the object frame
from the visible fallback geometry; this keeps SVG fallback and native object
placement aligned. Complete explicit bounds are absolute slide coordinates;
marker/ancestor `translate` and `scale` transforms apply only when at least one
bound is inferred. `x`, `y`, `width`, and `height` must be finite and resolve
inside PowerPoint's 32-bit DrawingML coordinate range; `width` and `height`
must resolve to at least one EMU. Native table frames must additionally resolve
to at least one EMU per resolved row and column.
**Validation**: `svg_quality_checker.py` validates replacement marker kind, JSON
metadata, bounds/fallback availability, table rows/columns, supported chart
type, chart data shape, and any imported fallback baseline before export.
Imported marker freshness, fallback classification, provenance, and legacy
read compatibility are operational import concerns. Keep generated authoring
free of those attributes; use the exact behavior and field index in
[`conversion.md`](../scripts/docs/conversion.md#native-table-and-chart-import-claims).
```xml
<g id="p03-revenue-chart" data-pptx-replace-with="chart">
<metadata type="application/json">
{
"x": 120, "y": 150, "width": 520, "height": 320,
"type": "column",
"title": "Revenue by Segment",
"categories": ["Q1", "Q2", "Q3"],
"series": [
{"name": "Cloud", "values": [12, 15, 19]},
{"name": "Services", "values": [8, 9, 11]}
]
}
</metadata>
<!-- Visible SVG fallback for live preview / non-native export goes here. -->
</g>
```
**Table schema**: Native tables are rectangular DrawingML grids. Use `columns`
for the optional header row and `rows` for body rows; shorter rows are padded
with blank cells unless `strict_grid: true` is set. Tables may contain at most
1000 resolved rows and 1000 resolved columns. Use `column_widths` and
`row_heights` as relative weights. Weight lists must match the resolved grid,
contain finite non-negative numbers, and include at least one positive value.
If present, `header_rows` must be an integer from `0` through the resolved row
count. Write `strict_grid`, `style.band_row`, and cell `bold` as JSON booleans.
Cell objects accept `text`, `fill`, `color`,
`align`, `valign`, `bold`, `font_size`, `padding`, `border_color`, and
`border_width`, plus optional `lang`; the same `padding`, `border_color`,
`border_width`, and `lang` keys may also live under `style` as table defaults.
For multi-paragraph text, replace cell `text` with a non-empty `paragraphs`
list. Each entry is either a string or an object containing optional
`align: "l|ctr|r"` and exactly one of `text` or non-empty `runs`; empty
paragraph strings are preserved, and cell `text` / `paragraphs` are mutually
exclusive. Each run is an object with required string `text` and optional JSON
boolean `bold`, `italic`, `underline`, and `strike`, plus optional `color`,
`font_size`, one-typeface `font_family`, `lang`, and `alt_lang`. Unknown fields,
wrong types, empty run lists, multi-typeface `font_family`, and unsupported
colors fail fast. PPTX import requires exact physical row/grid topology and
normalizes source presentation-only run XML outside this closed schema only
when it contains no non-empty `rPr` / `defRPr` / `endParaRPr` `effectLst` or
`effectDag`. A table-cell run effect follows the blocking effect contract above
instead of entering either the native payload or an effect-free fallback.
Relationship-bearing text, extensions, structural line breaks, fields, tabs,
bullets, malformed run topology, and unsupported text-body structure remain
fallback-only.
Per-side cell borders use `borders.left|right|top|bottom`, where each value is
either `{ "style": "none" }` or
`{ "style": "solid", "color": "#RRGGBB", "width": <positive-px> }`.
Per-side borders are cell-only; legacy uniform `border_color` / `border_width`
remain supported as defaults that an individual side may override.
When `lang` is absent, export derives `zh-CN` for CJK text and `en-US`
otherwise. `style.band_row: false` disables both `<a:tblPr bandRow>` and
materialized alternating row fills. Native table typography mirrors the
visible SVG fallback: put `style.font_family` and `style.font_size` on the
marker from the table text already drawn, then use `style.header_font_size` or
per-cell `font_size` only when the fallback visibly differs. If the fallback
has no explicit table font, use the deck body family and locked body size from
`spec_lock.md`.
**Hard rule — table metadata is the native source of truth**: Every row,
summary line, value, and cell-level style that must survive
`--native-charts-and-tables` must be present in `columns` / `rows`. SVG fallback text is
discarded during native export. `svg_quality_checker.py` warns when visible
fallback `<text>` inside a native table marker does not appear in metadata.
For numeric or currency columns, use cell objects with `align: "r"`; SVG
`text-anchor="end"` does not carry into the native table.
**Merged table cells — canonical rectangular contract only**: Put positive JSON
integer `row_span` / `col_span` values on the merge anchor and keep every
covered grid cell blank. Spans must stay within the resolved rectangular grid
and may not overlap. The exporter emits the canonical DrawingML topology
(`rowSpan` on the top edge, `gridSpan` on the left edge, `hMerge` / `vMerge` on
covered cells). CamelCase aliases, raw OOXML merge fields, top-level merge lists,
nonblank covered cells, invalid spans, and overlaps fail fast. The PPTX importer
activates native reconstruction only for that same explicit rectangular topology
with empty merge-slave text bodies; other merge encodings remain fallback-only
with `unsupported-merge-topology`.
**Category chart schema**: `column`, `bar`, `line`, `area`, `pie`,
`doughnut`, `pieOfPie`, `barOfPie`, and `radar` use `categories` plus
`series[].values`. Pie-family charts (`pie`, `doughnut`, `pieOfPie`, and
`barOfPie`) must have exactly one series; the exporter assigns per-category
slice colors so single-series charts do not collapse into one solid color.
Column and bar charts may set per-point colors with `series[].point_colors`
or `series[].pointColors`; the list must match `series[].values` length.
Classic category charts may set native PowerPoint data labels with
`data_labels`. Use `data_labels: true` for default value labels, or an object
with `show_value`, `position`, `number_format`, `font_size`, `font_family`,
`bold`, `color`, and optional per-point `colors`. Supported label positions
depend on chart type: clustered column/bar labels may use `outside_end`,
`inside_end`, `inside_base`, or `center`; stacked / percent-stacked column/bar
labels may use `inside_end`, `inside_base`, or `center`; line labels may use
`above`, `center`, or `best_fit`; area labels do not emit a native label
position. To label only selected data points, use `data_labels.points` with
zero-based `idx` plus optional per-point `position`, `number_format`,
`font_size`, `font_family`, `bold`, and `color`.
**Combo chart schema**: `combo` uses shared `categories` plus either `plots[]`
or typed `series[]`. Each plot supports `type: "column" | "line" | "area"`,
its own `series`, and optional `axis: "secondary"` for a right-side value axis.
When primary and secondary plots genuinely use different category caches,
`plots[]` may also carry its own `categories` and `category_numeric`; the
workbook writer allocates independent category/value ranges. Typed `series[]`
continues to require the shared top-level categories.
Imported `plots[]` may carry `series_indices` so the verified source identity
where each `c:idx` equals its `c:order` survives when physical plot order differs
from legend order. If one plot supplies it, every plot must supply a same-length
list of unique non-negative JSON integers, and the combined values must form one
contiguous `0..N-1` range. Sources whose `idx` and `order` differ stay
fallback-only; typed `series[]` does not accept this plot-scoped field.
Typed `series[]` accepts the same `type` and `axis` fields per series, and
adjacent compatible series are grouped into the same PowerPoint plot. Area
series may set `fill_opacity` / `fillOpacity` as a `0..1` SVG opacity value
when the SVG fallback uses a transparent area fill under an opaque line. A line plot with `area_fill: true`
is exported as a PowerPoint area chart under the hood; `fill_opacity` only sets
the fill style and does not trigger conversion by itself. Combo export layers
area plots below columns and lines while preserving the original series indices.
Line and area series may set `line_width` / `lineWidth` in SVG px units to
match fallback `stroke-width`.
**Narrow classic-axis schema**: `axes` is a closed object with the roles
`category`, `value`, `secondary_category`, and `secondary_value`. Each role may
set only `kind` (`text`, `date`, or `value`, as appropriate), `position`,
`visible`, `label_position` (`next_to`, `none`, `low`, or `high`),
`number_format`, `minimum`, `maximum`, `major_unit`, `reverse`, and
`major_gridlines`. `major_unit` applies to value axes only. PPTX date-axis
**import** is deliberately narrow: numeric Excel date serials are accepted for
area charts and OHLC stock charts; arbitrary date-axis source families are not.
This contract is not a full `AxisSpec`: logarithmic scales, minor units/gridlines,
crossing values, display units, tick skipping, and other unlisted OOXML semantics
remain unsupported and fail closed on import.
**Narrow XY-axis schema**: `scatter` and `bubble` may use a closed `axes` object
with only `x` and `y` roles. Both roles have `kind: "value"`; `x.position`
is `bottom` or `top`, while `y.position` is `left` or `right`. Each accepts the
same closed fields above, and `major_unit` is valid on both value axes. PPTX
import requires the plot to reference exactly two mutually cross-linked
`c:valAx` nodes and separately enforces the closed field/topology gates. The
native writer emits and the importer reads back every field in this closed
contract. Scatter import derives the effective `scatter_style` from a uniform
per-series line/marker/smooth state; unsupported or nonuniform states remain
fallback-only. The normalized SVG fallback newly consumes only
`axes.x.major_gridlines` and `axes.y.major_gridlines`; the other fields do not
imply full visual-axis parity.
**XY chart schema**: `scatter` and `bubble` use `series[].x` + `series[].y`;
`bubble` also requires one `series[].size` / `series[].sizes` value per point.
`series[].points` is also accepted as `[x, y]` / `[x, y, size]` tuples or
`{x, y, size}` objects.
**Chart typography**: Metadata sizes use the same px-style unit as SVG text
(`1px = 0.75pt`). `style.font_family` and the role-specific
`title_font_size`, `subtitle_font_size`, `axis_font_size`,
`axis_title_font_size`, `legend_font_size`, and `note_font_size` fields are
required only when the native object must preserve typography that cannot be
inferred unambiguously from the visible fallback.
**Chart chrome metadata**: Text that is visually part of the chart must be in
metadata, not only in SVG fallback children; metadata MUST still match visible
fallback chrome. `title` becomes the native chart title on classic charts; it
is not an object name, so use `name` for semantic object naming. `subtitle`
becomes the second rich-text line of that classic chart title. `title`,
`subtitle`, and axis-title values may be strings or objects with `text`,
`font_size`, `font_family`, and `color` when the fallback uses local role
typography. `svg_quality_checker.py` rejects `title`, `subtitle`, or axis-title
metadata whose text is not visible inside the replacement marker's fallback. Direct
`--native-charts-and-tables` export keeps the chart native but omits that inconsistent
chrome with a warning. chartEx keeps PowerPoint's empty `<cx:title>` and emits
the title / subtitle as companion editable text boxes until chartEx rich titles
are validated. Axis
titles are optional and explicit: use `axis_titles` with
`category`, `value`, `x`, `y`, or `secondary_value` keys, or the root aliases
`category_axis_title`, `value_axis_title`, `x_axis_title`, `y_axis_title`, and
`secondary_value_axis_title`; do not add semantic axis titles that are not
visible in the fallback. Set `show_value_axis_labels: false` when the fallback
keeps category labels but omits numeric value-axis tick labels, such as a radar
chart without radial coordinates. Native legends are metadata-controlled: use
`show_legend: true` and `legend_position` only when the fallback's legend is
meant to be replaced by PowerPoint's native legend.
Companion text such as `caption`, `source`, `note`, `notes`, `footnote`, and
`footnotes` is exported as editable PPT text boxes next to the native chart. A
companion entry may be a string or an object with `text`, `x`, `y`, `width`,
`height`, `font_size`, `color`, `align`, and `bold`; explicit bounds are
recommended so the native export matches the SVG fallback placement. Explicit
companion bounds are slide coordinates, not local coordinates inside a
transformed marker group. Use companion text for chart captions, source notes,
center labels, and freeform annotations; use `data_labels` for values that
belong to chart points.
**Chart color styling**: For classic native charts, `style.colors` sets series
colors. The exporter also writes explicit chart-area fill, plot-area fill,
axis line, gridline, and label text colors so PowerPoint does not substitute a
white/default-theme chart. If omitted, the exporter infers these colors from
the visible SVG fallback: the largest panel-like `<rect>` becomes the chart
background, fallback text supplies label color, and fallback strokes supply
axis/grid colors. Override any of them explicitly under `style` with
`chart_area_fill`, `plot_area_fill`, `text_color`, `axis_color`, and
`grid_color`; use `"none"` for transparent chart or plot area fill. Generated
payloads default to uppercase `#RRGGBB`. The exporter retains compatibility for
`#RGB`, `rgb(...)` / `rgba(...)`, and common CSS names, normalizing them to
6-digit OOXML RGB. Bar and column series also disable PowerPoint's negative-value
inversion so negative bars keep the same series fill instead of turning into
white/theme fill.
For ChartEx native charts, valid payload `style.colors` (or root `colors`)
populate the ChartEx color-style part instead of being replaced by a fixed
accent1accent6 list. Other ChartEx style semantics remain normalized.
**PowerPoint chartEx schema**: `treemap`, `sunburst`, `histogram`, `pareto`,
`boxWhisker`, `waterfall`, and `funnel` use Office 2016+ chartEx parts. Use
these input shapes:
| Type | Required data |
|---|---|
| `treemap`, `sunburst` | `values` plus either `levels` (`levels[level][point]`) or path-style `categories` (`[["Region", "Group", "Leaf"], ...]`) |
| `treemap` display note | Top-level group labels default to `overlapping`; override with `parent_label_layout: "banner" \| "overlapping" \| "none"`. PowerPoint labels only the top level and leaves — intermediate levels group tiles spatially without labels (sunburst shows every ring). |
| `histogram` | `values` |
| `pareto`, `waterfall`, `funnel` | `categories` + `values`; `waterfall` also accepts `subtotals` / `subtotal_indices` point indexes |
| `boxWhisker` | `series[].values`; optional `series[].categories` per value |
> Note: chartEx files are valid PPTX and editable in PowerPoint; non-Microsoft
> renderers can display a limited subset.
**Stock chart schema**: `stock` uses numeric Excel date serials in
`categories` or `dates`, plus exactly four series in open / high / low / close
order. Use either `series` with four entries, or top-level `open`, `high`,
`low`, and `close` arrays. PPTX import currently recognizes only canonical OHLC
stock charts with shared numeric date caches, `hiLowLines`, and `upDownBars`.
Safe stock series style may pass the structural gate, but stock series,
`hiLowLines`, and up-down bar local styling can still normalize under the
data-object-first contract. HLC, volume, noncanonical structure, and style XML
outside the safe parsing boundary stay fallback-only.
**PPTX chart-import boundary**: The importer recognizes conservative classic
single-plot charts plus the verified scatter/bubble XY-axis, column/line/area
combo, area date-axis, canonical OHLC stock, radar, safe `of_pie` `serLines`,
axis/title/legend normalization, and bar/column gap/overlap subsets. Imported
`gapWidth` must be one canonical integer in `0..500`; imported `overlap` must be
one canonical integer in `-100..100`. Both values intentionally normalize to
the native writer contract rather than claiming exact source-style retention.
Malformed, duplicate, or out-of-range values fail closed.
ChartEx import is closed to seven validated data models: `treemap`, `sunburst`,
`histogram`, `pareto`, `box_whisker`, `waterfall`, and `funnel`. The importer
retains their supported hierarchy/category/value/series/subtotal topology for
native read-back. Numeric cache values must be non-empty and finite, and cache
counts/indexes must be canonical non-negative decimal integers with exact,
contiguous topology; malformed, non-numeric, `NaN`, infinite, sparse, duplicate,
or mismatched caches fail closed. ChartEx style, axis, label, and binning details
outside the payload normalize. Full `AxisSpec`, arbitrary ChartEx families or
presentation fidelity, arbitrary stock variants, and axis/combo/date-axis
semantics outside the closed fields above remain fallback-only. The C4/C5
import work does not expand the normalized SVG renderer and does not reduce
existing SVG-marker-to-native writer support.
**Deferred chart types**: Exploded pie / doughnut variants, `map`, `heatmap`,
`bullet`, and `gantt` are intentionally outside the current native-object
support boundary. The exporter fails fast for these types until each mapping is
implemented and validated one by one.
**Supported chart types**:
- `column`, `bar`: `clustered`, `stacked`, or `percentStacked` (`grouping`)
- `line`: `standard`, `stacked`, or `percentStacked` (`grouping`); `line` or `lineMarker` (`line_style`, default `line` / no markers)
- `area`: `standard`, `stacked`, or `percentStacked` (`grouping`)
- `pie`: exactly one series, per-slice colors
- `doughnut`: exactly one series, per-slice colors
- `pieOfPie`, `barOfPie`: exactly one series, per-slice colors
- `radar`, `radarMarkers`, `radarFilled`
- `scatter`: `marker` (default), `lineMarker`, `line`, `smoothMarker`, or `smooth` (`scatter_style`)
- `bubble`: x/y/size series
- `combo`: `column`, `line`, and `area` plots, optional secondary value axis
- `treemap`, `sunburst`: hierarchical chartEx charts
- `histogram`, `pareto`
- `boxWhisker`
- `waterfall`, `funnel`
- `stock`: open / high / low / close series
3D chart aliases (`3DColumn`, `3DBar`, `3DLine`, `3DArea`, `3DPie`, cone,
cylinder, pyramid variants, and `surface`) are unsupported.
Native legends are opt-in through `show_legend: true`; `legend_position`
defaults to `bottom` and accepts `top`, `left`, or `right`.
**Forbidden — replacement marker transforms**: Do not rotate, skew, or matrix-transform table/chart replacement groups. Translate / scale is accepted; complex transforms fail export because PowerPoint-native table/chart frames do not preserve arbitrary SVG transforms.
@@ -1,11 +1,11 @@
> See [`shared-standards.md`](./shared-standards.md) §§1.41.5 for the native-shape metadata and validation contracts.
> See [`shared-standards-core.md`](./shared-standards-core.md) §§1.41.5 for the native-shape metadata and validation contracts.
# Native Preset Shape Authoring Reference
Use this reference during Executor SVG construction when one standard
PowerPoint shape can express one complete geometric object. The helper does
not create the preset shape's own `p:txBody`; keep visible text outside the
atomic fragment.
Use this reference during Executor SVG construction or project-owned canonical
template maintenance when one standard PowerPoint shape can express one
complete geometric object. The helper does not create the preset shape's own
`p:txBody`; keep visible text outside the atomic fragment.
## 1. Selection Gate
@@ -19,14 +19,15 @@ Apply this decision order before drawing a stock geometric object.
| Condition | Action |
|---|---|
| Plain rectangle, symmetric rounded rectangle, circle, or ellipse | Write the ordinary SVG primitive; the exporter already emits an editable native shape. |
| One DrawingML preset exactly expresses the intended object | Run `preset_shape_svg.py render`, then insert its complete stdout fragment into the hand-authored page. |
| One DrawingML preset exactly expresses the intended object | Run `preset_shape_svg.py render`, then insert its complete stdout fragment into the hand-authored page or canonical template. |
| The visual meaning or contour exceeds one stock shape | Write ordinary `<path>` / `<polygon>` geometry; export keeps it as editable custom geometry. |
| The shape only resembles a preset | Keep ordinary SVG; never infer a preset from contour similarity. |
| Mirror/preserve input already owns native-shape metadata | Keep the existing object and metadata; never reselect its preset. |
**Hard rule**: `preset_shape_svg.py` is the only authoring entry for
`data-pptx-authoring="preset"`. Never add `data-pptx-prst`, frame, adjustment,
carrier, preview, or fingerprint attributes to a hand-written path.
or registry path data by hand. Insert the helper's complete compact `<g>` and
rerun the helper whenever its geometry or paint changes.
---
@@ -45,7 +46,7 @@ paths or contours, or upgrade ordinary SVG during export.
| Literal geometric body | `triangle`, `diamond`, `pentagon`, `hexagon`, `octagon`, `star5` | Use only when the named geometry itself is the intent. |
| Solid block direction | `rightArrow`, `leftArrow`, `upArrow`, `downArrow`, `leftRightArrow`, `upDownArrow`, `chevron` | Thin relationship geometry remains an ordinary SVG `<line>` / `<path>` with no attachment semantics. |
| Standard flowchart node | `flowChartProcess`, `flowChartDecision`, `flowChartInputOutput`, `flowChartTerminator`, `flowChartDocument` | Use only for an actual flowchart; ordinary content cards remain cards. |
| Standard connector route | `straightConnector1`, `bentConnector*`, `curvedConnector*` | The SVG route must match the selected preset; otherwise use a free path. |
| Explicit standalone connector | `straightConnector1`, `bentConnector*`, `curvedConnector*` | Use only when the user explicitly requests a PowerPoint Connector object. Diagram relationships otherwise stay ordinary SVG line/path shapes with no attachment semantics. |
| Stock callout | `wedgeRectCallout`, `wedgeRoundRectCallout`, `wedgeEllipseCallout`, `cloudCallout` | Brand-specific or custom-tail callouts remain free SVG. |
| Stock ribbon or scroll | `ribbon*`, `ellipseRibbon*`, `verticalScroll`, `horizontalScroll` | Select only when the stock contour is visually acceptable. |
| Standalone math symbol | `mathPlus`, `mathMinus`, `mathMultiply`, `mathDivide`, `mathEqual`, `mathNotEqual` | Inline formulas and prose symbols remain text/formula assets. |
@@ -58,6 +59,14 @@ python3 ${SKILL_DIR}/scripts/preset_shape_svg.py list --search arrow
python3 ${SKILL_DIR}/scripts/preset_shape_svg.py describe rightArrow
```
**Shape-first diagram rule**: chart-template adaptations use ordinary line/path
shapes for thin relationships and ordinary `shape` presets for solid block
directions. Connector-family presets are reserved for an explicit request for
a standalone PowerPoint Connector; they are not the default for architecture,
process, hierarchy, or framework diagrams and do not gain attachment semantics.
Existing Connector topology imported from a source PPTX remains owned by the
preserve/mirror round-trip contract.
**Forbidden — false native semantics**:
- `actionButton*` when navigation or trigger behavior is expected; the helper
@@ -69,8 +78,10 @@ python3 ${SKILL_DIR}/scripts/preset_shape_svg.py describe rightArrow
## 3. Fragment Generation
Run one command for one selected object. Colors must come from the current
page's re-read `spec_lock.md`.
Run one command for one selected object. Generated project pages take colors
from the current page-context projection of `spec_lock.md`; `create-template` takes colors
from the confirmed brief and template `design_spec.md`. Mirror/preserve input
keeps the source object's paint instead of regenerating this authored form.
```bash
python3 ${SKILL_DIR}/scripts/preset_shape_svg.py render rightArrow \
@@ -81,7 +92,7 @@ python3 ${SKILL_DIR}/scripts/preset_shape_svg.py render rightArrow \
--adjust "adj1=val 50000"
```
For a native connector:
For an explicitly requested standalone native connector only:
```bash
python3 ${SKILL_DIR}/scripts/preset_shape_svg.py render bentConnector3 \
@@ -98,33 +109,52 @@ and a visible stroke. It exports as an unconnected `p:cxnSp`; a connector
preset can never be authored as an ordinary `shape`.
**Hard rule — stdout-only exception**: the helper prints one deterministic
`<g>` fragment. Read that output and insert it with the normal page
`apply_patch` edit. Do not redirect it into `svg_output/`, loop over pages,
batch shapes, or let it choose layout. The main Agent still authors every
complete SVG page sequentially.
`<g>` fragment. Read that output and insert it with the normal page/template
`apply_patch` edit. Do not redirect it into `svg_output/`, loop over pages or
templates, batch shapes, or let it choose layout. The main Agent still authors
every complete SVG page sequentially and maintains each reusable template
explicitly.
---
## 4. Atomic Fragment Contract
The helper emits one logical group containing exactly one hidden native carrier
and one complete visible preview.
The helper emits one compact logical group. Metadata and base paint are written
once on the group; its direct children are the visible paths regenerated from
the locked preset registry.
| Component | Ownership |
|---|---|
| Logical `<g data-pptx-authoring="preset">` | Stable id, object kind, preset, frame, adjustments, and preview fingerprint. |
| `path[data-pptx-part="geometry"]` | Native PowerPoint export authority; hidden from the browser. |
| `g[data-pptx-part="geometry-preview"]` | Browser-visible preset drawing. |
| `path[data-pptx-part="geometry-detail"]` | One visible layer per DrawingML path, including path-level fill/stroke behavior. |
| Logical `<g data-pptx-authoring="preset">` | Stable id, object kind, preset, frame, adjustments, and explicit local base paint. |
| Direct `<path>` children | Ordered browser-visible registry layers. A child writes only a path-specific fill/stroke override when the preset requires one. |
| Deliberately absent transport fields | No hidden carrier, preview wrapper, `data-pptx-part`, or stored fingerprint belongs in project-authored SVG. Those fields remain part of expanded PPTX import/round-trip transport. |
**Hard rule**: treat the returned group as atomic. Put labels, icons, or other
decorations beside it in a parent `<g>`, never inside the preset group.
**Hard rule**: treat the returned group as atomic. Keep it as the content group
when it stands alone. When it needs labels, icons, or other decorations, put
the preset and those siblings in a separate parent content group; never put
them inside the preset group itself. Do not edit the direct paths; they are
validation evidence generated from the registry, not a freehand contour
surface.
Canonical page/template authoring also keeps paint and opacity off ancestor
groups that contain the preset. Compatible ancestor paint still exports under
the general SVG composition rules, but the checker warns because the atom is no
longer paint-self-contained; rerun the helper with channel alpha instead.
On a structured template, a validated authored-preset group is one semantic
atom. It may be Slide-local, the single carrier of an `object` slot, or a direct
Master/Layout fixed atom. This narrow exception does not permit ordinary nested
`<g>` structures in Master/Layout layers or placeholder carriers. The template
workflow may add the registered structural ownership attributes to the complete
helper group; it still must not alter preset metadata, paint, or direct paths.
**Frame coordinate space**: `--frame x y w h` is expressed in the coordinate
space where you insert the fragment. At the page root that is page coordinates;
inside a `<g transform="translate(…)">` use **group-local** coordinates — the
ancestor transform stacks on top, so page-absolute values would double-offset
the shape off-canvas.
the shape off-canvas. Keep the helper's exact space-separated ordinary-decimal
`data-pptx-frame` spelling; compact authoring does not accept alternate numeric
spellings.
**Regeneration rule**: rerun the helper when preset, frame, adjustment, fill,
stroke, or stroke width changes. Moving, scaling, rotating, or flipping the
@@ -132,7 +162,12 @@ complete logical group is allowed; zero-scale transforms and shear/skew are
forbidden, and the transformed frame must remain inside DrawingML's coordinate
range. Stroke width must remain inside DrawingML's line-width range. To freely
edit the contour, replace the whole fragment with ordinary SVG rather than
modifying a `geometry-detail` path.
modifying a generated direct path.
For a canonical reusable template, the complete helper fragment may remain as
an executable exemplar. A final-page adaptation may copy it unchanged only
when all registry metadata, frame, adjustments, and paint remain unchanged;
otherwise regenerate the complete compact group.
---
@@ -144,9 +179,13 @@ modifying a `geometry-detail` path.
| Connector attachment | Authoring helper v1 creates an unconnected `p:cxnSp` and does not accept endpoint/site metadata. Do not hand-add it. The imported-shape contract may preserve an attachment that already exists in a source PPTX; creating a new attached connector is currently unsupported. |
| Action button behavior | `actionButton*` presets map visual geometry only. No action, navigation target, or hyperlink is created automatically. |
| Gradient/pattern paint | Authoring helper v1 accepts solid HEX paint only. Use ordinary SVG when a complex paint treatment is essential. |
| Multi-path darken/lighten | Browser preview uses the shared normalized paint behavior from the PPTX importer. |
| External edits | Any preview or semantic mismatch fails quality check and export; regenerate the fragment. |
| Multi-path darken/lighten | Direct visible layers use the shared normalized paint behavior from the PPTX importer. Their registry-derived HEX values are authorized derivatives of the locked base color, not spec-lock drift. |
| Expanded compatibility | Existing helper-authored carrier/preview fragments remain readable as ordinary Slide-local input and receive a non-blocking migration warning; they do not become structured fixed atoms or object-slot carriers. Imported expanded fragments remain the lossless mirror/preserve form. |
| External edits | Any registry-path, style, or semantic mismatch fails quality check and export; regenerate the fragment. |
**Validation**: `svg_quality_checker.py` independently rerenders every authored
preset from registry metadata and compares the carrier, visible layers, style,
and fingerprint before export.
**Validation**: `svg_quality_checker.py` independently rerenders every compact
authored preset from registry metadata and compares its direct visible paths
and paint. The exporter performs the same validation, then expands the compact
group only in memory to reuse the lossless native-shape conversion path.
Compatible expanded authored input remains under its separate carrier/preview
freshness contract.
@@ -0,0 +1,195 @@
> See [`shared-standards-core.md`](./shared-standards-core.md) for the mandatory SVG foundation.
# PPTX Structure Interface
Conditional interface for PowerPoint Master, Layout, fixed-layer, and placeholder authoring. Load only when `spec_lock.md pptx_structure.mode` is `structured`.
**Cross-reference map**: unqualified §1.5 and §4.2 references point to [`shared-standards-core.md`](./shared-standards-core.md); this file's own sections are §1–§3.
## 1. PPTX Structure Routing
Every new SVG project declares one deterministic route. Free-design, brand-only, and `template_reuse_scope: style` projects use `pptx_structure.mode: flat`, omit `pptx_masters` / `pptx_layouts` / `page_pptx_layouts` / `page_layouts`, and author no Master/Layout/layer/placeholder metadata. Export keeps all represented content Slide-local while materializing one clean project-owned Master plus one Blank Layout from the current color/typography lock; stock content placeholders and unused built-in Layouts are removed, while the standard date/footer/slide-number capability hooks remain. Deck/layout template projects whose AI-derived lock records `template_reuse_scope: mirror|layout` use `mode: structured`; `standard` / `fidelity` templates use their authored contract, while mirror templates use the validated source identities and parentage declared by the newly materialized workspace.
**Hard rule — no structure inference**: Flat export performs no promotion or deduplication; every object stays Slide-local. Structured template export compiles only declared root identities, atomic fixed layers, and slot groups—it does not assign Layout families, cluster pages, infer placeholders, repair missing metadata, or migrate legacy contracts. Create a new current workspace through [`create-template`](../workflows/create-template.md) before generating structured pages.
**Layout reuse**: Reuse one Layout key only when its ordered fixed Layout atoms and slot ids/types/effective indices/default bounds/binding modes are identical. Different wording, data, imagery, crop, or Slide-local carrier geometry does not create a new Layout. A genuinely different reusable contract gets a new key even when both pages are semantically `content`.
**Zero-slot Layout**: A named Layout may contain no slots and no fixed Layout atoms. This is valid for a cover, poster, full-visual page, or other fixed composition. Do not manufacture an empty `utility` kind or full-page fake `object` slot.
**Adaptive change**: Template `strict` preserves the selected prototype contract. `adaptive` retains the prototype Master and may create a new Layout identity only when fixed Layout atoms or slot topology/bounds change. Update the page mapping immediately while authoring the first such page; never mutate a reused key silently.
## 2. Explicit PPTX Master / Layout / Placeholder Metadata
**Trigger**: This explicit metadata interface applies only to new pages generated from a current deck/layout template workspace with `template_reuse_scope: mirror|layout`. `spec_lock.md` declares `pptx_structure.mode: structured`, complete unique `pptx_masters` / `pptx_layouts` rosters, one `page_pptx_layouts` assignment per generated page, and `page_layouts` as authoring-prototype provenance. `template_reuse_scope: style`, free-design, and brand-only SVGs use `mode: flat` and none of these metadata fields.
**Project lock**: A Master row is `<master_key>: <PowerPoint picker name>`. A unique Layout row is `<layout_key>: <master_key> | <PowerPoint picker name> | <prototype source>`, where the source is a generated `P<NN>` or installed `template:<basename>`. A page assignment is `P<NN>: <layout_key>` under `page_pptx_layouts`. The SVG root values MUST match the assigned definition. A Layout key belongs to exactly one Master and must be globally unique. Reuse one key only when prototypes share identical ordered Layout atoms and slot ids/types/effective indices/default bounds/binding modes. An unused Layout uses a template SVG source and remains registered without a published carrier slide. Every structured route requires numeric `spec_lock.md` typography `title` / `body` rows.
**Template behavior**: Strict preserves the selected prototype's declared Master/Layout/slot contract. Adaptive retains its Master and may allocate a new Layout key/name only when fixed Layout atoms or slot topology/bounds change; update the lock during authoring. Mirror-created prototypes preserve validated source identity, literal paint, typography, effects, atomic geometry, and referenced assets in a new workspace. `standard` / `fidelity` never make source topology authoritative; mirror does not synthesize a replacement topology or fill missing facts.
Imported inherited-shape visibility remains an immutable analysis fact until a
structured mirror is materialized. The final mirror root carries that fact with
the two optional canonical booleans below so export can write the preserved source
package fields without inferring visibility from which shapes happen to be
present. Authored `standard` / `fidelity` templates normally omit both and use
the default `true`. See
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary).
**Master text-style contract**: Flat and structured export map the
locked `title` size to every `a:defRPr` in Master `p:titleStyle`. Level 1 in
both `p:bodyStyle` and `p:otherStyle` uses the locked `body` size; levels 29
use a deterministic descending hierarchy from `15/16` through `8/16` of that
size, rounded to 0.5 pt and floored at the smaller of 8 pt or the body size.
Existing per-level indentation and bullet properties remain unchanged.
| Master style | Locked source | XML field changed |
|---|---|---|
| `p:titleStyle` | `typography.title` | Every `a:defRPr@sz` |
| `p:bodyStyle` | `typography.body` | Level 1 plus derived level 29 `a:defRPr@sz` |
| `p:otherStyle` | `typography.body` | Level 1 plus derived level 29 `a:defRPr@sz` |
**Hard rule — narrow scope**: This Master update changes only Master
`p:txStyles//a:defRPr@sz`; it preserves level indentation, bullet, margin, and
paragraph settings. It does not rewrite direct run sizes on generated slides,
so the initial slide rendering remains controlled by the authored SVG. Missing
`title` or `body` rows fail flat or structured export.
**Layout level-one text-default contract**: For every text-bearing placeholder
whose first prototype run has a direct `a:rPr@sz`, explicit Layout export copies that
size to the generated Layout prompt run and
`p:txBody/a:lstStyle/a:lvl1pPr/a:defRPr@sz`. It does not rewrite Slide direct
runs or Layout levels 29. This preserves the layout-specific size when
level-one placeholder text is inserted or reset; placeholders without a direct
prototype size remain unchanged.
| Metadata | Placement | Behavior |
|---|---|---|
| `data-pptx-master="master-default"` | root `<svg>` | Binds the slide to one generated Slide Master key |
| `data-pptx-master-name="Default Master"` | root `<svg>` | Sets the Master picker/display name |
| `data-pptx-layout="content"` | root `<svg>` | Binds the slide to one generated reusable layout key |
| `data-pptx-layout-name="Title and Content"` | root `<svg>` | Sets the PowerPoint layout-picker name; defaults from the layout key |
| `data-pptx-show-master-shapes="false"` | root `<svg>` | Accepts exact lowercase `true` or `false` and writes the assigned Layout's `p:sldLayout@showMasterSp`; every SVG using the same Layout key must repeat the same value; omission means `true` |
| `data-pptx-show-inherited-shapes="false"` | root `<svg>` | Accepts exact lowercase `true` or `false` and writes this Slide's `p:sld@showMasterSp`; `false` hides inherited Layout and Master shapes without removing backgrounds, placeholders, parts, or parent relationships; omission means `true` |
| `data-pptx-layer="master"` | direct semantic atom | Moves one repeated static object/background into the named Slide Master; ordinary `<g>` is forbidden, while one validated compact authored-preset `<g>` (§1.5) is an atomic exception |
| `data-pptx-layer="layout"` | direct semantic atom | Moves one repeated static object/background into the selected Layout; ordinary `<g>` is forbidden, while one validated compact authored-preset `<g>` (§1.5) is an atomic exception |
| `data-pptx-layer="slide"` | direct full-canvas solid `<rect>` only | Writes a one-page override as Slide `p:bg` |
| `data-pptx-placeholder="..."` | direct slot `<g id>` | Declares a reusable Layout slot whose visible content remains Slide-local |
| `data-pptx-bounds="x y width height"` | slot `<g>` | Supplies the positive reusable design-zone frame in SVG user units with at most two decimals per value |
| `data-pptx-idx="1"` | slot `<g>` | Retains an imported source Layout placeholder index; optional for reconstructed layouts |
| `data-pptx-carrier="true"` | one compatible direct child of a normal slot | Binds that visible child as the real Slide placeholder carrier |
| `data-pptx-binding="proxy"` | composite `object` slot `<g>` only | Keeps the visible group ordinary and creates one hidden transparent binding proxy |
| `data-pptx-editable="false"` | master/layout element or slide background | Declares intentional editing outside ordinary slide content |
**Hard rule — explicit only**: On a structured `template_reuse_scope: mirror|layout` route, every SVG requires the four root Master/Layout identity attributes. Optional inherited-shape visibility uses only exact lowercase `true` / `false`; other spellings fail, and omission means `true`. Every Master/Layout atom and slot requires a unique stable `id` and is a direct root child. Layouts with zero slots are valid. `data-pptx-layout-kind`, `distilled`, and `utility` are legacy metadata and fail the structured contract. Flat `template_reuse_scope: style`, free-design, and brand-only pages omit the structural markers and visibility attributes; ordinary groups still use the shared `data-pptx-bounds` module contract.
**Layer order**: Author the SVG in PowerPoint paint order: Master background,
Layout background, optional Slide background, remaining Master atoms, remaining Layout atoms,
then slot groups and Slide-local content groups. Backgrounds are a special inheritance
plane beneath every shape; this order keeps standalone SVG preview and
PowerPoint rendering aligned. The exporter rejects interleaved layers.
**Solid background ownership**: Structured export deliberately narrows scoped
background ownership to a direct full-canvas solid `<rect>` and disables the
generic conversion-level promotion described in §4.2. Mark the solid rect
`data-pptx-layer="master"` for the deck-wide default,
`data-pptx-layer="layout"` for a page-type override, or
`data-pptx-layer="slide"` for a one-slide override. An unmarked direct
full-canvas solid rect in the background plane is also treated as Slide scope.
A Layout background overrides the Master background; a Slide background
overrides both. Use the Master for a globally stable color and the Layout for
cover/section/content variants under the same design language. Gradient and
preset-pattern rects remain ordinary shapes on declared Master/Layout layers
or as Slide-local content; images remain pictures. Textures, transformed rects,
and visible-stroke rects also remain ordinary objects.
| Placeholder value | Direct carrier inside slot `<g>` | PowerPoint placeholder |
|---|---|---|
| `title`, `subtitle`, `body` | one `<text data-pptx-carrier="true">` | `title`, `subTitle`, `body` |
| `date`, `footer`, `slide-number` | one `<text data-pptx-carrier="true">` | `dt`, `ftr`, `sldNum` |
| `picture` | one `<image>` or supported imported crop `<svg>`, marked as carrier | `pic` |
| `chart`, `table` | one matching `data-pptx-replace-with` marker group, marked as carrier | `chart`, `tbl` |
| `object` | one text, image, basic SVG shape, or validated compact authored-preset `<g>` marked as carrier; alternatively the slot group declares `binding="proxy"` | `obj` |
| `media` | one `<image>` or supported imported crop `<svg>`, marked as carrier | `media` |
**Text slot carrier**: A multiline text placeholder must remain one
native text frame. Use the default paragraph merge; `--no-merge` cannot supply
several line shapes as one
PowerPoint placeholder prototype/binding. Leave strict-line text Slide-local
when separate frames are the required result.
For a materialized mirror, an imported text carrier may additionally keep the
source shape's positive `data-pptx-frame="x y width height"`. That frame owns
the Slide carrier `a:xfrm`; the converter reconstructs text-body insets from the
visible SVG anchor/baseline instead of shrinking the shape to glyph bounds.
`data-pptx-bounds` remains the reusable Layout default and may
legitimately differ. Do not add `data-pptx-frame` to an authored
`standard` / `fidelity` carrier merely to duplicate its Layout bounds.
**Blank text carrier**: Leave a marked text carrier empty or whitespace-only
when the placeholder must remain visually blank. Export materializes one
invisible U+200B run so the carrier still becomes a native PowerPoint text
shape. Do not insert a dummy dash, shrink text below the DrawingML 1pt minimum,
or hide a visible glyph with opacity/background paint; those workarounds either
leak content or produce a PPTX that PowerPoint repairs.
`title` is normally type-matched without an index in reconstructed layouts; if
an imported source title explicitly has one, preserve that exact index. Every
indexed placeholder on one layout uses a unique OOXML UInt32 index. Structured export writes the semantic type on both the Layout and Slide carrier (except `obj`, whose OOXML default is already `obj`) so PowerPoint and `python-pptx` retain the same identity. A composite object slot instead keeps its visible group ordinary and uses a hidden transparent proxy.
Date, footer, and slide-number placeholders enable their matching Layout `p:hf`
flags; a date placeholder also gets a `datetimeFigureOut` field in the reusable
Layout definition. The current Slide keeps its authored date content.
Because an omitted `p:ph@idx` has the effective value `0`, an omitted-index
title reserves `0`; no other placeholder on that Layout may use the same
effective index.
**Slot prototype**: The prototype source declared by the unique Layout definition supplies that Layout's placeholder formatting. `data-pptx-bounds` supplies the reusable default frame and is mandatory on every slot. Derive it from
the intended design zone, column, panel inset, safe area, or picture frame —
never from text length, glyph width, line count, or a tight content bounding
box. Repeat the same slot ids/types/effective indices/default bounds/binding modes on every slide using that Layout. The Layout owns the reusable `p:ph`; normal visible carriers keep a matching Slide binding so approved rendering stays identical. A composite `object` proxy adds one hidden transparent binding shape to suppress empty inherited placeholder paint. Bounds define the Layout default only; actual Slide content and local carrier geometry may differ.
**Final-package read-back gate**: After writing a temporary structured PPTX and before publishing it, export reopens the package and
verifies that each published Slide targets exactly one Layout, one Layout key always resolves to the
same part, different keys do not collapse onto one part, and every declared Layout—including one unused by all published Slides—is
registered through its Master and the Presentation. Physical Slide/Layout/
Master part rosters, their content-type overrides, and their Presentation/
Master registrations must be exact. It also verifies the Layout picker name,
Master picker identity, placeholder type and effective index, matching `p:hf` flags, explicit design-zone frame, direct prompt size, and level-one default size.
Every owned `p:bg` is checked as an exact zero-or-one payload against the pre-
promotion result; this includes preserving the base Master background when no
authored Master background replaces it. During the same export, every finished
Slide, Layout, and Master must reproduce its exact top-level shape-name roster
and order after packaging. The gate verifies that each carrier-bound slot owns the expected Slide binding, each composite visible carrier remains ordinary, and every composite binding proxy is hidden. A zero-slot Layout must read back with no placeholder. Later slides may keep different Slide-local geometry; only the reusable
Layout frame is checked against the explicit/prototype contract. Any mismatch
fails export without replacing the requested output.
**Static structure consistency**: Repeat the same master element ids on every
slide and the same layout element ids on every slide sharing a layout. Their
generated OOXML must be identical within the affected master/layout group.
Static structure may carry shapes, text, or images; non-image/external
relationships are rejected. Every static object is atomic. An ordinary
`<g data-pptx-layer="master|layout">` is forbidden; the validated compact
authored-preset group from §1.5 is the sole group exception because it compiles
to one native object. A full-canvas first rect may be marked as a Master or
Layout background.
**Native object slot carriers**: `chart` / `table` slots require
`--native-charts-and-tables`; fallback groups contain several shapes and cannot map to one
PowerPoint placeholder. `object` is the generic PowerPoint content slot and
uses either one carrier object—including one validated compact authored-preset
group—or the explicit composite proxy downgrade. `media` currently binds
an authored image/crop to a native `media` placeholder; it does not synthesize
video or audio media from a decorative SVG group.
## 3. Legacy Template Input Boundary
Existing structured/template projects or packages that carry `native_structure.json` / `source_template.pptx`, `pptx_structure.mode: baseline|template|preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled` / `utility`, direct atomic placeholders, or an incomplete root Master identity are not generation/export inputs and are never upgraded in place. Create a separate current workspace through [`create-template`](../workflows/create-template.md). A project explicitly declaring `pptx_structure.mode: flat` is the current free-design/brand-only route and needs no conversion merely because it has no Master/Layout metadata.
| Available source | Allowed create-template behavior |
|---|---|
| Original PPTX Type A | `standard` / `fidelity` author new topology; `mirror` preserves supported Master/Layout/placeholder facts that still exist in the package |
| Legacy or unstructured SVG Type B | `standard` / `fidelity` use pages as visual/contextual reference and author a complete new contract; old metadata is not output topology |
| Complete current SVG Type B | `mirror` may preserve the explicit current contract in a new workspace; authored modes may replace it |
Without an original PPTX or complete current Type B contract, do not claim mirror or source-topology recovery. After template creation, Generate PPTX Step 3 authors new structured `svg_output/` pages; the exporter only compiles those declarations and never derives, repairs, or migrates structure.
---
@@ -6,6 +6,7 @@ PPT Master uses rendering-neutral compiler hints only where ordinary SVG cannot
| Marker | Placement | Purpose |
|---|---|---|
| `data-pptx-page-role` | Root `<svg>` on flat pages only | Classify a free-design/brand-only page as `cover`, `toc`, `section`, `content`, or `ending`. |
| `data-pptx-master` / `data-pptx-master-name` | Root `<svg>` | Bind the page to one named PowerPoint Slide Master. |
| `data-pptx-layout` / `data-pptx-layout-name` | Root `<svg>` | Bind the page to one named Layout under that Master. |
| `data-pptx-layer="master"` | Direct atomic child of root | Promote one fixed visual object to the named Master. |
@@ -15,15 +16,15 @@ PPT Master uses rendering-neutral compiler hints only where ordinary SVG cannot
The completed SVG remains the full visible page. Removing the metadata must not change browser rendering. Do not copy visible text, geometry, style, or asset values into metadata.
**Hard rule — route boundary**: Free-design and brand-only pages use `pptx_structure.mode: flat` and omit every Master/Layout/layer/placeholder marker in this document. Deck/layout template pages declare their final Master and Layout before drawing begins; the structured exporter compiles that contract and never selects, clusters, distills, or visually infers it.
**Hard rule — route boundary**: Free-design, brand-only, and `template_reuse_scope: style` pages use `pptx_structure.mode: flat`, declare one canonical root `data-pptx-page-role`, and omit every Master/Layout/layer/placeholder marker in this document. Only deck/layout template pages whose AI-derived lock records `template_reuse_scope: mirror|layout` declare their final Master and Layout before drawing begins and omit `data-pptx-page-role`; the structured exporter compiles that contract and never selects, clusters, distills, or visually infers it.
**Hard rule — specialized metadata wins**: Use Master/Layout/placeholder metadata for native structure, `data-pptx-native` for chart/table reconstruction, and the imported/authored shape metadata defined in [`shared-standards.md`](./shared-standards.md) §§1.41.5. Do not duplicate those facts with `data-pptx-role`.
**Hard rule — specialized metadata wins**: Use Master/Layout/placeholder metadata for native structure, `data-pptx-replace-with` for optional PowerPoint-native Chart/Table replacement, and the imported/authored shape metadata defined in [`shared-standards-core.md`](./shared-standards-core.md) §§1.41.5. Do not duplicate those facts with `data-pptx-role`.
---
## 2. Master and Layout Atoms
On structured deck/layout template routes, Master and fixed Layout visuals are atomic root children:
On structured `template_reuse_scope: mirror|layout` routes, Master and fixed Layout visuals are atomic root children:
```xml
<svg xmlns="http://www.w3.org/2000/svg"
@@ -60,9 +61,9 @@ Use one direct root group as the authoring boundary and one compatible direct ch
```xml
<g id="title-slot"
data-pptx-placeholder="title"
data-pptx-placeholder-bounds="72 48 1136 72">
data-pptx-bounds="72 48 1136 72">
<text id="title-carrier"
data-pptx-placeholder-carrier="true"
data-pptx-carrier="true"
x="72" y="100">Actual title</text>
</g>
```
@@ -70,12 +71,12 @@ Use one direct root group as the authoring boundary and one compatible direct ch
| Requirement | Rule |
|---|---|
| Placement | The slot `<g id>` is a direct root child. Structural metadata may not be nested below it. |
| Bounds | `data-pptx-placeholder-bounds="x y width height"` is mandatory, finite, and positive. It describes the reusable design zone, not the current glyph/content tight bounds. |
| Carrier | The group contains exactly one compatible direct drawable child marked `data-pptx-placeholder-carrier="true"`. Export unwraps that child into the real Slide placeholder binding. |
| Identity | `data-pptx-placeholder-idx` is optional; effective indices must be unique within one Layout. Preserve a source index when reconstructing an existing PPTX. |
| Bounds | `data-pptx-bounds="x y width height"` is mandatory, finite, and positive. It describes the reusable design zone, not the current glyph/content tight bounds. |
| Carrier | The group contains exactly one compatible direct drawable child marked `data-pptx-carrier="true"`. Export unwraps that child into the real Slide placeholder binding. |
| Identity | `data-pptx-idx` is optional; effective indices must be unique within one Layout. Preserve a source index when reconstructing an existing PPTX. |
| Fixed decoration | Reusable decoration does not belong in the slot. Author it as a root Layout atom. Page-specific labels/captions use another slot or remain Slide-local. |
Canonical placeholder values are `title`, `subtitle`, `body`, `picture`, `chart`, `table`, `object`, `media`, `date`, `footer`, and `slide-number`. Carrier compatibility is defined in [`shared-standards.md`](./shared-standards.md) §7.
Canonical placeholder values are `title`, `subtitle`, `body`, `picture`, `chart`, `table`, `object`, `media`, `date`, `footer`, and `slide-number`. Carrier compatibility is defined in [`pptx-structure-interface.md`](./pptx-structure-interface.md) §2.
### 3.2 Explicit composite proxy
@@ -84,8 +85,8 @@ When one reusable region is a composite object that cannot bind to one real Powe
```xml
<g id="hero-composite-slot"
data-pptx-placeholder="object"
data-pptx-placeholder-binding="proxy"
data-pptx-placeholder-bounds="544 160 664 472">
data-pptx-binding="proxy"
data-pptx-bounds="544 160 664 472">
<rect x="544" y="160" width="664" height="472" fill="#E2E8F0"/>
<text x="576" y="214">Visible composite content</text>
</g>
@@ -116,11 +117,11 @@ Do not add structural roles to ordinary titles, body copy, cards, KPIs, diagrams
## 5. Validation and Migration
For structured deck/layout template projects, validation rejects:
For structured `template_reuse_scope: mirror|layout` projects, validation rejects:
- a missing root Master/Layout identity or a page-to-lock mismatch;
- a Master/Layout `<g>`, nested structure marker, missing/stale id, or inconsistent shared atom contract;
- a slot without positive bounds, a carrier-bound slot without exactly one compatible carrier, or a proxy binding on a non-`object` slot;
- incomplete page mappings, cross-Master Layout-key reuse, or conflicting same-key Layout contracts.
Legacy structured/template SVGs using unmapped `baseline`, `preserve`, `layout_strategy: distill`, `data-pptx-layout-kind`, `distilled`, `utility`, direct atomic placeholders, or an incomplete Master identity are not a second supported structured contract. Run [`restore-pptx-structure`](../workflows/restore-pptx-structure.md) before generation or export. An explicit `mode: flat` free-design/brand-only project is current and intentionally has no Master identity. When original PPTX/native facts exist, migration restores those identities first; otherwise the main Agent explicitly derives structured template metadata. Export never performs that derivation.
Legacy structured/template SVGs using unmapped `baseline`, `preserve`, `layout_strategy: distill`, `data-pptx-layout-kind`, `distilled`, `utility`, direct atomic placeholders, or an incomplete Master identity are not a second supported structured contract. Create a new workspace through [`create-template`](../workflows/create-template.md) before generation or export. An explicit `mode: flat` free-design/brand-only project is current and intentionally has no Master identity. Original PPTX Type A may preserve native identities that still exist in the package; legacy SVG-only Type B may guide `standard` / `fidelity` visually but does not authorize topology recovery. Export never derives, repairs, or migrates structure.
@@ -0,0 +1,642 @@
# Shared SVG Core Standards
Mandatory reference for every route that authors or regenerates slide visuals through SVG. It owns XML validity, the closed generated-authoring surface, basic converter compatibility, page closure, semantic grouping, and shared fidelity vocabulary.
**Conditional module routing**:
| Trigger | Load |
|---|---|
| Noncanonical/alpha paint, advanced line or text treatment, gradient/filter/effect, transform, freeform/radial geometry, or constructed style | [`svg-effects.md`](./svg-effects.md) |
| A page will use a preset pattern fill or evaluate native chart/table replacement | [`native-data-interface.md`](./native-data-interface.md) before deciding eligibility or emitting metadata |
| `pptx_structure.mode: structured` | [`pptx-structure-interface.md`](./pptx-structure-interface.md) |
**Fidelity labels**:
| Label | Meaning |
|---|---|
| `Native-stable` | Generated PPTX uses the corresponding native DrawingML property or object and retains the documented semantics within the technique-specific limits. |
| `Native-normalized` | Export targets an editable DrawingML equivalent, but normalizes the SVG into another structure such as a freeform, run property, or simplified paint/effect. |
| `Approximate` | DrawingML has no exact SVG equivalent; export targets the intended effect through a documented approximation, and material differences require output review. |
| `Bake-required` | The runtime effect is outside the native contract; pre-render it into an image or rebuild it with explicit supported geometry. |
**Reading rules**:
- **Required** / **Forbidden** statements are non-negotiable technical boundaries.
- **Conditional** contracts apply only when the corresponding feature is used.
- **Reference — not a constraint** passages expose capabilities and recipes; they do not require every page or visual style to use them.
- The locked `visual_style` controls whether and how strongly a compatible effect is used. It never expands the technical boundary.
**Hard rule — generated authoring is fail-closed**: `svg_output/` and reusable
template SVGs may use only properties and conditional interfaces explicitly
listed in this file or a triggered module in the routing table above. `svg_quality_checker.py` rejects unknown inline visual
properties and conditional contracts that have no reliable compatibility
mapping; documented fallback forms remain valid and receive warnings.
**Default — recommended authoring and supported input stay separate (may
preserve supported input)**: generated SVG uses one predictable default
spelling, while converter-supported equivalent spellings remain valid input.
The checker may recommend normalization, but such warnings do not require
modification or block export. Only invalid, unsafe, or unreliably convertible
input is an error; do not remove converter support to enforce a narrower
generation preference.
**Hard rule — one-way fidelity vocabulary**: the labels above describe the
`svg_output/` → generated PPTX path. They do not promise reconstruction of the
original SVG syntax, `<defs>` graph, `<use>` structure, path commands, or
`<tspan>` layout after PPTX-to-SVG import, nor pixel identity across PowerPoint,
LibreOffice, Keynote, and WPS.
**Hard rule — capability boundary**: a recipe never expands converter support.
Use only the target elements and syntax documented by each conditional
contract. Unsupported element tags fail preflight; browser-rendered attributes
outside these contracts must not be assumed to have a DrawingML mapping.
---
## 1. Required Foundation, Forbidden Features, and Conditional Interfaces
### 1.0 Text characters: must be well-formed XML
SVG is strict XML. Two rules for all text and attribute values:
| Character category | Required form | Forbidden form |
|---|---|---|
| Typography & symbols (em dash, en dash, ©, ®, →, ·, NBSP, full-width punctuation, emoji…) | **Raw Unicode characters** — write `—` `` `©` `®` `→` directly | HTML named entities — `&mdash;` `&ndash;` `&copy;` `&reg;` `&rarr;` `&middot;` `&nbsp;` `&hellip;` `&bull;` etc. |
| XML reserved characters (`&`, `<`, `>`, `"`, `'`) | **XML entities only**`&amp;` `&lt;` `&gt;` `&quot;` `&apos;` (e.g. `R&amp;D`, `error &lt; 5%`) | Bare `&` `<` `>` (e.g. `R&D`, `error < 5%`) |
One offending character invalidates the file and aborts export.
**Structural blacklist** (in addition to the character rules above):
| Banned Feature | Description |
|----------------|-------------|
| `mask` | Masks |
| `<style>` | Embedded stylesheets |
| `class` | CSS selector attributes |
| External CSS | External stylesheet links |
| `<foreignObject>` | Embedded external content |
| `textPath` | Text along a path |
| `@font-face` | Custom font declarations |
| `<animate*>` / `<set>` | SVG animations |
| `<script>` / event attributes | Scripts and interactivity |
| `<iframe>` | Embedded frames |
The blacklist above is exhaustive for globally forbidden structural syntax.
It is not a positive allowlist for every browser-rendered property. Features
that require a restricted form are valid only under the conditional contracts
below; unlisted visual properties are unsupported.
**Hard rule — inline visual-property allowlist**:
| Property family | Allowed inline `style` properties |
|---|---|
| Paint and line | `fill`, `stroke`, `stroke-width`, `stroke-dasharray`, `stroke-linecap`, `stroke-linejoin`, `fill-opacity`, `stroke-opacity`, `vector-effect` |
| Text | `font-family`, `font-size`, `font-weight`, `font-style`, `text-anchor`, `letter-spacing`, `text-decoration` |
| Alpha and definition paint | `opacity`, `stop-color`, `stop-opacity`, `flood-color`, `flood-opacity` |
| Literal geometry | The element-specific properties in §2.1 |
| Preview-only | `shape-rendering`; it does not change native geometry |
**Default — ordinary generated paint**: the table allows inline placement of
those property names. New solid paint uses uppercase six-digit `#RRGGBB`;
`fill` / `stroke` may instead use lowercase `none` or an exact local
`url(#id)`. Load [`svg-effects.md`](./svg-effects.md) before authoring any
alternative compatible color spelling, alpha/opacity channel, dash/cap/join,
gradient, filter, or constructed paint/effect. Existing compatible alternatives
remain valid input and receive recommendation warnings rather than errors.
Conditional properties with a required XML form stay out of inline style:
write `filter="url(#id)"`, `clip-path="url(#id)"`, and
`marker-start` / `marker-end` as direct attributes. `!important`, unknown CSS
properties, blend modes, isolation, and backdrop filters fail quality check.
The table registers property names, not arbitrary CSS values. Ordinary generated
text uses a non-empty `font-family`, a finite positive unitless-px `font-size`,
`font-weight` of `normal` / `bold` / an integer hundred from `100` through
`900`, `font-style` of `normal` / `italic`, and `text-anchor` of `start` /
`middle` / `end`. Inheritable text declarations belong only on `<svg>`, `<g>`,
`<text>`, or `<tspan>`; `text-anchor` is invalid on `<tspan>`. Load
[`svg-effects.md`](./svg-effects.md) §6.7 before authoring tracking,
underline/strike, text outline/alpha, gradient text, or text filter effects.
Unknown or unmapped declarations fail Checker preflight and native export.
> **`marker-start` / `marker-end` is conditional** — see §1.1.
>
> **`clipPath` on `<image>` is conditional** — see §1.2.
>
> **Static same-document `<use>` is conditional** — see §1.3.
>
> **Imported native-shape metadata is conditional** — see §1.4.
>
> **Authored native preset fragments are conditional** — see §1.5.
>
> **Inline CSS geometry, simple gradients, filters, and approximate group
> opacity are conditional** — see §2 and [`svg-effects.md`](./svg-effects.md).
>
> **PPT preset patterns and native chart/table/template metadata are
> conditional** — see [`native-data-interface.md`](./native-data-interface.md) and [`pptx-structure-interface.md`](./pptx-structure-interface.md).
DrawingML has no arbitrary per-pixel alpha-compositing path. Effects that rely
on one, including text-knockout image fills and arbitrary alpha composites,
must be baked into a raster asset before SVG export.
---
### 1.1 Line-end Markers (Conditional Contract)
`marker-start` and `marker-end` are supported on `<line>` and `<path>` only
when the referenced marker fits this native-arrow contract:
| Concern | Required form |
|---|---|
| Reference | Exact local `url(#id)` to a `<marker>` in `<defs>` |
| Orientation | `orient="auto"` or `orient="auto-start-reverse"`; the latter reverses `marker-start` while behaving like `auto` at `marker-end` |
| Shape | One direct shape representing a DrawingML `triangle`, `stealth`, `arrow`, `diamond`, or `oval` line end: a 3-vertex `<polygon>` / closed path (triangle), a simple concave 4-vertex `<polygon>` / closed path (stealth), an open 3-vertex path (arrow), a simple convex 4-vertex `<polygon>` / closed path (diamond), or one `<circle>` / `<ellipse>` (oval) |
| Path grammar | Use one explicit `M`/`L` command per vertex. Triangle, stealth, and diamond paths end in `Z`; arrow paths remain open after the third vertex. Do not use `H`, `V`, curves, or an implicit multi-point `L` command inside a marker path |
| Color parity | Triangle, stealth, diamond, and oval use a fill matching the parent line stroke. The open arrow uses `fill="none"` and a stroke matching the parent line stroke. DrawingML line ends inherit the line color |
The converter maps these five shapes to their corresponding DrawingML line-end
types. Prefer `<polygon>` for the closed triangle, stealth, and diamond forms;
the open arrow form requires `<path>`. Four-vertex shapes must be simple and
non-degenerate: convex geometry maps to diamond and concave geometry maps to
stealth. Checker and exporter preflight consume this same contract; other
marker shapes have no native mapping and block export instead of being silently
dropped.
PPTX import compatibility, tolerant recovery, strict-mode rejection, and
diagnostic behavior are indexed in
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary).
---
### 1.2 Image Clipping (Conditional Contract)
`clip-path` has a native picture-geometry mapping only on SVG-namespace
`<image>` elements (plus the exact imported crop wrapper defined under Images)
and only under this contract:
| Concern | Required form |
|---|---|
| SVG-namespace `<clipPath>` defined inside `<defs>` | Converter looks up one exact local id; missing, duplicate, foreign-namespace, or malformed references fail |
| Contains exactly one direct SVG-namespace supported shape child | Multiple shapes are not composited |
| Shape is one of: `<circle>`, `<ellipse>`, `<rect>` (optional rx/ry), `<path>`, `<polygon>` | These map to DrawingML geometry (preset or custom) |
| No `clip-rule` or `fill-rule`, whether direct or in inline `style` | DrawingML picture geometry has no equivalent winding-rule control |
| Used only on `<image>` or an exact imported crop wrapper | Shapes, groups, text, and generalized nested SVG targets are **forbidden** |
| SVG clip shape | DrawingML output |
|---|---|
| `<circle>` / `<ellipse>` | Full-frame `<a:prstGeom prst="ellipse"/>`; the child must exactly cover the image frame. A `userSpaceOnUse` circle requires a square physical frame; a normalized `objectBoundingBox` circle may fill any frame |
| `<rect>` / `<rect rx="..."/>` | A plain full-frame rect is a compatible no-op; rounded form maps to full-frame `<a:prstGeom prst="roundRect"/>` with one physical radius adjustment. The rect must exactly cover the image frame and cannot express non-uniform physical corner radii |
| `<path>` / `<polygon>` | `<a:custGeom>` with coordinates mapped into the image frame |
`clip-path` on shapes, groups, or text is forbidden; author the target geometry
directly instead. Use a path/polygon clip when the intended contour does not
cover the full picture frame. A contour that depends on even-odd or another
explicit winding rule is outside this mapping and must be rebuilt as one
unambiguous visible contour or pre-rendered.
---
### 1.3 Static Same-Document `<use>` (Conditional Contract)
**Expansion contract**: Static local reuse is compile-time authoring shorthand. `finalize_svg.py` and
native export replace each qualifying instance with cloned primitive content;
PPTX-to-SVG import emits the resulting primitives and does **not** reconstruct
the original `<use>` / `<symbol>` structure.
| Concern | Required form |
|---|---|
| Reference syntax | Author new SVG with the SVG 2 form `href="#id"`. Legacy `xlink:href="#id"` remains read-compatible and Live Preview normalizes it to `href`; if both attributes exist, their values MUST match. |
| Referenced target | One of `<symbol>`, `<g>`, `<use>`, `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<path>`, `<polygon>`, `<polyline>`, `<text>`, or `<image>`. Nested local `<use>` is recursively expanded. |
| Instance position | Generated `<use x>` / `<use y>` use finite unitless values; an explicit `px` suffix is read-compatible. Omitted values default to `0`. |
| Symbol viewport | A referenced `<symbol>` MUST have a finite four-number `viewBox` with positive width/height. Its `<use>` MUST have positive finite unitless `width` and `height`; an explicit `px` suffix is read-compatible. |
| Aspect ratio | Default/aligned `meet` values and plain `preserveAspectRatio="none"` are supported. `slice`, `refX`, and `refY` are forbidden. |
| Viewport boundary | Symbol artwork MUST stay inside its `viewBox`; expansion does not reproduce symbol overflow clipping. |
| Internal references | Author exact `href="#id"` and `url(#id)` fragments. The expander also reads legacy `xlink:href="#id"` and rewrites all instance-local cloned IDs. |
| Structural metadata | Neither the `<use>` instance nor its referenced subtree may carry `data-pptx-layer*`, chart/table replacement metadata (`data-pptx-replace-with`, `data-pptx-replacement-*`, `data-pptx-import-source`, or `data-pptx-fallback-*`), or `data-pptx-placeholder*`. Author those objects directly instead of reusing them. |
| Safety limits | A reachable reference chain may contain at most 64 instances, and one SVG may expand at most 10,000 local `<use>` instances. |
**Forbidden — unsafe local references**:
- External/file/data URLs, missing targets, conflicting `href` / `xlink:href`,
unsupported target elements, and circular reference chains
- Duplicate IDs on the referenced target, the `<use>` instance, or anywhere in
the reused subtree
- Quoted/whitespace CSS fragment variants such as `url('#id')`; use exact
`url(#id)` when an internal paint/filter/clip reference must be rewritten
**Contract example**:
```xml
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="statusDot" viewBox="0 0 20 20" preserveAspectRatio="xMidYMid meet">
<circle cx="10" cy="10" r="8" fill="#16A34A"/>
</symbol>
<g id="legendRow">
<rect width="120" height="32" rx="8" fill="#F1F5F9"/>
<text x="42" y="22" font-size="16" fill="#0F172A">Ready</text>
</g>
</defs>
<use href="#statusDot" x="80" y="120" width="32" height="32"/>
<use href="#legendRow" x="120" y="120"/>
</svg>
```
---
### 1.4 Imported Native PowerPoint Shapes (Conditional Contract)
`pptx_to_svg.py` emits rendering-neutral metadata when a visible SVG object
originates from `p:sp`, `p:cxnSp`, or `p:grpSp`. This contract is for lossless
import SVGs and unchanged imported objects that remain Slide-local or inside a
slot during mirror materialization. Ordinary authored SVG does not need these
attributes, and no separate source-payload opt-in marker exists.
| Metadata | Placement | Required behavior |
|---|---|---|
| `data-pptx-object` | Logical `<g>` and native carrier | `shape`, `connector`, `group`, or `picture`; never infer the object kind from path appearance. |
| `data-pptx-shape-id` + `data-pptx-shape-scope` | Logical `<g>` and carrier | Preserve the source part-scoped identity. Export remaps duplicate Master/Layout/Slide ids into page-unique ids before rebinding connector references. |
| `data-pptx-frame="x y width height"` | Logical `<g>` and carrier | Own native `a:xfrm` position and size. Lossless import SVGs and tool-side native records use sufficient precision for exact EMU recovery; the model-facing authoring IR may use the compact page-coordinate spelling defined below. Path bounds, stroke, markers, shadows, and text glyph bounds never replace this frame. |
| `data-pptx-prst` | Preset carrier and logical `<g>` | One of the locked 187 DrawingML `ST_ShapeType` values. |
| `data-pptx-av-*` | Preset carrier and logical `<g>` | Preserve the complete validated DrawingML adjustment formula, including non-`val` formulas. |
| `data-pptx-part="geometry"` | One hidden carrier path | The single native export authority for frame, base fill/line/effect, preset/custom geometry, and object identity. |
| `data-pptx-part="geometry-preview"` / `geometry-detail` | Visible preview group/paths | Render the preset's independent path fill/stroke layers. A hash-locked preview group may mirror the carrier's one filter so a multi-path preset renders one aggregate imported effect; these elements are never emitted as duplicate PowerPoint shapes. |
| `data-pptx-preview-sha256` | Logical preset `<g>` and carrier | Detect edits to visible preset paths or paint. A stale preview fails quality check/export instead of silently reusing old native metadata. |
| `data-pptx-geometry-kind="custom"` + `data-pptx-custgeom` or `data-pptx-custgeom-ref` | Custom-geometry carrier | Preserve the validated original `a:custGeom` subtree. If the visible path hash is unchanged, export writes formulas, handles, connection sites, text rectangle, and path list exactly; edited paths compile from current SVG geometry. |
| `data-pptx-start/end-shape-id/site` | Connector logical `<g>` and carrier | Restore `a:stCxn` / `a:endCxn` after scoped shape-id allocation. A connector may retain one zero frame axis; it must not be expanded from visible stroke or marker bounds. |
| `data-pptx-shape-style` or `data-pptx-shape-style-ref` | Native carrier | Preserve a relationship-free `p:style` independently of text, including shapes with no visible text. |
| `data-pptx-effect-status="unsupported"` + `data-pptx-effect-reason` | Imported `p:sp` / `p:cxnSp` logical object and native carrier; imported `p:pic` carrier and logical object; imported `p:grpSp` logical group; imported table `p:graphicFrame` logical group | Record why an encountered source object or text-run `effectLst` / `effectDag` cannot enter the registered target-specific effect mapping without changing semantics. Checker and export stop with the recorded reason; these attributes are diagnostics, not a preserved effect payload or authoring syntax. |
| `metadata[data-pptx-part="txbody"]` with inline Base64 or `data-pptx-ref` | Logical shape `<g>` | Preserve unchanged `p:txBody`, including an empty text body. Content, whitespace, positioning, visible typography, or incompatible child-topology edits invalidate the payload. A source payload with run-level effects then blocks checker/export instead of losing those effects; an effect-free payload uses the normal SVG text fallback. |
**Hard rule — compact native metadata transport**: Type A mirror
materialization moves `p:txBody`, relationship-free `p:style`, and
`a:custGeom` payloads into the content-addressed
`templates/native_payloads.json.gz` store. It also deduplicates repeated native
restoration fields—object identity, frame, preset/custom-geometry guards,
preview/text hashes, connector endpoints, payload references, and adjustment
formulas—into short `data-pptx-native-ref` records in the same store. Checker,
template-structure validation, and export validate and hydrate both layers in
memory. Keep Master/Layout, placeholder, layer, editable-object, diagnostic,
and editable chart/table metadata inline. Legacy inline Base64 and v1
payload-only stores remain readable.
One effect reason remains its existing plain token. If one imported object has
multiple independent unsupported reasons, both marker copies store the same
deduplicated, lexicographically sorted compact JSON string array in
`data-pptx-effect-reason`; adding a later reason must not overwrite an earlier
one. This array is still diagnostic metadata, not an authoring surface.
**Import/authoring representation split**:
| Representation | Contract |
|---|---|
| Lossless import SVG | Keep complete native payload, hidden carriers, and preview evidence in the temporary analysis workspace. It is immutable native-payload backing, not the editable template source. |
| Authoring IR bundle | Keep editable SVGs plus model-readable `authoring_summary.json` and tool-only `authoring_manifest.json`. Exclude opaque payload and duplicate hidden carriers from model context while retaining visible shape intent and a stable document-local `data-pptx-source-ref` on each imported logical object. Compact model-facing imported frames and safe transform page coordinates to at most two decimals before hashing the IR. The summary owns the compact current-file index; the manifest owns source paths and initial hashes and never enters model context. |
| `standard` / `fidelity` output | Use the compact authored-preset contract (§1.5) for newly authored stock shapes; do not transplant opaque import payload or source topology. |
| `mirror` output | Materialize from the edited authoring IR. Rehydrate supported imported metadata only when a Slide-local/slot object's source ref and initial authoring hash still match; otherwise keep the current SVG fallback. Expand fixed Master/Layout group wrappers into direct semantic atoms while preserving source ownership, paint order, and visible appearance. |
**Hard rule — model-facing page-coordinate precision**:
| Surface | Precision contract |
|---|---|
| Imported `data-pptx-frame` in authoring IR | At most two decimals. An unchanged mirror source ref recovers the exact lossless frame before tool-side native-record externalization. |
| `data-pptx-bounds` in generated and final template SVG | At most two decimals. |
| `translate(...)`, `rotate(... cx cy)`, and `matrix(... e f)` | Translation values and rotation centers use at most two decimals. Keep the rotation angle and matrix `a b c d` coefficients unchanged. |
| Protected values | Do not apply this compaction to path/points geometry, normalized crop or nested `viewBox` ratios, gradient offsets, opacity, scale arguments, canonical authored-preset frames, or lossless/tool-side native frames. |
**Hard rule — authoring source refs**: `data-pptx-source-ref` is reserved for
the create-template authoring IR. Its value is unique within one authoring SVG,
not across the workspace, and must be resolved through that document's
`authoring_manifest.json` record by the owning tool. Models MUST NOT read that
machine manifest. Moving a referenced subtree into
`icons/imported/` for readability must preserve the attribute and record it in
the vector inventory; re-inlining re-establishes the same mapping. Final materialized
template SVGs and normal project `svg_output/` must not contain this attribute.
**Hard rule — structural-layer boundary**: An unchanged imported logical object
may keep currently supported metadata while it remains Slide-local or inside a
slot. An imported logical `<g>` cannot be assigned to Master/Layout because
those layers require direct semantic atoms. Mechanically expand a fixed-layer
source group into direct atoms, rebuilding a preset when supported and
otherwise retaining the visible SVG fallback. A newly authored compact preset
`<g>` from §1.5 is the sole group exception: validation proves that it compiles
to exactly one native shape/connector. Do not use this normalization to change
ownership or appearance.
**Hard rule — selective payload**: Do not copy every imported metadata block into
an authored template. Keep the full lossless import SVG separately as immutable
audit/fallback backing. Mirror may reuse only metadata already supported by the
converter on source-ref/hash-matching Slide-local/slot objects; unsupported or
edited objects use the current SVG fallback. `data-pptx-replace-with` remains reserved for the
optional PowerPoint-native Chart/Table replacement contract.
**Registry and rendering rules**:
- The hash-locked shared registry must equal the independent 187-value shape
catalog. Missing, duplicate, unknown, or corrupt definitions fail closed.
- Preset preview paths come from the shared DrawingML formula evaluator; do not
add per-shape Python geometry handlers.
- Preset size is controlled only by `data-pptx-frame` / `a:xfrm`. Adjustment
formulas control the contour inside that frame and are not rescaled when the
frame changes.
- A group transform may move, scale, rotate, or flip the complete logical
shape without invalidating its preview fingerprint. Editing a generated
`geometry-detail` path directly is unsupported unless the carrier metadata
and preview fingerprint are regenerated together.
- Unknown or malformed SVG transform operations fail closed. DrawingML cannot
represent arbitrary shear, so a non-orthogonal transform must stop native
export instead of being silently approximated as rotation and scale.
- Opaque XML payloads containing any `r:*` relationship attribute are never
copied into a new slide part. Relationship-bearing text content and
shape-level `a:blipFill` use the existing rebuilt visual fallback and are
not covered by atomic `p:sp + p:txBody` rehydration.
- Unknown future presets and explicit `unsupported` geometry status never
downgrade silently to `rect`; native export stops with the recorded reason.
**Fidelity boundary**: native preset/custom geometry, logical frame, scoped
identity, connector topology, and relationship-free unchanged horizontal
text-body semantics on ordinary shape fills are `Native-stable`. The SVG
preview paint for gradient/pattern
`darken`/`lighten` layers is `Native-normalized`; original group child
coordinates, shape-level image-fill reconstruction, and vertical-text
reconstruction are also normalized rather than byte-identical OOXML.
---
### 1.5 Authored Native PowerPoint Presets (Conditional Contract)
New SVG pages and project-owned canonical reusable templates may opt one
complete geometric object into a native DrawingML preset through the
deterministic fragment helper. Selection behavior lives in
[`native-shape-authoring.md`](./native-shape-authoring.md); this section owns
the machine contract. This compact canonical form describes the intended
preset, frame, adjustments, and paint once, keeps only registry-generated
visible SVG paths, and embeds no source OOXML or serialized preview fingerprint.
| Metadata / structure | Required behavior |
|---|---|
| `data-pptx-authoring="preset"` | Appears once on the logical `<g>`; distinguishes strict project authoring from legacy/imported metadata. |
| `data-pptx-object` | `shape` or `connector`; connector-family presets must use `connector`, and `connector` must use a connector-family preset. Authored connectors require `fill="none"` plus a visible stroke and export as unconnected `p:cxnSp`. |
| `data-pptx-prst`, `data-pptx-frame`, `data-pptx-av-*` | Generated together from the locked registry and written once on the logical group. The frame is the helper's exact four-part, space-separated ordinary-decimal spelling and remains authoritative even when visible path bounds differ; commas, scientific notation, leading `+`, and redundant decimal spellings are rejected. |
| Local `fill` / `stroke` plus supported paint attributes | Base paint is written once on the group; a visible stroke also carries an explicit width. Canonical page/template authoring keeps channel paint local. Compatible ancestor paint/opacity may compose under the general SVG rules and receives a recommendation warning. |
| Ordered direct `<path>` children | Browser-visible registry layers only. Each child writes just its required path-level fill/stroke override; labels and decorations stay outside the atomic group. |
| No carrier / wrapper / fingerprint | `data-pptx-part`, hidden geometry carriers, preview wrappers, and `data-pptx-preview-sha256` belong to expanded import/compatibility transport, not canonical project authoring. |
Generate one fragment at a time:
```bash
python3 ${SKILL_DIR}/scripts/preset_shape_svg.py render rightArrow \
--id p03-growth-arrow \
--frame 160 210 320 112 \
--fill "#2563EB" \
--stroke none \
--adjust "adj1=val 50000"
```
**Hard rule — helper-only metadata**: never add or edit authored preset
metadata or registry paths by hand. The compact helper output is atomic.
Regenerate it when preset, frame, adjustment, fill, stroke, or stroke width
changes. Replace the whole fragment with ordinary SVG when free contour editing
is required.
Template ownership metadata is orthogonal to preset geometry. After inserting
the complete helper output, `create-template` may add only the registered
`data-pptx-layer`, `data-pptx-editable`, `data-pptx-carrier`, or
`data-pptx-role` attribute needed by the surrounding structured contract. It
must not change preset/frame/adjustment/paint metadata or any direct path.
**Reusable-template boundary**: a project-owned canonical template may retain
one complete helper-generated atomic fragment when the stock preset is an exact
semantic match and its paint stays inside the authoring boundary below. The
fragment is an executable exemplar and one semantic atom, not a freely editable
template primitive. It may be Slide-local, the one carrier of an `object` slot,
or a direct Master/Layout fixed atom. An adaptation may reuse it unchanged only
when preset, frame, adjustments, and paint are unchanged; otherwise regenerate
the whole fragment with the helper.
Imported, mirror, and third-party templates are never upgraded by contour
inference.
**Hard rule — visible page closure**: the helper prints a complete visible
fragment to stdout; export never invents its preview. The main Agent inserts
that output into the hand-authored page or canonical reusable template. The
helper cannot write a project, select layout, or generate a page.
**Authoring paint boundary**: v1 accepts `none` or six-digit solid HEX fill and
stroke, optional fill/stroke opacity, stroke width, line cap, and line join.
Generated pages take colors from `spec_lock.md`; `create-template` authored
templates take them from the confirmed brief and template `design_spec.md`.
Use ordinary SVG for gradients, patterns, filters, or other treatments outside
this narrow contract. Registry-derived multi-path darken/lighten colors are
authorized derivatives of the locked base paint and do not count as color
drift. Mirror preserves source paint under §1.4 instead.
**Validation**: quality check and export both rerender authored fragments from
`preset + frame + adjustments + group paint` and compare every visible path and
path-level paint override directly. Registry-path edits, geometry metadata that
leaves those paths stale, unknown adjustments, out-of-range frames/transforms,
zero-scale transforms, and shear/skew fail closed. Export expands the validated
compact group only in memory and reuses the lossless native-shape conversion
path. Older authored carrier/preview fragments remain compatible as ordinary
Slide-local input and
receive a non-blocking migration warning; they do not gain the new compact
group's structured-atom exception. `pptx_to_svg` expanded output remains the
lossless round-trip form and is not warned as authored input.
**Fidelity boundary**: an unchanged authored fragment is `Native-stable` as
one `p:sp` or `p:cxnSp`. Text remains outside the atomic fragment and may export
as a grouped editable text box. Authoring v1 creates only unconnected
`p:cxnSp`; it does not accept hand-written endpoint/site metadata. An
`actionButton*` preset maps visual geometry only. Preset appearance never
invents connector attachment, action behavior, navigation targets, or
hyperlinks.
---
## 2. Conditional Compatibility Mappings
### 2.1 Literal Geometry Lengths and Inline Geometry
**Hard rule — direct geometry length grammar**: New generated SVG writes the
following XML geometry values and `stroke-width` as finite unitless ordinary
decimals in the page `viewBox` coordinate space, for example `x="120"` and
`stroke-width="2"`. The explicit `px` suffix is read-compatible and receives a
recommendation warning. No other unit is registered for this surface.
| Element / surface | Direct length attributes |
|---|---|
| `<svg>`, `<rect>`, `<image>`, `<use>` | `x`, `y`, `width`, `height`; `<rect>` also `rx`, `ry` |
| `<circle>` | `cx`, `cy`, `r` |
| `<ellipse>` | `cx`, `cy`, `rx`, `ry` |
| `<line>` | `x1`, `y1`, `x2`, `y2` |
| `<text>` / positional `<tspan>` | `x`, `y`; `<tspan>` also `dx`, `dy` |
| Any supported painted element | `stroke-width` |
`width`, `height`, `r`, `rx`, `ry`, and `stroke-width` must be non-negative;
the stricter positive `<use>` symbol-viewport rule remains in §1.3. `pt`,
`pc` / `pica`, `in`, `cm`, `mm`, `q`, `em`, `rem`, percentages, unknown units,
non-finite values, expressions, scientific notation, leading plus signs, and
trailing decimal points are invalid here even when generic SVG/CSS defines
them. A missing attribute may use its documented SVG/project default; an
explicitly supplied invalid value never falls back to that default.
The following geometry properties may appear in the same element's
`style="..."`. The pipeline materializes them as
XML geometry attributes before SVG post-processing and native PPTX conversion.
An inline geometry declaration overrides an existing same-name XML attribute.
| Element | Recognized properties |
|---|---|
| `<rect>` | `x`, `y`, `width`, `height`, `rx`, `ry` |
| `<circle>` | `cx`, `cy`, `r` |
| `<ellipse>` | `cx`, `cy`, `rx`, `ry` |
| `<image>` | `x`, `y`, `width`, `height` |
| `<svg>` | `x`, `y`, `width`, `height` |
| `<use>` | `x`, `y`, `width`, `height` |
**Hard rule — inline geometry grammar**: every non-zero value is one finite
`px` literal, such as `120px` or `-8.5px`; exact zero may be unitless. `width`,
`height`, `rx`, `ry`, and `r` must be non-negative. Percentages, `auto`,
`calc()`, `var()`, `!important`, `inherit`, and every other unit are forbidden.
Do not put geometry on an unsupported element: line endpoints, text positions,
path data, and polygon/polyline points remain XML attributes.
**Forbidden — CSS geometry cascade**: `<style>`, `class`, selector rules,
external stylesheets, and imported styles remain forbidden. This contract is
only for literal declarations in an element's own `style` attribute; PPT Master
does not compute CSS cascade or custom properties. Root canvas authority remains
the `viewBox`, regardless of root `<svg>` compatibility width/height values.
### 2.2 Group Opacity Compatibility
**Default — descendant alpha (may preserve compatible group opacity)**: New
`svg_output/` and reusable templates put alpha on the affected descendant
paint, text run, picture, or supported effect. DrawingML has no isolated
group-alpha model, so overlapping descendants can look different when one
group value is distributed across them.
The converter nevertheless accepts `<g opacity="...">` and inline group
`opacity` by multiplying group alpha into descendants. That path is
`Approximate`; nested group/child alpha multiplies, and `--native-charts-and-tables`
rejects transparent native table/chart markers. The quality checker reports a
non-blocking fidelity warning so existing or intentionally authored input can
continue without modification.
---
## 3. Canvas Format Quick Reference
Use the already locked canvas id and exact viewBox. [`canvas-formats.md`](canvas-formats.md) owns format selection; this core owns only SVG conformance on that canvas.
---
## 4. Required Page Contract and Conditional Packaging
### 4.0 Complete Page-Design Contract
| Concern | Requirement |
|---|---|
| Visible slide result | The completed `svg_output/<slide>.svg` MUST contain every visible text, image, shape, diagram, chart/table fallback, background, and template-derived layout element intended for that slide. External visual assets are valid when the SVG references them explicitly. |
| Template/control inputs | Templates, `design_spec.md`, and `spec_lock.md` guide authoring. Do not depend on them to add visible elements after the page SVG is complete. |
| PPTX translation | The exporter may map represented SVG content to DrawingML/native objects and deduplicate represented elements into Master/Layout/Slide parts. It MUST NOT invent visible slide content absent from the SVG. |
| Excluded package behavior | Speaker notes, animations, transitions, narration audio, PPTX relationships, and direct native-PPTX workflows remain separately owned. They are not part of the SVG page-design contract. |
**Hard rule — page-design closure**: A final page SVG is the sole visual/design authority for that page on every SVG-authoring route. SVG is not the authority for the entire PPTX package.
### 4.1 Semantic SVG Marker Contract
Semantic markers are minimal compiler hints. Flat pages declare one root `data-pptx-page-role` and omit Master/Layout/layer/placeholder markers. Structured pages carry their final root identity, layer atoms, slots, and native-object metadata from authoring start and omit `data-pptx-page-role`. Use `data-pptx-role` with a stable `id` only when no specialized marker expresses page-frame behavior. Keep ordinary visible content in SVG attributes/text; [`semantic-svg.md`](semantic-svg.md) owns the vocabulary.
- **Canvas authority**: New authoring writes `viewBox="0 0 W H"` with positive
integer pixels from the lock. Numerically equivalent spellings and positive
fractional imported dimensions remain compatible; export quantizes once at
`1 SVG px = 9,525 EMU`. Invalid/non-finite values, non-zero origin,
non-positive size, or unsupported PowerPoint dimensions are errors. All pages
and Layout prototypes in one build share the numeric canvas and match
`spec_lock.md canvas.viewBox`; standalone templates match `design_spec.md
canvas_viewbox`. Optional root `width`/`height` do not override `viewBox`.
Root `<svg>` transform is forbidden; nested crop and `<symbol viewBox>` keep
their own contracts.
- **Font portability**: font families used by the deck must resolve to installed
export faces. `@font-face` remains forbidden; the typography contract lives in
[`strategist.md §g`](strategist.md).
- **Icon placeholders**: `<use data-icon="library/name">` is a pipeline-specific
form, distinct from local SVG reuse. Follow the contract in
[`../templates/icons/README.md`](../templates/icons/README.md).
- **Local reuse**: ordinary same-document `<use>` follows §1.3.
### 4.2 Conditional Editability and Package Promotion
These forms are needed only when the stated PPT behavior matters:
| Desired behavior | Required form |
|---|---|
| One editable PPT text frame with mixed inline formatting or wrapped prose | Keep one logical paragraph in one `<text>`. Use non-positional `<tspan>` children for inline runs. Keep the first wrapped line as direct text and put each later line in a direct positioned `<tspan>` that repeats the parent `x` and uses positive relative `dy`; an all-`<tspan>` form may start with `dy="0"`. Same-size, evenly stacked lines flow in the current paragraph; a font-size change, list marker, or larger accepted gap starts another paragraph in that frame. Sibling `<text>` elements are forbidden as line breaks for one paragraph; they remain valid for semantically independent frames. |
| Stable object grouping or object-level animation anchor | Wrap the intended object in `<g id="...">`. Content grouping is **mandatory** per §4.3 — a top-level `<g id>` is also the animation anchor; it is not an optional convenience. |
| Native PowerPoint background promotion | Outside structured mode, the first eligible visual layer may be a direct full-canvas `<rect>` or one inside a simple single-child group. Its fill must have a registered native mapping (solid, linear/radial gradient, or preset pattern), and it must have no transform, filter, clip, rounding, or visible stroke. Export writes the fill as Slide `p:bg`; image elements remain pictures. Structured routes use the narrower explicit solid-background ownership contract in [`pptx-structure-interface.md`](./pptx-structure-interface.md). |
| Free-design / brand-only PowerPoint structure | Use `pptx_structure.mode: flat`. Keep every represented object Slide-local; export materializes one clean project-owned Master plus one Blank Layout from the current lock, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks. Do not author Master/Layout identities, layers, or placeholder slots. |
| Reusable template-based PowerPoint Layout | Select one complete authoring SVG per page in `page_layouts`, declare each unique Master/Layout definition once, and assign pages through `page_pptx_layouts`. Strict preserves the prototype contract; adaptive retains its Master and may define and assign a new explicit Layout key during page authoring. Non-mirror skin follows `spec_lock`. |
**Hard rule — supported shape conversion**: Every PPT editability claim in this specification refers to the project converter reading `svg_output/` and emitting native DrawingML. `svg_final/` is a self-contained visual preview that may be inserted into PowerPoint as an SVG picture. PowerPoint's manual Convert-to-Shape operation is unsupported; do not narrow the authoring contract to its undocumented SVG subset.
### 4.3 Element Grouping (Mandatory)
**Hard rule — root groups protect body-text layout**: Every visible direct root `<g>` declares positive root-coordinate `data-pptx-bounds="x y width height"`. Keep it when frame/native coordinates size one PowerPoint object; placeholder bounds also supply the slot frame. Checker validates this subcanvas against the root `viewBox`, then recursively validates only estimable `<text>` descendants against it. Nested groups and all shapes, images, paths, `<use>` instances, effects, and object frames are not content-boundary inputs. Per side, Checker ignores text/bounds overflow through `1px`, warns through `5%` of the containing boundary dimension, and fails above `5%`. Bounds do not clip or reflow.
Wrap each logical Slide-local body unit in a descriptive top-level `<g id>`; aim for **38 ordinary groups**, each becoming one animation step. Nested implementation groups may remain anonymous and need no bounds; any nested bounds are ignored. Flat pages use ordinary groups; structured slots already qualify, while titles, direct atomic Master/Layout elements, and canvas-level decoration may remain root primitives.
**Structural atoms and slots are excluded automatically.** `data-pptx-layer` and `data-pptx-placeholder` semantics are read first; otherwise explicit `data-pptx-role` values (`background`, `decoration`, `header`, `footer`, `chrome`, `watermark`, `page-number`, `logo`) mark Slide-local static framing (§4.1, [`semantic-svg.md`](semantic-svg.md)). A normal slot group has exactly one direct compatible carrier; several drawing atoms require the explicit composite `object` proxy fallback. Native chart/table carrier groups retain their specialized [`native-data-interface.md`](./native-data-interface.md) contract.
**What to group** (one `<g id>` per unit):
| Grouping unit | Contains |
|---|---|
| Card / panel | Background rect + optional shadow (only if it floats over a photo/colored panel, [`svg-effects.md`](./svg-effects.md) §6.4) + icon + title + body text |
| Process step | Number/marker + icon + label + description |
| List item | Bullet / number + icon + title + description |
| Icon-text combo | Icon element + adjacent label |
| Page header | Title + subtitle + accent decoration |
| Page footer | Page number + branding |
| Decorative cluster | Related decorative shapes (rings, dots, orbs) |
An authored native preset fragment (§1.5) is already an atomic `<g id>` and
counts as one content group. Keep it top-level when it stands alone. When it
needs a label or decoration, place the preset and those siblings inside a
separate parent content group; never put them inside the preset group itself.
**Forbidden**:
- One giant `<g>` around the whole slide (collapses to a single animation step).
- Many ungrouped Slide-local `<rect>` / `<text>` / `<path>` atoms — they have no stable sidecar target and selection/editing degrades. Primitive fallback applies only when the root contains no top-level `<g>` at all; it is capped at 8 visible primitives.
- One group per icon / text line / mark (too many steps).
- Anonymous top-level groups — every top-level semantic group needs a descriptive `id`.
**Naming — required.** A descriptive, page-unique `id` on every top-level content `<g>` (`card-1`, `step-discover`, `header`, `footer`) is mandatory; it is the stable SVG-side animation and trace anchor. An anonymous top-level group still converts, but `animations.json` cannot reference it; an anonymous one-child implementation wrapper may also flatten. Primitive fallback is unrelated and applies only to roots with no top-level groups.
```xml
<g id="card-benefits-1" data-pptx-bounds="60 115 565 260">
<!-- Shadow only if the card floats over a colored panel; on flat white, omit it. -->
<rect x="60" y="115" width="565" height="260" rx="20" fill="#FFFFFF" filter="url(#shadow)"/>
<use data-icon="chunk-filled/bolt" x="108" y="163" width="44" height="44" fill="#0071E3"/>
<text x="105" y="270" font-size="56" font-weight="bold" fill="#0071E3">10×</text>
<text x="250" y="270" font-size="30" font-weight="bold" fill="#1D1D1F">Faster</text>
<text x="105" y="310" font-size="18" fill="#6E6E73">Reduce production time from days to hours.</text>
</g>
```
---
## 5. Workflow Authority
The serial post-processing and export workflow belongs to
[`generate-pptx.md`](../workflows/generate-pptx.md) Step 7. This file defines SVG authoring boundaries
and intentionally does not mirror commands, flags, or output behavior.
---
## 8. Scope Boundary
Generate project structure, commands, quality-gate order, and export products
are owned by [`generate-pptx.md`](../workflows/generate-pptx.md). They are
intentionally outside this SVG authoring policy.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
> See [`strategist.md`](./strategist.md) for the core role and load trigger.
# Strategist Image Planning
Conditional extension for formula assets, proposed / confirmed image elaboration, AI rendering selection, and `design_spec.md §VIII` resource planning.
**Trigger**: Core first derives proposed `recommend.image_usage`. Load this module before Stage-2 direction construction when that proposal contains any non-`none` source, when the user supplied an explicit non-`none` image constraint, or when formula handling is triggered. After confirmation, the confirmed sources bound production: confirmed `none` with no formula trigger stops before resource authoring. On a formula-only path, read §3 and the formula-row rules in §4; skip non-formula planning. [`strategist.md`](./strategist.md) owns source recommendation; this module owns image-dependent candidates, production detail, and §VIII rows.
---
## 1. Proposed and Confirmed Image Plan
Before Stage 2, use proposed sources only for candidate construction. After confirmation, discard candidate-only sources, map the confirmed set through [`strategist.md`](./strategist.md) §h, and honor every explicit role or page instruction in `image_notes`; this module never adds a source. The confirmed set is a production requirement, not a fresh candidate list: represent every confirmed non-`none` source at least once. Asset inventory and later aesthetic judgment may shape the unconfirmed count, subject, placement, and composition, but must not delete, substitute, or demote a confirmed source.
For illustration, apply this precedence: confirmed `none` → explicit user intent → the locked visual style's `Illus.` propensity (`core` / `supportive` / `sparse`) → none. Propensity controls the lean, not the source or a page quota. When illustration is active, prefer one coherent motif family across hero/section anchors and local spots, but only when the confirmed assets can form that family.
For ≥3 AI-generated same-family spots, plan one unplaced `ai` Illustration Sheet row plus one placed `slice` row per used element; only slice rows enter `spec_lock.md images`. State the intended placement shape family in the sheet reference and use separate sheets for incompatible shapes. [`image-generator.md`](./image-generator.md) §4.3 owns grid, ratio, slicing, and execution details. Stage 3 chooses the AI execution path under `image-generator.md` §7; do not pre-empt or re-pick it here.
## 2. AI Image Strategy — propose before Stage 2; lock only for confirmed `ai`
When proposed sources include `ai`, read every entry in [`image-renderings/_index.md`](./image-renderings/_index.md) before constructing Stage 2. Unless the user or active template already names a rendering, place at least three credible, distinct preset renderings across the coordinated safe/shifted/bold directions; a genuine compatibility shortfall may return fewer with a reason. Each preset `image_strategy` carries localized `rendering`, `visual`, and `mood` only. Mood includes a recognizable real-world analogy. Image colors always inherit that direction's deck HEX roles; never add an image palette or alter deck colors to rescue a rendering.
Also write one `custom_candidates.image_strategy` under the Confirm UI contract: localized `name` / `visual` / `mood`, `rendering: custom`, and non-empty localized `behavior` satisfying the catalog grammar. Keep it unselected unless the user supplied it (`recommend.image_strategy: custom`); under a template it obeys inherited identity and application. Only a selected custom locks its edited behavior as `image_rendering_behavior`; otherwise discard it downstream. Ignore legacy `image_palette`.
For specialized or regulated paper-figure subjects, preserve the prompt depth required by [`image-generator.md`](./image-generator.md) §4.2 rather than shortening to a generic brief. Scan the outline for genuine image-led pages, list the proposed hero pages in Stage-2 `image_notes` so the user can retain, edit, or remove them in the same confirmation, then mark only the confirmed pages' AI rows `page_role: hero_page`; local is the default. `text_policy: embedded` is reserved for lettering that must be fused into the artwork; ordinary titles, data, labels, and prose remain editable SVG. Analyze confirmed provided assets before writing §VIII.
## 3. Formula Asset Policy
Formula rendering is a conditional choice surfaced in Stage 3 production confirmation. Recommend one policy and let the user confirm or override it:
| Policy | Behavior | Use |
|---|---|---|
| `mixed` (default) | Render complex expressions to PNG; keep simple inline math as editable text / Unicode | Most academic, engineering, educational, and technical decks |
| `render-all` | Render every formula-worthy expression to PNG | Formula-heavy teaching / research decks where consistency matters more than editability |
| `text-only` | Keep expressions as editable text / Unicode | Business decks, light technical briefs, or an explicit editability preference |
`$...$` / `$$...$$` in source material are input signals only. Never scan output files for dollar-delimited formulas. Fractions, radicals, integrals, sums, limits, matrices, multiline derivations, and complex super/subscripts are formula-worthy; short variables, simple assignments, percentages, and expressions such as `O(n log n)` normally remain text. Never invent an equation for decoration.
For `mixed` or `render-all`, write selected source expressions to `<project_path>/images/formula_manifest.json` before writing the final spec, then run:
```bash
python3 skills/ppt-master/scripts/latex_render.py <project_path>
python3 skills/ppt-master/scripts/analyze_images.py <project_path>/images
```
Follow `latex_render.py --help` for the manifest fields. The renderer writes dimensions, ratio, file, provider, and status back into it. Formula PNGs default to transparent; use an opaque final background only when the asset requires it.
## 4. Image Resource List
Add §VIII for every confirmed non-`none` source and selected formula; a formula-only plan contains only formula rows. Fill the scaffold's filename, dimensions/ratio, layout suggestion/pattern, purpose/type, acquisition, status, reference, and conditional AI fields. `Acquire Via` is `ai`, `web`, `user`, `formula`, `placeholder`, or `slice`; status follows [`svg-image-embedding.md`](./svg-image-embedding.md). When an asset is not yet available, retain its confirmed-source row as `Pending` or `Needs-Manual`; never remove the row or change `Acquire Via` to make the Design Spec look complete. Only after §VIII passes the final-confirmation fidelity gate, project the same planned filenames and acquisition sources into `spec_lock.md images`; do not redesign the image plan while writing the lock. References describe visual intent: AI uses subject + intent + composition without repeating rendering or HEX; web uses a concrete subject plus a few positive quality descriptors; formula preserves the source LaTeX and placement intent.
🚧 **GATE — non-formula rows**: read every entry in [`image-layout-patterns.md`](./image-layout-patterns.md). Copy one primary `#<id> <name>` plus any modifier names verbatim into each row; no empty, paraphrased, or invented ids. For decks with at least four image-bearing pages, use an Image-as-Canvas + Native Overlay pattern at least once unless every image is purely a cover, divider, or atmospheric backdrop; record the legitimate exception below the table. Reconsider a plan that collapses every row to the same left/right or top/bottom split.
Choose narrative intent before dimensions: hero/full-bleed, atmosphere/background, side-by-side, or accent/inline. Only side-by-side containers follow native ratio; portrait and multi-image calculations belong to [`image-layout-spec.md`](./image-layout-spec.md). Most assets are croppable. Add `no-crop` in `spec_lock.md images` only for screenshots, charts, certificates/contracts, dense diagrams, and every formula; formula rows use `Type: Latex Formula`, `Acquire Via: formula`, and `Rendered` or `Needs-Manual`.
Judge `text_policy` per AI row using [`image-generator.md`](./image-generator.md) §5.3; paper figures, academic schematics, panel comparisons, and data-axis graphics are positive triggers for reconsidering an all-`none` plan. Step 5 dispatches pending `ai` / `slice` rows to Image_Generator and pending `web` rows to Image_Searcher; formula rows bypass both.
@@ -0,0 +1,82 @@
> See [`strategist.md`](./strategist.md) for the core role and load trigger.
# Strategist Template Planning
Conditional extension for applying an installed Brand/Layout/Deck workspace to Stage 2 recommendations and the execution lock.
**Trigger**: Load only when Generate Step 3 copied an explicit workspace path into `<project_path>/templates/`. Bare template names, style words, and free-design projects do not trigger this module.
---
## 1. AI-Authored Template Application Plan
**Template vs preset**: A style mention and a template directory are different inputs. Bare names and style words map to a visual-style preset; only the installed workspace activates the rules below. Its fused `<project_path>/templates/design_spec.md` is the template-design source.
**Legacy template boundary**: A template containing `native_structure.json`, `source_template.pptx`, missing root Master identity, direct atomic placeholders, or old `baseline` / `preserve` / distillation metadata is not a Generate Step 3 input. Create a current workspace through [`create-template`](../workflows/create-template.md), preferably from the original PPTX when native topology matters. Do not mutate the input in place.
**No template-mode confirmation**: Never ask the user to select `template_reuse_scope`, `template_adherence`, `mirror`, `layout`, `style`, `strict`, or `adaptive`. These are internal execution values for the current exporter. The user communicates intent in natural language; explicit instructions such as “全部原样保留”, “从中选合适的页面”, “可以重组”, or “只参考视觉” are authoritative. Without an explicit instruction, Strategist decides.
Immediately before authoring the Stage-2 solution, load each relevant template
resource once per path + SHA and inspect:
- the installed `design_spec.md`, actual Page Roster, and relevant SVG prototypes;
- the current communication contract, source obligations, planned page count, and content shape of every planned page;
- the user's natural-language instructions, including any page names/numbers or elements they explicitly require.
Then author one plan that decides all of the following without presenting an option menu:
- whether the full prototype set, a relevant subset, or only the design language is useful;
- which prototype each generated page starts from, which template pages are skipped, and which prototypes are repeated or reordered;
- whether content is inserted directly, reorganized inside the existing structure, or rebuilt while retaining only visual language;
- which visible elements must remain literal because the user said so, and which may change to serve the current content.
Template size is evidence, not policy. A short template may use every prototype when the content genuinely fits; a 2030 page source may contribute only a few suitable pages, or several pages may be reorganized into a new sequence. Never infer that all pages must be kept or that visible sample content is protected merely because it exists in the template.
Record the resulting exporter plan internally:
| Internal value | When the authored plan requires it |
|---|---|
| `template_reuse_scope: mirror` | The workspace has `replication_mode: mirror`, the plan calls for literal page reuse, and each page changes only allowed visible text values while preserving visual and text-node topology. |
| `template_reuse_scope: layout` | The plan reuses the template Master/Layout system and prototypes while allowing current-project content and appearance decisions. |
| `template_reuse_scope: style` | The plan uses only color, typography, decoration language, or rhythm and intentionally creates flat free-design pages. |
| `template_adherence: strict` | Every structured page fits an existing prototype contract without changing its Layout identity or slot topology. Mandatory for `template_reuse_scope: mirror`. |
| `template_adherence: adaptive` | Structured reuse remains useful, but at least one page needs a new explicit Layout under the selected Master. |
Write only the derived values to `spec_lock.md pptx_structure`; omit `template_adherence` for `style`. Do not put these internal values in `design_spec.md`, `recommendations.json`, the Confirm UI, or `result.json`.
**Mandatory — natural-language Stage-2 plan**: Summarize which prototypes are used/skipped/repeated/reordered, what stays literal, and what may be replaced or reorganized. Write it to top-level `template_application.value` in Stage-2 `recommendations.json`; omit it without an active template. After Stage 2, re-read the confirmed `result.json` value (or exact chat answer), never the initial recommendation. Blank returns the decision to Strategist. Persist the effective plan on one line as `- **Template Application**: <prose>` in `design_spec.md §I`, then derive internal reuse/adherence values and mappings; never copy the prose to `spec_lock.md`. Do not add a questionnaire, internal controls, or fixed template-use options.
**Three-stage boundary**: An installed template changes the content of Stage 2, never the confirmation sequence. Run Stage 1 → Stage 2 → Stage 3 in order in both Confirm UI and chat fallback; do not skip a stage or treat template inspection as user confirmation. On browser timeout, return to the same stage in chat.
---
## 2. Scenario Fit and Inherited Design
**Mandatory — decide from the §1 inspection**: For an installed `kind: deck`, compare the retained Template Overview with the confirmed audience, intent, outcome, delivery context, artifact afterlife, and source obligations. Compare the retained Page Roster/relevant SVG prototypes with required narrative roles, content shapes, slots, and capacity. Reopen a resource only when its path + SHA changed. The template describes what exists; it never overrides the current project or own required/optional/repeatable or fixed/replaceable/example-only policy. For `kind: layout`, compare only structural roles, slots, and capacity.
| Internal scope | Appropriate when |
|---|---|
| `mirror` | The artifact repeats a known form; literal appearance and text topology are requirements; new content fits existing roles and slots. |
| `layout` | The structural system and brand continue, but the communication outcome requires reflow, new emphasis, or an adaptive Layout. |
| `style` | Only visual identity is reusable, or the outcome requires a different sequence, density, or composition system. |
When the communication contract conflicts with the workspace, choose and state the best-fit application plan in the complete Stage-2 solution. Surface the mismatch only when it materially limits the result; do not respond with a mode questionnaire. Template capability constrains what is legal; scenario fit decides what is useful.
> Internal note: `content_divergence` controls source reorganization; the AI-derived `template_reuse_scope` records the reused layer; `template_adherence` records whether a structured plan keeps or extends existing Layout identities.
**Template design precedence**: User overrides win. Otherwise template colors and title/body stacks are fixed anchors, not industry defaults. Each of ≥3 Stage-2 directions still carries all six palette roles and complete font objects: repeat fixed values and vary only template-open roles. Keep declared icon and image constraints.
---
## 3. Structured Lock Planning
For `mirror` / `layout`, write `pptx_structure.mode: structured` plus `template_adherence: strict|adaptive`; mirror always writes `strict`. Do not write legacy `baseline`, `template`, `preserve`, `layout_strategy`, or Layout-kind rows.
- **Master roster**: Write one `pptx_masters` row per Master as `<master_key>: <picker name>` and copy the workspace's prototype roster. Keys use 164 ASCII letters, digits, dots, underscores, or hyphens, start with a letter/digit, and contain no spaces; human-readable spaces belong only in the picker name. Master visuals are root-level atomic elements and may never be `<g>`.
- **Reusable Layout roster**: Write every unique Layout once as `<layout_key>: <master_key> | <PowerPoint layout name> | <prototype source>`. Copy installed `template:<basename>` sources, including currently unused Layouts. A new adaptive Layout uses its first generated `P<NN>` as source. Reuse a key only when fixed atoms and slot ids/types/indices/bounds/binding modes are identical. Name authored keys after composition, never page topic. A Layout may intentionally have zero slots; do not manufacture an empty `utility` kind or full-page fake slot.
- **Page assignment**: Write exactly one `page_pptx_layouts` row per page. Each key must exist in `pptx_layouts`. Check that distinct compositions do not collapse into role-only keys and that one skeleton does not split into topic-specific keys.
- **Slot planning**: Each reusable slot is a direct root `<g id>` with `data-pptx-placeholder`, positive design-zone bounds, and exactly one compatible direct carrier. Bounds come from the intended safe area, column, panel inset, or media frame—not sample text ink. A genuinely composite region may use only the explicit `object` + `proxy` downgrade.
- **Adaptive refinement**: Initial definitions are complete. If construction changes reusable framing or slot topology/bounds, Executor creates one new definition sourced from that page and updates its assignment; it never mutates a reused contract silently. Export only compiles declared structure and never discovers or clusters Layouts.
- **Input prototypes**: Add one `page_layouts` row per page. Strict preserves that SVG's contract; adaptive keeps its Master and may declare a new output Layout; mirror also preserves literal visuals and text-node topology.
**Chart compatibility**: Use `page_layouts` together with `page_charts` only when the selected prototype shell is compatible. For a chart page without an exact roster match, adaptive mode starts from the closest neutral prototype and declares an output Layout; strict mode selects an existing compatible Layout or revises the outline. Never omit `page_layouts` on a structured route.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,746 @@
> See [`shared-standards-core.md`](./shared-standards-core.md) for the mandatory SVG foundation.
# SVG Effects and Geometry Specification
Conditional reference for advanced paint, effects, transforms, freeform/radial geometry, and constructed visual styles. Load only when the page uses one of these capabilities.
**Cross-reference map**: unqualified §1, §2, and §4 references point to [`shared-standards-core.md`](./shared-standards-core.md); §6 references are local to this file.
## 6. Advanced SVG Effects and Authoring Techniques
**Reference — not a constraint**: “Advanced” means capability depth, not rarity.
Use any compatible technique when it serves the locked visual style and content.
### 6.1 Availability, Precedence, and Fidelity
| Decision layer | Authority |
|---|---|
| Technical validity | Required / Forbidden / Conditional contracts in this file |
| Project values | `<project_path>/spec_lock.md` colors, fonts, icons, and images |
| Aesthetic fit | Locked `visual_style` / `visual_style_behavior` |
| Per-page choice | Content purpose, hierarchy, legibility, semantics, and rhythm |
**Hard rule — illustrative colors**: colors below demonstrate syntax only;
generated pages use matching `spec_lock.md` roles. Fidelity labels are defined
in [`shared-standards-core.md`](./shared-standards-core.md). Review an `Approximate` result in native PPTX
when the effect carries material meaning.
---
### 6.2 Color, Alpha, and Opacity
Compatible paint grammar includes recognized named colors, `rgb()` / `rgba()`,
`hsl()` / `hsla()`, and `#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`. The
converter also tolerates legacy bare 3/4/6/8-digit hexadecimal tokens.
**Default — canonical generated paint tokens (may preserve compatible
alternatives)**: New `svg_output/` and reusable template SVGs write solid paint
as uppercase six-digit `#RRGGBB`. `fill` / `stroke` may instead use lowercase
`none` or the exact local reference form `url(#id)`. Named colors, lowercase or
short/alpha HEX, functional colors, and bare legacy HEX remain supported input.
The quality checker prints an optional canonical rewrite as a recommendation
warning; it does not require modification or block export.
Explicit empty, malformed, or unrecognized paint values are errors in both
Checker and exporter preflight; neither converts unknown intent into
`noFill` or default black. Omitted properties still follow their own element
contract, such as SVG's default fill or §6.3's required gradient-stop color.
| Intent | Canonical authoring | Native result / fidelity |
|---|---|---|
| Solid fill or text paint | `fill="#RRGGBB"` | Solid DrawingML paint; `Native-stable` |
| Fill/text alpha | Opaque `fill` + `fill-opacity="0..1"` | Fill/run alpha; `Native-stable` |
| Stroke alpha | Opaque `stroke` + `stroke-opacity="0..1"` | Line/outline alpha; `Native-stable` |
| Gradient-stop alpha | Opaque `stop-color` + `stop-opacity="0..1"` | Per-stop alpha; `Native-stable` |
| Shadow/glow alpha | Opaque `flood-color` + `flood-opacity="0..1"` | Effect alpha; `Native-stable` within §6.4 |
| Picture fade | `<image opacity="0..1">` | Picture `<a:alphaModFix>`; `Native-stable` |
| One atomic whole-object fade | Non-group element `opacity="0..1"` | Alpha compiled into its supported paint/effect channels; `Native-normalized` |
| Pattern alpha | Opaque pattern child paint + child fill/stroke opacity | Conditional; [`native-data-interface.md`](./native-data-interface.md) |
| CSS color alpha | Alpha-bearing named/functional/HEX paint | `Native-normalized`; recommendation warning only |
| Group fade | `<g opacity>` compatibility | `Approximate`; fidelity warning; §2.2 |
```text
effective fill alpha
= color alpha × ancestor group opacity × element opacity × fill-opacity
```
**Default — opaque color authority (may preserve compatible alpha colors)**:
New generated SVG puts alpha on the semantic channel that owns it. Existing or
intentional alpha-bearing color tokens remain convertible; they normalize into
the matching DrawingML color/alpha channels.
**Default — channel-specific alpha (may override for one atomic whole-object
fade)**: use `fill-opacity`, `stroke-opacity`, `stop-opacity`, or
`flood-opacity` when only that channel fades. Use element `opacity` only when
an image or one non-group atomic object intentionally fades all of its
supported paint/effect channels together. Do not use element `opacity` as an
alias for `rgba()` on a fill-only object.
**Default — alpha grammar (may preserve compatible alternatives)**: write
`opacity`, `fill-opacity`, `stroke-opacity`, `stop-opacity`, and
`flood-opacity` as finite unitless numbers from `0` to `1`. The converter also
accepts finite numeric values that SVG/CSS clamps into that interval;
`stop-opacity` and `flood-opacity` additionally accept finite percentages. The
checker reports those supported non-default spellings as recommendation warnings.
Malformed or non-finite values are errors in both Checker and exporter
preflight; neither substitutes an opaque default for unknown intent.
`fill="transparent"` / `stroke="transparent"` become no fill/line; use a color
plus alpha when a painted transparent layer must remain represented. Prefer
descendant alpha over group opacity when isolated compositing matters (§2.2).
PPTX import is a user-input boundary, not generated authoring. Tolerant mode
retains recognized color semantics, omits only unsupported paint properties,
and records the decision in `conversion-report.json`; `--strict` keeps the
closed parser checks. See
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary).
---
### 6.3 Gradients and Paint Effects
| Concern | Contract |
|---|---|
| Definition | Direct `<linearGradient>` / `<radialGradient>` child of `<defs>` with unique `id` |
| Reference | Exact local `url(#id)` |
| Stops | Direct `<stop>` children; explicit color; finite offset `0..1` or `0%..100%`; optional stop alpha |
| Coordinates | Normalized values / percentages; do not depend on `gradientUnits` user-space geometry |
| Forbidden | External/quoted refs, `href` inheritance, `gradientTransform`, `spreadMethod`, CSS gradients |
| Target | Contract and fidelity |
|---|---|
| `<rect>`, `<circle>`, `<ellipse>`, `<path>`, `<polygon>` fill/stroke | Linear `Native-normalized`; radial `Approximate` |
| `<line>` / `<polyline>` | Gradient stroke only; linear `Native-normalized`, radial `Approximate` |
| `<text>` / non-positional `<tspan>` | Gradient fill only; no gradient text outline |
| `<image>` | No gradient paint; use §6.5 overlays |
Linear export preserves stops/alpha/direction but reduces coordinates to an
angle. Radial export becomes a centered circular gradient and does not preserve
`cx/cy/r/fx/fy`. Gradient strokes remain editable, but PPTX-to-SVG re-import may
retain only the first stop. Stop alpha and element opacity multiply.
PPTX import normalizes compatible gradients and records any property-level
degradation without aborting the deck; `--strict` keeps the closed parser
contract. See
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary).
The quality checker and exporter preflight both validate definition location,
references, gradient structure, and paint context from the same closed contract.
**Hard rule — non-degenerate gradient geometry**: an `objectBoundingBox`
gradient stroke requires non-zero intrinsic width and height. SVG stroke width
does not expand that object bounding box, so a perfectly horizontal or vertical
gradient ribbon disappears even when its stroke is thick. Author such a ribbon
as a closed shape with gradient `fill`, or use a path whose intrinsic geometry
has both dimensions. Checker and exporter reject the degenerate stroke form.
```xml
<defs>
<linearGradient id="flow" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#2563EB"/>
<stop offset="100%" stop-color="#10B981" stop-opacity="0.7"/>
</linearGradient>
</defs>
<path d="M100 200 C260 80 420 320 620 180" fill="none"
stroke="url(#flow)" stroke-width="12"/>
```
Preset patterns are a separate PPT interface in [`native-data-interface.md`](./native-data-interface.md).
---
### 6.4 Shadows, Glow, and Elevation
Filters are native-effect metadata, not a general pixel-filter surface.
| Concern | Contract |
|---|---|
| Definition/reference | Direct `<defs><filter id="...">` child with unique id; direct `filter="url(#id)"` attribute, never inline style |
| Public targets | `<rect>`, `<circle>`, `<path>`, `<text>` |
| Required primitive | `feDropShadow` or `feGaussianBlur` |
| Required parameters | Explicit `stdDeviation` on either effect primitive; explicit `dx`, `dy`, and `flood-opacity` on `feDropShadow`; explicit `flood-opacity` on `feFlood`; explicit `slope` on linear `feFuncA` |
| Accepted helpers | `feOffset`, `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feComponentTransfer`, linear `feFuncA` |
| Alpha transfer | Linear `feFuncA` maps multiplicative `slope` only; `intercept` is unsupported |
| Blur sampling | `feGaussianBlur edgeMode` is unsupported; native effects do not expose the SVG edge-sampling modes |
| Primitive coordinates | Omit `primitiveUnits` or use `userSpaceOnUse`; `objectBoundingBox` coordinates are unsupported |
| Numeric values | Finite unitless values; non-negative `stdDeviation`; finite `dx` / `dy`; `feFuncA slope` within `0..1`; mapped glow `rad = stdDeviation × 9525`, shadow `blurRad = stdDeviation × 2 × 9525`, and shadow `dist = hypot(dx,dy) × 9525` must round into DrawingML `0..27273042316900` |
| Classification | Meaningful non-zero offset → one outer shadow; zero/no offset → one glow |
| Fidelity | `Approximate`; one filter becomes one DrawingML effect |
Flood opacity, linear `feFuncA slope`, and element opacity multiply. The
converter-only historical path may also multiply flood-color alpha and
ancestor group opacity.
Native export does not preserve filter-region, `in/in2/result`, merge order, or
composite topology. Other primitives, multiple independent effects, filters on
`<image>` / `<tspan>` / `<g>` / unsupported targets are forbidden; apply the
effect to supported objects or use explicit layers.
The sole `<g filter>` exception is the hash-locked
`data-pptx-part="geometry-preview"` transport in §1.4: it must be a direct child
of an imported preset object and reference the same filter as that object's one
hidden geometry carrier. The preview is render-only and never becomes a second
PowerPoint object; this exception does not authorize filters on ordinary groups.
PPTX import preserves one registered shape/connector shadow or glow and records
unsupported object/run effects as import diagnostics instead of exposing a new
authoring surface. See
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary)
for tolerant, strict, and release-handling behavior.
The quality checker and exporter preflight enforce the same definition,
reference, primitive, target, and numeric-value contract. Missing required
geometry and malformed values are never replaced by effect defaults during
native export.
```xml
<defs>
<filter id="softShadow" x="-15%" y="-20%" width="130%" height="150%">
<feDropShadow dx="0" dy="6" stdDeviation="8"
flood-color="#000000" flood-opacity="0.10"/>
</filter>
<filter id="expandedShadow" x="-15%" y="-20%" width="130%" height="150%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8" result="b"/>
<feOffset in="b" dx="0" dy="6" result="o"/>
<feFlood flood-color="#000000" flood-opacity="0.10" result="c"/>
<feComposite in="c" in2="o" operator="in" result="s"/>
<feMerge><feMergeNode in="s"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="titleGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="b"/>
<feFlood flood-color="#38BDF8" flood-opacity="0.45" result="c"/>
<feComposite in="c" in2="b" operator="in" result="g"/>
<feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
```
Even `feDropShadow` with `dx="0" dy="0"` becomes glow. Use an existing accent
color for glow; black reads as diffuse shadow.
| Elevation | Use | `dy` | `stdDeviation` | Alpha |
|---|---|---:|---:|---:|
| Floor | Backgrounds, dividers, equal peers, body containers, decorative lines/icons, single-layer pages | — | — | — |
| Resting | Card over photo/panel, secondary callout | 24 | 48 | 0.060.10 |
| Raised | Primary CTA, focused card, overlay | 610 | 1016 | 0.120.20 |
| Glow | Short display text, metric, focus accent | 0 offset | 48 | 0.350.55 |
**Strong default — single light source per page**: every `feOffset` shadow on
one slide shares the same `dx`/`dy` direction (default `dx="0"`, `dy="4"``dy="8"`,
light from upper front). Contradictory shadow directions read as multiple light
sources — a clear low-quality tell. The one sanctioned exception is a deliberate
upward paper-layer light, where every affected layer flips direction together;
never mix directions on the same plane. This is a strong default, not a
checker-enforced hard rule.
**Reference — not a constraint**: keep at most two
non-floor tiers; two or three shadowed objects usually suffice. Do not lift
every peer card or stack strong shadow, border, gradient, and tint on one
container. Same-family colored shadow is reserved for a focal accent. On dark
backgrounds, prefer a light hairline or restrained glow; never glow body copy.
Negative `dy` is valid for an intentional upward paper-layer light source when
every affected layer uses the same direction. For older/strict renderers,
replace a filter with two or three offset translucent shapes behind the object:
alpha `0.030.05`, increasing offset/radius, and optional same-family tint near
`0.04` (`Native-stable`).
---
### 6.5 Image Treatments, Overlays, and Glass-like Surfaces
| Need | Authoring contract | Fidelity |
|---|---|---|
| Cover/crop | Readable raster dimensions + aligned `slice` | Native `srcRect`; `Native-stable`; otherwise native crop cannot be guaranteed |
| Contain/fit | Aligned `meet` | Fitted picture frame; `Native-normalized` |
| Stretch | `preserveAspectRatio="none"` | Native stretched frame |
| Uniform fade | `<image opacity="...">` | Native picture alpha |
| Shaped picture | §1.2 image-only `clip-path` | Preset/custom picture geometry |
**Hard rule — closed image aspect-ratio grammar**: on `<image>`, omit
`preserveAspectRatio` for the default `xMidYMid meet`, use `none` alone for
stretch, or use one of the nine case-sensitive alignments (`xMinYMin`,
`xMidYMin`, `xMaxYMin`, `xMinYMid`, `xMidYMid`, `xMaxYMid`, `xMinYMax`,
`xMidYMax`, `xMaxYMax`) followed by explicit `meet` or `slice`. Generated SVG
always includes the mode on an aligned value. An alignment without a mode and
values needing whitespace normalization are compatible input and receive a
Checker recommendation. Empty values, `defer`, unknown/wrong-case alignments or
modes, `none` with a mode, and extra tokens are errors; the converter never
guesses a fallback.
**Hard rule — fit/clip interaction**: a non-trivial clip disables `meet`
frame-fit. Match the image box to the source ratio or use `slice`. Do not apply
filters directly to `<image>`.
**Hard rule — picture frames and sources are explicit and decodable**: every
SVG `<image>` has explicit positive `width`/`height` and exactly one non-empty
`href` or compatible `xlink:href`. A data URI must use a supported `image/*`
MIME type, valid strict base64 when marked
`base64`, a non-empty payload, and bytes that decode as the declared format.
An external asset must resolve, use a supported extension, be non-empty, and
decode as that extension. The registered formats are PNG, JPEG, GIF, WebP,
BMP, TIFF, SVG, EMF, and WMF. Explicit template substitution tokens may remain
unresolved only during template checking; export requires the resolved image.
Missing, ambiguous, corrupt, mislabeled, or unsupported sources are errors and
must never be dropped or packaged as invalid zero-byte media.
**Hard rule — nested SVG is an imported crop transport, not a general
viewport**: every non-root `<svg>` must be the exact picture-crop wrapper emitted
by `pptx_to_svg`. The outer element has explicit registered project-geometry
`x`, `y`, positive `width`/`height`, a unit-coordinate `viewBox` made of four
ordinary decimal values, and
`preserveAspectRatio="none"`; it contains exactly one direct, empty `<image>`
with exactly one non-empty `href` or `xlink:href`, `x="0"`, `y="0"`, `width="1"`,
`height="1"`, and `preserveAspectRatio="none"`. Its ancestor chain contains
only the root SVG and ordinary visual `<g>` wrappers; definitions, text,
render-only geometry details, and other non-visual containers cannot own this
transport. The outer wrapper may additionally carry `id`, a supported
`transform`, registered structure metadata (`data-pptx-layer` or
`data-pptx-carrier`), and the importer metadata
`data-pptx-frame`, `data-pptx-object`, `data-pptx-shape-id`,
`data-pptx-shape-name`, and `data-pptx-shape-scope`. A shape clip is present
only when exact `data-pptx-crop="1"` and a registered image-only `clip-path`
occur together and the local clip definition resolves. The inner image may
add only registered `opacity`. The `viewBox` must quantize without clamping to
a DrawingML `srcRect` with a positive visible region: each signed crop value
must fit the OOXML percentage integer range `-2147483648..2147483647`, while
`l + r < 100000` and
`t + b < 100000` preserve a positive visible region. Negative crop values and
crop windows extending outside the source unit rectangle are retained exactly,
not clamped. `0 0 1 1` is redundant and must be written as a plain `<image>`.
Extra visual children, indirect images, character data, unknown attributes,
malformed or unrepresentable crop coordinates, and generalized nested
viewports are errors. Checker and the converter share this parser so a nested
subtree cannot pass validation and then silently disappear during export.
| Overlay | Construction | Typical stops / alpha |
|---|---|---|
| Directional scrim | Linear rect, darkest beside text | `0%: 0.88; 55%: 0.30; 100%: 0` |
| Bottom title fade | Vertical rect over lower image | black `0 → 0.72` |
| Vignette/spotlight | Centered radial rect (`cx=50%`, `cy=50%`, `r=70%`); native center only | black `0 → 0.58` |
| Brand wash | Directional existing brand-color gradient | `0.80 → 0.10` |
| Faux glass | Visible fields + diagonal linear panel (`0,0 → 1,1`) + highlight stroke; optional §6.4 elevation | white `0.38 → 0.12`; stroke about `0.55` |
Layer in document order: image → scrim/wash → text. True source/backdrop blur is
`Bake-required`; faux glass is explicit layering, not blur. Validate contrast
against the actual image. All overlay gradients follow §6.3 linear/radial
fidelity.
---
### 6.6 Lines, Connectors, Borders, and Markers
| Surface | Contract / native result |
|---|---|
| Solid stroke/width/alpha | `Native-stable` editable line |
| `4,4`; `6,3`; `2,2`; `8,4`; `8,4,2,4` (comma or space separators) | `dash`; `dash`; `sysDot`; `lgDash`; `lgDashDot` (`Native-normalized`) |
| Canonical custom dash | Exactly two positive finite unitless ordinary decimals (`dash gap`); export scales/quantizes against stroke width; `Native-normalized` |
| Compatible custom dash | Three or more positive finite unitless values are accepted but reduce to the first pair with a Checker recommendation; compatible numeric spellings also warn |
| `stroke-linecap` | `butt`, `round`, `square`; `Native-stable` |
| `stroke-linejoin` | `miter`, `round`, `bevel`; `Native-stable` |
| `vector-effect` | Exactly `none` or `non-scaling-stroke`; export resolves the choice into native line width (`Native-normalized`) |
| `stroke-dashoffset` | No general line mapping; allowed only as a direct finite unitless ordinary-decimal attribute on a §6.10 thick-circle shorthand (`px` suffix is compatible input and warns) |
| Gradient stroke | §6.3; re-import may flatten to first stop |
| `marker-start` / `marker-end` | §1.1 native line end; type `Native-normalized`, size `Approximate` (`sm/med/lg`) |
PPTX import treats unsupported line properties as source diagnostics: tolerant
mode retains the object and omits only the unsupported outline; `--strict`
retains the closed rejection behavior. See
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary).
The dash grammar is closed: exact lowercase `none`, or at least two finite
unitless numbers separated by whitespace or one comma. Generated SVG uses
ordinary decimal spellings. A leading plus sign, exponent, trailing decimal
point, surrounding whitespace, or longer custom list is compatible input and
produces a non-blocking normalization recommendation. Unknown units, one-value
arrays, empty or repeated comma fields, non-finite values, and negative or zero
entries are errors. The only zero exception is a gap declared directly on the
§6.10 thick-circle element.
Generated cap, join, and `vector-effect` values use the exact lowercase tokens
in the table. Surrounding whitespace is compatible input and produces a
recommendation; every other token is an error.
Match marker paint to the parent stroke using the shape-specific channel from
§1.1: fill for closed/oval line ends and stroke for the open arrow. Use markers
for connectors and §6.10 calculated geometry for a manual diagonal arrowhead.
When exact grid spacing matters, use one multi-subpath path rather than a
fixed-density preset pattern:
```xml
<path d="M40 0V120 M80 0V120 M0 40H120 M0 80H120"
fill="none" stroke="#2E6EA8" stroke-width="0.8"/>
```
---
### 6.7 Advanced Text Treatments
**Hard rule — closed text property grammar**: generated text uses only the
values in the `Canonical authoring` column. Registered compatible input remains
convertible and receives a non-blocking normalization recommendation. Every
other value is invalid; the converter must not replace it with a default.
| Property | Canonical authoring | Compatible input | DrawingML mapping / rejection boundary |
|---|---|---|---|
| `font-weight` | `normal`, `bold`, or an exact integer hundred from `100` through `900` | `medium``500`; `semibold``600` | `normal` and `100..500` map to regular; `bold` and `600..900` map to `b="1"`; therefore numeric weights are `Native-normalized` |
| `font-style` | `normal` or `italic` | None | `italic` maps to `i="1"`; oblique, angle, relative, and CSS-wide values are invalid |
| `text-anchor` | `start`, `middle`, or `end` on `<svg>`, `<g>`, or `<text>` | None | Maps to left/center/right paragraph alignment plus normalized frame position; it is invalid on `<tspan>` because run-level anchoring has no mapping |
| `text-decoration` | `none`, `underline`, `line-through`, or `underline line-through` | `line-through underline` → canonical order | Maps to the single underline and strike run properties; unknown, repeated, or substring-like tokens are invalid |
| `letter-spacing` | Finite unitless ordinary decimal SVG px | The same ordinary decimal with `px`, `pt`, or `em`; normalize to unitless px | Maps to `a:rPr@spc`; the final value must fit DrawingML `-400000..400000`, and negative tracking must leave every generated DrawingML run with a positive estimated advance and its text frame with a positive extent; keywords, percentages, exponents, leading plus signs, trailing decimal points, non-finite values, and other units are invalid |
The registered text properties follow SVG inheritance, including declarations
on the root `<svg>`: inline `style` overrides the same element's direct
attribute, which overrides its ancestor. Relative font sizes and `em` tracking
resolve against the same effective inherited size in Checker and converter.
Every declaration is validated even when a later declaration overrides it, so
hidden garbage cannot bypass preflight.
The DrawingML character-spacing range is necessary but not sufficient for
negative tracking. After run assembly, each output run must retain a positive
estimated advance using the quantized `sz` and `spc` values that will actually
be written; a wider sibling run or paragraph line cannot hide a run whose
aggregate advance would reverse or collapse, which can reorder or drop
characters across PowerPoint-compatible renderers. The generated text frame
must also retain a positive horizontal and vertical extent. Checker rejects
directly measurable single-line violations, and the converter revalidates
every generated run and text frame before writing OOXML. It must not clamp,
take the absolute value of, or otherwise hide a non-positive advance or extent.
Adjacent authored runs with identical final DrawingML run properties form one
output run before sizing and validation; splitting text across equivalent
`<tspan>` nodes is not a tracking escape hatch. Tracking and width estimates
count the registered project text clusters rather than raw Unicode code points:
combining marks, variation selectors, emoji modifiers and ZWJ sequences,
paired regional indicators, and same-script virama conjuncts do not receive
internal spacing.
An unchanged imported native text body reuses the geometry carrier's positive
shape frame and attaches the preserved `txBody` payload instead of regenerating
runs or a text frame from the SVG estimate.
**Hard rule — element-specific text surface**:
- Inheritable text declarations belong only on `<svg>`, `<g>`, `<text>`, or
`<tspan>`; placing them on geometry, image, definition, or reuse elements is
an error rather than ignored decoration.
- `<text>` accepts `x`, `y`, registered paint/alpha/run properties, the text
properties above, `font-family`, `font-size`, direct `filter`, direct
`transform`, `xml:space`, `id`, and project `data-*` metadata.
- `<tspan>` accepts `x`, `y`, `dx`, `dy`, registered paint/alpha/run
properties, `font-family`, `font-size`, `font-weight`, `font-style`,
`letter-spacing`, `text-decoration`, `xml:space`, `id`, and project `data-*`
metadata. It does not accept `text-anchor`, `filter`, or `transform`.
- `word-spacing`, `dominant-baseline`, `alignment-baseline`, `baseline-shift`,
font shorthand/variant/stretch/feature/variation/synthesis controls,
`font-kerning`/`kerning`, `font-size-adjust`, `line-height`, text alignment,
indent/shadow/rendering controls, white-space/word-break/hyphenation
controls, `writing-mode`, `vertical-align`, `direction`, `unicode-bidi`, and
`text-transform` have no registered native mapping and are errors as direct
attributes or inline style.
- Any other unregistered `font-*` or `text-*` property is also an error; the
closed grammar must not grow through an ignored CSS spelling.
**Hard rule — project text whitespace**:
- `xml:space` is the project's closed authoring control for significant text
whitespace. It is valid only as an exact direct attribute on `<text>` or
`<tspan>`, accepts only the case-sensitive values `default` and `preserve`,
inherits through the text tree, and may be reset on a child `<tspan>`.
- The project maps this control to the visible Chromium/SVG2 behavior used by
Live Preview; it does not claim the legacy SVG 1.1 newline-deletion model.
XML line endings and tabs become U+0020 SPACE. In `default` mode, contiguous
U+0020 characters collapse across inline run boundaries and leading or
trailing default-mode spaces in the resulting text chunk are removed. In
`preserve` mode, every resulting U+0020 character remains significant.
- Only XML whitespace is normalized. NBSP, ideographic space, and other
Unicode spacing characters remain literal text and must not be rewritten by
a generic Unicode-whitespace regular expression.
- Source line breaks do not create PowerPoint paragraphs. Use the registered
positioned-`tspan`/paragraph structure for visual lines, and preserve DOM
text/tail order plus original style inheritance when normalizing that
structure.
These allowlists are additive to the global structural blacklist and the
paint, font-size, opacity, filter, and transform value contracts owned by their
respective sections; they do not weaken those contracts.
| Treatment | SVG surface | Result / boundary |
|---|---|---|
| Underline / strike / both | `text-decoration="underline"`, `line-through`, or both | `Native-stable`; both emits both run properties |
| Mixed runs | Non-positional `<tspan>` | One `Native-normalized` editable frame; §4.2 |
| Font size | Generated default is a finite unitless SVG px value; compatible `px`, `pt`, `pc`/`pica`, `in`, `cm`, `mm`, `q`, `em`, and `rem` values receive a recommendation warning only | Converted to SVG px, then editable DrawingML point size; unsupported units/percentages error |
| Tracking | §6.7 closed `letter-spacing` grammar | `Native-normalized`; compatible units normalize to SVG px before DrawingML conversion |
| Transparency | `opacity` / `fill-opacity` on text/run | `Native-normalized` run alpha, not isolated compositing |
| Gradient fill | §6.3 gradient on text/run | Editable fill; geometry normalizes |
| Outline | Solid `stroke`, `stroke-width`, `stroke-opacity` | `Native-normalized` editable run outline; re-import does not reconstruct it |
| Shadow/glow | §6.4 filter on `<text>` only | Shape shadow / run glow; `Approximate` |
| Native bullet | Leading `· • ● ▪ ■ ◆ ◇ ◦ ‣` + non-empty content | `·`/`•``•`; others unchanged; color/alpha from marker run; font/size follow text |
```xml
<text x="100" y="200" font-size="20" xml:space="preserve">Current <tspan
fill="#999999" text-decoration="line-through">old</tspan> value</text>
```
Use strikethrough for removed/former values; it is ordinary notation, not a
style-exclusive effect. Imported double underline/strike normalizes to single.
Bullet detection allows optional leading whitespace, requires non-empty content,
and leaves non-leading decorative glyphs as ordinary text.
Keep body tracking normal; CJK tracking defaults near/below 2% of font size and
above 5% triggers review. Text outline is solid only. `textPath`, masks, blend
modes, generated effects, and text-image knockouts are outside editable text.
---
### 6.8 Transforms, Layering, and Static Reuse
| Surface | Contract / fidelity |
|---|---|
| `rotate(angle[, cx, cy])` | Geometry/image/text/ordinary group; `Native-normalized` |
| `translate(x y)` | Geometry/image/group; pure translation also safe on text; `Native-normalized` |
| Positive scale / negative mirror | Geometry/image or a group/use whose expanded visual subtree is geometry/image only; explicit pivot; `Native-normalized` |
| `matrix(a b c d e f)` | Geometry/image or the same geometry/image-only group/use; transformed axes finite, non-zero, orthogonal; excludes rounded rectangles and subtrees containing them; `Native-normalized` |
| Source order | Back-to-front PPT z-order; `Native-stable` |
| `<g opacity>` | Compatible approximate mapping; generated SVG prefers descendant alpha, §2.2 |
| Local `<use>` | §1.3 compile-time reuse; `Native-normalized` |
**Hard rule — closed transform grammar**: Use only lowercase `translate`,
`scale`, `rotate`, and `matrix` with exact finite unitless argument counts:
`translate` 1/2, `scale` 1/2, `rotate` 1/3, and `matrix` 6. Separate arguments
and operations with whitespace or one comma. Leading/trailing/repeated commas,
adjacent operations without a separator, units, unknown functions, and
incomplete input fail quality check and export. Generated numeric tokens use
ordinary decimals; a supported leading `+`, exponent, or trailing decimal point
remains compatible input and receives a non-blocking normalization warning.
Model-facing translation values, rotation centers, and matrix `e/f` use at
most two decimals under §1.4; angles, scale arguments, and matrix `a/b/c/d`
retain the precision required by the transform.
Set text size/position directly. A text transform is either a translate-only
list or one rotate operation; do not scale, matrix-transform, or mix operations
on text. A group containing text follows the same translate-only/single-rotate
limit. `skewX`, `skewY`, zero/non-orthogonal axes, and shear matrices are
forbidden. Native chart/table markers allow translate/scale only. The §6.10
thick-circle shortcut does not inherit general transform support. Positive
rotation is clockwise and pivoted rotation normalizes the native frame. Every
cumulative matrix, including transforms split across ancestors, must remain
finite, non-zero, and orthogonal; importer/live-editor matrices do not expand
the hand-authored contract.
Mirror around vertical pivot `cx` with
`translate(cx 0) scale(-1 1) translate(-cx 0)`; use the analogous Y sequence
for a horizontal pivot. During mirror materialization, imported PowerPoint
groups with an axis flip keep their geometry reflection, while each descendant
SVG text node receives the matching counter-reflection so browser previews keep
glyphs upright. The tool-side native record retains the source group flip.
Layer back-to-front: background/image → scrim/shadow → main geometry → labels /
icons → top annotation. Finalization and native export independently expand
`<use>` into cloned editable primitives; PowerPoint does not retain a symbol /
instance graph.
---
### 6.9 Freeform Shapes and Curves
| Input | Native normalization | Fidelity |
|---|---|---|
| `M/L/H/V`, absolute or relative | Absolute `M/L` | `Native-normalized` |
| `C` | Cubic Bézier | `Native-normalized` |
| `S/Q/T` | Explicit cubic controls | `Native-normalized` |
| `A` | Cubic segments of at most 90° | `Approximate` |
| `Z`; polygon/polyline | Closed/open freeform | `Native-normalized` |
**Hard rule — complete freeform grammar**: Generated `path@d` and
`polygon` / `polyline@points` use finite unitless ordinary decimals and only
the commands registered above. Native export consumes the complete attribute;
it never extracts recognizable fragments while ignoring other characters.
Finite scientific notation, a leading plus sign, and a trailing decimal point
remain read-compatible and receive recommendation warnings; generated SVG does
not write them. Unknown commands or characters, misplaced/repeated commas,
non-finite numbers, missing attributes, incomplete command groups, and odd
point counts are invalid. A path starts with `M` / `m`; `A` radii are
non-negative and both arc flags are exactly `0` or `1`. Each registered path
command accepts its uppercase absolute and lowercase relative form. Legal
separator-free arc flag sequences remain valid and are parsed as individual
flag tokens. A polygon has at least three coordinate pairs and a polyline at
least two.
**Validation**: Checker and native export consume the same parser in
[`paths.py`](../scripts/svg_to_pptx/drawingml/paths.py); native-object fallback
bounds reuse its normalized commands rather than a second path grammar.
Command identity, relative coordinates, shorthand, arc parameters, and original
handles are not retained. Geometry needs non-zero bounds. Use a closed cubic
path for organic silhouettes, polygon/closed path for ribbons/facets, open path
for curved connectors, multi-`M` path for exact linework, and a [`shared-standards-core.md`](./shared-standards-core.md) §1.2 path clip
for organic pictures. Filled silhouettes end with `Z`; open paths use
`fill="none"`. Do not depend on `fill-rule="evenodd"`; build explicit visible
geometry or bake an essential knockout.
For a fixed background, a background-colored overlay is also valid.
| Rounded rect input | Result |
|---|---|
| One positive radius, or `0 < rx == ry <= min(width,height)/2` | `Native-stable` adjustable `roundRect` without distorting transforms; the same short-side limit applies to one-radius input |
| `0 < abs(rx-ry) < 0.5px` after scaling | One normalized native radius; `Approximate` |
| `abs(rx-ry) >= 0.5px`, either positive | Cubic custom geometry; no radius handle; `Approximate` |
| Equal radius above half the short side | Native short-side clamp may differ from SVG; `Approximate` |
---
### 6.10 Radial Geometry, Donuts, Gauges, Sunbursts, and Diagonal Arrowheads
For center `(cx,cy)`, radius `r`, and degrees `θ`:
```text
x = cx + r × cos(θ × π / 180)
y = cy + r × sin(θ × π / 180)
```
For clockwise pie/donut sectors, default to `-90°` only when the chart starts at
12 o'clock. A full-circle percentage sector spans `percentage × 360°`;
large-arc is `1` above `180°`; outer sweep is `1`, inner return is `0`. Split
both outer and inner boundaries of a full ring into at least two arcs each.
Calculated endpoints survive subject to EMU rounding; `A` curves remain cubic
approximations. Verify all spans plus gaps against the planned sweep.
Explicit arc sectors are editable `Approximate` freeforms. Thin circles using a
§6.6 preset/two-number dash stay `Native-normalized` ellipse lines.
```xml
<!-- 75% donut: center 400,400; outer 180; inner 100; -90° → 180°. -->
<path d="M400 220 A180 180 0 1 1 220 400
L300 400 A100 100 0 1 0 400 300 Z" fill="#2563EB"/>
```
**Gauge**: require `max > min`, `p = clamp((value-min)/(max-min),0,1)`, and
`0 < planned clockwise sweep <= 360°`; value sweep is `p × planned sweep`.
`valueEndAngle = startAngle + valueSweep`; large-arc is `1` iff
`abs(valueSweep) > 180°`.
Omit the value sector at `p=0`. At `p=1` with `360°`, split both boundaries into
at least two arcs. Track/value share center, radii, start, and sweep flags.
**Sunburst — `Approximate`**: one explicit annular sector per node; each depth owns one radius
band and child angular intervals partition the parent. Do not use one `evenodd`
compound ring.
**Thick-circle shorthand — `Approximate`, non-position-sensitive only**:
- One circle per segment; `fill="none"`; the circle may use one `rotate` for its
start angle, and ancestor transforms must be translate-only.
- Exactly two non-preset finite unitless ordinary-decimal values (`dash gap`);
`stroke-dashoffset` is a direct finite unitless ordinary-decimal attribute.
- `0 < stroke-width < 2r`, `stroke-width/r >= 0.15`,
`0 < dash < 2πr`, `gap >= 0`, and `dash + gap >= 2πr - 1` SVG unit. The
one-unit tolerance exists only for integer-rounded circumference values.
- Native construction uses only the first dash and re-imports as a freeform.
Its native start is 90° counterclockwise from the SVG preview; use explicit
arcs whenever start angle, cap, or radial precision matters.
```xml
<circle cx="400" cy="400" r="140" fill="none" stroke="#2563EB"
stroke-width="48" stroke-dasharray="615.75 263.90" stroke-dashoffset="0"/>
```
**Diagonal polygon arrowhead**: for a non-zero line, calculate rather than use a
fixed triangle:
```text
dx=x2-x1; dy=y2-y1; len=√(dx²+dy²); ux=dx/len; uy=dy/len
px=-uy; py=ux
tip=(x2,y2)
back1=(x2-ux×12+px×5, y2-uy×12+py×5)
back2=(x2-ux×12-px×5, y2-uy×12-py×5)
```
Use §1.1 markers for ordinary connectors; the polygon is for a manually drawn
filled `Native-normalized` arrowhead. Example:
`<polygon points="370,430 365.6,417.8 358.2,424.6"/>`.
---
### 6.11 Constructed Visual Styles
**Hard rule — explicit construction**: these are supported-layer recipes, not
browser-filter permissions.
**Reference — not a constraint**: use them only when they match the locked style.
| Intent | Construction | Boundary / fidelity |
|---|---|---|
| Faux glass | §6.5 translucent panel + highlight stroke + visible fields | No backdrop blur; `Native-normalized` |
| Hand-drawn mark | Rotated translucent bar + irregular `Q/C` paths + round caps | No roughness filter; `Native-normalized` |
| Ink wash | Few same-family translucent closed curves/strokes | No feather/wet edge; `Native-normalized` |
| Riso offset | Duplicate text/shape with small offset, second ink, lower alpha | No blend mode; `Native-normalized` |
| Pixel grid | Integer-aligned rects on one cell grid | `shape-rendering` preview-only; `Native-stable` |
| Halftone | Sparse calculated circles | `Native-stable`; bake dense screens / use suitable [`native-data-interface.md`](./native-data-interface.md) preset |
| Isometric facets | Shared-vertex top/front/side polygons, one light direction | 2D only; `Native-normalized` |
| Paper cut | Ordered organic paths + consistent §6.4 shadow per layer | Filter each layer, not group; `Approximate` |
| Gradient ribbon | Non-degenerate cubic path + §6.3 gradient stroke; closed gradient-filled shape for horizontal/vertical ribbons | `Native-normalized`; no mesh gradient; re-import may flatten color |
| Line-plus-area data | Low-alpha closed area first, crisp line above | Keep area subordinate; `Native-normalized` |
**Minimal construction anchors**:
```xml
<!-- Hand-drawn + ink. -->
<rect x="80" y="80" width="240" height="28" fill="#FDE68A"
opacity="0.72" transform="rotate(-1,200,94)"/>
<path d="M90 150 Q210 142 330 151" fill="none" stroke="#1F2937"
stroke-width="3" stroke-linecap="round"/>
<path d="M80 220 C160 160 250 180 330 230 Z" fill="#1F2937" opacity="0.16"/>
<path d="M90 240 C180 210 250 260 340 220" fill="none" stroke="#1F2937"
stroke-width="10" stroke-linecap="round" opacity="0.70"/>
<!-- Riso, pixel cells, sparse dots. -->
<text x="86" y="320" font-family="Arial, sans-serif" font-size="64"
fill="#EC4899" opacity="0.85">PRINT</text>
<text x="92" y="326" font-family="Arial, sans-serif" font-size="64"
fill="#2563EB">PRINT</text>
<g id="pixel-cells" shape-rendering="crispEdges" fill="#2563EB">
<rect x="400" y="80" width="16" height="16"/><rect x="416" y="80" width="16" height="16"/>
</g>
<g id="sparse-dots" fill="#EC4899"><circle cx="410" cy="140" r="3"/><circle cx="426" cy="140" r="6"/></g>
<!-- Isometric facets + line-over-area. -->
<g id="isometric-facets" transform="translate(520 160)">
<polygon points="0,0 80,-24 160,0 80,24" fill="#60A5FA"/>
<polygon points="0,0 0,48 80,72 80,24" fill="#3B82F6"/>
<polygon points="80,24 80,72 160,48 160,0" fill="#2563EB"/>
</g>
<path d="M760 260 L860 220 L960 250 L960 340 L760 340 Z" fill="#2563EB" opacity="0.10"/>
<path d="M760 260 L860 220 L960 250" fill="none" stroke="#2563EB" stroke-width="4"/>
```
**Default — integer pixel grid (may override for deliberate irregular
treatment)**: avoid soft scaling; use explicit dots only for sparse editable
halftone and route dense full-slide texture to §6.12.
---
### 6.12 Unsupported Effects and Native-Safe Alternatives
| Unsupported intent | Do not author | Fidelity | Alternative |
|---|---|---|---|
| Source/backdrop blur; procedural texture | Plain blur, `feTurbulence`, `feDisplacementMap`, `feColorMatrix`, arbitrary filter graph | `Bake-required` | §6.4 effect, explicit geometry, translucent layers, or baked texture |
| Inner shadow, soft edge, reflection | Non-outer-shadow/glow graph | `Bake-required` | Explicit inset/highlight/shadow layers or image |
| Per-pixel compositing | Mask, blend mode, knockout, arbitrary alpha composite | `Bake-required` | Direct geometry; §1.2 image clip; otherwise bake |
| Exact custom tile | Unannotated `<pattern>` / `patternTransform` | `Bake-required` | Multi-subpath geometry, suitable [`native-data-interface.md`](./native-data-interface.md) preset, or bake |
| Sheared object | Skew/shear matrix | `Bake-required` | Pre-transform geometry path; bake text/image |
**Hard rule — blur semantics**: within §6.4, zero-offset `feGaussianBlur` means
glow; it does not blur the object or backdrop. Use a low-alpha raster for dense
grain and explicit circles/paths only for sparse editable marks.
Unsupported source effects remain visible where possible and retain their
import diagnostics. Resolve those diagnostics before release export; see
[`conversion.md`](../scripts/docs/conversion.md#import-compatibility-and-recovery-boundary).
---
### 6.13 Scenario Quick Reference
**Reference — not a constraint**: fidelity remains authoritative in the owning
subsection; this table only routes scenarios.
| Decision family | Scenario routing | Authority / boundary |
|---|---|---|
| Elevation | Floating card → resting shadow; one CTA → colored shadow; equal peers/background → flat; maximum predictability → layered shapes; title/metric → glow | §6.4; never body-copy glow |
| Image/material | Text over image → directional scrim; bottom title → bottom fade; centered hero → vignette; brand wash → brand overlay; glass card → faux glass | §6.5; no backdrop blur |
| Lines | Draft/optional → dash; process direction → marker; flow/series → gradient stroke; exact grid → multi-subpath path | §6.6 / §6.3 |
| Text | Removed/former value → line-through; eyebrow → tracking; watermark/outline heading → text outline; list → native bullet | §6.7 |
| Composition | Move/rotate/mirror → §6.8 transform; repeated static mark → local `<use>` | §6.8; preserve z-order |
| Hand/print | Annotation → highlighter/curve; ink wash → layered alpha paths; Riso → offset duplicate | §6.11; no turbulence, true bleed, or blend mode |
| Pixel/halftone | Pixel accent → integer rect grid; sparse screen → circles | §6.11; dense screen → §6.12 |
| Faceted/layered | Pseudo-3D → 2D facets; paper cut → direct shadow per layer | §6.11; no 3D transform/group composite shadow |
| Data/freeform | Series depth → area first + line above; organic card → closed cubic; shaped image → [`shared-standards-core.md`](./shared-standards-core.md) §1.2 path clip | §6.11 / §6.9 |
| Radial | Donut/gauge → explicit arcs; sunburst → sector per node; position-insensitive ring → shorthand | §6.10; shorthand has 90° preview/native offset |
| Arrow | Manual diagonal arrowhead → calculated triangle; ordinary connector → marker | §6.10 / §1.1 |
| Unsupported | Dense grain, complex composite, or skew → explicit alternative or baked asset | §6.12; foreground text/data stay editable SVG |
---
@@ -1,4 +1,4 @@
> See shared-standards.md for common technical constraints.
> See [`shared-standards-core.md`](./shared-standards-core.md) for common technical constraints.
# SVG Image Embedding Guide
@@ -28,7 +28,7 @@ Defined in the Design Specification & Content Outline; each image carries an `Ac
|--------|---------|-------------------|
| **Pending** | Acquisition needed (`Acquire Via: ai` / `web`) or derivation needed (`Acquire Via: slice`); not yet attempted | Image Acquisition Phase (Step 5) consumes this; must not remain after Step 5 |
| **Generated** | AI-generated file exists at expected path, or sliced element file exists at expected path | Reference from `../images/`; no on-slide credit needed. **Exception**: an `Illustration Sheet` row is only a slice source — it lives in §VIII but never in `spec_lock.md images`, so the Executor never places it |
| **Sourced** | Web-sourced file exists at expected path | Reference from `../images/`; check `image_sources.json` for `license_tier` — if `attribution-required`, render an inline credit element on the slide (see [executor-base.md §6](./executor-base.md) and [image-searcher.md §7](./image-searcher.md) for the visual spec) |
| **Sourced** | Web-sourced file exists at expected path | Reference from `../images/`; check `image_sources.json` for `license_tier` — if `attribution-required`, render an inline credit element on the slide (see [`executor-web-image.md`](./executor-web-image.md) §1 and [`image-searcher.md`](./image-searcher.md) §7 for the visual spec) |
| **Rendered** | Deterministic formula PNG exists at expected path (`Acquire Via: formula`) | Reference from `../images/`; use `preserveAspectRatio="xMidYMid meet"` and do not crop |
| **Needs-Manual** | Acquisition attempted once + one retry, failed; for `slice`, parent sheet is unavailable | Dashed placeholder unless user has manually supplied the file. For `slice` rows, place the parent sheet and rerun `slice_images.py`; do not hand-place individual element files |
| **Existing** | User already has image (`Acquire Via: user`) | Place in `images/`, reference with `<image>` |
@@ -53,7 +53,7 @@ Defined in the Design Specification & Content Outline; each image carries an `Ac
├── Rendered formula → <image href="../images/formula_001.png" preserveAspectRatio="xMidYMid meet" .../>
└── Placeholder / Needs-Manual without file → Dashed border + description text
4. Preview: python3 -m http.server -d <project_path> 8000 → /svg_output/<filename>.svg
5. Post-processing & Export → follow [`SKILL.md` Step 7](../SKILL.md)
5. Post-processing & Export → follow [`generate-pptx.md`](../workflows/generate-pptx.md) Step 7
```
> Keep external references in `svg_output/` during generation. `finalize_svg.py` auto-embeds images into the mandatory `svg_final/` visual preview; native PPTX export independently reads `svg_output/`.
@@ -130,12 +130,11 @@ python3 -m http.server -d <project_path> 8000
## Conversion Process
Use the unified pipeline in [`SKILL.md` Step 7](../SKILL.md). `finalize_svg.py` remains mandatory and embeds image references into the self-contained `svg_final/` preview. The following PPTX command still reads `svg_output/` by default and converts it directly to native DrawingML; it does not consume `svg_final/` in the supported release route.
```bash
python3 scripts/finalize_svg.py <project_path>
python3 scripts/svg_to_pptx.py <project_path>
```
Follow [`generate-pptx.md`](../workflows/generate-pptx.md) Step 7; it owns the
serial post-processing and export commands. Its mandatory finalization step
embeds image references into the self-contained `svg_final/` preview, while the
supported native PPTX release still reads `svg_output/` and maps it directly to
DrawingML.
### Standalone: align_embed_images.py (advanced)
@@ -176,7 +175,7 @@ project/
### Rounded Corner / Non-rectangular Image Cropping
`clipPath` **on `<image>` elements** is conditionally allowed — authoritative constraints in [shared-standards.md §1.2](shared-standards.md); do not restate or relax here.
`clipPath` **on `<image>` elements** is conditionally allowed — authoritative constraints in [`shared-standards-core.md`](./shared-standards-core.md) §1.2; do not restate or relax here.
Fallback when `clipPath` doesn't fit: bake rounded corners into the source image (PNG with alpha) before embedding.
@@ -1,23 +1,23 @@
> See shared-standards.md for common technical constraints.
> See [`shared-standards-core.md`](./shared-standards-core.md) for common technical constraints.
# Template Designer — Template Design Role
## Core Mission
Generate reusable page templates inside the complete workspace selected by `create-template`, and write a concise `design_spec.md` that captures the source-derived basic norms that make the template reusable.
Generate reusable structured page templates inside the workspace selected by Create Template's Create Layout or Create Deck child workflow, and write a concise `design_spec.md` that captures the source-derived rules that make the template reusable. For Deck, include descriptive recurring-application context; for Layout, keep structure brand-neutral and application-neutral.
> This is a standalone role: only triggered via the `/create-template` workflow. Library and project outputs use one workspace shape; it is not the template selection step in the main PPT generation pipeline.
> This is a standalone role: only triggered by the Create Layout or Create Deck child workflow under `/create-template`. Create Brand never invokes it. Library and project outputs use one workspace shape; this is not the template selection step in the main PPT generation pipeline.
## Usage
- **Trigger**: `/create-template` workflow
- **Trigger**: `/create-template` → Create Layout or Create Deck child workflow
- **Workspace root**: `library` (default) → `skills/ppt-master/templates/<kind_dir>/<template_name>/`; `project` → the confirmed `<target_project>/`
- **Template source**: `<template_workspace>/templates/` in both scopes
- **Input**: finalized template brief (output scope, target project when project-scoped, template ID, display name, kind, applicable scenarios, tone, theme mode, canvas format, optional reference assets, accepted basic template norms)
- **Input**: finalized template brief (output scope, target project when project-scoped, template ID, display name, kind, structural use cases or Deck application context, tone, theme mode, canvas format, optional reference assets, accepted basic template norms)
**Hard rule — scope is execution metadata**: Use `output_scope` and `target_project` to route files, but do not write either field into portable `design_spec.md` frontmatter. Do not create a new PPTX structure mode; deck/layout output declares `native_structure_mode: structured`.
**Workspace precondition**: The workflow has already resolved the selected root, confirmed an empty `<template_workspace>/templates/`, and checked collision-free destination filenames in `images/`, `icons/`, and `templates/icons/`. Check `exports/` only when an on-demand review PPTX was requested. Optional directories may be absent until their first real file is written. Project scope additionally requires an initialized target project. Do not begin final writes before that all-at-once preflight passes.
**Workspace precondition**: The workflow has already resolved the selected root, confirmed an empty `<template_workspace>/templates/`, and checked collision-free destination filenames in `images/` and `icons/imported/`. Check `exports/` when review was requested or the confirmed roster contains multiple Masters. Optional directories may be absent until their first real file is written. Project scope additionally requires an initialized target project. Do not begin final writes before that all-at-once preflight passes.
When the workflow provides a PPTX reference source, the effective input package comes from the unified `pptx_template_import.py` preparation workspace and becomes:
@@ -25,13 +25,13 @@ When the workflow provides a PPTX reference source, the effective input package
- `manifest.json` — single source of truth for source-deck facts (slide size, theme, per-master themes, assets, asset map, placeholders, layouts, masters, slides, SVG file paths, page-type candidates)
- `native_structure.json` — stable source master/layout keys, picker names, parent-master relationships, placeholder type/index/geometry, source hash, and source-graph quality facts
- `source_template.pptx` — byte-preserved analysis copy for visual/package cross-checking; never a final template asset
- `summary.md` — short orientation digest derived from manifest.json
- `conversion-report.json` — source-recovery and fidelity diagnostics, when present
- exported `assets/`
- `svg/master_*.svg` / `svg/layout_*.svg`lossless layered import evidence; every master / layout in the deck rendered once, including ones no sample slide references
- `svg/slide_NN.svg`lossless slide-local import evidence; do not bulk-read because opaque native payload is retained
- `svg/master_*.svg` / `svg/layout_*.svg`immutable layered native-payload backing; every master / layout in the deck rendered once, including ones no sample slide references
- `svg/slide_NN.svg`immutable slide-local native-payload backing; do not bulk-read because opaque native payload is retained
- `svg/inheritance.json` — which layout / master each slide consumes
- `svg-flat/slide_NN.svg`lossless complete-page import evidence; use it as mirror restoration authority, not as model-facing context
- `authoring-svg/` / `authoring-svg-flat/` — lightweight non-destructive projections created by `svg_authoring_view.py`; these are the model-facing layered and complete-page views
- optional `svg-flat/slide_NN.svg`immutable complete-page verification backing generated only when explicitly requested; do not use it as the editable source
- `authoring-svg/` and optional `authoring-svg-flat/` — lightweight non-destructive IR bundles created by `svg_authoring_view.py`; each contains editable SVGs, model-readable `authoring_summary.json`, and tool-only `authoring_manifest.json`
- optional screenshots for visual cross-checking
PPTX import interpretation:
@@ -40,25 +40,51 @@ PPTX import interpretation:
- Charts, SmartArt, diagrams, and OLE objects may appear as typed placeholders in layered SVGs. In flat SVGs they may show preview images. Treat them as source intent markers, not reusable decorative assets.
- The asset filenames referenced by SVGs are governed by the manifest asset map. Prefer those references over inventing duplicate asset names.
Input priority for PPTX-backed template creation depends on replication mode:
Input priority for PPTX-backed template creation depends on the AI-derived internal strategy recorded as `replication_mode`:
| Mode | Authoritative inputs | Model-facing inputs |
|---|---|---|
| `standard` / `fidelity` | Finalized brief for the newly designed output; `manifest.json` for factual canvas/theme/assets | Lightweight `authoring-svg-flat/` pages and exported assets as visual references. Source Master/Layout topology is informational only and is not mined into output structure. |
| `mirror` | `manifest.json`, `native_structure.json`, lossless layered `svg/`, lossless complete-page `svg-flat/`, and `svg/inheritance.json` | Matching lightweight `authoring-svg/` and `authoring-svg-flat/` projections for inspection. The projections never replace the lossless restoration source. |
| `standard` / `fidelity` | Finalized brief for the newly designed output; `manifest.json` for factual canvas/theme/assets | `authoring-svg/authoring_summary.json`, layered authoring SVGs, optional flat spot checks, and exported assets as visual references. Do not read `authoring_manifest.json`. Source Master/Layout topology is informational only and is not mined into output structure. |
| `mirror` | `manifest.json`, `native_structure.json`, and `svg/inheritance.json`; the compiler validates the tool-only authoring manifest | `authoring-svg/authoring_summary.json` plus layered authoring SVGs as the editable preservation IR; optional `authoring-svg-flat/` for complete-page verification; matching lossless `svg/` and optional `svg-flat/` only as immutable backing. |
Use `summary.md` only for orientation. Open screenshots or the original PPTX only for visual cross-checking.
Use the compact facts in `manifest.json` for orientation. Open screenshots or the original PPTX only for visual cross-checking.
**Native structure output**: Always set `native_structure_mode: structured`.
**Hard rule — native objects are compiled output**: Treat Theme, Master,
Layout, and Placeholder as PowerPoint implementation objects, not template
kinds. Layout owns topology, placement, semantic text roles, and spatial text
behavior. Deck identity owns paint, typeface identity, and fixed identity
assets; its application context describes the recurring presentation family. Under
downstream `layout` scope, resolve final placeholder formatting from the Layout
roles plus the confirmed identity, reading mode, and type scale; downstream
`mirror` scope preserves literal source formatting and text topology. Compile
the applicable rules into the same native graph without merging their source
ownership.
| Mode | Output structure contract |
|---|---|
| `standard` / `fidelity` | Author new SVG prototypes and an intentional new Master/Layout/slot system. Source visual language and assets may guide the design, but source ownership, keys, picker names, parent relationships, placeholders, and repeated Slide-local elements do not define or seed the output topology. Use compact canonical metadata for authored objects. |
| `mirror` | Restore the source graph one-to-one: keep source Master/Layout identities and parentage, slide assignments, placeholder type/index/bounds, and supported visual/native-object facts. Mechanical normalization maps fixed-layer source groups into the direct atoms required by the current explicit SVG contract while preserving ownership, paint order, and appearance; it must not semantically redesign the graph. |
| `standard` / `fidelity` | Author project-canonical SVG prototypes and an intentional new Master/Layout/slot system. Source visual language and assets may guide the design, but source ownership, keys, picker names, parent relationships, placeholders, and repeated Slide-local elements do not define or seed the output topology. Use the compact authored-preset group only for exact registered preset matches. |
| `mirror` | Materialize a new workspace from the validated source graph one-to-one: keep the Master/Layout identities and parentage, slide assignments, placeholder type/index/bounds, and supported visual/native-object facts that are actually present. Edit the authoring IR; materialization may rehydrate converter-supported native payload only for unchanged source refs. Mechanical normalization maps fixed-layer source groups into the direct atoms required by the current explicit SVG contract while preserving ownership, paint order, and appearance; it must not invent missing facts or semantically redesign the graph. |
Every page remains a complete standalone SVG preview.
**Hard rule — complete mirror graph**: Restore every supported source Layout,
**Authored preset rule**: In `standard` / `fidelity`, when one registered
PowerPoint preset exactly expresses one complete object, use
`preset_shape_svg.py` as defined by
[`native-shape-authoring.md`](./native-shape-authoring.md). Its compact
canonical `<g>` is one semantic atom after validation: it may remain
Slide-local, serve as the one direct carrier of an `object` slot, or carry
Master/Layout fixed-layer ownership. This is the only `<g>` exception to the
fixed-layer atomicity rule; ordinary groups remain forbidden there. Preset
paint comes from the confirmed brief and this template's `design_spec.md`
color scheme. Do not copy an expanded import carrier/preview/fingerprint
bundle into an authored template. `mirror` instead preserves the supported
expanded lossless source representation. The exact syntax and validation
contract remain owned by
[`shared-standards-core.md`](./shared-standards-core.md) and the native-shape reference.
**Hard rule — complete mirror graph**: Preserve every supported source Layout represented by the validated import,
including Layouts unused by source Slides. Emit one complete source-page
prototype per source Slide and one definition-only
`layout_<layout_key>.svg` prototype for each otherwise unrepresented Layout.
@@ -69,23 +95,23 @@ Never silently drop or merge an identity, and never invent a carrier page.
**Hard rule — no duplicate authored Layout contracts**: In `standard` / `fidelity`, distinct output Layout keys must differ in fixed Layout atoms or slot topology/type/index/bounds/binding. Topic, sample wording, or Slide-local content alone never justifies another authored key. Mirror keeps source Layout identities even when two source contracts are visibly equivalent.
**Downstream boundary**: The Strategist confirmation stage selects `strict` or `adaptive` when a project consumes this package. Both export through `pptx_structure.mode: structured`. Strict keeps the referenced Layout contract; adaptive may create a new Layout identity while retaining the template Master. Template_Designer does not preselect that project-level choice.
**Downstream boundary**: Stage 1 independently confirms the current communication contract. Strategist then inspects the installed prototypes, the Deck's descriptive application context, and the current content to author one application plan. It records `mirror`, `layout`, or `style` and, where applicable, `strict` or `adaptive` only as internal exporter values. Explicit user language overrides AI judgment, but the confirmation UI never asks the user to choose these implementation labels. Template_Designer does not preselect that project-level plan.
For `mirror`, `design_spec.md §V` must be followed by a `Source Restoration Map` that records each source slide's Master/Layout assignment and output file. The map is evidence of one-to-one preservation, not a design-decision log. `standard` and `fidelity` record only their newly authored output roster and structure; do not add a source-topology disposition table.
For `mirror`, `design_spec.md §V` must be followed by a `Source Preservation Map` that records each source slide's Master/Layout assignment and output file. The map is evidence of one-to-one preservation, not a design-decision log. `standard` and `fidelity` record only their newly authored output roster and structure; do not add a source-topology disposition table.
---
## Page Roster
The output page set is determined by **replication mode**, declared in the finalized template brief:
The output page set is determined by the confirmed natural-language creation intent. Template_Designer derives one internal `replication_mode` so the deterministic authoring tools can execute:
| Mode | When to use | Roster |
|------|-------------|--------|
| `standard` (default) | Most templates clean, reusable, balanced coverage | `01_cover`, `02_chapter`, `03_content`, `04_ending`, optional `02_toc` |
| `fidelity` | User wants a broader, source-aligned but newly designed template | Standard roster + intentionally designed variants that cover useful reference compositions |
| `mirror` | User wants the source template restored | One SVG prototype restored from the lossless source per source slide, named `<NNN>_<page_type>.svg` by source order |
| `standard` (default internal strategy) | The requested result is a clean, reusable, compact system | Cover, chapter, ending, optional TOC, and one or a small explicitly required set of distinct content Layouts; typically 46 prototypes |
| `fidelity` | The natural-language intent calls for broader, source-aligned but newly designed coverage | Canonical roles plus intentionally designed variants that cover the useful source composition range |
| `mirror` | The natural-language intent calls for preserving validated native source facts | One SVG prototype materialized from the authoring IR per source slide, named `<NNN>_<page_type>.svg` by source order |
**Hard rule — mode controls authorship**: `standard` and `fidelity` create new SVG documents and their own Master/Layout system. `mirror` restores the imported source contract and must not reauthor, distill, or reinterpret its structure.
**Hard rule — mode controls authorship**: `standard` and `fidelity` create new SVG documents and their own Master/Layout system. `mirror` maps the validated imported source contract into a new workspace and must not reauthor, distill, reinterpret, or supplement its structure.
### Standard mode
@@ -97,13 +123,15 @@ The output page set is determined by **replication mode**, declared in the final
| 04 | `04_ending.svg` | Ending page | Fixed structure: thank-you message, contact info |
| -- | `02_toc.svg` | Table of contents | Optional: TOC title, chapter list (number + title) |
**Design philosophy**: Templates define visual consistency and structural pages; content pages maintain maximum flexibility.
**Default — compact authored roster (may override when the confirmed Deck application requires distinct roles)**: Keep Layout content pages structurally flexible. For Deck, add only the distinct prototypes needed to express its confirmed recurring narrative/content roles; do not manufacture variants from hypothetical future uses.
**Naming note**: TOC page keeps `02_toc.svg` naming for template library compatibility and sort order.
**Intent-derived compact variants**: `standard` may include more than one Layout for the same canonical role when the brief requires genuinely different reusable structures, such as two-column evidence and three-card KPI content. Keep the roster compact and brief-driven rather than mining the source page set. When siblings exist, suffix every sibling (`03a_content_two_col.svg`, `03b_content_three_card.svg`) instead of treating one arbitrary variant as the unsuffixed default. This does not require `fidelity`; derive `fidelity` when the broader roster is driven by complete PPTX/SVG page evidence.
**Naming note**: The numeric prefix is the template's own presentation order. Its base sequence stays contiguous; sibling variants share their parent's number only through unique lowercase suffixes such as `03a` / `03b`. When the optional TOC page is included it takes `02_toc.svg` and the later types shift by one: `01_cover`, `02_toc`, `03_chapter`, `04_content`, `05_ending`. Numbers carry no meaning across templates — tooling derives the page type from the token after the underscore, so both spellings of each type are equivalent.
### Fidelity mode
When the brief sets `Replication mode: fidelity`, design a broader reusable roster that stays close to the source's visual language and useful composition examples. The output Master/Layout system is authored independently from source topology.
When the derived implementation writes `replication_mode: fidelity`, design a broader reusable roster that stays close to the source's visual language and useful composition examples. The output Master/Layout system is authored independently from source topology.
**Variant naming**: append a lowercase letter suffix to the parent type's index, preserving sort order:
@@ -113,7 +141,7 @@ When the brief sets `Replication mode: fidelity`, design a broader reusable rost
| Content | `03a_content_two_col.svg`, `03b_content_data_card.svg`, `03c_content_quote.svg` |
| Ending | `04a_ending_thanks.svg`, `04b_ending_contact.svg` |
Extension page types beyond the canonical four (transition / appendix / disclaimer / divider) take the next free index: `05_section_break.svg`, `06_appendix.svg`, `07_disclaimer.svg`.
Extension page types beyond the canonical four (transition / appendix / disclaimer / divider) take the next free index after the roster: `05_section_break.svg`, `06_appendix.svg`, `07_disclaimer.svg` in a four-page roster (one higher when `02_toc` is present).
**Roster decision**:
@@ -126,19 +154,20 @@ Extension page types beyond the canonical four (transition / appendix / disclaim
### Mirror mode
When the brief sets `Replication mode: mirror`, restore the imported template rather than designing a new one:
When the derived implementation writes `replication_mode: mirror`, materialize a new template workspace from validated imported facts rather than designing a new system:
- Restoration source: lossless `svg/master_*.svg`, `svg/layout_*.svg`, `svg/slide_NN.svg`, `svg-flat/slide_NN.svg`, `svg/inheritance.json`, and `native_structure.json`. Use matching lightweight projections only to inspect the result without loading opaque payload into model context.
- Kind eligibility: Create Layout mirror is legal only when the validated source contract is already brand-neutral and application-neutral. If supported source facts retain organization-specific identity or reusable application policy, stop and return to Create Template dispatch: use `standard` / `fidelity` to author a new Layout, or Create Deck to retain those facts. Removing, repainting, retyping, or discarding application rules is never mirror.
- Model-facing authoring source: `authoring-svg/authoring_summary.json`, layered `authoring-svg/*.svg`, `svg/inheritance.json`, and `native_structure.json`. Do not read `authoring-svg/authoring_manifest.json`; materialization validates it internally. When present, use `authoring-svg-flat/` only for full-page verification. Matching lossless `svg/` and optional `svg-flat/` files are immutable backing; materialization resolves only the layered backing.
- Precondition: the import evidence identifies every source Master/Layout, parent relationship, picker name, placeholder contract, and fixed visual layer. Stop only when required facts or supported geometry are missing; unused identities are not a stop condition.
- Output: `<template_workspace>/templates/<NNN>_<page_type>.svg` for every source slide, plus `layout_<layout_key>.svg` for every source Layout unused by all source slides. `<NNN>` is the zero-padded source slide index (3 digits) and `<page_type>` is derived from `manifest.json` `pageTypeCandidates``cover` / `toc` / `chapter` / `content` / `ending`. When the page-type heuristic is ambiguous, fall back to `content`. Preserve source slide order via the numeric prefix; definition-only files are not generated pages.
- Required restoration: preserve source Master/Layout keys and picker names, Layout-to-Master parentage, slide assignments, placeholder type/index/bounds, supported native-object metadata, geometry, decoration, sprite-sheet wrappers, original example text, chart previews, fonts, effects, and paint order whenever the importer represents them.
- Required preservation: preserve source Master/Layout keys and picker names, Layout-to-Master parentage, slide assignments, placeholder type/index/bounds, supported native-object metadata, geometry, decoration, sprite-sheet wrappers, original example text, chart previews, fonts, effects, and paint order whenever the importer represents them.
- Allowed normalization: add or normalize explicit root declarations and asset paths, and recursively expand fixed Master/Layout group wrappers into direct atoms. The mapping must remain one-to-one at the ownership level and must not change paint order or appearance.
- Forbidden: commonality extraction, semantic synthesis, merging, splitting, promotion, demotion, renaming, re-parenting, decorative simplification, placeholder invention, or replacement of supported source-native metadata / SVG fallback with a model-authored approximation.
- `design_spec.md` §V Page Roster lists every emitted file and marks definition-only prototypes explicitly. `Source Restoration Map` records each source-slide assignment plus every unused Layout definition and its parent Master.
- `design_spec.md` §V Page Roster lists every emitted file and marks definition-only prototypes explicitly. `Source Preservation Map` records each source-slide assignment plus every unused Layout definition and its parent Master.
**Mirror consumption boundary**: `mirror` applies only while creating the template package from the source deck. Once created, the package is consumed as an ordinary deck / layout template roster: downstream generation may select, repeat, skip, reorder, or adapt pages according to the new content. The `replication_mode: mirror` field must not force the generated deck to preserve the source page count, source order, or one-output-slide-per-template-slide mapping.
**Mirror consumption boundary**: `replication_mode: mirror` describes source-to-workspace fidelity and only makes literal downstream reuse technically possible. Strategist independently derives the application plan from the current communication contract, content, actual prototype roster, and any explicit natural-language instruction. It may select, repeat, skip, reorder, or reorganize prototypes; no internal scope forces source page count, source order, or one output slide per source slide.
**What mirror is not**: a redesign or topology-cleanup mode. It may mechanically transcode the imported representation into the current explicit SVG/package contract, so byte identity is not promised. Charts, SmartArt, OLE objects, and EMF / WMF media that fail to round-trip in `pptx_template_import.py` will fail the same way in mirror. If the import workspace has missing media or unsupported objects, mirror inherits those gaps — report them before restoration begins.
**What mirror is not**: a redesign, topology-cleanup, or recovery mode. It may mechanically transcode the imported representation into the current explicit SVG/package contract, so byte identity is not promised. Charts, SmartArt, OLE objects, and EMF / WMF media that fail to round-trip in `pptx_template_import.py` will fail the same way in mirror. If the import workspace has missing media or unsupported objects, mirror inherits those gaps — report them before materialization begins.
---
@@ -146,26 +175,30 @@ When the brief sets `Replication mode: mirror`, restore the imported template ra
### 1. Must Generate design_spec.md
**Scope rule — personality only.** A template `design_spec.md` describes **what makes this template recognizable**: brand colors, signature decorative motifs, page-by-page visual character, bundled assets. It does **not** restate generic constraints — those live in the canonical references and are already loaded by every downstream role:
**Scope rule — package-specific rules only.** A Deck `design_spec.md` describes its recurring application plus integrated identity and structure. A Layout spec describes only brand-neutral reusable structure and may state supported content shapes/delivery settings without owning a communication objective or narrative. Neither restates generic constraints — those live in the canonical references and are already loaded by every downstream role:
- General SVG required / forbidden / conditional interfaces → [`shared-standards.md`](shared-standards.md)
- Generic layout pattern library, spacing bands, font-size ratio bands → [`templates/design_spec_reference.md`](../templates/design_spec_reference.md) (read by Strategist when authoring the **project** design_spec)
- Always-on SVG rules and conditional-module routing → [`shared-standards-core.md`](./shared-standards-core.md)
- Generic layout pattern library, spacing bands, font-size ratio bands → [`strategist.md`](strategist.md) (used when authoring the **project** design spec)
- Canonical placeholder vocabulary → §4 below
- Content methodology (pyramid / SCQA / MECE) → [`strategist.md`](strategist.md)
Re-declaring any of these in a template `design_spec.md` is noise — Strategist already has them in context, and duplication forces every relaxation to sweep N templates instead of one source. **If a rule is generic, omit it. If this template breaks a generic rule, write only the deviation.**
**Required skeleton:**
**Required skeleton by kind:**
The frontmatter is portable across library and project scope. Do not add
`output_scope` or `target_project`; those belong only to the workflow execution
brief.
brief. Use `deck_id` or `layout_id`; do not invent a generic `template_id`
field that the registrar cannot bind to its library kind.
**Deck**:
```markdown
---
template_id: <id>
deck_id: <id>
kind: deck
category: brand | general | scenario | government | special
summary: <one-line tone & use case>
summary: <one-line recurring presentation family and intended outcome>
keywords: [tag1, tag2, tag3]
primary_color: "#......"
canvas_format: ppt169
@@ -179,8 +212,9 @@ source_viewbox: "0 0 1280 720"
replication_mode: standard | fidelity | mirror
# Required for every deck/layout template. Source packages remain analysis-only.
native_structure_mode: structured
page_count: <N>
# Optional — only when this template overrides canonical placeholder vocabulary.
# Omit only when the page truly exposes no replaceable content slots.
# Omit the map when canonical vocabulary is sufficient; use [] for an intentional zero-marker page.
# placeholders:
# 01_cover: ["{{TITLE}}", "{{SUBTITLE}}", "{{BRAND_LOGO}}"]
# 03_content: ["{{KEY_MESSAGE}}", "{{CONTENT_AREA}}"]
@@ -189,8 +223,14 @@ native_structure_mode: structured
# [Template Name] — Design Specification
## I. Template Overview
- Use cases, design tone, theme mode (light / dark / mixed)
- One paragraph: what visually identifies this template at a glance
| Application context | Definition |
|---|---|
| Recurring presentation family | <repeatable situations this Deck serves> |
| Intended audiences and outcomes | <who it serves and what the presentation should enable> |
| Delivery and reading assumptions | <presented / close-read / handoff / mixed> |
| Representative narrative/page roles | <roles commonly present in this presentation family; descriptive, not mandatory> |
- Design tone, theme mode (light / dark / mixed), and the visual identity visible at a glance
## II. Color Scheme
- HEX values with role labels (primary / accent / background / text / etc.)
@@ -207,9 +247,9 @@ native_structure_mode: structured
- Optional XML snippet for any reusable component unique to this template
## V. Page Roster
One row per emitted SVG describing what this template's version of cover / chapter / content / ending looks like (background treatment, decorative anchors, layout rhythm, image behavior, content density, intended content slot). For `standard` / `fidelity`, record the newly authored Layout key and PowerPoint picker name. For `mirror`, record the restored source Master/Layout keys and picker names without redesigning them. Roster entries must match the actual SVG files on disk.
One row per emitted SVG describing what this template's version of cover / chapter / content / ending looks like: background treatment, decorative anchors, layout rhythm, image behavior, content density, intended role, reusable slots, and structural capacity. Do not add required/optional/repeatable status or fixed/replaceable/example-only content policy. For `standard` / `fidelity`, record the newly authored Layout key and PowerPoint picker name. For `mirror`, record the preserved source Master/Layout keys and picker names without redesigning them. Roster entries must match the actual SVG files on disk.
For `mirror`, add `### Source Restoration Map` immediately after the roster with columns `Source slide`, `Source Master`, `Source Layout`, `Output SVG`, and `Restoration status`. This is a one-to-one mapping record. Do not add synthesis rationale or source-structure disposition rows to `standard` / `fidelity` templates.
For `mirror`, add `### Source Preservation Map` immediately after the roster with columns `Source slide`, `Source Master`, `Source Layout`, `Output SVG`, and `Preservation status`. This is a one-to-one mapping record. Do not add synthesis rationale or source-structure disposition rows to `standard` / `fidelity` templates.
## VI. Assets (omit when none)
Logos, cover backgrounds, brand textures bundled with the template package — file name, dimensions, intended usage.
@@ -218,14 +258,65 @@ Logos, cover backgrounds, brand textures bundled with the template package — f
Reference the `placeholders:` frontmatter declaration and explain the rationale (e.g. "consulting decks lead with `{{KEY_MESSAGE}}` instead of `{{PAGE_TITLE}}`").
```
**Layout**:
```markdown
---
layout_id: <id>
kind: layout
category: general | scenario | government | special
summary: <one-line structural use case>
keywords: [tag1, tag2, tag3]
canvas_format: ppt169
canvas_width: 1280
canvas_height: 720
canvas_viewbox: "0 0 1280 720"
# Required when a PPTX/SVG source canvas is known.
source_canvas_width: 1280
source_canvas_height: 720
source_viewbox: "0 0 1280 720"
replication_mode: standard | fidelity | mirror
native_structure_mode: structured
page_count: <N>
page_types: [cover, toc, chapter, content, ending]
# Optional vocabulary override.
# placeholders:
# 01_cover: ["{{TITLE}}", "{{SUBTITLE}}"]
---
# [Layout Name] — Design Specification
## IV. Signature Design Elements
- Structure-specific grid, zones, page chrome, image behavior, density rhythm, semantic text roles, alignment/wrapping/capacity behavior, and slot conventions
- Neutral preview paint/font/size may expose hierarchy, but it is not a color, typeface, or final type-scale identity
## V. Page Roster
One row per emitted SVG with Layout key, picker name, supported content shape, and slot behavior. Roster entries must match the actual files on disk.
For `mirror`, append the same `### Source Preservation Map` required above.
## VII. Placeholder Overrides (omit when none)
Reference the `placeholders:` frontmatter declaration and explain the structural vocabulary deviation.
```
**Layout boundary**: Omit Template Overview, Color Scheme, Typography, Logo,
Voice & Tone, and Icon Style. A scenario category records geometric fit only.
Structural text roles, alignment, wrapping, and capacity remain valid Layout
rules; final font families, weights, colors, and absolute sizes do not.
Do not prescribe communication objectives, audience outcomes, required
narrative order, fixed boilerplate, or example-content retention. The
frontmatter `summary` carries concise structural selection context; the
deck-only Template Overview remains the application segment used during
fusion.
Sections to **omit** from template `design_spec.md` (sourced elsewhere — listing them here is noise):
| Don't write | Source |
|---|---|
| General SVG technical / compatibility rules | `shared-standards.md` |
| Generic layout pattern library (centered card / three-column / timeline / …) | `design_spec_reference.md` §V |
| Generic spacing bands (margin 40-60px, card gap 20-32px, etc.) | `design_spec_reference.md` §V |
| Generic font-size hierarchy (cover 2.5-5x body, page title 1.5-2x, …) | `design_spec_reference.md` §IV |
| Always-on SVG rules and conditional-module routing | `shared-standards-core.md` |
| Generic layout pattern library (centered card / three-column / timeline / …) | `strategist.md` §4 |
| Generic spacing bands (margin 40-60px, card gap 20-32px, etc.) | `strategist.md` §4 |
| Generic font-size hierarchy (cover 2.5-5x body, page title 1.5-2x, …) | `strategist.md` §g |
| Canonical placeholder table (`{{TITLE}}`, `{{PAGE_NUM}}`, …) | §4 below |
| Content methodology (pyramid / SCQA / MECE) | `strategist.md` |
| "Usage Instructions" boilerplate (copy template / select page / …) | `create-template.md` |
@@ -233,7 +324,7 @@ Sections to **omit** from template `design_spec.md` (sourced elsewhere — listi
When rewriting an existing template that contains an omitted generic section,
delete it rather than leaving a pointer. Keep a template-specific boundary only
inside the personality section it qualifies (asset system, motif, image
inside the package-owned section it qualifies (asset system, motif, image
treatment, or page roster); do not preserve a generic technical-rules heading.
### 2. Inherit Design Specification
@@ -245,6 +336,7 @@ Templates must strictly follow the finalized template brief and the generated `d
- **Font plan**: Uses the per-role font families declared in the spec
- **Layout principles**: Margins and spacing conform to the spec
- **Image system**: Image placement, crop / mask behavior, full-bleed zones, and overlay rules follow the source-derived norms in the spec
- **Deck application**: Template Overview describes the recurring situations, audiences/outcomes, and representative roles; Page Roster factually describes the actual prototypes and reusable slots without prescribing future use
If PPTX import output exists:
- Prefer imported theme colors and fonts over visually guessed values
@@ -253,9 +345,9 @@ If PPTX import output exists:
**Precondition**:
- For `standard`, inspect enough lightweight complete-page projections to understand the requested visual direction and reusable assets; do not analyze source topology.
- For `fidelity`, inspect every lightweight complete-page projection so the newly designed roster covers the useful source composition range; do not derive output ownership from source Master/Layout recurrence.
- For `mirror`, verify every projected Master, Layout, and Slide against `native_structure.json` and `svg/inheritance.json`, then restore from the matching lossless files. Before restoration begins, report the verified source slide indexes.
- For `standard`, inspect enough lightweight complete-page IR documents to understand the requested visual direction and reusable assets; do not analyze source topology.
- For `fidelity`, inspect every lightweight complete-page IR document so the newly designed roster covers the useful source composition range; do not derive output ownership from source Master/Layout recurrence.
- For `mirror`, verify every authoring Master, Layout, and Slide listed by `authoring_summary.json` against `native_structure.json` and `svg/inheritance.json`, then materialize from the IR with matching lossless payload backing. The compiler validates each machine-manifest record. Before materialization begins, report the verified source slide indexes.
### 2.1 PPTX Import Mode Rule
@@ -263,49 +355,58 @@ The imported PPTX has a different authority level in each replication mode.
| Mode | Required behavior |
|---|---|
| `standard` | Use source visuals/assets as references, then author the compact canonical roster and its Master/Layout/slot structure from the confirmed brief. Do not preserve or distill source topology. |
| `standard` | Use source visuals/assets as references, then author the project-canonical roster and its Master/Layout/slot structure from the confirmed brief. Do not preserve or distill source topology. |
| `fidelity` | Use the complete visual roster as design reference, then author a broader canonical roster and its own Master/Layout/slot structure. Match the source visual language closely, but do not cluster, merge, or split source Layouts into output families. |
| `mirror` | Restore source pages, inheritance, placeholders, native objects, and visuals from the lossless import. Do not simplify, redesign, rename structure, or infer new common structure. |
| `mirror` | Preserve validated source pages, inheritance, placeholders, native objects, and visuals from the lossless import while creating a new workspace. Do not simplify, redesign, rename structure, infer new common structure, or fill gaps. |
**Hard rule — restoration is mechanical**: Mirror may normalize namespaces,
**Hard rule — mirror materialization is mechanical**: Mirror may normalize namespaces,
portable asset paths, explicit root declarations, and fixed-layer group wrappers
required by the current compiler. Expanding a source Master/Layout group must
produce direct atoms with the same ownership, transforms, paint order, and
appearance. A maintainability preference is not authority to alter the source
template.
### 2.2 Native Shape Payload and Authoring Projection
### 2.2 Native Shape Payload and Authoring IR
| Representation | Purpose | Payload rule |
|---|---|---|
| Lossless import SVG | Round-trip and mirror-restoration source | Retain complete imported metadata, native object boundaries, hidden carriers, and source-scope identity. It is authoritative for mirror restoration. |
| Lightweight authoring projection | Model-readable inspection surface | Omit opaque native payload and duplicate hidden carriers from model context; retain visible shape intent and logical ids needed to locate an adopted object in the lossless import |
| `standard` / `fidelity` output | Newly authored contract | Use compact canonical metadata for new shapes. Reuse exported image/vector assets, not opaque source shape payload or source topology. |
| `mirror` output | Restored contract | Keep currently supported imported metadata on unchanged Slide-local/slot objects. Normalize fixed structural layers into direct atoms from the lossless source. The projection never becomes the restoration source. |
| Lossless import SVG | Native-payload backing | Retain complete imported metadata, native object boundaries, hidden carriers, and source-scope identity. Keep it immutable and resolve it only through validated source refs. |
| Authoring IR bundle | Editable template-creation source | Omit opaque native payload and duplicate hidden carriers from model context; retain visible shape intent and stable document-local source refs. Models read `authoring_summary.json`; tools read `authoring_manifest.json` for source paths and initial hashes. |
| `standard` / `fidelity` output | Newly authored contract | Use `preset_shape_svg.py` compact canonical `<g>` output for exact preset matches, with paint from the confirmed brief / `design_spec.md`; use ordinary project SVG for other geometry. Reuse exported image/vector assets, not opaque source shape payload or source topology. |
| `mirror` output | Materialized preserved contract | Preserve currently supported imported metadata on unchanged Slide-local/slot refs, use the edited SVG fallback otherwise, and normalize fixed structural layers into semantic atoms. Strip IR-only source refs from final templates. |
**Validation**: Mirror does not silently use stale metadata or a lightweight
projection. If an imported object cannot use the converter's supported native
metadata after normalization, keep its current SVG fallback and report the
limitation. `standard` / `fidelity` regenerate compact canonical shapes instead
of transplanting opaque source payload. `data-pptx-native` remains reserved for
native chart/table markers.
**Validation**: Mirror does not silently use stale metadata. Materialization
validates source-document hashes and each referenced object's initial authoring
hash before reusing native payload. If an imported object cannot use the
converter's supported native metadata after normalization, keep its current SVG fallback and report the
limitation. For exact registered preset matches, `standard` / `fidelity`
regenerate the compact helper group instead of transplanting opaque source
payload; other geometry stays ordinary project SVG. `data-pptx-replace-with` remains
reserved for optional PowerPoint-native Chart/Table replacement markers.
**Explicit template SVG contract**:
| Authored/restored fact | Template SVG declaration |
| Authored/preserved fact | Template SVG declaration |
|---|---|
| Master/Layout identity | Root `data-pptx-master` / `data-pptx-master-name` plus `data-pptx-layout` / `data-pptx-layout-name`; authored keys for `standard` / `fidelity`, source keys for `mirror` |
| Authored Master/Layout visual | In `standard` / `fidelity`, use a direct atomic child with `data-pptx-layer="master|layout"` and `data-pptx-editable="false"`; `<g>` is forbidden for a structural layer |
| Restored Master/Layout visual | In `mirror`, recursively expand each fixed-layer source group into direct atoms with the same Master/Layout ownership, transforms, styles, paint order, and appearance; semantic regrouping is forbidden |
| Content slot | Direct `<g id>` with `data-pptx-placeholder` and explicit `data-pptx-placeholder-bounds`; `standard` / `fidelity` author the slot, while `mirror` preserves source type/index/bounds and carrier identity |
| Authored Master/Layout visual | In `standard` / `fidelity`, use a direct atomic child with `data-pptx-layer="master|layout"` and `data-pptx-editable="false"`. An ordinary `<g>` is forbidden; one validated compact canonical authored-preset `<g>` is a semantic atom and is the sole group exception. |
| Preserved source Master/Layout visual | In `mirror`, recursively expand each fixed-layer source group into direct atoms with the same Master/Layout ownership, transforms, styles, paint order, and appearance; semantic regrouping is forbidden |
| Content slot | Direct `<g id>` with `data-pptx-placeholder` and explicit `data-pptx-bounds`; `standard` / `fidelity` author the slot, while `mirror` preserves source type/index/bounds and carrier identity |
| Page-only background | Direct full-canvas solid rect with `data-pptx-layer="slide"` |
| Structural page-frame hint | Optional `data-pptx-role` only when background/decoration/header/footer/logo/watermark/chrome/page-number behavior is not already expressed by layer/placeholder metadata; stable unique `id` required |
Repeat inherited visuals in every standalone SVG so browser preview remains complete. Template export validates their equality and restores or creates the declared Master/Layout parts. It does not infer ownership.
Repeat inherited visuals in every standalone SVG so browser preview remains complete. Template export validates their equality and materializes the declared Master/Layout parts. It does not infer ownership.
**Forbidden — legacy structure contract**: Do not carry `data-pptx-layout-kind`, `distilled`, `utility`, unmapped `baseline`, `preserve`, or direct atomic placeholders into a reusable template package. Route that source through [`restore-pptx-structure`](../workflows/restore-pptx-structure.md) first.
**Forbidden — legacy structure contract**: Do not carry `data-pptx-layout-kind`, `distilled`, `utility`, unmapped `baseline`, `preserve`, or direct atomic placeholders into a reusable template package. In `standard` / `fidelity`, treat such Type B inputs only as visual reference and author a complete current contract in a new workspace. Require the original PPTX Type A path when mirror must preserve existing native topology; see [`create-template`](../workflows/create-template.md).
**Composite slot boundary**: A normal slot group has exactly one compatible direct carrier. Only a genuinely composite region may declare `data-pptx-placeholder="object"` with `data-pptx-placeholder-binding="proxy"`; the visible group stays Slide-local and export creates a hidden transparent binding proxy. Do not use proxy binding as the default template slot form.
**Composite slot boundary**: A normal slot group has exactly one compatible
direct carrier. A validated compact canonical authored-preset `<g>` counts as
one carrier for an `object` slot because it compiles to one native shape; an
ordinary multi-object `<g>` does not. Only a genuinely composite region may
declare `data-pptx-placeholder="object"` with
`data-pptx-binding="proxy"`; the visible group stays Slide-local
and export creates a hidden transparent binding proxy. Do not use proxy binding
as the default template slot form.
In `mirror`, preserve imported placeholder types, indices, bounds, and carrier
identity exactly when the importer supports them. Do not replace source
@@ -316,7 +417,7 @@ identity exactly when the importer supports them. Do not replace source
assign stable indices only when repeated roles need disambiguation inside the
new Layout.
**Hard rule — explicit design-zone bounds**: Every slot carries `data-pptx-placeholder-bounds="x y width height"`. Mirror uses the source Layout placeholder frame. `standard` / `fidelity` author bounds from the intended safe area, column, panel inset, or media frame. Do not use character count, glyph width, current wrapping, or the tight sample-content box. An authored Layout may intentionally have zero slots.
**Hard rule — explicit design-zone bounds**: Every slot carries `data-pptx-bounds="x y width height"` with at most two decimals per value. Mirror uses the source Layout placeholder frame. `standard` / `fidelity` author bounds from the intended safe area, column, panel inset, or media frame. Do not use character count, glyph width, current wrapping, or the tight sample-content box. An authored Layout may intentionally have zero slots.
### 3. Placeholder Markers
@@ -327,8 +428,8 @@ Use clear placeholder markers for replaceable content:
```xml
<!-- Text slot -->
<g id="title-slot" data-pptx-placeholder="title"
data-pptx-placeholder-bounds="80 280 1120 96">
<text id="title-carrier" data-pptx-placeholder-carrier="true"
data-pptx-bounds="80 280 1120 96">
<text id="title-carrier" data-pptx-carrier="true"
x="80" y="320" fill="#FFFFFF" font-size="48" font-weight="bold">
{{TITLE}}
</text>
@@ -337,8 +438,8 @@ Use clear placeholder markers for replaceable content:
<!-- Content area placeholder (content page only) -->
<rect x="40" y="90" width="1200" height="550" fill="#FFFFFF" rx="8"/>
<g id="body-slot" data-pptx-placeholder="body"
data-pptx-placeholder-bounds="40 90 1200 550">
<text id="body-carrier" data-pptx-placeholder-carrier="true"
data-pptx-bounds="40 90 1200 550">
<text id="body-carrier" data-pptx-carrier="true"
x="640" y="365" text-anchor="middle" fill="#CBD5E1" font-size="16">
{{CONTENT_AREA}}
</text>
@@ -411,17 +512,17 @@ Standard mode (default):
├── templates/
│ ├── design_spec.md
│ ├── 01_cover.svg
│ ├── 02_chapter.svg
│ ├── 02_toc.svg # Optional
│ ├── 03_content.svg
── 04_ending.svg
│ └── icons/ # Package/validation copy, when used
│ ├── 02_toc.svg # Optional; without it: 02_chapter, 03_content, 04_ending
│ ├── 03_chapter.svg
│ ├── 04_content.svg
── 05_ending.svg
├── images/ # Optional; omit when unused
│ └── *.png / *.jpg # SVG href is ../images/<name>
├── icons/ # Optional; omit when unused
│ └── *.svg # Runtime copy, when used
└── exports/ # Optional; created only for on-demand review
└── <template_id>_template_preview.pptx
│ └── imported/
│ └── *.svg # Canonical imported vectors, when used
└── exports/ # Optional; requested review or required multi-Master evidence
└── <deck_id|layout_id>_template_preview.pptx
```
Fidelity mode changes only the roster under `templates/`, e.g.:
@@ -430,14 +531,14 @@ Fidelity mode changes only the roster under `templates/`, e.g.:
<template_workspace>/templates/
├── design_spec.md
├── 01_cover.svg
├── 02a_chapter_full.svg
├── 02b_chapter_minimal.svg
├── 02_toc.svg
├── 03a_content_two_col.svg
├── 03b_content_data_card.svg
├── 03c_content_quote.svg
├── 04_ending.svg
── 05_section_break.svg
├── 03a_chapter_full.svg
├── 03b_chapter_minimal.svg
├── 04a_content_two_col.svg
├── 04b_content_data_card.svg
── 04c_content_quote.svg
├── 05_ending.svg
└── 06_section_break.svg
```
Mirror mode emits one SVG per source slide, named by source order:
@@ -456,20 +557,20 @@ Mirror mode emits one SVG per source slide, named by source order:
└── 050_ending.svg
```
Filenames preserve the source slide order via the 3-digit prefix; `<page_type>` is derived from `manifest.json` `pageTypeCandidates`. Literal source text and the source native structure are restored; the lightweight projection is not copied into the output.
Filenames preserve the source slide order via the 3-digit prefix; `<page_type>` is derived from `manifest.json` `pageTypeCandidates`. Literal source text and validated native structure facts are preserved when the authoring IR is materialized into the new workspace; IR-only refs and its manifest are not copied into the template output.
**Hard rule — common routing**: Keep `design_spec.md`, template SVGs, and non-bitmap template-source assets in `templates/`; place every bitmap in `images/`; duplicate each extracted icon into `templates/icons/` and runtime `icons/`. Write a review deck to `exports/` only when explicitly requested. Create optional directories only when they contain real files; never add placeholders for empty directories. Do not branch asset placement by output scope.
**Hard rule — common routing**: Keep `design_spec.md`, template SVGs, and non-bitmap template-source assets in `templates/`; place every bitmap in `images/`; place each imported vector exactly once in `icons/imported/` and reference it as `data-icon="imported/<name>"`. Never create `templates/icons/`. Write a review deck to `exports/` when explicitly requested and always for a multi-Master package gate. Create Template must not create optional directories or placeholder files solely to retain empty paths. An initialized project may already contain empty scaffolding; leave it untouched and omit it from completion unless real template files were written or adopted there. Do not branch asset placement by output scope.
### Template Preview
When the user requests a PowerPoint review file, run `template_preview_pptx.py <template_workspace>` after SVG validation. The command creates `exports/` on demand and verifies one slide per SVG prototype plus the expected Master/Layout counts. The first export refuses a collision; an intentional post-fix replacement uses `--force`. The review PPTX is derived evidence and never a template-application input.
When the user requests a PowerPoint review file or the validated roster declares multiple Masters, run `template_preview_pptx.py <template_workspace>` after SVG validation. The command creates `exports/` on demand and verifies one slide per SVG prototype plus the expected Master/Layout counts. In authored modes, it shortens canonical marker text only in ephemeral review copies so prompts remain readable without changing the source SVG, carrier typography, or placeholder frames. The first export refuses a collision; an intentional post-fix replacement uses `--force`. The review PPTX is derived evidence and never a template-application input.
When a review deck was generated, include its path in the completion summary. Otherwise omit `exports/` from the workspace inventory.
When a review deck was generated, include its path in the completion summary. Omit `exports/` only for an unrequested one-Master package.
If the template is based on PPTX import output, briefly note:
- which extracted assets were reused directly
- for `standard` / `fidelity`, which visual references influenced the newly authored roster
- for `mirror`, whether any source feature could not be restored and the exact affected source object/page
- for `mirror`, whether any source feature could not be preserved and the exact affected source object/page
- whether any page-type filename mapping required judgment beyond the import heuristic
---
@@ -484,7 +585,7 @@ If suitable template resources already exist, use them directly instead of gener
This section describes downstream reuse of an existing workspace. Library and project scopes carry the same portable template contract.
**Example library structure** (query the appropriate kind's index — `templates/layouts/layouts_index.json` for structure-only templates, `templates/decks/decks_index.json` for complete identity + structure templates, `templates/brands/brands_index.json` for identity-only presets):
**Example library structure** (query the appropriate kind's index — `templates/layouts/layouts_index.json` for brand-neutral structure templates, `templates/decks/decks_index.json` for recurring applications with integrated identity/structure, `templates/brands/brands_index.json` for identity-only presets):
```
templates/
@@ -507,15 +608,16 @@ templates/
- [x] Read `references/template-designer.md`
- [x] Output scope confirmed: `library` | `project`; the common workspace preflight passed before final writes
- [x] Replication mode confirmed: `standard` | `fidelity` | `mirror`
- [x] Internal creation strategy derived from the confirmed natural-language intent: `standard` | `fidelity` | `mirror`; Layout mirror source is already brand-neutral and application-neutral
- [x] Every page listed in `design_spec.md §V Page Roster` saved to `<template_workspace>/templates/`
- [x] Naming convention applied (standard / fidelity: letter-suffix variants; mirror: `<NNN>_<page_type>.svg`)
- [x] Templates follow design spec (colors, fonts, layout)
- [x] `standard` / `fidelity` SVGs and Master/Layout contracts were newly authored; `mirror` SVGs restore the lossless source graph without semantic redesign
- [x] Placeholder markers are clear and standardized for `standard` / `fidelity`; mirror preserves literal source text plus source placeholder type/index/bounds
- [x] Deck Template Overview and factual Page Roster describe the recurring application and actual prototypes without mandatory use policy; Layout output contains no application or identity contract
- [x] `standard` / `fidelity` SVGs and Master/Layout contracts were newly authored; `mirror` SVGs were materialized from the authoring IR while preserving the source graph without semantic redesign
- [x] Placeholder markers are clear and standardized for `standard` / `fidelity`; preview-only sample text remains readable without changing source markers, while mirror preserves literal source text plus source placeholder type/index/bounds
- [x] Every SVG is a complete preview with explicit root Master/Layout identity and `native_structure_mode: structured`; authored modes use canonical fixed layers/slots, while mirror preserves source ownership and mechanically expands fixed-layer groups into direct atoms
- [x] Authored `standard` / `fidelity` Layout keys are non-duplicative; mirror keeps distinct source Layout identities even when their current visible contracts are equivalent
- [x] Model context used lightweight projections; lossless imports remained authoritative for mirror restoration, while `standard` / `fidelity` used compact canonical authored metadata
- [x] Both scopes route bitmaps to `images/` and copy extracted icons to both `templates/icons/` and runtime `icons/`
- [ ] **Next step**: Validate assets, optionally export a review PPTX, then register only library scope
- [x] Template creation used the authoring IR; lossless expanded imports remained immutable payload backing for mirror materialization, while `standard` / `fidelity` used helper-generated compact canonical preset groups and `design_spec.md` paint
- [x] Both scopes route bitmaps to `images/` and keep one canonical copy of every imported vector under `icons/imported/`
- [ ] **Next step**: Validate assets, export review evidence when requested or required for multiple Masters, then register only library scope
```
@@ -1,6 +1,6 @@
# Visual Review Rubric
> Per-page visual self-check rubric for slide SVGs. Read by the subagents spawned during the `visual-review` workflow. Companion to the [`visual-review` workflow](../workflows/visual-review.md) and the [`visual_review.py`](../scripts/visual_review.py) renderer.
> Per-page visual self-check rubric for slide SVGs. Read by the subagents spawned during the `visual-review` stage. Companion to the [`visual-review` stage](../workflows/stages/visual-review.md) and the [`visual_review.py`](../scripts/visual_review.py) renderer.
## §0 Prerequisites
@@ -188,7 +188,7 @@ Each subagent writes exactly one file to `<project>/.review/<page>.json`:
## §6 Dispatch & messaging contract
This rubric is consumed by subagents spawned via the `visual-review` workflow. Mandatory dispatch invariants:
This rubric is consumed by subagents spawned via the `visual-review` stage. Mandatory dispatch invariants:
### §6.1 Orchestrator → subagent (batched dispatch)
@@ -2,7 +2,7 @@
A **visual style** is how the deck **looks** — shape language, decoration density, whitespace rhythm, typographic character, texture / elevation. Lock **one per deck**; it anchors the aesthetic of the SVG layout itself (cards, dividers, spacing, corner radius, shadow use).
> **Styles carry NO HEX and lock no palette.** Color truth lives in `design_spec.colors` / `spec_lock.colors` (confirmation `e`); color *behavior* lives in [`image-palettes/`](../image-palettes/). A visual style only describes how the deck's existing colors are *used* — never which colors. (Same discipline as [`image-renderings/`](../image-renderings/) for AI images.)
> **Styles carry NO HEX and lock no palette.** Color truth and role behavior live in `design_spec.colors` / `spec_lock.colors` (confirmation `e`). A visual style only describes how those existing colors are used in SVG composition—never which colors to substitute. Generated images follow the same single source of truth: their rendering comes from [`image-renderings/`](../image-renderings/), while their exact colors inherit the deck roles directly. [`image-palettes/`](../image-palettes/) is legacy compatibility material only.
>
> A visual style is *not* a mode. **Visual style = how it looks; mode = how you argue** (see [`modes/_index.md`](../modes/_index.md)). Locked independently — any style pairs with any mode.
@@ -92,15 +92,15 @@ Each style has its own file with: shape & decoration, typography character, colo
## 3. Escape hatch — `custom`
When no preset captures the intended aesthetic, set `- visual_style: custom` in `spec_lock.md` and add a `- visual_style_behavior:` line: one paragraph naming shape language, composition geometry (page-scale moves), decoration density, whitespace, typographic character, and texture — **no HEX, no color names as values**. `custom` is a tail-case, not a default; reach for a preset first.
Stage 2 always authors one visible, non-empty AI custom proposal beside the preset spectrum. Its paragraph names shape language, composition geometry (page-scale moves), decoration density, whitespace, typographic character, and texture — **no HEX, no color names as values**. The proposal is initially unselected and remains recommendation-only unless the user chooses it; a template-backed proposal must stay inside the inherited identity and confirmed application plan. When selected, record the edited aesthetic in the Design Spec first, then project `- visual_style: custom` plus `- visual_style_behavior:` to `spec_lock.md`. The candidate is mandatory; selecting `custom` remains a tail-case, not the default.
---
## 4. How to use
1. Strategist reads this index at confirmation `d. Layer 2`.
2. Pick one style from the auto-selection table + the deck's vibe.
3. Lock it: write `- visual_style: <name>` into `spec_lock.md`, record rationale in `design_spec.md`.
2. Preselect one style from the auto-selection table + the deck's vibe; separately author the visible AI custom proposal from §3.
3. Record the confirmed style and rationale in `design_spec.md`, then project `- visual_style: <name>` into `spec_lock.md`.
4. Executor reads **only** `visual-styles/<locked-style>.md` at generation entry — never globs this directory.
**Lock scope**: deck-wide (one style per deck). It anchors taste as a **reference**, not a whitelist — pages may deviate with reason.
@@ -12,6 +12,7 @@ This directory contains user-facing scripts for conversion, project setup, direc
- `scripts/template_import/`: internal PPTX reference-preparation helpers used by `pptx_template_import.py`
- `scripts/svg_finalize/`: internal post-processing helpers used by `finalize_svg.py`
- `scripts/docs/`: topic-focused script documentation
- `scripts/prompt_audit.py` + `scripts/prompt_audit_manifest.json`: maintainer-only prompt budget/governance lint (see [`docs/prompt_audit.md`](docs/prompt_audit.md)); the manifest is audit-only and never loaded as prompt context
- `scripts/assets/`: static assets consumed by scripts
## Quick Start
@@ -44,11 +45,11 @@ python3 scripts/update_repo.py
| Area | Primary scripts | Documentation |
|------|-----------------|---------------|
| Conversion | `source_to_md.py`, `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`, `pptx_to_svg.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`, `native_enhance_pptx.py` | [docs/project.md](./docs/project.md) |
| SVG pipeline | `preset_shape_svg.py`, `svg_authoring_view.py`, `finalize_svg.py`, `svg_to_pptx.py`, `template_preview_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); [native preset authoring](../references/native-shape-authoring.md) |
| Project management | `project_manager.py`, `page_context.py`, `batch_validate.py`, `generate_examples_index.py`, `error_helper.py`, `pptx_template_import.py`, `template_fill_pptx.py`, `native_enhance_pptx.py` | [docs/project.md](./docs/project.md) |
| SVG pipeline | `preset_shape_svg.py`, `svg_authoring_view.py`, `compact_svg_coordinates.py`, `mirror_template_materialize.py`, `finalize_svg.py`, `svg_to_pptx.py`, `template_preview_pptx.py`, `total_md_split.py`, `svg_quality_checker.py`, `extract_svg_assets.py`, `extract_svg_pictures.py`, `animation_config.py`, `notes_to_audio.py` | [docs/svg-pipeline.md](./docs/svg-pipeline.md); [native preset authoring](../references/native-shape-authoring.md) |
| PPTX transitions | `pptx_transitions.py` | [docs/pptx-transitions.md](./docs/pptx-transitions.md) |
| PPTX animations | `pptx_animations.py`, `animation_config.py` | [docs/pptx-animations.md](./docs/pptx-animations.md) |
| Spec maintenance | `update_spec.py` | [docs/update_spec.md](./docs/update_spec.md) |
| Spec maintenance | `update_spec.py`, `chart_recall.py` | [docs/update_spec.md](./docs/update_spec.md); [docs/chart-recall.md](./docs/chart-recall.md) |
| Image tools | `image_gen.py`, `latex_render.py`, `analyze_images.py`, `gemini_watermark_remover.py` | [docs/image.md](./docs/image.md) |
| Repo maintenance | `update_repo.py` | README install/update section |
| Troubleshooting | validation, preview, export, dependency issues | [docs/troubleshooting.md](./docs/troubleshooting.md) |
@@ -72,7 +73,26 @@ Project setup:
```bash
python3 scripts/project_manager.py init <project_name> --format ppt169
python3 scripts/project_manager.py import-sources <project_path> <source_files_or_dirs...> --move
python3 scripts/project_manager.py scaffold-spec <project_path>
python3 scripts/project_manager.py scaffold-lock <project_path>
python3 scripts/project_manager.py validate <project_path>
python3 scripts/project_manager.py page-context <project_path> P07 --record-usage
python3 scripts/project_manager.py page-context-report <project_path>
```
`page-context` prints a read-only compact current-page projection. Its global
lock projection repeats per page as an anti-drift guard; large Design Specs,
prototype, and `templates/charts/` references are emitted only as scoped
path/SHA fingerprints and are read once per execution context. `--bundle` is a
deprecated compatibility no-op. `--record-usage` writes one derived snapshot
under `analysis/page-context/`; exact `o200k_base` token counts are optional and
degrade to `tokens: null` when `tiktoken` is absent.
Chart candidate recall:
```bash
python3 scripts/chart_recall.py recall --page P03 --tag "time series" --tag "three metrics" --tag "direction over time"
python3 scripts/chart_recall.py validate line_chart
```
Template source import:
@@ -81,23 +101,63 @@ Template source import:
python3 scripts/pptx_template_import.py <template.pptx>
python3 scripts/pptx_template_import.py <template.pptx> --manifest-only
python3 scripts/pptx_template_import.py <template.pptx> --inheritance-mode both
python3 scripts/svg_authoring_view.py <imported-svg-or-dir> -o <output-dir>
python3 scripts/svg_authoring_view.py <imported-svg-or-dir> -o <output-dir> --projection-kind layered
python3 scripts/svg_authoring_view.py <authoring-dir> --refresh-summary
python3 scripts/compact_svg_coordinates.py <template_workspace>/templates --inplace --keep-native-frames
python3 scripts/mirror_template_materialize.py <import_workspace> <empty_template_workspace>
python3 scripts/template_preview_pptx.py <template_workspace>
python3 scripts/template_preview_pptx.py <legacy_template_workspace> --visual-only
```
`svg_authoring_view.py` creates a lightweight, non-destructive inspection copy
of PPTX-imported SVGs. It removes embedded `txbody` payloads,
Template import defaults to the canonical layered `svg/` tree. Use
`--inheritance-mode both` only when a separate self-contained `svg-flat/`
verification tree is required. No derived narrative digest is generated
because `manifest.json` already owns those facts.
`svg_authoring_view.py` creates a lightweight, non-destructive editable IR
bundle from PPTX-imported SVGs. It removes embedded `txbody` payloads,
duplicate hidden geometry carriers, and import-identity attributes from the
copy while retaining visible fallback geometry, text, images, stable element
ids, root Master/Layout markers, and supported compact native-shape intent.
ids, root Master/Layout markers, selected native-shape intent, and
document-local `data-pptx-source-ref` values.
Relative local image references are rewritten so the projected copy still
renders from its new location. The full imported SVG remains unchanged and is
the evidence source for mirror restoration; the projection is inspection-only.
The exporter never reads the import workspace or the projection. A projected
view is not a final template or release export source.
renders from its new location. The bundle's `authoring_summary.json` is the
model-readable current-file index; `authoring_manifest.json` records
source/authoring hashes and object paths for tools without duplicating opaque
payload and does not enter model context. Imported model-facing frames and safe
transform page coordinates use at most two decimals; immutable lossless SVGs
retain the original precision. In-place vector/picture extraction
refreshes the summary automatically; use `--refresh-summary` after other direct
IR edits. The full imported SVG remains unchanged as native-payload backing.
Template creation edits the IR and materializes validated `templates/*.svg`;
the IR directory itself is not a final template or direct release export
source.
`template_preview_pptx.py` reads a template workspace, exports every `templates/*.svg` prototype as one structured review slide, and verifies the resulting Master/Layout package. This is an on-demand review action: its default output is `exports/<template_id>_template_preview.pptx`, and that directory need not exist before the command runs. It refuses an existing output unless an intentional re-export passes `--force`. `--visual-only` is an explicit migration aid for legacy SVG rosters: it creates a slide-local visual review deck without validating or claiming a reusable Master/Layout contract. This diagnostic path does not require a project `spec_lock.md`; it may retain generic theme/text defaults inside its clean one-Master/one-Layout shell. New structured templates use the default mode when a review deck is requested.
`mirror_template_materialize.py` is the deterministic Type A mirror compiler.
It consumes only the layered `authoring-svg/` IR as editable input, loads its
tool-only manifest internally, and validates it against immutable `svg/`,
`native_structure.json`,
`svg/inheritance.json`, `source_template.pptx`, and any extracted-vector
inventory, then publishes a complete structured template roster atomically.
Unchanged supported Slide-local/slot refs may recover native payload; edited
refs keep their current SVG fallback. Fixed Master/Layout wrappers are expanded
mechanically into direct atoms, source visibility flags become canonical root
metadata, and imported vectors are copied once to `icons/imported/`. Large
opaque `txBody`, shape-style, and custom-geometry payloads are deduplicated into
`templates/native_payloads.json.gz`; repeated native restoration attributes
are stored there as short `data-pptx-native-ref` records. Structural metadata
stays inline, while checker, template-structure validation, and export hydrate
both layers in memory. Legacy inline payload and v1 payload-only stores remain
readable. The v1 execution manifest points to per-prototype
`ppt-master.template-text-slots.v2-min` diagnostic sidecars. They are derived
tool metadata and are not injected into model context. Checker and export
validate output attributes, topology, and resource hashes against the complete
prototype internally. Bitmap assets
go to `images/`; other referenced source assets go to `templates/assets/`.
The destination must be empty, and the command does not write
`templates/design_spec.md`; Template_Designer owns that authored brief.
`template_preview_pptx.py` reads a template workspace, exports every public `templates/*.svg` prototype as one structured review slide, and verifies the resulting Master/Layout package. Canonical definition-only `layout_<layout_key>.svg` prototypes are registered as reusable Layouts through internal carrier slides that are removed before publication; they never increase the review deck's visible slide count. This is an on-demand review action: its default output is `exports/<template_id>_template_preview.pptx`, and that directory need not exist before the command runs. It refuses an existing output unless an intentional re-export passes `--force`. `--visual-only` is an explicit migration aid for legacy SVG rosters: it creates a slide-local visual review deck without validating or claiming a reusable Master/Layout contract. This diagnostic path does not require a project `spec_lock.md`; it may retain generic theme/text defaults inside its clean one-Master/one-Layout shell. New structured templates use the default mode when a review deck is requested.
Template fill (direct PPTX, no SVG conversion):
@@ -131,12 +191,44 @@ python3 scripts/preset_shape_svg.py describe rightArrow
python3 scripts/preset_shape_svg.py render rightArrow --id process-arrow --frame 120 180 240 96 --fill '#2563EB'
```
The helper never writes a page or project file. Select one exact semantic stock-shape match, inspect the emitted fragment, and insert it into the hand-authored SVG with the normal patch workflow. Keep ordinary rectangles, ellipses, freeform geometry, charts, icons, and ambiguous silhouettes as regular SVG. See [`references/native-shape-authoring.md`](../references/native-shape-authoring.md) for the selection and metadata contract.
The helper never writes a page or project file. Select one exact semantic
stock-shape match, inspect the emitted fragment, and insert it into the
hand-authored SVG with the normal patch workflow. Its project-authored output
is one compact atomic `<g>` with direct registry-generated visible paths;
quality check and export rerender the registry instead of relying on a hidden
carrier, preview wrapper, or stored preview fingerprint. PPTX import and
round-trip SVGs deliberately keep their expanded carrier/preview evidence and
are not rewritten into this authored form. Keep ordinary rectangles, ellipses,
freeform geometry, charts, icons, and ambiguous silhouettes as regular SVG.
See [`references/shared-standards-core.md`](../references/shared-standards-core.md) §1.5 for
the normative contract and
[`references/native-shape-authoring.md`](../references/native-shape-authoring.md)
for selection and authoring guidance.
Create-template/source normalization (optional; never part of automatic export):
```bash
python3 scripts/extract_svg_assets.py <layered_svg_dir> --icons-dir <icons_dir> --icon-namespace imported --inplace --id-prefix layered
python3 scripts/extract_svg_assets.py <flat_svg_dir> --icons-dir <icons_dir> --icon-namespace imported --reuse-inventory <layered_inventory.json> --inplace --id-prefix flat
python3 scripts/extract_svg_pictures.py "<svg_file>" --select "<group_id>" --resource-root "<workspace>" --images-dir "<picture_assets_dir>" --inplace # optional create-template normalization: one selected group -> one SVG picture
python3 scripts/compact_svg_coordinates.py <template_workspace>/templates --inplace --keep-native-frames
python3 scripts/mirror_template_materialize.py <import_workspace> <empty_template_workspace> # Type A mirror only
```
`extract_svg_assets.py` fingerprints each extracted subtree before generated-ID
namespacing. Process the layered authoring view first, then pass its inventory to
the flat view with `--reuse-inventory`; matching flat subtrees reference the
existing layered asset instead of creating a duplicate file. Only unmatched
flat-only vectors create new assets. Create-template stores these assets once in
`<workspace>/icons/imported/` and writes `data-icon="imported/<name>"` references.
Inventories retain any `data-pptx-source-ref` values carried by the extracted
subtree, so re-inlining preserves authoring-manifest object identity.
Rerunning a namespaced pass against an already rewritten projection inventories
the existing references without progressively wrapping more parent geometry.
Post-processing and export:
```bash
python3 scripts/extract_svg_assets.py <svg_dir> --icons-dir <icons_dir> --inplace --id-prefix <prefix> # optional: shrink imported/reference SVGs before AI review
python3 scripts/total_md_split.py <project_path>
python3 scripts/finalize_svg.py <project_path>
python3 scripts/svg_to_pptx.py <project_path>
@@ -148,21 +240,21 @@ python3 scripts/svg_to_pptx.py <project_path>
For SVG-authoring routes, `svg_output/` is the complete visible page-design source: every exported text, image, shape, background, and template-derived layout element is present in the page SVG or explicitly referenced by it. Export may translate represented content into Master/Layout/Slide parts or native objects, but it does not retrieve missing visible content from templates or planning files. Speaker notes, animation, narration, transitions, `template-fill-pptx`, and `native-enhance-pptx` remain separately owned capabilities.
Native `svg_to_pptx.py` release export reads the project's explicit structure mode. Free-design and brand-only projects use `flat`, omit Master/Layout mappings and SVG structure metadata, keep every represented object Slide-local, and materialize one clean project-owned Master plus one Blank Layout from the current color/typography lock. Stock content placeholders and unused built-in Layouts are removed; only the standard date/footer/slide-number capability hooks remain. Deck/layout template projects use `structured`: each project supplies unique Master/Layout definitions and one Layout assignment per generated page before SVG generation, and every SVG root repeats its assigned identity. A template-backed definition may remain unused and still register without a published carrier slide. Fixed Master/Layout visuals are direct atomic children; reusable slots are top-level groups with positive design-zone bounds plus one compatible carrier. Composite `object` regions use explicit proxy binding, and zero-slot Layouts are valid.
Native `svg_to_pptx.py` release export reads the project's explicit structure mode. Free-design and brand-only projects use `flat`, omit Master/Layout mappings and SVG structure metadata, keep every represented object Slide-local, and materialize one clean project-owned Master plus one Blank Layout from the current color/typography lock. Stock content placeholders and unused built-in Layouts are removed; only the standard date/footer/slide-number capability hooks remain. Deck/layout template projects use `structured`: each project supplies unique Master/Layout definitions and one Layout assignment per generated page before SVG generation, and every SVG root repeats its assigned identity. A template-backed definition may remain unused and still register without a published carrier slide. Fixed Master/Layout visuals are direct semantic atoms; ordinary groups are invalid there, while one validated compact authored-preset `<g>` is the sole group exception because it compiles to one native shape. Reusable slots are top-level groups with positive design-zone bounds plus one compatible carrier. Composite `object` regions use explicit proxy binding, and zero-slot Layouts are valid.
Structured template export compiles only the declared structure, maps locked typography/colors into PowerPoint defaults, creates the named Master/Layout parts, and reads the package back before publication. It never clusters pages, promotes repeated chrome heuristically, or invents placeholders. Flat export is the normal free-design/brand-only route: it creates only the clean project-owned shell and performs no promotion or deduplication of Slide content.
Template `page_layouts` records authoring-input provenance, `pptx_masters` / `pptx_layouts` own unique reusable definitions, and `page_pptx_layouts` owns page assignment. Strict preserves its Master/Layout/slot contract; adaptive retains its Master and may use a new Layout key only when fixed Layout atoms or slot topology/bounds change. `standard` / `fidelity` author new SVGs and a new Master/Layout/slot contract. `mirror` restores the complete source identity graph—including unused Layout definitions—without semantic synthesis, while mechanically expanding fixed-layer group wrappers into the direct atoms required by the structured contract.
Template `page_layouts` records authoring-input provenance, `pptx_masters` / `pptx_layouts` own unique reusable definitions, and `page_pptx_layouts` owns page assignment. Strict preserves its Master/Layout/slot contract; adaptive retains its Master and may use a new Layout key only when fixed Layout atoms or slot topology/bounds change. `standard` / `fidelity` author new SVGs and a new Master/Layout/slot contract. `mirror` materializes a new workspace from the complete validated source identity graph—including unused Layout definitions—without semantic synthesis or gap filling, while mechanically expanding fixed-layer group wrappers into the direct atoms required by the structured contract.
Legacy structured/template contracts using `baseline`, `template`, `preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled`/`utility`, direct atomic placeholders, or incomplete root Master identity must run [`restore-pptx-structure`](../workflows/restore-pptx-structure.md) before release export. Explicit flat free-design/brand-only projects intentionally omit root Master identity.
Legacy structured/template contracts using `baseline`, `template`, `preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled`/`utility`, direct atomic placeholders, or incomplete root Master identity must be replaced by a new workspace created through [`create-template`](../workflows/create-template.md). Generate new structured SVG pages from that workspace; do not upgrade the existing PPTX/SVG in place. Explicit flat free-design/brand-only projects intentionally omit root Master identity.
`pptx_to_svg.py` annotates verified text-grid tables and conservative chart data with `data-pptx-native` metadata. Table import covers exact physical row/grid topology, canonical rectangular merges, safe solid/no-fill per-side borders, plain multi-paragraph cells, and a closed run-rich paragraph schema. Each rich run requires `text` and may use only `bold`, `italic`, `underline`, `strike`, `color`, `font_size`, one `font_family`, `lang`, and `alt_lang`. A merge must use the exact `rowSpan` / `gridSpan` / `hMerge` / `vMerge` physical topology with empty merge slaves. Presentation-only source run XML normalizes, while relationship-bearing text, extensions, line breaks, fields, tabs, bullets, broken text topology, unsafe border XML, non-solid fills, and other merge encodings remain fallback-only. For table style `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`, the normalized SVG fallback resolves `wholeTbl`, `firstRow`, horizontal banding, theme colors/fonts, and direct cell/run overrides; other built-in/custom style families are not implied.
`pptx_to_svg.py` annotates verified text-grid tables and conservative chart data with `data-pptx-replace-with` beside the visible SVG fallback and places the payload in `<metadata type="application/json">`; the parent claim selects the chart or table schema. Imported table/chart groups under this contract carry `data-pptx-import-source="pptx"`, whether active or fallback-only. Table import covers exact physical row/grid topology, canonical rectangular merges, safe solid/no-fill per-side borders, plain multi-paragraph cells, and a closed run-rich paragraph schema. Each rich run requires `text` and may use only `bold`, `italic`, `underline`, `strike`, `color`, `font_size`, one `font_family`, `lang`, and `alt_lang`. A merge must use the exact `rowSpan` / `gridSpan` / `hMerge` / `vMerge` physical topology with empty merge slaves. Presentation-only source run XML without a non-empty `effectLst` / `effectDag` normalizes; a table-cell run effect disables native replacement and adds a blocking effect diagnostic. Relationship-bearing text, extensions, line breaks, fields, tabs, bullets, broken text topology, unsafe border XML, non-solid fills, and other merge encodings remain fallback-only. For table style `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`, the normalized SVG fallback resolves `wholeTbl`, `firstRow`, horizontal banding, theme colors/fonts, and direct cell/run overrides; other built-in/custom style families are not implied.
Supported parsed column/bar/line/area, pie/doughnut, scatter, and bubble charts without a baked preview receive a deterministic readable fallback marked `data-pptx-visual-status="normalized"`. The importer additionally activates verified column/line/area combo charts, canonical OHLC stock charts, area charts with numeric date axes, verified scatter/bubble charts whose two value axes fit the closed `axes.x` / `axes.y` contract, radar charts, safe `of_pie` `serLines`, axis/title/legend normalization, and validated bar/column gap/overlap cases. Combo plots may retain independent primary/secondary category caches and workbook ranges. Both the category/value and XY contracts retain kind/position/visibility/label position/number format/min/max/major unit/reverse/major gridlines for native read-back. Scatter import derives effective `scatter_style` from uniform per-series line/marker/smooth state. The normalized XY fallback consumes only the two major-gridline flags; the C4/C5 additions do not expand the normalized renderer. `gapWidth` is accepted only as an integer in `0..500` and `overlap` only as an integer in `-100..100`; both normalize in native output, while malformed or out-of-range values fail closed. Safe common series paint forms and theme scheme colors are resolved; unknown series paint/style XML outside the explicit normalization boundaries still fails closed. Safe stock series style may pass the structural gate, but stock series, `hiLowLines`, and up-down bar local styling can still normalize under the editable-first contract. The editable native replacement remains allowed to normalize unmodeled no-fill/alpha/line/marker details and reports the route-level loss risk. Chart title/legend/axis titles and supported data-label flags are retained when the current schema can represent them. Fallback-only objects keep rendered SVG content or a baked chart preview and carry `data-pptx-native-status`, which validation and `--native-objects` export report as a warning. An active marker without a renderer keeps `data-pptx-visual-status="placeholder"` plus `data-pptx-route-status="reconstruction-only"`; default export keeps the placeholder and native opt-in may still reconstruct it.
Supported parsed column/bar/line/area, pie/doughnut, scatter, and bubble charts without a baked preview receive a deterministic readable fallback marked `data-pptx-fallback-kind="normalized"`. The importer additionally activates verified column/line/area combo charts, canonical OHLC stock charts, area charts with numeric date axes, verified scatter/bubble charts whose two value axes fit the closed `axes.x` / `axes.y` contract, radar charts, safe `of_pie` `serLines`, axis/title/legend normalization, and validated bar/column gap/overlap cases. Combo plots may retain independent primary/secondary category caches and workbook ranges. Both the category/value and XY contracts retain kind/position/visibility/label position/number format/min/max/major unit/reverse/major gridlines for native read-back. Scatter import derives effective `scatter_style` from uniform per-series line/marker/smooth state. The normalized XY fallback consumes only the two major-gridline flags; the C4/C5 additions do not expand the normalized renderer. `gapWidth` is accepted only as an integer in `0..500` and `overlap` only as an integer in `-100..100`; both normalize in native output, while malformed or out-of-range values fail closed. Safe common series paint forms and theme scheme colors are resolved; unknown series paint/style XML outside the explicit normalization boundaries still fails closed. Safe stock series style may pass the structural gate, but stock series, `hiLowLines`, and up-down bar local styling can still normalize under the data-object-first contract. The PowerPoint-native replacement remains allowed to normalize unmodeled no-fill/alpha/line/marker details and reports the route-level loss risk. Chart title/legend/axis titles and supported data-label flags are retained when the current schema can represent them. Fallback-only objects keep rendered SVG content or a baked chart preview and carry `data-pptx-replacement-status`, which validation and `--native-charts-and-tables` export report as a warning. An active marker without a renderer keeps `data-pptx-fallback-kind="placeholder"`; default export keeps the reconstruction-only placeholder and the native Chart/Table opt-in may still reconstruct it.
The ChartEx importer accepts exactly the validated treemap, sunburst, histogram, pareto, box-whisker, waterfall, and funnel data models. Supported hierarchy/category/value/series/subtotal data round-trips to native output; source style, axes, labels, and binning may normalize. Numeric caches must be non-empty and finite with exact contiguous point topology. This is not arbitrary ChartEx import or presentation fidelity, and the ChartEx native writer still only promises valid payload palette entries rather than full source styling.
Active imported table/chart markers carry `data-pptx-fallback-sha256`. Visible fallback edits, reachable SVG fragment-definition changes, marker-local reference-target changes, and marker transforms make the baseline stale: the mandatory quality checker warns, default export remains available, and `--native-objects` fails instead of discarding the SVG edit. Legacy markers without a baseline remain convertible with a checker/native-route warning.
Active imported table/chart markers carry `data-pptx-fallback-sha256`. Visible fallback edits, reachable SVG fragment-definition changes, marker-local reference-target changes, and marker transforms make the baseline stale: the mandatory quality checker warns, default export remains available, and `--native-charts-and-tables` fails instead of discarding the SVG edit. Generated authoring and reusable templates omit import provenance and a static baseline without warning. Hashless legacy imported markers that still carry PPTX import provenance remain convertible with a checker/replacement-route warning. Legacy `data-pptx-native*`, `data-pptx-visual-status`, and `data-pptx-route-status` spellings and the `--native-objects` option remain read-compatible; generated output and canonical commands use the replacement/fallback names and `--native-charts-and-tables`.
Exporter-canonical classic charts also recover canonical solid series/slice
colors and exact one- or two-paragraph title styling; two paragraphs retain
@@ -5,7 +5,7 @@ Image Size Analysis Tool
Reports objective parameters (width, height, aspect ratio, category) for all
images in a folder. Intentionally does NOT prescribe a layout the Strategist
decides narrative intent (hero / atmosphere / side-by-side / accent) per
references/strategist.md §h; this tool only supplies the numbers.
references/strategist-image.md; this tool only supplies the numbers.
When a canvas is specified, also reports the reference image/text area sizes
that would apply *if* an image is placed side-by-side with body text. Those
@@ -406,7 +406,7 @@ def print_results(results: list[ImageAnalysis]) -> None:
if has_layout:
print("\nNote: 'Img (SxS)' shows the image area *if* the Strategist chooses the")
print("side-by-side intent for this image. Decide narrative intent first — see")
print("references/strategist.md §h. Hero / atmosphere / accent intents ignore it.\n")
print("references/strategist-image.md. Hero / atmosphere / accent intents ignore it.\n")
print(f"{'No.':<4} {'Width':<7} {'Height':<7} {'Ratio':<7} {'Source':<8} {'Refs':<5} {'Size':<10} {'Category':<20} {'Img (SxS)':<14} {'Filename'}")
else:
print(f"\n{'No.':<4} {'Width':<7} {'Height':<7} {'Ratio':<7} {'Source':<8} {'Refs':<5} {'Size':<10} {'Category':<20} {'Filename'}")
@@ -484,7 +484,7 @@ def generate_markdown(results: list[ImageAnalysis], canvas_key: str) -> None:
print(f"\n## Image Resource Inventory (Auto-scan Results — {fmt_name})\n")
print("> Decide narrative intent per image (hero / atmosphere / side-by-side /")
print("> accent) per `references/strategist.md` §h before filling the table. The")
print("> accent) per `references/strategist-image.md` before filling the table. The")
print("> `Img Area (SxS)` / `Text Area (SxS)` columns only apply if the chosen")
print("> intent is side-by-side; ignore them for hero / atmosphere / accent intents.\n")
@@ -520,7 +520,7 @@ def save_csv(results: list[ImageAnalysis], csv_path: str) -> None:
has_layout = 'layout_type' in results[0] if results else False
# NOTE: ImageArea_SxS / TextArea_SxS apply only if Strategist picks the
# side-by-side intent for this image (see strategist.md §h). The tool
# side-by-side intent for this image (see strategist-image.md). The tool
# does not prescribe a layout.
with open(csv_path, 'w', encoding='utf-8') as f:
if has_layout:
@@ -89,15 +89,20 @@ def main(argv: list[str] | None = None) -> int:
output_path=args.output,
force=args.force,
)
except FileExistsError as exc:
except (FileExistsError, ValueError) as exc:
print(f'Error: {exc}', file=sys.stderr)
if isinstance(exc, FileExistsError):
print('Use --force to overwrite.', file=sys.stderr)
return 1
print(f'Animation config scaffold written: {output_path}')
return 0
if args.command == 'list-groups':
try:
lines, anonymous = build_group_listing(project_path)
except ValueError as exc:
print(f'Error: {exc}', file=sys.stderr)
return 1
for line in lines:
print(line)
for warning in anonymous:
@@ -200,7 +200,7 @@ class BatchValidator:
if self.summary['missing_readme'] > 0:
print(f" 1. Create documentation for projects missing README")
print(
f" Reference: examples/google_annual_report_ppt169_20251116/README.md")
f" Include the project goal, sources, canvas, artifacts, and export path")
if self.summary['svg_issues'] > 0:
print(f" 2. Check SVG root viewBox settings")
@@ -2,7 +2,7 @@
"""
PPT Master - Beautify Identity Extractor
Extract a source deck's visual identity as JSON for the beautify-pptx workflow:
Extract a source deck's visual identity as JSON for the beautify-pptx profile:
the declared `theme` (palette + major/minor fonts + master placeholder sizes,
full bodyStyle ramp in `sizes.body_levels`) plus `observed` usage (run-level
fonts incl. CJK `ea`, explicit point sizes, and frequent explicit fill colors)
@@ -23,7 +23,7 @@ Examples:
Dependencies:
None beyond the standard library (reuses scripts/pptx_to_svg/).
See workflows/beautify-pptx.md for how the emitted identity is consumed.
See workflows/profiles/beautify-pptx.md for how the emitted identity is consumed.
"""
from __future__ import annotations
@@ -184,7 +184,7 @@ def extract_identity(pptx_path: Path) -> dict:
first = pkg.get_slide(1)
master = first.master if first else None
theme = pkg.resolve_theme(master)
palette_resolver = ColorPalette(master, theme)
palette_resolver = ColorPalette(master, theme, strict=False)
# Presentation-level scheme names; ColorPalette applies clrMap + aliases.
scheme = {
@@ -3,7 +3,7 @@
PPT Master - Beautify Inventory Builder
Mechanically merge a source deck's extracts into one per-slide ledger for the
beautify-pptx workflow: text blocks + tables + charts + SmartArt structure (from a
beautify-pptx profile: text blocks + tables + charts + SmartArt structure (from a
`template_fill_pptx.py analyze` slide_library.json) joined with the images
bound to each slide (from a `ppt_to_md.py` image_manifest.json). The deterministic
join only `ignored` and `needs_confirmation` are emitted empty for the agent
@@ -19,7 +19,7 @@ Examples:
Dependencies:
None (standard library only).
See workflows/beautify-pptx.md Step 4 for how the inventory is consumed.
See workflows/profiles/beautify-pptx.md Step 4 for how the inventory is consumed.
"""
from __future__ import annotations
@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
PPT Master - Chart Candidate Recall
Recalls a deterministic chart-template shortlist from page-shape semantic tags,
or validates selected chart keys against the live chart catalog.
Usage:
python3 scripts/chart_recall.py recall --page P03 --tag "time series" --tag "three metrics" --tag "trend"
python3 scripts/chart_recall.py validate line_chart column_chart
Examples:
python3 scripts/chart_recall.py recall --page P07 --tag "named quadrants" \
--tag "bullet lists" --tag "SWOT" --limit 6
python3 scripts/chart_recall.py validate quadrant_text_bullets
Dependencies:
None (only uses standard library)
See scripts/docs/chart-recall.md for the Strategist workflow and output contract.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import unicodedata
from pathlib import Path
from typing import Optional
from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
_SCRIPTS_DIR = Path(__file__).resolve().parent
_INDEX_PATH = _SCRIPTS_DIR.parent / "templates" / "charts" / "charts_index.json"
_TOKEN_RE = re.compile(r"[a-z0-9]+")
_PAGE_RE = re.compile(r"^P\d{2,}$")
_KEY_RE = re.compile(r"^[a-z0-9]+(?:_[a-z0-9]+)*$")
_SKIP_RE = re.compile(r"\bskip\s+(?:if|for)\b", re.IGNORECASE)
_STOP_WORDS = {
"a",
"an",
"and",
"as",
"at",
"by",
"for",
"from",
"if",
"in",
"into",
"of",
"on",
"or",
"per",
"the",
"to",
"use",
"with",
}
def _normalize(text: str) -> str:
normalized = unicodedata.normalize("NFKC", text).casefold().replace("_", " ")
return " ".join(_TOKEN_RE.findall(normalized))
def _stem(token: str) -> str:
if len(token) > 5 and token.endswith("ies"):
return token[:-3] + "y"
if len(token) > 5 and token.endswith("ing"):
return token[:-3]
if len(token) > 4 and token.endswith("ed"):
return token[:-2]
if len(token) > 4 and token.endswith("es"):
return token[:-2]
if len(token) > 3 and token.endswith("s"):
return token[:-1]
return token
def _tokens(text: str) -> set[str]:
return {
_stem(token)
for token in _TOKEN_RE.findall(_normalize(text))
if token not in _STOP_WORDS
}
def load_catalog() -> dict[str, str]:
"""Load and validate the live chart catalog."""
try:
payload = json.loads(_INDEX_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(f"Cannot read chart catalog {_INDEX_PATH}: {exc}") from exc
if not isinstance(payload, dict):
raise RuntimeError(f"Chart catalog {_INDEX_PATH} root must be an object")
raw_charts = payload.get("charts")
if not isinstance(raw_charts, dict) or not raw_charts:
raise RuntimeError(f"Chart catalog {_INDEX_PATH} has no non-empty 'charts' object")
charts: dict[str, str] = {}
for key, item in raw_charts.items():
if (
not isinstance(key, str)
or _KEY_RE.fullmatch(key) is None
or not isinstance(item, dict)
):
raise RuntimeError(f"Chart catalog entry {key!r} is malformed")
summary = item.get("summary")
if not isinstance(summary, str) or not summary.strip():
raise RuntimeError(f"Chart catalog entry {key!r} has no non-empty summary")
charts[key] = summary.strip()
return charts
def _score_candidate(key: str, summary: str, tags: list[str]) -> tuple[int, list[str]]:
skip_match = _SKIP_RE.search(summary)
if skip_match is None:
pick_clause, skip_clause = summary, ""
else:
pick_clause = summary[:skip_match.start()]
skip_clause = summary[skip_match.start():]
key_text = _normalize(key)
pick_text = _normalize(pick_clause)
skip_text = _normalize(skip_clause)
key_tokens = _tokens(key)
pick_tokens = _tokens(pick_clause)
skip_tokens = _tokens(skip_clause)
score = 0
matched_tags: list[str] = []
for tag in tags:
tag_text = _normalize(tag)
tag_tokens = _tokens(tag)
positive_score = 0
negative_score = 0
if tag_text and tag_text in key_text:
positive_score += 20
elif tag_text and tag_text in pick_text:
positive_score += 14
if tag_text and tag_text in skip_text:
negative_score += 12
for token in tag_tokens:
if token in key_tokens:
positive_score += 9
elif token in pick_tokens:
positive_score += 5
if token in skip_tokens:
negative_score += 6
if positive_score:
matched_tags.append(tag)
score += positive_score - negative_score
return score, matched_tags
def recall_candidates(page: str, tags: list[str], limit: int) -> dict[str, object]:
"""Recall a deterministic shortlist for one page."""
charts = load_catalog()
scored: list[tuple[int, str, str, list[str]]] = []
for key, summary in charts.items():
score, matched_tags = _score_candidate(key, summary, tags)
if score > 0 and matched_tags:
scored.append((score, key, summary, matched_tags))
scored.sort(key=lambda item: (-item[0], item[1]))
candidates = []
for score, key, summary, matched_tags in scored[:limit]:
candidates.append(
{
"key": key,
"path": f"templates/charts/{key}.svg",
"summary": summary,
"score": score,
"matched_tags": matched_tags,
}
)
top_score = candidates[0]["score"] if candidates else 0
if top_score >= 35:
confidence = "high"
elif top_score >= 15:
confidence = "medium"
elif top_score > 0:
confidence = "low"
else:
confidence = "none"
return {
"page": page,
"semantic_tags": tags,
"confidence": confidence,
"candidates": candidates,
"no_template_match": {
"allowed": True,
"key": "no-template-match",
"instruction": "Use when every candidate conflicts with the page content shape or a Skip clause.",
},
}
def _dedupe(values: list[str]) -> list[str]:
result: list[str] = []
seen: set[str] = set()
for value in values:
stripped = value.strip()
normalized = _normalize(stripped)
if not stripped or not normalized or normalized in seen:
continue
seen.add(normalized)
result.append(stripped)
return result
def _run_recall(args: argparse.Namespace) -> int:
page = args.page.upper()
if not _PAGE_RE.fullmatch(page):
print("Error: --page must match P<NN>, for example P03.", file=sys.stderr)
return 2
tags = _dedupe(args.tag)
if not 3 <= len(tags) <= 8:
print("Error: recall requires 3-8 distinct non-empty --tag values.", file=sys.stderr)
return 2
result = recall_candidates(page, tags, args.limit)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
def _run_validate(args: argparse.Namespace) -> int:
charts = load_catalog()
selected = _dedupe(args.keys)
invalid = sorted(key for key in selected if key not in charts)
result = {
"invalid": invalid,
"valid": sorted(key for key in selected if key in charts),
}
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if invalid:
print(
"Error: replace each invalid key with a key returned by the recall command, "
"or record no-template-match without a page_charts entry.",
file=sys.stderr,
)
return 1
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Recall chart-template candidates or validate selected keys.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
recall = subparsers.add_parser("recall", help="Recall candidates for one planned page.")
recall.add_argument("--page", required=True, help="Planned page key, for example P03.")
recall.add_argument(
"--tag",
action="append",
required=True,
help="English semantic content-shape tag; repeat 3-8 times.",
)
recall.add_argument(
"--limit",
type=int,
choices=range(3, 9),
default=6,
metavar="3..8",
help="Candidate count (default: 6).",
)
recall.set_defaults(handler=_run_recall)
validate = subparsers.add_parser("validate", help="Validate selected catalog keys.")
validate.add_argument("keys", nargs="+", help="One or more selected chart keys.")
validate.set_defaults(handler=_run_validate)
return parser
def main(argv: Optional[list[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
return args.handler(args)
except RuntimeError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,345 @@
#!/usr/bin/env python3
"""
PPT Master - SVG Coordinate Compactor
Compact model-facing page-space SVG coordinates to at most two decimal places
without rounding normalized crop ratios or transform linear coefficients.
Usage:
python3 scripts/compact_svg_coordinates.py <svg-file-or-directory> [--inplace]
Examples:
python3 scripts/compact_svg_coordinates.py projects/example/templates --inplace
python3 scripts/compact_svg_coordinates.py imported/authoring-svg
Dependencies:
None (standard library only).
"""
from __future__ import annotations
import argparse
import json
import math
import re
import stat
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from xml.etree import ElementTree as ET
from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
COORDINATE_DECIMAL_PLACES = 2
_NUMBER_TOKEN = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?"
_NUMBER_RE = re.compile(rf"^{_NUMBER_TOKEN}$")
_TRANSFORM_FUNCTION_RE = re.compile(r"([A-Za-z]+)\s*\(([^)]*)\)")
_COMPACTABLE_ATTRIBUTE_RE = re.compile(
r"(?<![A-Za-z0-9_.:-])"
r"(?P<name>data-pptx-frame|data-pptx-bounds|transform)"
r"(?P<spacing>\s*=\s*)"
r"(?P<quote>[\"'])"
r"(?P<value>.*?)"
r"(?P=quote)",
re.DOTALL,
)
@dataclass
class CoordinateCompactionStats:
"""Count safely compacted coordinate-bearing SVG attributes."""
native_frames: int = 0
bounds: int = 0
transforms: int = 0
@property
def changed_attributes(self) -> int:
return (
self.native_frames
+ self.bounds
+ self.transforms
)
def merge(self, other: "CoordinateCompactionStats") -> None:
self.native_frames += other.native_frames
self.bounds += other.bounds
self.transforms += other.transforms
def as_dict(self) -> dict[str, int]:
return {
"native_frames": self.native_frames,
"bounds": self.bounds,
"transforms": self.transforms,
"changed_attributes": self.changed_attributes,
}
def _number_tokens(value: str) -> list[str] | None:
stripped = value.strip()
if not stripped:
return None
tokens = re.split(r"[\s,]+", stripped)
if not tokens or any(_NUMBER_RE.fullmatch(token) is None for token in tokens):
return None
return tokens
def format_coordinate(value: str | float) -> str:
"""Format one finite page-space coordinate with at most two decimals."""
numeric = float(value)
if not math.isfinite(numeric):
raise ValueError(f"Coordinate is not finite: {value!r}")
compact = (
f"{numeric:.{COORDINATE_DECIMAL_PLACES}f}".rstrip("0").rstrip(".")
)
return "0" if compact in {"", "-0"} else compact
def _compact_coordinate_quad(value: str) -> str:
tokens = _number_tokens(value)
if tokens is None or len(tokens) != 4:
return value
return " ".join(format_coordinate(token) for token in tokens)
def _compact_transform(value: str) -> str:
def replace(match: re.Match[str]) -> str:
name, arguments = match.groups()
tokens = _number_tokens(arguments)
if tokens is None:
return match.group(0)
lowered = name.lower()
compacted: list[str]
if lowered == "translate" and len(tokens) in {1, 2}:
compacted = [format_coordinate(token) for token in tokens]
elif lowered == "rotate" and len(tokens) == 3:
compacted = [
tokens[0],
format_coordinate(tokens[1]),
format_coordinate(tokens[2]),
]
elif lowered == "matrix" and len(tokens) == 6:
compacted = [
*tokens[:4],
format_coordinate(tokens[4]),
format_coordinate(tokens[5]),
]
else:
return match.group(0)
return f"{name}({' '.join(compacted)})"
return _TRANSFORM_FUNCTION_RE.sub(replace, value)
def _compact_attribute_value(
name: str,
value: str,
*,
compact_native_frames: bool,
) -> str:
if name == "data-pptx-frame":
return _compact_coordinate_quad(value) if compact_native_frames else value
if name == "data-pptx-bounds":
return _compact_coordinate_quad(value)
if name == "transform":
return _compact_transform(value)
return value
def _record_change(stats: CoordinateCompactionStats, name: str) -> None:
if name == "data-pptx-frame":
stats.native_frames += 1
elif name == "data-pptx-bounds":
stats.bounds += 1
elif name == "transform":
stats.transforms += 1
def compact_svg_tree(
root: ET.Element,
*,
compact_native_frames: bool = True,
) -> CoordinateCompactionStats:
"""Compact safe coordinate metadata in one parsed SVG tree."""
stats = CoordinateCompactionStats()
for element in root.iter():
for name in (
"data-pptx-frame",
"data-pptx-bounds",
"transform",
):
current = element.get(name)
if current is None:
continue
compacted = _compact_attribute_value(
name,
current,
compact_native_frames=compact_native_frames,
)
if compacted == current:
continue
element.set(name, compacted)
_record_change(stats, name)
return stats
def compact_svg_text(
text: str,
*,
compact_native_frames: bool = True,
) -> tuple[str, CoordinateCompactionStats]:
"""Compact safe coordinates while preserving unrelated SVG formatting."""
stats = CoordinateCompactionStats()
def replace(match: re.Match[str]) -> str:
name = match.group("name")
current = match.group("value")
compacted = _compact_attribute_value(
name,
current,
compact_native_frames=compact_native_frames,
)
if compacted == current:
return match.group(0)
_record_change(stats, name)
return (
f"{name}{match.group('spacing')}{match.group('quote')}"
f"{compacted}{match.group('quote')}"
)
return _COMPACTABLE_ATTRIBUTE_RE.sub(replace, text), stats
def _svg_files(input_path: Path) -> list[Path]:
if input_path.is_file():
return [input_path] if input_path.suffix.lower() == ".svg" else []
return sorted(path for path in input_path.rglob("*.svg") if path.is_file())
def _write_atomic(path: Path, payload: str) -> None:
mode = stat.S_IMODE(path.stat().st_mode)
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
newline="\n",
prefix=f".{path.name}.",
suffix=".tmp",
dir=path.parent,
delete=False,
) as handle:
temporary_path = Path(handle.name)
handle.write(payload)
try:
temporary_path.chmod(mode)
temporary_path.replace(path)
except OSError:
temporary_path.unlink(missing_ok=True)
raise
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Compact safe page-space SVG coordinates to at most two decimal "
"places. Runs as a dry-run unless --inplace is supplied."
),
)
parser.add_argument("input", type=Path, help="SVG file or directory")
parser.add_argument(
"--inplace",
action="store_true",
help="Atomically replace changed SVG files",
)
parser.add_argument(
"--keep-native-frames",
action="store_true",
help=(
"Leave data-pptx-frame unchanged while compacting "
"data-pptx-bounds and transform translations"
),
)
return parser
def main(argv: Optional[list[str]] = None) -> int:
args = build_parser().parse_args(argv)
input_path = args.input.resolve()
if not input_path.exists():
print(
json.dumps(
{"error": f"Input does not exist: {input_path}"},
ensure_ascii=False,
),
file=sys.stderr,
)
return 1
paths = _svg_files(input_path)
if not paths:
print(
json.dumps(
{"error": f"No SVG files found under {input_path}"},
ensure_ascii=False,
),
file=sys.stderr,
)
return 1
staged: list[tuple[Path, str]] = []
totals = CoordinateCompactionStats()
bytes_before = 0
bytes_after = 0
try:
for path in paths:
original = path.read_text(encoding="utf-8")
ET.fromstring(original)
compacted, stats = compact_svg_text(
original,
compact_native_frames=not args.keep_native_frames,
)
totals.merge(stats)
before = len(original.encode("utf-8"))
after = len(compacted.encode("utf-8"))
bytes_before += before
bytes_after += after
if compacted != original:
staged.append((path, compacted))
except (OSError, UnicodeDecodeError, ET.ParseError, ValueError) as exc:
print(
json.dumps({"error": str(exc)}, ensure_ascii=False),
file=sys.stderr,
)
return 1
if args.inplace:
try:
for path, payload in staged:
_write_atomic(path, payload)
except OSError as exc:
print(
json.dumps({"error": str(exc)}, ensure_ascii=False),
file=sys.stderr,
)
return 1
print(json.dumps({
"input": str(input_path),
"inplace": args.inplace,
"files_scanned": len(paths),
"files_changed": len(staged),
"bytes_before": bytes_before,
"bytes_after": bytes_after,
"bytes_saved": bytes_before - bytes_after,
"coordinates": totals.as_dict(),
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -489,11 +489,18 @@ LAYOUT_MARGINS = {
# SVG Policy Reference
# ============================================================
# Do not mirror element/attribute rules here. The shared standard is the
# authoring authority and the quality checker enforces it.
# Keep the exported key for compatibility with existing config consumers.
# Do not mirror element/attribute rules here. The router selects the mandatory
# core and feature-triggered interfaces; the quality checker enforces them.
# Keep the exported authority key as the compatibility router for existing
# config consumers.
SVG_CONSTRAINTS = {
'authority': 'skills/ppt-master/references/shared-standards.md',
'core_authority': 'skills/ppt-master/references/shared-standards-core.md',
'conditional_authorities': {
'effects': 'skills/ppt-master/references/svg-effects.md',
'native_data': 'skills/ppt-master/references/native-data-interface.md',
'pptx_structure': 'skills/ppt-master/references/pptx-structure-interface.md',
},
'validator': 'skills/ppt-master/scripts/svg_quality_checker.py',
}
@@ -9,9 +9,9 @@ clickable page (color swatches, live font previews, candidate picks). On
submit it writes the user's final choices to
``<project>/confirm_ui/result.json`` for the AI to read back.
This is the confirmation surface only. The chat fallback always remains valid:
if the browser cannot open (remote / headless / web host), the AI presents the
same Strategist confirmation stage in chat.
This is the default confirmation surface. The chat fallback is used only when
the user explicitly requests chat-only confirmation or the browser launch
fails; it preserves the same staged semantics.
See scripts/docs/confirm_ui.md for the round-trip data contract and schema.
@@ -80,6 +80,17 @@ RECOMMENDATIONS_NAME = 'recommendations.json'
RESULT_NAME = 'result.json'
SESSION_NAME = 'session.json'
_PALETTE_ROLES = (
'background',
'secondary_bg',
'primary',
'accent',
'secondary_accent',
'body_text',
)
_TYPOGRAPHY_SIZE_ROLES = ('title', 'subtitle', 'annotation')
_HEX_COLOR_RE = re.compile(r'#?(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})\Z')
# Static option universe served at /api/catalogs (canvas synced live from config).
_CATALOGS_PATH = Path(__file__).resolve().parent / 'static' / 'catalogs.json'
_ICON_LIBRARY_DIR = Path(__file__).resolve().parents[2] / 'templates' / 'icons'
@@ -106,7 +117,6 @@ STARTUP_TIMEOUT = 10
# result.json before falling back to chat.
WAIT_TIMEOUT_DEFAULT = 590
def _read_json_object(path: Path, retries: int = 2, delay: float = 0.08) -> dict:
"""Read a JSON object, retrying briefly around non-atomic external writes."""
last_error: Exception = ValueError('unknown JSON read error')
@@ -261,6 +271,15 @@ def _wait_for_result(
if result_file.exists():
try:
if result_file.stat().st_mtime >= started_at:
actual_stage = _result_stage(result_file)
expected_stage = _expected_result_stage(result_file.parent)
if actual_stage != expected_stage:
logger.error(
'confirmation stage mismatch: expected %s, found %s',
expected_stage,
actual_stage or 'invalid/absent',
)
return 2
logger.info('confirmation received: %s', result_file)
try:
proc.wait(timeout=3)
@@ -384,11 +403,360 @@ def _stage_skip_error(confirm_dir: Path) -> Optional[str]:
return (
f'stage skip detected: recommendations.json is {_stage_name(rec_stage_number)} but the last '
f'confirmed result is {result_stage or "absent"} — the page will not render a skipped stage. '
f'Stages confirm in order and an active template does not exempt stage2 (SKILL.md Step 4). '
f'Stages confirm in order and an active template does not exempt stage2 (generate-pptx Step 4). '
f'Overwrite recommendations.json with the {expected} recommendations, then re-run with {reattach}.'
)
def _template_confirmation_required(project_path: Path, recommendations: dict) -> bool:
"""Return whether this project must use the staged template confirmation."""
return (
'template_application' in recommendations
or (project_path / 'templates' / 'design_spec.md').is_file()
)
def _template_stage2_error(
recommendations: dict,
*,
template_required: bool,
) -> Optional[str]:
"""Require the natural-language template plan in template Stage 2."""
if template_required and 'template_application' not in recommendations:
return (
'template Stage 2 recommendations must include '
'template_application.value'
)
return None
def _localized_text_present(candidate: dict, field: str) -> bool:
"""Return whether a candidate carries non-empty localized prose."""
return any(
isinstance(candidate.get(key), str) and bool(candidate[key].strip())
for key in (field, f'{field}_zh', f'{field}_en', f'{field}_ja')
)
def _recommended_image_usage(recommendations: dict):
"""Return the Stage 2 image-source recommendation in either schema."""
recommend = recommendations.get('recommend')
usage = recommend.get('image_usage') if isinstance(recommend, dict) else None
if usage is None:
usage = recommendations.get('image_usage')
if isinstance(usage, dict):
usage = usage.get('value')
return usage
def _uses_ai_images(recommendations: dict) -> bool:
"""Return whether Stage 2 proposes AI-generated images."""
usage = _recommended_image_usage(recommendations)
return 'ai' in usage if isinstance(usage, list) else usage == 'ai'
def _palette_error(color: object, label: str) -> Optional[str]:
"""Validate one complete user-facing palette."""
if not isinstance(color, dict):
return f'{label} must be an object'
palette = color.get('palette')
if not isinstance(palette, dict):
palette = color
for role in _PALETTE_ROLES:
value = palette.get(role)
if role == 'body_text' and value is None:
value = palette.get('text')
if not isinstance(value, str) or not _HEX_COLOR_RE.fullmatch(value.strip()):
return f'{label}.palette.{role} must be a HEX color'
return None
def _positive_number(value: object) -> bool:
"""Return whether a JSON value is a positive finite number."""
try:
number = float(value)
except (TypeError, ValueError):
return False
return number > 0 and number != float('inf')
def _typography_error(typography: object, label: str, *, require_sizes: bool) -> Optional[str]:
"""Validate one complete user-facing typography recommendation or choice."""
if not isinstance(typography, dict):
return f'{label} must be an object'
for role in ('heading', 'body'):
font = typography.get(role)
if not isinstance(font, dict):
return f'{label}.{role} must be an object'
for field in ('cjk', 'latin', 'css'):
if not isinstance(font.get(field), str) or not font[field].strip():
return f'{label}.{role}.{field} must be non-empty'
if not _positive_number(typography.get('body_size')):
return f'{label}.body_size must be a positive number'
if not require_sizes:
return None
sizes = typography.get('sizes')
if not isinstance(sizes, dict):
return f'{label}.sizes must be an object'
for role in _TYPOGRAPHY_SIZE_ROLES:
if not _positive_number(sizes.get(role)):
return f'{label}.sizes.{role} must be a positive number'
return None
def _candidate_list(spec: object) -> list:
"""Return candidates from the current or legacy recommendation shape."""
if not isinstance(spec, dict):
return []
candidates = spec.get('candidates')
if not isinstance(candidates, list):
candidates = spec.get('options')
return candidates if isinstance(candidates, list) else []
def _stage2_design_directions_error(recommendations: dict) -> Optional[str]:
"""Require three complete coordinated Stage 2 design systems."""
directions = recommendations.get('design_directions')
if isinstance(directions, dict):
candidates = _candidate_list(directions)
if len(candidates) < 3:
return 'Stage 2 design_directions must include at least 3 candidates'
for index, candidate in enumerate(candidates, start=1):
label = f'design_directions.candidates[{index - 1}]'
if not isinstance(candidate, dict):
return f'{label} must be an object'
if not _localized_text_present(candidate, 'name'):
return f'{label} requires a non-empty localized name'
for field in ('visual_style', 'icons'):
if not isinstance(candidate.get(field), str) or not candidate[field].strip():
return f'{label}.{field} must be non-empty'
error = _palette_error(candidate.get('color'), f'{label}.color')
if error:
return error
error = _typography_error(
candidate.get('typography'),
f'{label}.typography',
require_sizes=False,
)
if error:
return error
if _uses_ai_images(recommendations):
image_strategy = candidate.get('image_strategy')
if not isinstance(image_strategy, dict) or not str(
image_strategy.get('rendering') or ''
).strip():
return f'{label}.image_strategy.rendering must be non-empty'
return None
# Legacy staged files remain readable, but they must still provide three
# complete color combinations and at least one complete typography choice.
colors = _candidate_list(recommendations.get('color'))
if len(colors) < 3:
return 'Stage 2 recommendations must include 3 complete color candidates'
for index, color in enumerate(colors):
error = _palette_error(color, f'color.candidates[{index}]')
if error:
return error
typography = _candidate_list(recommendations.get('typography'))
if not typography:
return 'Stage 2 recommendations must include typography candidates'
for index, candidate in enumerate(typography):
error = _typography_error(
candidate,
f'typography.candidates[{index}]',
require_sizes=False,
)
if error:
return error
return None
def _stage2_custom_candidates_error(recommendations: dict) -> Optional[str]:
"""Require visible AI-authored custom alternatives in new Stage 2 files."""
candidates = recommendations.get('custom_candidates')
if not isinstance(candidates, dict):
return 'Stage 2 recommendations must include custom_candidates'
for field in ('mode', 'visual_style'):
candidate = candidates.get(field)
if not isinstance(candidate, dict):
return f'custom_candidates.{field} must be an object'
for prose_field in ('name', 'behavior'):
if not _localized_text_present(candidate, prose_field):
return (
f'custom_candidates.{field} requires non-empty localized '
f'{prose_field}'
)
if not _uses_ai_images(recommendations):
return None
image_candidate = candidates.get('image_strategy')
if not isinstance(image_candidate, dict):
return 'custom_candidates.image_strategy must be an object when image_usage includes ai'
if image_candidate.get('rendering') != 'custom':
return 'custom_candidates.image_strategy.rendering must be custom'
for prose_field in ('name', 'visual', 'mood', 'behavior'):
if not _localized_text_present(image_candidate, prose_field):
return (
'custom_candidates.image_strategy requires non-empty localized '
f'{prose_field}'
)
return None
def _submission_stage_error(
project_path: Path,
confirm_dir: Path,
submitted_stage: Optional[str],
) -> Optional[str]:
"""Reject a confirmation that does not match the staged recommendation."""
try:
recommendations = _read_json_object(confirm_dir / RECOMMENDATIONS_NAME)
except (OSError, json.JSONDecodeError, ValueError) as exc:
return f'cannot confirm without valid recommendations.json: {exc}'
rec_stage_number = _recommendation_stage(recommendations)
template_required = _template_confirmation_required(
project_path,
recommendations,
)
if rec_stage_number == 0:
if template_required:
return (
'an installed template requires the Stage 1 → Stage 2 → Stage 3 '
'flow; legacy single-pass confirmation is not allowed'
)
if submitted_stage not in {None, 'stage3', 'final'}:
return 'legacy single-pass recommendations accept only a final submission'
return None
if rec_stage_number == 2:
recommendation_error = _template_stage2_error(
recommendations,
template_required=template_required,
)
if recommendation_error:
return recommendation_error
recommendation_error = _stage2_custom_candidates_error(recommendations)
if recommendation_error:
return recommendation_error
recommendation_error = _stage2_design_directions_error(recommendations)
if recommendation_error:
return recommendation_error
allowed_submissions = {
1: {'stage1'},
2: {'stage2'},
3: {'stage3', 'final'},
}
if submitted_stage not in allowed_submissions[rec_stage_number]:
expected = 'final' if rec_stage_number == 3 else _stage_name(rec_stage_number)
return (
f'confirmation stage mismatch: recommendations.json is '
f'{_stage_name(rec_stage_number)}, so the submitted stage must be '
f'{expected}'
)
previous_stage = _result_stage(confirm_dir / RESULT_NAME)
allowed_predecessors = {
1: {None, 'stage1', 'stage2', 'final'},
2: {'stage1', 'stage2'},
3: {'stage2', 'final'},
}
if previous_stage not in allowed_predecessors[rec_stage_number]:
expected_previous = 'stage1' if rec_stage_number == 2 else 'stage2'
return (
f'confirmation predecessor mismatch: {_stage_name(rec_stage_number)} '
f'requires a confirmed {expected_previous} result, found '
f'{previous_stage or "absent"}'
)
return None
def _custom_selection_error(result: dict) -> Optional[str]:
"""Require behavior prose whenever a creative custom choice is selected."""
if result.get('mode') == 'custom' and not str(
result.get('mode_behavior') or ''
).strip():
return 'mode=custom requires non-empty mode_behavior'
if result.get('visual_style') == 'custom' and not str(
result.get('visual_style_behavior') or ''
).strip():
return 'visual_style=custom requires non-empty visual_style_behavior'
image_strategy = result.get('image_strategy')
if isinstance(image_strategy, dict) and image_strategy.get('rendering') == 'custom':
behavior = image_strategy.get('behavior') or image_strategy.get('custom')
if not str(behavior or '').strip():
return 'image_strategy.rendering=custom requires non-empty behavior'
return None
def _stage2_solution_error(result: dict) -> Optional[str]:
"""Reject a Stage 2/final payload with an incomplete design system."""
color = result.get('color')
color_error = _palette_error(color, 'color')
color_custom = (
isinstance(color, dict)
and color.get('name') == 'custom'
and str(color.get('custom') or '').strip()
)
if color_error and not color_custom:
return color_error
typography = result.get('typography')
typography_error = _typography_error(
typography,
'typography',
require_sizes=True,
)
typography_custom = (
isinstance(typography, dict)
and typography.get('name') == 'custom'
and str(typography.get('custom') or '').strip()
and _positive_number(typography.get('body_size'))
and isinstance(typography.get('sizes'), dict)
and all(
_positive_number(typography['sizes'].get(role))
for role in _TYPOGRAPHY_SIZE_ROLES
)
)
if typography_error and not typography_custom:
return typography_error
return None
def _normalize_custom_selections(result: dict) -> None:
"""Keep custom prose only for the creative choices actually selected."""
if result.get('mode') != 'custom':
result.pop('mode_behavior', None)
if result.get('visual_style') != 'custom':
result.pop('visual_style_behavior', None)
image_strategy = result.get('image_strategy')
if not isinstance(image_strategy, dict):
return
legacy_behavior = image_strategy.pop('custom', None)
if image_strategy.get('rendering') == 'custom':
if not image_strategy.get('behavior') and legacy_behavior:
image_strategy['behavior'] = legacy_behavior
return
image_strategy.pop('behavior', None)
def _expected_result_stage(confirm_dir: Path) -> str:
"""Return the result stage expected from the current recommendations."""
try:
recommendations = _read_json_object(confirm_dir / RECOMMENDATIONS_NAME)
except (OSError, json.JSONDecodeError, ValueError):
return 'final'
return {
1: 'stage1',
2: 'stage2',
3: 'final',
}.get(_recommendation_stage(recommendations), 'final')
def _file_version(path: Path) -> Optional[float]:
"""Return a cheap file version for polling state, or None when absent."""
try:
@@ -504,39 +872,35 @@ def _sync_session_state(
return session
# Stage-1 anchors and Stage-2 design-system choices. On later pages these sections
# are not rendered (they were already confirmed), so their values live only in
# browser STATE — lost on a refresh. Folding them from result.json into the
# served recommendations lets a refresh / reopen re-initialize from the user's
# actual choices instead of catalog defaults.
_ANCHOR_RECOMMEND_KEYS = (
# Earlier-stage choices are not rendered on later pages, so their values live
# only in browser STATE and would be lost on refresh. Fold them from result.json
# into the served recommendations so a refresh / reopen resumes from the user's
# actual communication contract and complete deck-solution choices.
_CONTRACT_RECOMMEND_KEYS = (
'canvas',
)
_CONTRACT_VALUE_KEYS = (
'audience',
'communication_intent',
'audience_outcome',
'core_message',
'delivery_context',
'artifact_afterlife',
'content_divergence',
)
_DECK_DIRECTION_RECOMMEND_KEYS = (
'delivery_purpose',
'mode',
'visual_style',
'delivery_purpose',
'template_adherence',
'icons',
'image_usage',
)
_ANCHOR_VALUE_KEYS = ('audience', 'content_divergence')
_DESIGN_RECOMMEND_KEYS = ('icons', 'formula_policy')
def _template_adherence_enabled(project_path: Path) -> bool:
"""Return whether Step 3 loaded a deck/layout template into the project."""
spec_path = project_path / 'templates' / 'design_spec.md'
try:
lines = spec_path.read_text(encoding='utf-8').splitlines()
except OSError:
return False
if not lines or lines[0].strip() != '---':
return False
for line in lines[1:]:
stripped = line.strip()
if stripped == '---':
return False
match = re.fullmatch(r'kind\s*:\s*["\']?(brand|layout|deck)["\']?', stripped)
if match:
return match.group(1) in {'layout', 'deck'}
return False
_PRODUCTION_RECOMMEND_KEYS = (
'formula_policy',
'image_ai_path',
'generation_mode',
)
_LOCKED_RECOMMENDATIONS_KEY = '_locked_recommendations'
def _merge_confirmed_choices(data: dict, result_file: Path) -> None:
@@ -548,26 +912,108 @@ def _merge_confirmed_choices(data: dict, result_file: Path) -> None:
recommend = data.setdefault('recommend', {})
if not isinstance(recommend, dict):
recommend = data['recommend'] = {}
for key in _ANCHOR_RECOMMEND_KEYS:
for key in _CONTRACT_RECOMMEND_KEYS:
if res.get(key) not in (None, ''):
recommend[key] = res[key]
for key in _ANCHOR_VALUE_KEYS:
for key in _CONTRACT_VALUE_KEYS:
if key in res:
data[key] = {'value': res.get(key) or ''}
if _recommendation_stage(data) < 3:
return
for key in _DESIGN_RECOMMEND_KEYS:
for key in _DECK_DIRECTION_RECOMMEND_KEYS:
if res.get(key) not in (None, ''):
recommend[key] = res[key]
if 'page_count' in res:
data['page_count'] = {'value': res.get('page_count') or ''}
if 'image_notes' in res:
data['image_notes'] = {'value': res.get('image_notes') or ''}
if 'template_application' in res:
data['template_application'] = {
'value': res.get('template_application') or '',
}
if isinstance(res.get('color'), dict):
data['color'] = {'selected': 0, 'candidates': [res['color']]}
if isinstance(res.get('typography'), dict):
typography = {'selected': 0, 'candidates': [res['typography']]}
if res.get('formula_policy') not in (None, ''):
typography['formula_policy'] = {'value': res['formula_policy']}
data['typography'] = typography
data['typography'] = {'selected': 0, 'candidates': [res['typography']]}
if isinstance(res.get('image_strategy'), dict):
data['image_strategy'] = {
'selected': 0,
'candidates': [res['image_strategy']],
}
custom_candidates = data.get('custom_candidates')
if not isinstance(custom_candidates, dict):
custom_candidates = {}
data['custom_candidates'] = custom_candidates
for field, behavior_field in (
('mode', 'mode_behavior'),
('visual_style', 'visual_style_behavior'),
):
behavior = res.get(behavior_field)
if res.get(field) != 'custom' or not str(behavior or '').strip():
continue
candidate = custom_candidates.get(field)
if not isinstance(candidate, dict):
candidate = {}
candidate['behavior'] = behavior
custom_candidates[field] = candidate
image_strategy = res.get('image_strategy')
if isinstance(image_strategy, dict) and image_strategy.get('rendering') == 'custom':
custom_candidates['image_strategy'] = image_strategy
# Stage 3 must retain its own production recommendations until final
# confirmation. A final-result reopen reflects those confirmed mechanics.
# Legacy single-pass results have no stage but do carry status=confirmed.
result_stage = _stage_key(res.get('stage'))
is_final = result_stage == 'final' or (
result_stage is None and res.get('status') == 'confirmed'
)
if not is_final:
return
for key in _PRODUCTION_RECOMMEND_KEYS:
if res.get(key) not in (None, ''):
recommend[key] = res[key]
if 'refine_spec' in res:
data['refine_spec'] = {'value': bool(res.get('refine_spec'))}
def _apply_locked_recommendations(
result: dict,
recommendations_file: Path,
previous_result_file: Path,
) -> dict:
"""Restore profile-locked fields and return locks for staged carry-over."""
# This marker is server-owned; never accept a client-supplied carry-over map.
result.pop(_LOCKED_RECOMMENDATIONS_KEY, None)
locked_values = {}
try:
previous = _read_json_object(previous_result_file)
except (OSError, json.JSONDecodeError, ValueError):
previous = {}
previous_locks = previous.get(_LOCKED_RECOMMENDATIONS_KEY)
if isinstance(previous_locks, dict):
locked_values.update(previous_locks)
try:
recommendations = _read_json_object(recommendations_file)
recommendations_loaded = True
except (OSError, json.JSONDecodeError, ValueError):
recommendations = {}
recommendations_loaded = False
# Stage 1 starts a new contract and therefore replaces any stale locks left
# by an earlier run. Later stages inherit those locks across server restarts.
if (
recommendations_loaded
and _recommendation_stage(recommendations) in {0, 1}
):
locked_values = {}
for key, field in recommendations.items():
if not isinstance(field, dict) or field.get('locked') is not True:
continue
if 'value' in field:
locked_values[key] = field.get('value') or ''
for key, value in locked_values.items():
result[key] = value
return locked_values
def _wait_only_for_result(
@@ -586,9 +1032,17 @@ def _wait_only_for_result(
logger.info('waiting for browser confirmation stage=%s...', target_stage)
deadline = None if timeout <= 0 else time.time() + timeout
while True:
if _result_stage(result_file) == target_stage:
current_stage = _result_stage(result_file)
if current_stage == target_stage:
logger.info('confirmation stage=%s received: %s', target_stage, result_file)
return 0
if _result_stage_number(current_stage) > _result_stage_number(target_stage):
logger.error(
'confirmation skipped expected stage=%s and advanced to %s',
target_stage,
current_stage,
)
return 2
skip_error = _stage_skip_error(result_file.parent)
if skip_error:
@@ -741,8 +1195,6 @@ def _ai_comparison_items(kind: str) -> list[dict[str, str]]:
def _build_ai_image_comparison() -> dict:
return {
'rendering': _ai_comparison_items('rendering'),
'palette': _ai_comparison_items('palette'),
'type': _ai_comparison_items('type'),
}
@@ -862,7 +1314,7 @@ def create_app(
@app.route('/api/ai-image-comparison')
def get_ai_image_comparison_manifest():
"""Serve generated-image reference options from ai-image-comparison."""
"""Serve generated-image rendering references for the current UI."""
try:
resp = jsonify(_build_ai_image_comparison())
resp.headers['Cache-Control'] = 'no-store'
@@ -872,8 +1324,8 @@ def create_app(
@app.route('/ai-image-comparison/<kind>/<filename>')
def get_ai_image_comparison(kind: str, filename: str):
"""Serve reference images for generated-image strategy candidates."""
if kind not in {'rendering', 'palette', 'type'}:
"""Serve rendering images for generated-image strategy candidates."""
if kind != 'rendering':
return jsonify({'error': 'invalid comparison kind'}), 404
if not re.fullmatch(r'[A-Za-z0-9_.-]+\.png', filename or ''):
return jsonify({'error': 'invalid comparison filename'}), 404
@@ -895,21 +1347,36 @@ def create_app(
# Later stages render only downstream sections, so fold earlier confirmed
# choices from result.json back in. A refresh / reopen then re-inits from
# the user's choices instead of catalog defaults.
if _recommendation_stage(data) >= 2 and result_file.exists():
rec_stage_number = _recommendation_stage(data)
if rec_stage_number >= 2 and result_file.exists():
_merge_confirmed_choices(data, result_file)
template_adherence_enabled = _template_adherence_enabled(project_path)
data['_template_adherence_enabled'] = template_adherence_enabled
if rec_stage_number == 2:
recommendation_error = _template_stage2_error(
data,
template_required=_template_confirmation_required(
project_path,
data,
),
)
if recommendation_error:
return jsonify({'error': recommendation_error}), 409
recommendation_error = _stage2_custom_candidates_error(data)
if recommendation_error:
return jsonify({'error': recommendation_error}), 409
recommendation_error = _stage2_design_directions_error(data)
if recommendation_error:
return jsonify({'error': recommendation_error}), 409
# Template application is authored by Strategist from the installed
# workspace and current content. Never expose legacy mode fields as
# user-facing confirmation controls.
recommend = data.get('recommend')
if template_adherence_enabled:
if not isinstance(recommend, dict):
recommend = data['recommend'] = {}
recommend.setdefault('template_adherence', 'adaptive')
else:
if isinstance(recommend, dict):
recommend.pop('template_reuse_scope', None)
recommend.pop('template_adherence', None)
data.pop('template_reuse_scope', None)
data.pop('template_adherence', None)
# The page polls this endpoint after a stage-1 confirm until the AI
# overwrites the file with the re-derived stage-2 recommendations, so it
# overwrites the file with the once-authored stage-2 recommendations, so it
# must never be served from a cache.
resp = jsonify(data)
resp.headers['Cache-Control'] = 'no-store'
@@ -923,18 +1390,52 @@ def create_app(
return jsonify({'error': 'invalid payload'}), 400
confirm_dir.mkdir(parents=True, exist_ok=True)
result = dict(payload)
# Staged flow: stage-1 / stage-2 submits record intermediate choices but do
result_file = confirm_dir / RESULT_NAME
raw_stage = result.get('stage')
stage = _stage_key(raw_stage)
if raw_stage is not None and stage is None:
return jsonify({'error': 'invalid confirmation stage'}), 400
stage_error = _submission_stage_error(
project_path,
confirm_dir,
stage,
)
if stage_error:
return jsonify({'error': stage_error}), 409
custom_error = _custom_selection_error(result)
if custom_error:
return jsonify({'error': custom_error}), 400
try:
current_recommendations = _read_json_object(
confirm_dir / RECOMMENDATIONS_NAME,
)
except (OSError, json.JSONDecodeError, ValueError):
current_recommendations = {}
if stage == 'stage2' or _recommendation_stage(current_recommendations) == 3:
solution_error = _stage2_solution_error(result)
if solution_error:
return jsonify({'error': solution_error}), 400
_normalize_custom_selections(result)
locked_values = _apply_locked_recommendations(
result,
confirm_dir / RECOMMENDATIONS_NAME,
result_file,
)
result.pop('template_reuse_scope', None)
result.pop('template_adherence', None)
# Staged flow: Stage 1 / Stage 2 submits record intermediate choices but do
# NOT close the page. Only a final submit is a full confirmation. A
# payload with no stage is a single-pass confirmation (chat-opt-out parity).
stage = _stage_key(result.get('stage'))
# payload with no stage is a legacy free-design single-pass confirmation.
if stage in {'stage1', 'stage2'}:
result['stage'] = stage
result['status'] = f'{stage}-confirmed'
if locked_values:
result[_LOCKED_RECOMMENDATIONS_KEY] = locked_values
else:
result.pop(_LOCKED_RECOMMENDATIONS_KEY, None)
result['stage'] = 'final'
result['status'] = 'confirmed'
result['confirmed_at'] = time.strftime('%Y-%m-%dT%H:%M:%S')
result_file = confirm_dir / RESULT_NAME
_write_json_atomic(result_file, result)
_sync_session_state(
confirm_dir,
@@ -975,7 +1476,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument(
'--wait-stage', default='final', metavar='{stage2,final}',
help='With --wait-only, wait for this result.json stage (default: final). '
'Use stage2 for the middle handoff in the three-stage flow.',
'Use stage2 for the direction handoff in the three-stage flow.',
)
parser.add_argument(
'--wait-timeout', type=int, default=WAIT_TIMEOUT_DEFAULT,
File diff suppressed because it is too large Load Diff
@@ -555,28 +555,6 @@
"label_ja": "分割モード"
}
],
"template_adherence": [
{
"id": "adaptive",
"label": "adaptive",
"label_zh": "适应性使用",
"label_en": "adaptive",
"label_ja": "適応的に使用",
"desc_zh": "每页选择一个模板 SVG 作为架构参考;没有合适版式时沿用同一 Master 创建新的显式 Layout。",
"desc_en": "Choose a template SVG as the architecture reference for every page; when no layout fits, create a new explicit Layout under the same Master.",
"desc_ja": "各ページでテンプレートSVGを構造参照として選び、適合する版式がない場合は同じMaster配下に新しい明示的Layoutを作成する。"
},
{
"id": "strict",
"label": "strict",
"label_zh": "严格套用",
"label_en": "strict",
"label_ja": "厳密に適用",
"desc_zh": "每一页都从模板 SVG roster 选择版式,并保持其显式 Master、Layout 与 Placeholder 契约不变。",
"desc_en": "Map every page to the template SVG roster and keep its explicit Master, Layout, and Placeholder contract unchanged.",
"desc_ja": "すべてのページをテンプレートSVG一覧へ割り当て、明示されたMasterLayoutPlaceholder契約を変更せずに維持する。"
}
],
"delivery_purpose": [
{
"id": "text",
@@ -584,9 +562,9 @@
"label_zh": "文字型 · 近读",
"label_en": "text / read-close",
"label_ja": "文書型・近距離閲覧",
"desc_zh": "当文件读、近距离看(报告、数据密集 brief、留底材料)。正文 20px。",
"desc_en": "Read close as a file (report, data-dense brief, leave-behind). Body 20px.",
"desc_ja": "資料として手元で読む用途(報告書、データ密度の高いブリーフ、配布資料)。本文20px。"
"desc_zh": "页面脱离讲者也能独立读懂:使用完整句、短段落、图注、表格和必要细节;bullet 只用于真正并列的项目。",
"desc_en": "The page stands alone without a presenter: use complete sentences, short prose, captions, tables, and necessary detail; reserve bullets for genuinely parallel items.",
"desc_ja": "話者なしでもページ単体で理解できる構成。完全な文、短い段落、注釈、表、必要な細部を用い、箇条書きは本当に並列な項目だけに使う。"
},
{
"id": "balanced",
@@ -594,9 +572,9 @@
"label_zh": "均衡 · 商务",
"label_en": "balanced / business",
"label_ja": "バランス型・ビジネス",
"desc_zh": "既投影也阅读(路演业务评审)。默认档,正文 24px。",
"desc_en": "Both projected and read (roadshow, business review). The default option, body 24px.",
"desc_ja": "投影でも閲覧でも使う用途(ロードショー事業レビュー)。デフォルトの選択、本文24px。"
"desc_zh": "页面与讲者共同承担信息:一条主结论配合简短说明、结构化证据或必要列表,适合路演业务评审。",
"desc_en": "The page and presenter share the load: pair one primary claim with concise explanation, structured evidence, or a necessary list. Best for roadshows and business reviews.",
"desc_ja": "ページと話者で情報を分担する構成。主張を一つ置き、短い説明、構造化した根拠、または必要なリストを組み合わせる。ロードショー事業レビュー向け。"
},
{
"id": "presentation",
@@ -604,9 +582,9 @@
"label_zh": "展示型 · 演讲",
"label_en": "presentation / keynote",
"label_ja": "プレゼン型・講演",
"desc_zh": "投影、远距离扫一眼、内容稀疏(keynote、发布、课堂)。正文 32px。",
"desc_en": "Projected, glanced from the back, sparse (keynote, launch, classroom). Body 32px.",
"desc_ja": "投影用で後方からも一目で分かる、情報量を絞った構成(キーノート、発表会、講義)。本文32px。"
"desc_zh": "讲者与视觉承担主要表达:一页一意,以简短主张、关键词、大图或大数字为主,解释进入讲述或备注,避免段落堆积。",
"desc_en": "The presenter and visuals carry the explanation: use one idea per page, concise claims, keywords, large visuals, or hero numbers; move detail into speech or notes and avoid paragraph dumps.",
"desc_ja": "話者とビジュアルが主に説明を担う構成。1枚1メッセージで、短い主張、キーワード、大きな図や数値を中心にし、詳細は話者またはノートに置いて段落の詰め込みを避ける。"
}
]
}
@@ -125,6 +125,11 @@ body {
margin: 0;
color: var(--ink);
}
#topbar-preview .style-preview + .style-preview {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid var(--shell-line);
}
#topbar-preview .style-preview-label {
margin-bottom: 10px;
}
@@ -292,7 +297,23 @@ body {
margin: 4px 0 0;
}
.chip-preview-visual_style {
width: 320px;
width: min(100%, 430px);
flex-direction: row;
align-items: center;
}
.chip-preview-visual_style .option-preview {
flex: 0 0 clamp(116px, 36%, 160px);
}
.chip-preview-visual_style .chip-copy {
flex: 1;
}
.visual-style-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: stretch;
}
.visual-style-grid .chip-preview-visual_style {
width: 100%;
}
.option-preview {
width: 100%;
@@ -370,7 +391,14 @@ body {
color: var(--ink);
}
.text-input:focus, .num-input:focus { outline: none; border-color: var(--accent); }
textarea.text-input { resize: vertical; line-height: 1.5; }
.num-input { width: 140px; }
.text-input.locked-field {
background: #f1f5f9;
color: #475569;
cursor: not-allowed;
}
.locked-field-hint { margin-top: 6px; }
/* ---- color candidates ---- */
.color-grid { display: flex; flex-wrap: wrap; gap: 12px; }
@@ -422,12 +450,49 @@ body {
}
.font-card-name { font-size: 13.5px; font-weight: 600; }
.font-card-meta { font-size: 11.5px; color: var(--muted); }
.design-direction-grid {
flex-direction: row;
flex-wrap: nowrap;
align-items: stretch;
overflow-x: auto;
padding-bottom: 2px;
}
.design-direction-card {
flex: 1 0 calc((100% - 20px) / 3);
min-width: 200px;
}
.design-direction-preview {
width: 100%;
aspect-ratio: 16 / 9;
margin-bottom: 8px;
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.design-direction-preview img { width: 100%; height: 100%; object-fit: cover; }
.design-direction-swatches { display: flex; gap: 5px; margin-top: 8px; }
.design-direction-swatches .swatch { width: 24px; height: 24px; }
.locked-summary-chip { cursor: default; }
.locked-summary-chip:hover { border-color: var(--accent); }
.image-strategy-previews {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
margin: 8px 0 10px;
}
.image-strategy-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: stretch;
}
.image-strategy-grid > .font-card:not(.image-strategy-custom-card) {
min-width: 0;
padding: 10px;
}
.image-strategy-grid > .image-strategy-custom-card,
.image-strategy-grid > .toggle-desc {
grid-column: 1 / -1;
}
.image-strategy-preview {
position: relative;
aspect-ratio: 16 / 9;
@@ -470,32 +535,9 @@ body {
.image-strategy-preview-empty {
padding: 14px 0 4px;
}
.image-strategy-manual-card {
.image-strategy-custom-card {
background: var(--card);
}
.image-strategy-manual-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin: 10px 0;
}
.image-strategy-select-wrap {
display: flex;
flex-direction: column;
gap: 5px;
}
.image-strategy-select-label {
font-size: 11.5px;
color: var(--muted);
}
.image-strategy-select {
width: 100%;
min-height: 34px;
padding: 6px 8px;
}
.image-strategy-manual-preview-host > .image-strategy-previews {
margin-bottom: 0;
}
.font-sample-heading { font-size: 24px; font-weight: 700; line-height: 1.3; }
.font-sample-body { font-size: 14px; color: #333; margin-top: 4px; }
.custom-typography-input { margin-top: 8px; min-height: 58px; resize: vertical; }
@@ -553,6 +595,40 @@ body {
/* ---- custom input ---- */
.custom-input { margin-top: 8px; }
.custom-chip-row,
.standalone-chip-row { margin-top: 8px; }
.custom-chip-row .ai-custom-candidate { width: 100%; }
.ai-custom-candidate {
display: block;
width: 100%;
padding: 12px 14px;
text-align: left;
color: inherit;
font-weight: 400;
}
.ai-custom-candidate-head {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
}
.ai-custom-candidate-head .chip-text { font-weight: 650; }
.ai-custom-candidate-hint { font-size: 11.5px; color: var(--muted); }
.ai-custom-candidate-copy {
margin-top: 8px;
color: var(--ink);
font-size: 12.5px;
line-height: 1.55;
white-space: pre-wrap;
}
.ai-custom-candidate.selected .ai-custom-candidate-copy { color: inherit; }
.ai-custom-candidate .custom-input,
.ai-custom-candidate .image-strategy-custom-input {
width: 100%;
min-height: 86px;
margin-top: 8px;
resize: vertical;
}
/* ---- recommended chip + group labels ---- */
.chip.recommended {
@@ -830,7 +906,7 @@ body {
width: min(100%, 230px);
}
.chip-preview-visual_style {
width: min(100%, 320px);
width: 100%;
}
.style-preview-card {
flex-wrap: wrap;
@@ -0,0 +1,45 @@
# Chart Candidate Recall
`chart_recall.py` gives the Strategist a bounded, deterministic shortlist without loading the full chart catalog into the runtime prompt. It reads `templates/charts/charts_index.json` on every invocation, so the catalog remains the only template registry.
## Recall candidates
Describe one page's information shape with 3-8 concise English semantic tags. Translate source-language or industry terms into structural meaning before invoking the script.
```bash
python3 skills/ppt-master/scripts/chart_recall.py recall \
--page P03 \
--tag "time series" \
--tag "three metrics" \
--tag "direction over time" \
--limit 6
```
`--limit` accepts 3-8 and defaults to 6. It is a maximum, not a padding target: the deterministic JSON contains only positive-scoring candidates, up to the requested limit. Zero positive matches return an empty `candidates` list plus the explicit `no-template-match` option.
| Field | Contract |
|---|---|
| `page` | Input `P<NN>` page key |
| `semantic_tags` | Deduplicated input tags |
| `confidence` | Lexical recall strength; never a selection decision |
| `candidates` | Ranked keys, SVG paths, verbatim catalog summaries, scores, and matched tags |
| `no_template_match` | Explicit fallback option when every candidate conflicts with the page |
The scorer treats the key and the summary's Pick clause as positive evidence and the Skip clause as negative evidence. A term found only in Skip cannot make a candidate eligible, and Skip matches explicitly reduce a candidate's score. Unicode input is NFKC-normalized before matching. The Strategist still applies semantic judgment: inspect every returned summary, reject candidates whose Skip clause matches, and prefer the most specific valid structure. A low score does not authorize a forced match; when no candidate has a positive final score, the result carries an empty shortlist and the explicit fallback.
## Validate selected keys
Validate every selected template key before writing `design_spec.md §VII` or `spec_lock.md page_charts`:
```bash
python3 skills/ppt-master/scripts/chart_recall.py validate line_chart quadrant_text_bullets
```
The command is read-only. It exits `0` when every key exists and `1` when any key is absent. A page recorded as `no-template-match` is not a key and must not appear in `page_charts`.
## Selection boundary
- Preserve the two-lens review: numeric/data pages and structural-information pages.
- Record the selected candidate's returned `summary` verbatim as the Section VII `summary-quote`.
- Record real returned runners-up and page-specific rejection reasons.
- Open only the selected `<key>.svg` before authoring that visualization; do not load unrelated catalog SVGs.
@@ -1,28 +1,28 @@
# Confirm UI — Strategist Confirmation Stage Page
> The interactive, visual surface for SKILL.md Step 4 (the Strategist confirmation stage). Enumerable fields list **all** options from a catalog with the AI's recommendation badged; generative fields (color, typography, generated-image style) show **≥3** AI candidates (creative recommendations always offer real choice — same rule as the h.5 image strategy; fewer only on the honest-shortfall exception, with a stated reason). Fields whose universe is open (canvas, mode, visual style, icons) also get a **Custom** box; image usage is a multi-select source list plus a free-text `image_notes` box. Fully closed fields (template adherence when a deck/layout template is active, AI source when applicable, formula policy, generation mode, refine spec) do not. The AI writes its recommendation to `recommendations.json`; the user's final choices are written back to `result.json` for the AI to read. On confirm the page saves the result and shuts the server down (auto-close). The chat path is always a valid fallback — if the browser cannot open (remote / headless / web host), the AI presents the same staged confirmation in chat.
> The interactive, visual surface for [`generate-pptx`](../../workflows/generate-pptx.md) Step 4. Stage 1 is an open communication brief: common purpose paths are prompt text, never checkboxes or a forced single label. Stage 2 offers **≥3 coordinated design directions** and then exposes their component values for deliberate override; color, typography, icons, and generated-image rendering are one system rather than unrelated grids. When a template workspace is active, Stage 2 also shows one editable natural-language template-application plan—never internal mode controls. Generated images inherit the selected deck colors directly—there is no image-palette control. Stage 3 contains production mechanics only. The AI writes `recommendations.json`; confirmed values accumulate into `result.json`. Final confirm saves the result and shuts the server down. The chat path mirrors the same staged semantics.
## Authority and Scope
| Concern | Owner |
|---|---|
| Step 4 gate and pipeline order | `SKILL.md` |
| Step 4 gate and pipeline order | [`generate-pptx.md`](../../workflows/generate-pptx.md) |
| Confirm UI schema | This document |
| Stage 1 / Stage 2 / Stage 3 field membership | This document |
| Server launch / wait / shutdown behavior | This document |
| Port and lock behavior | This document |
| Chat fallback equivalence | This document |
| Confirmed-value precedence | `SKILL.md` plus this document's `result.json` contract |
| Confirmed-value precedence | [`generate-pptx.md`](../../workflows/generate-pptx.md) plus this document's `result.json` contract |
**Hard rule**: Keep detailed Confirm UI behavior here. `SKILL.md` may summarize the orchestration, but it should not duplicate the full JSON schema, catalog behavior, or launcher lifecycle.
**Hard rule**: Keep detailed Confirm UI behavior here. The Generate route may summarize orchestration, but it should not duplicate the full JSON schema, catalog behavior, or launcher lifecycle.
**Fallback rule**: Browser failure never cancels Step 4. Re-check `result.json` once, then use the chat confirmation path with the same three-stage semantics.
**Fallback rule**: The page is default. Use chat only on explicit chat-only request or launch failure/timeout after one `result.json` re-check; a chat-question tool is not a launch failure. Preserve all three stages and keep Stage-1 prompts open-ended.
## `confirm_ui/server.py`
```bash
python3 scripts/confirm_ui/server.py <project_path> --daemon --wait # launch + wait for Stage 1
python3 scripts/confirm_ui/server.py <project_path> --wait-only --wait-stage stage2 # Stage 2: wait for the design-system handoff
python3 scripts/confirm_ui/server.py <project_path> --wait-only --wait-stage stage2 # Stage 2: wait for the direction handoff
python3 scripts/confirm_ui/server.py <project_path> --wait-only # Stage 3: wait for the final result
python3 scripts/confirm_ui/server.py <project_path> --daemon
python3 scripts/confirm_ui/server.py <project_path> --daemon --port 5051
@@ -35,12 +35,12 @@ python3 scripts/confirm_ui/server.py <project_path> --shutdown # Step 4 clean
- In `--daemon` mode the launcher starts the child server with browser opening suppressed, waits for `GET /api/health` to prove the server is accepting requests, then opens the printed `http://127.0.0.1:<port>` URL. If health never becomes reachable, the command fails before presenting a dead page.
- **Shares port 5050 with the live preview server** (`svg_editor/server.py`). The two never run at once: confirm is Step 4, live preview is Step 6, and Step 4 always shuts this server down on exit (see `--shutdown`) so the port is free. One port = one forward rule for the whole pipeline. They still keep **separate processes and locks** (`.confirm_ui.lock` vs `.live_preview.lock`).
- `--daemon` starts the Flask process in the background; add `--wait` in the main pipeline so the parent command returns only after the page writes a fresh `result.json`. The `--wait` budget defaults to **590 s** (`--wait-timeout`), kept under the typical 600 s tool ceiling — run the launch with a long tool timeout (≈600000 ms). On timeout the parent returns non-zero but the detached server keeps running, so the caller must re-check `result.json` once before the chat fallback (a slow user may confirm just after the wait returns).
- `--wait-only` attaches to the page already running from the first `--daemon --wait` and blocks until the page writes the requested stage. If the recorded server died (common on Windows hosts that clean up background children), it automatically restarts the confirm server on the recorded/default port so the browser's polling reconnects without user action. Use `--wait-stage stage2` for the middle design-system handoff, then the default `--wait-stage final` for the final Stage-3 confirmation. It keys on the **stage alone** (no mtime gate), because a user may submit before this wait command is issued. Same `--wait-timeout` budget; on timeout / recovery failure it returns non-zero and the caller re-checks `result.json` once before the chat fallback.
- `--shutdown` stops a confirm server left running for this project and exits — **idempotent** (a no-op when nothing is running). Tries a graceful `/api/shutdown`, falls back to killing the recorded pid, then clears the lock. SKILL.md Step 4 runs this on every path (page-confirm or chat-fallback) so the page never lingers on the shared port before live preview starts.
- `--wait-only` attaches to the page already running from the first `--daemon --wait` and blocks until the page writes the requested stage. If the recorded server died, it automatically restarts on the recorded/default port so polling reconnects. Use `--wait-stage stage2` for the complete-solution handoff, then the default `--wait-stage final` for Stage 3. It keys on stage alone (no mtime gate), because a user may submit before the wait command starts.
- `--shutdown` stops a confirm server left running for this project and exits — **idempotent** (a no-op when nothing is running). Tries a graceful `/api/shutdown`, falls back to killing the recorded pid, then clears the lock. Generate Step 4 runs this on every path (page-confirm or chat-fallback) so the page never lingers on the shared port before live preview starts.
- Refuses to start unless `<project_path>/confirm_ui/recommendations.json` exists (except `--shutdown`, which needs no recommendations).
- Per-project lock at `<project_path>/.confirm_ui.lock` — duplicate launches are refused; stale locks (dead pid) are overwritten.
- Idle auto-shutdown after 900 s by default; `/api/shutdown` exits gracefully and releases the lock.
- `/api/recommendations` derives `_template_adherence_enabled` from `<project>/templates/design_spec.md` frontmatter. Only `kind: deck` / `kind: layout` enables the Stage-1 field; no template and `kind: brand` force it off even if a stale recommendation contains the key. When enabled but no recommendation was authored, the server supplies `adaptive` as the default recommendation.
- `/api/recommendations` and `/api/confirm` strip legacy `template_reuse_scope` and `template_adherence` fields. Those exporter values are never user-facing controls; an active template instead exposes the editable natural-language `template_application` field in Stage 2.
Dependency:
@@ -48,21 +48,24 @@ Dependency:
pip install flask
```
## Two kinds of field
## Field shapes
- **Enumerable + custom** — canvas / mode / visual_style / icons. 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, 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.
- **Enumerable + custom** — canvas / icons retain blank manual inputs; mode / visual_style instead show a mandatory AI-authored proposal in full, initially unselected and editable after selection. Selected mode / style writes literal `custom` plus its behavior sibling.
- **Visual examples for hard-to-name choices** — the full-screen confirmation page loads real SVG page samples from `static/style_previews/` for `visual_style`, and renders real sample SVGs from `templates/icons` for `icons`. These thumbnails make style and icon-library choices visually comparable before the user locks them. Preview copy is fixed role text (big title / section title / body / points), not project content from `recommendations.json`, so users compare visual treatment rather than copywriting. These previews are a confirmation aid only: they do not add fields to `recommendations.json` or `result.json`, and they do not replace the later Step 6 live preview.
- **Image usage multi-select** — image sources are selected as one or more catalog ids: `ai` = AI-generated, `web` = Web-sourced, `provided` = User-provided, `placeholder` = Placeholder, `none` = No images. `none` is exclusive. Recommendation and result values may be a legacy single string, but new files should use an array. When several sources are recommended, write the source ids to `recommend.image_usage` and write the actual usage strategy to `image_notes`, not a custom prose value.
- **Closed enumerable**template adherence (conditional), formula policy / generation mode / refine spec, plus AI source only when image usage includes `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.
- **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).
- **Closed enumerable**PPT reading mode (`delivery_purpose` compatibility key), formula policy / generation mode / refine spec, plus AI source only when image usage includes `ai`. These have no Custom box; out-of-catalog values snap back to the recommended option.
- **Open prose**`audience`, `communication_intent`, `audience_outcome`, `core_message`, `delivery_context`, `artifact_afterlife`, `content_divergence`, and `page_count`. `communication_intent` may carry several purposes plus priority / sequence; common paths appear only as help text. `content_divergence` remains a separate source-treatment axis.
- **Coordinated generative directions**`design_directions` carries ≥3 safe / shifted / bold candidates. Each candidate bundles visual style, color, typography, icon id, and conditional generated-image rendering. The page can still render legacy top-level `color`, `typography`, and `image_strategy` candidates, but new staged recommendations use the coordinated bundle.
**Custom box** appears only on fields whose universe is genuinely open — `canvas`, `mode`, `visual_style`, and `icons`. Image usage uses a multi-select source list plus `image_notes` instead of a Custom box. Fully closed sets `template_adherence`, `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.
AI-authored custom proposals apply only to mode, visual style, and conditional AI-image rendering; a selected proposal cannot be blank. Color / typography keep their existing manual Custom cards. Image usage uses source ids plus `image_notes`; closed sets have no Custom path.
**Stage-1 current-value contract.** Each editable prose box starts with the Strategist's recommendation, if one exists. The user may retain, revise, or clear it; no Stage-1 prose field has a non-empty validation gate. On confirmation, the browser submits the current strings and the server preserves them through every later stage and the final `result.json`, including `""`. Blank means no explicit user constraint and may cause downstream default judgment, but it never causes the initial recommendation to be restored. A profile-declared `locked: true` field is read-only and remains the sole exception.
`image_ai_path` is conditional: the page shows it and writes it to `result.json` only when `image_usage` includes `ai`. Web-sourced / User-provided / Placeholder / No images paths do not carry an AI backend choice.
## Catalogs — `static/catalogs.json` (the finite option universe)
The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while trilingual labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `template_adherence`, `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`, `delivery_purpose`. Each entry is `{ "id", "label", "label_zh", "label_en", "label_ja", ... }`; descriptions use `desc_zh` / `desc_en` / `desc_ja`, and `visual_styles` groups use `group_zh` / `group_en` / `group_ja`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must cover all three languages (zh / en / ja). English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Chinese and Japanese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_en", "group_ja", "items": [...] }]`. For `canvas` you only need to maintain the trilingual labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`.
The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while trilingual labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`, `delivery_purpose`. Each entry is `{ "id", "label", "label_zh", "label_en", "label_ja", ... }`; descriptions use `desc_zh` / `desc_en` / `desc_ja`, and `visual_styles` groups use `group_zh` / `group_en` / `group_ja`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must cover all three languages (zh / en / ja). English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Chinese and Japanese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_en", "group_ja", "items": [...] }]`. For `canvas` you only need to maintain the trilingual labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`.
## Round-trip data contract
@@ -74,14 +77,14 @@ The page runs as a **three-stage wizard in one browser session**. `recommendatio
| `recommendations.json stage` | Page renders | Button | On submit |
|---|---|---|---|
| `"stage1"` | direction anchors — canvas, audience + `content_divergence` + `delivery_purpose` *(PPT only — omitted on non-PPT canvases, not written to the result)*, mode + visual_style, plus `template_adherence` only when a deck/layout template is active | **Next** | writes `result.json` `{ stage: "stage1", status: "stage1-confirmed", <anchors> }`; the page does **not** close — it shows a "deriving…" state and polls `GET /api/recommendations` |
| `"stage2"` | design system — page count, color, icons, typography, formula policy | **Next** | writes `result.json` `{ stage: "stage2", status: "stage2-confirmed", <anchors + design system> }`; the page stays open and polls for Stage 3 |
| `"stage3"` | images and execution — image usage + generated-image style, generation mode, refine spec | **Confirm** | writes `result.json` `{ stage: "final", status: "confirmed", <all fields> }`, then shuts the page down |
| *(absent)* | legacy single-pass — every section on one page | **Confirm** | single final write (`status: "confirmed"`) — backward-compatible |
| `"stage1"` | communication contract — audience; open `communication_intent`; audience outcome; core message / delivery context / artifact afterlife / `content_divergence` (all prose fields may be blank); canvas | **Confirm contract & continue** | writes `result.json` `{ stage: "stage1", status: "stage1-confirmed", <communication contract> }`; the page stays open and polls |
| `"stage2"` | complete deck solution — conditional natural-language template application, reading mode, mode, page count, visual direction, color, icons, typography, image usage, generated-image rendering | **Confirm solution & continue** | writes `result.json` `{ stage: "stage2", status: "stage2-confirmed", <contract + solution> }`; the page stays open and polls |
| `"stage3"` | production only — confirmed image-source summary, conditional AI acquisition path, formula policy, generation mode, refine spec | **Confirm** | writes `result.json` `{ stage: "final", status: "confirmed", <all fields> }`, then shuts the page down |
| *(absent)* | legacy free-design single-pass — every section on one page | **Confirm** | single final write (`status: "confirmed"`) — backward-compatible only when no template workspace / `template_application` is active |
The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-derives** the design-system candidates from the user's actual anchors, overwrites `recommendations.json` with `"stage": "stage2"`, and re-attaches with `--wait-only --wait-stage stage2`. After the Stage-2 result, it **re-derives** image and execution recommendations from the confirmed anchors + design system, overwrites `recommendations.json` with `"stage": "stage3"`, and re-attaches with `--wait-only` for the final result. The page preserves earlier selections across transitions (single JS session). `GET /api/session` is the browser's waiting-state endpoint: it is derived from `recommendations.json`, `result.json`, and the active server port, then persisted to `session.json` so a recovered server can resume the same stage state. Only after `/api/session` reports that the next recommendation stage is ready does the page fetch `GET /api/recommendations`. `GET /api/recommendations` is served `no-store` so polls see overwrites; on later stages the server folds already-confirmed choices from `result.json` back into the payload so a refresh / reopen re-initializes from the user's actual choices even though those sections are no longer rendered.
The AI launches Stage 1, authors the complete Stage-2 solution once from the user's actual contract, then authors Stage-3 production mechanics once from the confirmed solution. An edit inside the current stage never requests another recommendation. The page preserves earlier answers across transitions. `GET /api/session` is the waiting-state endpoint; `GET /api/recommendations` is `no-store`, and the server folds confirmed earlier-stage choices back into later payloads so refresh / reopen restores the user's actual values—including Stage-2 color, typography, icon, image-source, and rendering choices.
**Stage progression guard.** Stages confirm strictly in order — a staged `recommendations.json` may only run **one** stage past the last confirmed result. A file that skips ahead (e.g. `"stage3"` while only stage 1 is confirmed — typically an attempt to collapse Stage 2 because an active template already fixes color / typography) is never rendered: `/api/session` keeps reporting `waiting_agent` with `stage_skip: true`, and `--wait` / `--wait-only` exit `2` with a directive log line naming the expected stage to rewrite. An active deck/layout template — `strict` adherence included — does not exempt Stage 2: the template skin becomes the recommended color / typography candidate, not a reason to skip the confirmation. Legacy single-pass files (no `stage`) are not staged and bypass the guard.
**Stage progression guard.** Stages confirm strictly in order — a staged `recommendations.json` may only run **one** stage past the last confirmed result, and `/api/confirm` accepts only the submit stage matching that file plus its required predecessor. A file that skips ahead (e.g. `"stage3"` while only Stage 1 is confirmed) is never rendered: `/api/session` keeps reporting `waiting_agent` with `stage_skip: true`, and `--wait` / `--wait-only` exit `2` if a result skips the stage being awaited. An active template does not exempt Stage 2: its Stage-2 recommendations must include `template_application.value`, and an installed workspace or that field disables the no-stage legacy single-pass path. Legacy single-pass remains available only for non-template compatibility payloads.
### Input — `recommendations.json` (written by Strategist before launch)
@@ -90,85 +93,132 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri
"stage": "stage1",
"lang": "zh",
"recommend": {
"canvas": "ppt169",
"canvas": "ppt169"
},
"audience": { "value": "公司管理层,包括财务与产品负责人" },
"communication_intent": {
"value": "先汇报进展并暴露交付风险,再推动管理层决定下一阶段投入"
},
"audience_outcome": {
"value": "管理层能比较三个选项、接受风险判断,并选定一条获得预算的路径"
},
"core_message": {
"value": "现在为方案 B 增加投入,能以可接受的成本守住发布时间"
},
"delivery_context": {
"value": "管理层现场评审 20 分钟,有主讲;会后分享录屏和文件"
},
"artifact_afterlife": {
"value": "作为审批记录、项目交接依据和季度审计材料"
},
"content_divergence": { "value": "" }
}
```
All seven Stage-1 prose values may be blank and none blocks confirmation. The values shown in the boxes are editable recommendations; the submitted current values are authoritative, so clearing a box writes and retains `""`. A preservation profile may lock an open field, for example `"content_divergence": { "value": "keep source wording and page structure verbatim", "locked": true }`; the browser renders it read-only. The server carries that lock through the intermediate results and restores the value on every staged submit; the internal carry-over marker is removed from the final `result.json`.
The common paths — inform / explain / persuade / decide / align / teach / report and account / mobilize / record and hand off — appear only as help text for `communication_intent`. They are not catalog ids and must not be emitted as a `primary_job` field.
After Stage 1 is confirmed, overwrite the file with the complete Stage-2 solution (the server folds confirmed communication fields back in when serving the page):
```json
{
"stage": "stage2",
"lang": "zh",
"recommend": {
"delivery_purpose": "balanced",
"mode": "pyramid",
"visual_style": "swiss-minimal",
"template_adherence": "adaptive",
"icons": "tabler-outline",
"image_usage": ["ai", "provided"],
"image_ai_path": "auto",
"formula_policy": "mixed",
"generation_mode": "continuous",
"delivery_purpose": "balanced"
"image_usage": ["ai", "provided"]
},
"page_count": { "value": "12-15" },
"audience": { "value": "..." },
"content_divergence": { "value": "" },
"image_notes": { "value": "封面和章节页用 AI 主视觉;产品页优先用户素材,缺口页可用占位符。" },
"color": {
"selected": 0,
"candidates": [
{ "name": "...", "note": "...",
"palette": {
"background": "#FFFFFF",
"secondary_bg": "#F4F6F8",
"primary": "#1A3A6B",
"accent": "#E8A317",
"secondary_accent": "#4A7BB5",
"body_text": "#1D2430"
} }
]
"template_application": {
"value": "选用封面、章节页和数据页原型;跳过示例内容页。品牌标识和页脚保留,正文可按当前材料重组。"
},
"typography": {
"selected": 0,
"candidates": [
{ "name": "...", "note": "...",
"sample_heading": "主题标题示例", "sample_heading_latin": "Topic Title",
"sample_body": "关键信息摘要", "sample_body_latin": "Key message summary",
"heading": { "cjk": "思源黑体", "latin": "Inter", "css": "'Source Han Sans SC','Inter',sans-serif" },
"body": { "cjk": "思源黑体", "latin": "Inter", "css": "..." },
"body_size": 20 }
]
"image_notes": { "value": "封面和章节页用 AI 主视觉;产品页优先用户素材。" },
"custom_candidates": {
"mode": {
"name_zh": "冲突到决策",
"behavior_zh": "先建立业务冲突,再用结论先行结构推动决策。"
},
"visual_style": {
"name_zh": "编辑批注风",
"behavior_zh": "严格栅格配合边注和证据强调。"
},
"image_strategy": {
"name_zh": "证据拼贴",
"rendering": "custom",
"visual_zh": "纸面证据拼贴",
"mood_zh": "审慎可信",
"behavior_zh": "裁切纸面配少量批注,保持平面深度并继承演示色板。"
}
},
"design_directions": {
"selected": 0,
"candidates": [
{
"name": "方案 A",
"name_zh": "稳妥专业",
"note_zh": "像成熟咨询简报",
"visual_style": "swiss-minimal",
"icons": "tabler-outline",
"color": { "name_zh": "冷静专业", "palette": {
"background": "#FFFFFF", "secondary_bg": "#F4F6F8",
"primary": "#1A3A6B", "accent": "#E8A317",
"secondary_accent": "#4A7BB5", "body_text": "#1D2430"
} },
"typography": {
"name_zh": "清晰无衬线",
"heading": { "cjk": "Microsoft YaHei", "latin": "Arial", "css": "sans-serif" },
"body": { "cjk": "Microsoft YaHei", "latin": "Arial", "css": "sans-serif" },
"body_size": 24
},
"image_strategy": {
"name_zh": "克制矢量",
"rendering": "vector-illustration",
"palette": "cool-corporate",
"visual": "扁平矢量、实色块、少阴影",
"color": "背景 60-70% + 主色 25-30% + 强调色少量点题",
"mood": "稳定、可信、克制"
"visual_zh": "扁平矢量、实色块、少阴影",
"mood_zh": "稳定、可信、克制"
}
}
]
}
}
```
The example abbreviates the required ≥3 directions. Custom mode/style candidates remain mandatory; AI usage also requires the custom image candidate. Stage 2 rejects fewer than three bundles, incomplete six-role palettes, and incomplete heading/body stacks. Legacy grids remain readable only with three complete palettes and complete typography.
After Stage 2 is confirmed, overwrite it with Stage-3 production recommendations only:
```json
{
"stage": "stage3",
"lang": "zh",
"recommend": {
"image_ai_path": "auto",
"formula_policy": "mixed",
"generation_mode": "continuous"
},
"visual_style_spectrum": [
{ "id": "soft-rounded", "tag_zh": "稳妥专业", "tag_en": "Safe & professional", "tag_ja": "手堅くプロフェッショナル", "note_zh": "像 Notion 官网", "note_en": "like the Notion site", "note_ja": "Notion公式サイト風" },
{ "id": "editorial", "tag_zh": "编辑质感", "tag_en": "Editorial depth", "tag_ja": "エディトリアルな質感", "note_zh": "像经济学人专题", "note_en": "like an Economist feature", "note_ja": "The Economistの特集記事風" },
{ "id": "brutalist", "tag_zh": "硬核宣言", "tag_en": "Bold manifesto", "tag_ja": "大胆なマニフェスト", "note_zh": "像研究机构年度宣言", "note_en": "like a research-house manifesto", "note_ja": "研究機関の年次宣言風" }
],
"refine_spec": { "value": false }
}
```
> Each `candidates` array above shows **one** entry for brevity — `color` and `typography` must each carry **≥3** in a real file, while `image_strategy.candidates` should carry **exactly 3 non-custom recommendation** entries when AI image generation is offered. The UI adds the fourth **Custom** card itself; `selected` indexes the recommended default among the recommendation entries.
- `recommend.*` names the recommended `id` for each enumerable field (must match a `catalogs.json` id, or be a free string for a recommended custom value). The page badges and pre-selects it. **Guarantee**: if a `recommend.*` is omitted, the page falls back to the first catalog option so every enumerable field always shows one badged recommendation — but the AI should still set them for a meaningful default. Legacy aliases are accepted for old files (`line``tabler-outline`, `filled``tabler-filled`, `monochrome``chunk-filled`, `search` `web`, `default``auto`, `builtin``host-native`), but new files should write canonical ids.
- `recommend.template_adherence` is conditional. Write `adaptive` or `strict` only when Step 3 loaded a `kind: deck` or `kind: layout` template. The server independently checks the copied template spec and enables the field only for those two kinds; free design and brand-only templates cannot display or return it. If the template is active but the recommendation key is missing, the server supplies `adaptive`. Both values use explicit template export and require a reference SVG for every page: `strict` keeps the selected Layout contract; `adaptive` may create a new Layout under the same Master.
- `recommend.*` names each recommended id. New mode / style values use a catalog id or literal `custom`; arbitrary prose values are legacy-only. Use `recommend.image_strategy: "custom"` only when an explicit user-supplied image direction should start selected. Missing recommendations fall back to the normal preset. Legacy aliases remain accepted; new files write canonical ids.
- `custom_candidates` is recommendation-only. Mode / style carry localized `name` + `behavior`; conditional image strategy also carries `rendering: "custom"`, `visual`, and `mood`. The server rejects missing required candidates; the UI shows full copy, edits it only after selection, rejects a selected blank, and omits unselected candidates from `result.json`. Template-backed proposals obey inherited identity, prototype capacity, and `template_application`.
- `audience`, `communication_intent`, and `audience_outcome` are load-bearing Stage-1 reasoning inputs, so seed concrete recommendations when the evidence supports them; they are not required user inputs. Every Stage-1 prose field may be blank after confirmation. The complete six-field contract stays in `result.json` and `design_spec.md`; `spec_lock.md communication` receives only the compact `audience` / `objective` / `core_message` execution projection plus the applicable reading mode. `communication_intent` may preserve several purposes plus priority / sequence; never add a `primary_job` enum.
- Do not write `recommend.template_reuse_scope` or `recommend.template_adherence`. Strategist records those internal exporter values later in `spec_lock.md` after inspecting the actual template and current content.
- For an active template workspace, write one editable prose field as top-level `template_application.value`. It summarizes actual page/prototype use and preservation/reorganization decisions. Omit it for free design. The UI returns the current string through Stage 2, Stage 3, and final confirmation; Strategist then persists the final effective plan as `- **Template Application**: ...` in `design_spec.md §I`, and `page-context` projects it to Executor. Never replace it with internal reuse/adherence ids or a fixed option menu.
- `recommend.image_usage` should be an array of source ids when more than one source applies, e.g. `["ai", "provided"]`. A single string is still accepted for backward compatibility. Do not write bare `"custom"` and do not encode a mixed-source plan as prose here; write the prose to top-level `image_notes.value`.
- `image_notes` is the initial strategy note shown under the image source chips. Use it for page-role guidance and constraints: which source applies where, what to avoid, which user assets are authoritative, how realistic / abstract the imagery should be, and what can remain as placeholders. It is intent guidance, not a separate finite option.
- When `recommend.image_usage` includes `ai`, also set `recommend.image_ai_path` to one of `auto` / `api` / `host-native` / `manual`; the page presents these as explicit choices.
- **Color candidates carry the user-facing core `palette`**: `background`, `secondary_bg`, `primary`, `accent`, `secondary_accent`, and `body_text`. The page renders labelled swatches and offers per-role override inputs for precise single-role edits, plus a **Custom color card with a free-text box** (parallel to the custom typography box) — the user can describe the palette in words or paste HEX values instead of filling each role; this writes `color: { "name": "custom", "custom": "<text>" }` to `result.json` for the AI to interpret. Legacy `text` is accepted as an alias for `body_text`, but new files should write `body_text`. Strategist derives secondary text, borders, state colors, and visual-style neutral tiers later when writing `design_spec.md` / `spec_lock.md`; those are not user-facing confirmation choices.
- When confirmed Stage-2 `image_usage` includes `ai`, Stage 3 sets `recommend.image_ai_path` to one of `auto` / `api` / `host-native` / `manual`. Stage 2 never asks for the acquisition mechanism while the user is still deciding the image role.
- **Color candidates carry the user-facing core `palette`**: `background`, `secondary_bg`, `primary`, `accent`, `secondary_accent`, and `body_text`. The page renders every role as a labelled swatch with its HEX value visible, and offers per-role override inputs for precise single-role edits, plus a **Custom color card with a free-text box** (parallel to the custom typography box) — the user can describe the palette in words or paste HEX values instead of filling each role; this writes `color: { "name": "custom", "custom": "<text>" }` to `result.json` for the AI to interpret. Legacy `text` is accepted as an alias for `body_text`, but new files should write `body_text`. Strategist derives secondary text, borders, state colors, and visual-style neutral tiers while writing `design_spec.md`, then projects the machine values to `spec_lock.md`; those are not user-facing confirmation choices.
- **Candidate display text may be multilingual**: color / typography candidates can provide `name_zh` / `name_en` / `name_ja` and `note_zh` / `note_en` / `note_ja`; the page falls back to legacy `name` / `note`. Labels resolve in the page language first, then fall back across the others (a `ja` page: ja → en → zh; zh/en pages keep their zh↔en fallback and try `_ja` last), so when `lang` is `ja` always include the `_ja` variants — otherwise the candidate labels render in English.
- **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 include topic-matched `sample_heading`, `sample_heading_latin`, `sample_body`, and `sample_body_latin`; do not reuse unrelated fixed examples such as a digital-transformation headline for a travel, education, product, or brand deck. Each candidate should also include `body_size` — the body baseline in **px** (the system's only unit, every canvas). The initial value comes from the candidate's `body_size`, sized for the recommended delivery purpose: `text` ~20 · `balanced` ~24 · `presentation` ~32. On submit the page writes `typography.body_size` as px directly — no pt conversion, no `body_size_pt` provenance. The page exposes `body_size` as an editable numeric field whose hint shows the recommended size — **one fixed px per delivery purpose on PPT (`text` 20 · `balanced` 24 · `presentation` 32), not a range** (non-PPT ≈2.53.3% of height in px). The input area labels the value as px, shows the SVG px → PPT pt relation (`1px = 0.75pt`) above the input, and displays the current approximate pt value below it for user orientation only; the output contract remains px-only. The user may still edit it; an out-of-range flag only warns if the value strays far (e.g. a unit mistake). **Inputs are independent — the hint updates with canvas / delivery purpose but never rewrites a value the user can see.** It also offers a custom typography text box so the user is not limited to the proposed candidates.
- **Per-role size override** (parallel to color's per-role HEX override): besides `body_size`, the page exposes **independent** editable inputs for `title` / `subtitle` / `annotation`. Each role is pre-filled **once** with a starting value — the candidate's `typography.sizes[role]` if provided, otherwise a one-time ramp suggestion (`body × ` mid-band ratio) — and then holds its own value. **There is no cross-field cascade**: changing `body_size`, `delivery_purpose`, or canvas updates only the recommended-value hint, never the role values; a re-render preserves exactly what the user sees. Each role input is labelled as px and shows an approximate pt equivalent (`1px = 0.75pt`) for orientation. The final values are written to `result.json` as `typography.sizes: { "title", "subtitle", "annotation" }` in **px** — every canvas, no pt and no `sizes_pt` provenance. Seeding `sizes` in a candidate is optional — omit it and each role gets its one-time ramp suggestion.
- **`delivery_purpose`** (enumerable, PPT only) is the primary driver of the body baseline: `text` (read-close), `balanced` (business, the default), `presentation`. It is surfaced in the **§c key-information area** (beside audience, Stage 1) as a consumption-mode choice; the Stage-2 typography section then reads the confirmed value for the recommended body px — the pick itself does not rewrite the body field (inputs are independent). `recommend.delivery_purpose` pre-selects one (default `balanced`); the user's pick writes back to `result.json.delivery_purpose` as a plain id. Strategist uses it to set the px body baseline — **one fixed px per purpose (`text` 20 · `balanced` 24 · `presentation` 32), not a range** (see [strategist.md §g](../../references/strategist.md)). Non-PPT canvases omit it.
- **Typography candidates split CJK and Latin** for both `heading` and `body`; `css` is the fallback preview stack. Each candidate includes topic-matched sample text. Stage 2 is authored once with a reading-mode baseline of `text` 20 · `balanced` 24 · `presentation` 32 px on PPT. Font cards choose family / character and preserve the current sizing state; they do not introduce a competing size recommendation. The page writes px directly. `delivery_purpose` remains the compatibility key only.
- **Per-role size override** (parallel to color's per-role HEX override): besides `body_size`, the page exposes editable inputs for `title` / `subtitle` / `annotation`. The browser applies one documented deterministic dependency chain: `reading mode → body baseline → unpinned role sizes` (role ramp: `body ×` the §g ratios). Changing reading mode updates the body and all unpinned roles locally; changing body updates unpinned roles locally. Editing body or a role pins that value, so later reading-mode changes do not overwrite it. Font / direction-card selection preserves all current sizes. This is a browser-only state update: it performs no fetch, asks the backend to author no new recommendations, and a re-render preserves exactly what the user sees. Each role input is labelled as px and shows an approximate pt equivalent (`1px = 0.75pt`) for orientation. The final values are written to `result.json` as `typography.sizes: { "title", "subtitle", "annotation" }` in **px** — every canvas, no pt and no `sizes_pt` provenance. Candidate `sizes` remain accepted for compatibility, but the fresh Stage-2 baseline is normalized through the same local ramp before first render.
- **`delivery_purpose` compatibility key / Reading mode** (enumerable, PPT only) decides where meaning is carried, not merely how large type is: `text` makes pages self-contained with complete sentences, short prose, captions, tables, and necessary detail; `balanced` shares explanation between page and presenter; `presentation` uses one idea, concise claims, and visual evidence while speech / notes carry the detail. It therefore governs page grammar, granularity, density / rhythm, and note burden. Reading-mode cards intentionally show **no px value**; the typography section owns the separately visible body / role sizes and applies any local default. It is surfaced in Stage 2 beside the visual system, separate from communication intent. `recommend.delivery_purpose` pre-selects one; `result.json` retains the key, while `spec_lock.md` uses canonical `consumption_mode`. Non-PPT canvases omit it.
- **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` includes `ai`. Each candidate records `rendering`, `palette`, and short `visual` / `color` / `mood` lines from Strategist h.5. Author **exactly three non-custom recommendation candidates** here; the page displays those three, then appends one built-in **Custom** card. If extra candidates are present, the page shows only the first three non-custom entries. When `rendering` / `palette` match files under `references/ai-image-comparison/`, the left preview pane displays those reference PNGs for the selected candidate; when AI image generation is not selected, that left preview is hidden. The right-side option cards stay text-first and do not duplicate the gallery. The **Custom** card lists all reference-gallery `rendering` and `palette` ids from `ai-image-comparison/*/_manifest.json`, plus `custom` as a prose-only tail choice and a free-text prompt box; that prose is written to `result.json.image_strategy.custom`. If either selected dimension is `custom`, the preview intentionally falls back to prose and shows no reference image. `palette` means color behavior only — final AI image HEX values follow the confirmed `color` choice above. 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"/"tag_ja", "note_zh"/"note_en"/"note_ja" }` (include the `_ja` variants when `lang` is `ja`) 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.
- **Generated-image direction** appears only for `image_usage: ai`: up to three preset cards plus one full-width AI custom proposal. Custom has no preset dropdown; selection makes it editable and submits `rendering: "custom"` + `behavior`. The live preview follows the selection. No image palette is written; deck colors remain authoritative, and legacy `image_strategy.palette` is ignored.
- **`design_directions`** is the canonical Stage-2 spectrum: ≥3 safe / shifted / bold bundles with localized copy, style, icons, conditional image strategy, complete CJK/Latin typography, and HEX `background`, `secondary_bg`, `primary`, `accent`, `secondary_accent`, `body_text`. Selection applies the bundle; component controls override it. `result.json` stores components, not a direction id.
- `recommend.generation_mode` and `refine_spec` mirror the two mandatory notes in [`generate-pptx`](../../workflows/generate-pptx.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** Stage-1 source-treatment field. Blank means a balanced default; facts stay sourced at every level. Strategist consumes it while authoring §IX and records it in `design_spec.md §I`; it is not written to `spec_lock.md`. Beautify sends `{ "value": "keep source wording and page structure verbatim", "locked": true }`, so the UI displays it read-only and the server restores it on every staged submit. Template-fill does not use this confirmation flow and does not surface it.
- `lang` is a soft default (`zh` / `en` / `ja` — the page UI supports all three); an explicit user language choice in the page (persisted to `localStorage`) wins.
### Output — `result.json` (written on submit, read by the AI)
@@ -178,10 +228,15 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri
"canvas": "ppt169",
"page_count": "12-15",
"audience": "...",
"communication_intent": "Report progress and expose risk first; then obtain an investment decision",
"audience_outcome": "The committee compares the options and chooses one funded path",
"core_message": "Fund option B now to protect the launch date at acceptable incremental cost",
"delivery_context": "Presenter-led 20-minute leadership review; recording shared afterward",
"artifact_afterlife": "Approval record, hand-off reference, and audit trail",
"content_divergence": "freely restructure and expand within the source",
"template_application": "选用封面、章节页和数据页原型;跳过示例内容页。品牌标识和页脚保留,正文可按当前材料重组。",
"mode": "pyramid",
"visual_style": "swiss-minimal",
"template_adherence": "adaptive",
"color": { "name": "...", "palette": { "background": "#...", "secondary_bg": "#...", "primary": "#...", "accent": "#...", "secondary_accent": "#...", "body_text": "#..." } },
"icons": "tabler-outline",
"typography": { "name": "...", "heading": { "cjk": "...", "latin": "...", "css": "..." }, "body": { "cjk": "...", "latin": "...", "css": "..." }, "body_size": 24, "body_size_unit": "px", "sizes": { "title": 42, "subtitle": 32, "annotation": 18 } },
@@ -190,7 +245,7 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri
"image_usage": ["ai", "provided"],
"image_notes": "封面和章节页用 AI 主视觉;产品页优先用户素材,缺口页可用占位符。",
"image_ai_path": "auto",
"image_strategy": { "name": "方案 A", "rendering": "vector-illustration", "palette": "cool-corporate", "visual": "...", "color": "...", "mood": "..." },
"image_strategy": { "name": "方案 A", "rendering": "vector-illustration", "visual": "...", "mood": "..." },
"generation_mode": "continuous",
"refine_spec": false,
"stage": "final",
@@ -199,13 +254,15 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri
}
```
The shape above is the **final** (Stage 3) result, carrying Stage 1 anchors, Stage 2 design-system fields, and Stage 3 image / execution fields. The intermediate **Stage 1** write carries only the anchor fields plus `"stage": "stage1"`, `"status": "stage1-confirmed"`; the AI reads it to re-derive Stage 2 and never treats it as the final confirmation. The intermediate **Stage 2** write carries anchors + design-system fields plus `"stage": "stage2"`, `"status": "stage2-confirmed"`; the AI reads it to re-derive Stage 3 and never treats it as final. Legacy `tier1` / `tier2` results are accepted for old sessions but are no longer written. A legacy single-pass write has no `stage` (or `stage: "final"`) and `status: "confirmed"`.
The shape above is final. Selected custom values use `mode: custom` + `mode_behavior`, `visual_style: custom` + `visual_style_behavior`, or `image_strategy.rendering: custom` + `behavior`. Intermediate writes retain accumulated fields; legacy tier names remain read-compatible.
- Any option field may instead hold a **free-text custom string** (the user picked **Custom**); `color` / `typography` custom entries set `name: "custom"`. Image usage is not a custom string in new results: it is a source-id array, with free-text strategy captured in `image_notes`.
- `image_ai_path` and `image_strategy` are omitted from `result.json` unless `image_usage` includes `ai`. Both are honored downstream as confirmed choices — and the page is only a convenience surface over the **canonical chat channel**: the same choices made in chat are honored identically when no `result.json` exists. `image_ai_path` drives the Step 5 generation path (`image-generator.md` §7 — `host-native` forces the host tool even when `IMAGE_BACKEND` is set); the chosen `image_strategy` candidate is locked verbatim by Strategist h.5 (no re-pick).
- After the user clicks the **final Confirm** (Stage 3, or single-pass), the page saves `result.json` and shuts the server down (auto-close). Stage-1 and Stage-2 **Next** keep the page open while it polls for the re-derived downstream stage. In the default flow, the first `--daemon --wait` returns on the stage-1 result, `--wait-only --wait-stage stage2` returns on the stage-2 result, and the final `--wait-only` returns on the final result; the AI reads each immediately — no extra chat confirmation is required. Chat confirmation remains the fallback when the page cannot be used. Either way, Step 4 ends with a `--shutdown` cleanup so a never-confirmed page cannot keep holding port 5050 ahead of the Step 6 live preview.
**Final-result consumption contract.** A final result is the user-confirmed input contract for the Strategist's Design Spec, not another recommendation input. The Strategist re-reads the complete final object, writes and audits `design_spec.md` against every explicitly present field, and may autonomously decide only details that remain unconfirmed. Only after that audit passes does it project `spec_lock.md` from the completed Design Spec; lock authoring is not a second design pass. It must not omit, substitute, narrow, weaken, reinterpret, or re-recommend a confirmed value. If one value cannot be honored, the owning workflow reports or pauses under failure recovery; it never deletes the requirement to keep the pipeline moving.
- Bespoke mode / style prose lives only in the required behavior sibling; image custom prose lives in `image_strategy.behavior`. Canvas / icons retain free-text edge cases, color / typography retain `name: "custom"`, and image usage remains a source-id array plus `image_notes`.
- `image_ai_path` and `image_strategy` appear only with `image_usage: ai` and remain confirmed downstream. The page is default; explicit/failure chat fallback keeps identical fields. `image_ai_path` selects the Step 5 path, and [`strategist-image.md`](../../references/strategist-image.md) §2 locks the chosen strategy verbatim.
- After the user clicks the **final Confirm** (Stage 3, or single-pass), the page saves `result.json` and shuts the server down (auto-close). Stage-1 **Confirm contract & continue** and Stage-2 **Confirm solution & continue** keep the page open while it polls for the once-authored downstream stage. In the default flow, the first `--daemon --wait` returns on the stage-1 result, `--wait-only --wait-stage stage2` returns on the stage-2 result, and the final `--wait-only` returns on the final result; the AI reads each immediately — no extra chat confirmation is required. Chat fallback shows the same initially-unselected custom proposals. Either way, Step 4 ends with a `--shutdown` cleanup so a never-confirmed page cannot keep holding port 5050 ahead of the Step 6 live preview.
## Scope
- Confirmation surface only — Strategist authors every recommendation; the page never generates deck content.
- No SVG / layout preview here — that is the live preview server's job (`workflows/live-preview.md`, Step 6).
- No SVG / layout preview here — that is the live preview server's job (`workflows/stages/live-preview.md`, Step 6).
@@ -1,6 +1,9 @@
# Conversion Tools
> Architecture rationale (why native-Python first with pandoc fallback, why curl_cffi for TLS impersonation): see [docs/technical-design.md "Source Content Conversion"](../../../../docs/technical-design.md#source-content-conversion).
> **Design boundary**: use native-Python converters for supported formats,
> invoke Pandoc only for explicit fallback formats, and let web conversion use
> `curl_cffi` when available for sites that reject Python's default TLS
> fingerprint.
Source conversion tools turn PDFs, documents, slide decks, and web pages into Markdown before project creation.
@@ -223,7 +226,7 @@ Outputs (per source deck, prefixed by file stem):
- `<stem>.slide_library.json` — text slots, geometry, native tables, native chart display caches, and SmartArt nodes/connections
- `source_profile.json` — the single multi-deck index: a compact Strategist-facing digest per deck (over identity, tables, charts, SmartArt, 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 `<stem>.*` 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 `<stem>.*` artifacts.
`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 `<stem>.*` artifacts and a `decks[]` entry in the shared `source_profile.json` index (re-importing the same stem replaces its entry). The beautify profile and Fill Native PPTX route stay single-deck and read one chosen deck's `<stem>.*` 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.
@@ -238,6 +241,7 @@ Reconstruct a PPTX package as editable SVG views by reading OOXML directly.
python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode both
python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode layered
python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode flat
python3 scripts/pptx_to_svg.py deck.pptx --strict
```
| Mode | Output |
@@ -246,27 +250,68 @@ python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode flat
| `layered` | Only the layered `svg/` view and inheritance metadata |
| `flat` | One self-contained slide SVG per page under `svg/` |
Supported text-grid tables and conservative classic-chart caches carry
`data-pptx-native` metadata beside their SVG fallback. Table import requires
For Office pictures that carry both a raster compatibility preview on
`a:blip` and an editable SVG relationship in `asvg:svgBlip`, import resolves
the SVG relationship first. The raster relationship is used only when the SVG
relationship or media part cannot be read. The template manifest uses the same
relationship preference for asset identity; its existing missing-media gate
remains strict rather than silently treating the raster preview as the
template's canonical asset.
### Import compatibility and recovery boundary
Import is tolerant by default because the source deck is user-owned or comes
from third-party authoring tools. Recovery happens at the narrowest safe
boundary: first omit only an unsupported property or feature; if that is not
possible, replace only the affected object with a visible diagnostic
placeholder; omit a background without discarding its page. Corrupt ZIP/XML or
missing required package structure remains fatal because no safe local recovery
exists. Pass `--strict` for parser development or contract verification when
the first unsupported/malformed source construct should stop conversion.
Every successful run writes `<output>/conversion-report.json`. Its stable
top-level fields are `schemaVersion`, `source`, `mode`, `summary`, and
`diagnostics`. Each diagnostic records a reason `code`, source `message`, chosen
`fallback`, package `part_path`, and—when available—`slide_index`, `shape_id`,
`shape_name`, and `shape_kind`. The command also prints a bounded warning
summary instead of a raw Python traceback.
In the detailed native-object notes below, “fails closed” or “error” describes
the native replacement claim or strict mode. Default tolerant deck import
retains the usable fallback/object and records the degradation; it does not
discard unrelated shapes, pages, or the entire deck.
### Native table and chart import claims
Supported text-grid tables and conservative classic-chart caches carry a
`data-pptx-replace-with` claim beside their SVG fallback, with the replacement
payload in a child `<metadata type="application/json">`. The parent claim
selects the table or chart schema. Table import requires
exact physical row/grid topology and accepts canonical rectangular merges,
safe solid/no-fill per-side borders, plain multi-paragraph cells, and a closed
run-rich paragraph schema.
Each run requires `text` and may use only `bold`, `italic`, `underline`,
`strike`, `color`, `font_size`, one `font_family`, `lang`, and `alt_lang`.
Presentation-only source run XML normalizes. Relationship-bearing text,
extensions, noncanonical/overlapping merges, nonblank merge slaves, unsafe
border XML, non-solid fills, structural line breaks/fields/tabs/bullets, and
broken text topology remain fallback-only.
Presentation-only source run XML without a non-empty `effectLst` / `effectDag`
normalizes. A table-cell run effect disables native replacement and adds a
blocking effect diagnostic. Relationship-bearing text, extensions,
noncanonical/overlapping merges, nonblank merge slaves, unsafe border XML,
non-solid fills, structural line breaks/fields/tabs/bullets, and broken text
topology remain fallback-only.
Markers remain dormant
unless a later export uses `--native-objects`. That opt-in is editable-first:
it may normalize styling or omit marker-local details not represented by the
unless a later export uses `--native-charts-and-tables`. That opt-in is
data-object-first: the default fallback still exports as editable DrawingML
shapes, while the opt-in supplies a data source and PowerPoint's
chart/table-specific object model.
The native-object route may normalize styling or omit marker-local details not represented by the
payload, and export reports that risk without disabling an otherwise supported
active marker. Unsupported tables keep their
rendered SVG table; unsupported charts keep a baked preview when one exists.
For the currently supported parsed classic families (column/bar/line/area,
pie/doughnut, scatter, and bubble), a chart without a baked preview receives a
deterministic readable fallback marked
`data-pptx-visual-status="normalized"`. Unknown style XML still fails closed;
`data-pptx-fallback-kind="normalized"`. Unknown style XML disables the native
replacement claim or falls back to a diagnostic object in tolerant mode;
common solid/no-fill/line/marker forms and scheme colors are normalized for the
SVG fallback and core payload colors, while native opt-in may still normalize
unmodeled alpha, line, marker, or no-fill details. Common General, decimal,
@@ -274,27 +319,37 @@ grouped, percent, and simple currency-prefix data-label formats render
deterministically; an unknown Excel format program keeps the active payload but
does not claim a normalized fallback. Active types outside the current renderer
continue to use an explicit placeholder marked
`data-pptx-visual-status="placeholder"` and
`data-pptx-route-status="reconstruction-only"`. Validation and export report
that route as a warning. Default export keeps the placeholder; when the same
group has a valid active native-chart payload, `--native-objects` may still
reconstruct the editable chart. Invalid or contradictory status declarations
remain errors.
Fallback-only native capability uses `data-pptx-native-status` and remains a
warning when the SVG fallback itself is complete.
`data-pptx-fallback-kind="placeholder"`. Validation and export report that
reconstruction-only fallback as a warning. Default export keeps the
placeholder; when the same group has a valid active chart replacement payload,
`--native-charts-and-tables` may still reconstruct the PowerPoint-native chart.
Invalid or contradictory fallback declarations remain errors. Fallback-only
replacement capability uses `data-pptx-replacement-status` and remains a
warning when the SVG fallback itself is complete. Imported table/chart groups
under this contract carry `data-pptx-import-source="pptx"`, whether active or
fallback-only; generated authoring omits this provenance attribute.
Active imported markers also carry `data-pptx-fallback-sha256`, computed over
their canonical fallback plus reachable document-level SVG fragment definitions.
A later visible edit, reachable definition change, local reference-target
change, or marker transform makes the native metadata stale. The mandatory
change, or marker transform makes the replacement metadata stale. The mandatory
quality checker reports the mismatch; default export keeps the edited fallback,
while `--native-objects` fails before replacement so it cannot discard that edit.
while `--native-charts-and-tables` fails before replacement so it cannot discard that edit.
`visibility:hidden` content, marker-local unused definitions, and explicitly
referenced document-level target roots (even when hidden) are included
conservatively; marker-local `display:none` subtrees are excluded, and external
file bytes are not read.
A legacy marker without the hash remains native-compatible and warns in the
checker/native route that stale detection is unavailable.
Generated authoring and reusable templates omit import provenance and do not
preseed a static fallback hash; that state is normal and does not warn. A legacy
imported marker that still carries PPTX import provenance but lacks the hash
remains native-compatible and warns in the checker/native route that stale
detection is unavailable.
Legacy `data-pptx-native*`, `data-pptx-visual-status`, and
`data-pptx-route-status` spellings remain read-compatible. New importer output
and generated SVG use the replacement/fallback names above. The old
`--native-objects` option remains a compatibility alias for
`--native-charts-and-tables`.
For table style `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`, the importer resolves
the normalized `wholeTbl`, `firstRow`, `band1H`/`band2H`, theme color/font, and
@@ -315,11 +370,12 @@ axis fields. The importer also accepts radar, safe `of_pie` `serLines`, the
closed axis/title/legend normalization cases, and bar/column `gapWidth` /
`overlap`. `gapWidth` must be one integer in `0..500` and `overlap` one integer
in `-100..100`; both normalize in native output, while malformed, duplicate, or
out-of-range values fail closed. These additions do not expand the normalized
out-of-range values disable the native replacement claim in tolerant mode and
stop strict import. These additions do not expand the normalized
renderer.
Safe stock series style may pass the structural gate, while stock series,
`hiLowLines`, and up-down bar local styling can still normalize under the
editable-first contract.
data-object-first contract.
ChartEx import accepts exactly the validated treemap, sunburst, histogram,
pareto, box-whisker, waterfall, and funnel data models. Their supported
hierarchy/category/value/series/subtotal topology round-trips to native output.
@@ -338,6 +394,138 @@ Concrete slide SVGs resolve `<a:fld type="slidenum">` using the presentation's
`firstSlideNum` display numbering. Standalone master/layout SVGs keep the
literal field fallback because one shared part can serve multiple slides.
### Maintenance smoke checks
Run these checks from the repository root after changing `pptx_to_svg/` or its
CLI. They generate every required input under `/tmp`; do not replace them with
a committed `test_*.py` suite.
#### Healthy generated deck
```bash
python3 - <<'PY'
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE
from pptx.util import Inches
presentation = Presentation()
slide = presentation.slides.add_slide(presentation.slide_layouts[6])
shape = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(1),
Inches(1),
Inches(3),
Inches(1),
)
shape.text = "PPTX import smoke check"
presentation.save("/tmp/ppt-master-smoke-healthy.pptx")
PY
python3 "skills/ppt-master/scripts/pptx_to_svg.py" \
"/tmp/ppt-master-smoke-healthy.pptx" \
--inheritance-mode flat \
-o "/tmp/ppt-master-smoke-healthy"
python3 -c "import json; from pathlib import Path; report = json.loads(Path('/tmp/ppt-master-smoke-healthy/conversion-report.json').read_text()); assert report['summary'] == {'slides': 1, 'warnings': 0}, report['summary']; print('OK: 1 slide, 0 warnings')"
```
Expected: both commands exit `0`; the assertion prints
`OK: 1 slide, 0 warnings`.
#### Tolerant/strict color-structure probe
Generate a two-shape PPTX, then add one foreign attribute to the first shape's
valid `a:srgbClr` node:
```bash
python3 -c '
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.util import Inches
base = Path("/tmp/ppt-master-color-smoke-base.pptx")
target = Path("/tmp/ppt-master-color-smoke.pptx")
presentation = Presentation()
slide = presentation.slides.add_slide(presentation.slide_layouts[6])
for left, color in ((1, (0x44, 0x72, 0xC4)), (4, (0xED, 0x7D, 0x31))):
shape = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(left),
Inches(1),
Inches(2),
Inches(1),
)
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*color)
presentation.save(base)
with ZipFile(base) as source, ZipFile(target, "w", ZIP_DEFLATED) as destination:
patched = False
for member in source.infolist():
payload = source.read(member)
if member.filename == "ppt/slides/slide1.xml":
old = b"<a:srgbClr val=\"4472C4\"/>"
new = b"<a:srgbClr val=\"4472C4\" legacy=\"1\"/>"
if old not in payload:
raise RuntimeError("probe color node was not generated")
payload = payload.replace(old, new, 1)
patched = True
destination.writestr(member, payload)
if not patched:
raise RuntimeError("slide XML was not patched")
print(target)
'
```
Run tolerant import and verify both the recovery report and the visible SVG:
```bash
python3 "skills/ppt-master/scripts/pptx_to_svg.py" \
"/tmp/ppt-master-color-smoke.pptx" \
--inheritance-mode flat \
-o "/tmp/ppt-master-smoke-color-tolerant"
python3 -c '
import json
from pathlib import Path
output = Path("/tmp/ppt-master-smoke-color-tolerant")
report = json.loads((output / "conversion-report.json").read_text())
diagnostics = report["diagnostics"]
svg = (output / "svg" / "slide_01.svg").read_text()
assert report["summary"] == {"slides": 1, "warnings": 1}, report["summary"]
assert len(diagnostics) == 1, diagnostics
assert diagnostics[0]["code"] == "color-structure-normalized", diagnostics[0]
assert diagnostics[0]["fallback"] == "retain recognized color attributes and modifiers", diagnostics[0]
assert diagnostics[0]["slide_index"] == 1, diagnostics[0]
assert diagnostics[0]["shape_name"] == "Rectangle 1", diagnostics[0]
assert "#4472C4" in svg and "#ED7D31" in svg
print("OK: tolerant import recovered #4472C4 and preserved #ED7D31")
'
```
Expected: both commands exit `0`; the importer reports one
`color-structure-normalized` warning and the assertion prints
`OK: tolerant import recovered #4472C4 and preserved #ED7D31`.
Run the same probe in strict mode:
```bash
python3 "skills/ppt-master/scripts/pptx_to_svg.py" \
"/tmp/ppt-master-color-smoke.pptx" \
--inheritance-mode flat \
--strict \
-o "/tmp/ppt-master-smoke-color-strict"
```
Expected: exit `1`, no traceback, and one error line:
```text
Error: PPTX-to-SVG conversion failed: Invalid DrawingML sRGB color structure
```
## `source_to_md/web_to_md.py`
Convert web pages to Markdown and download images locally.
@@ -1,6 +1,9 @@
# Image Tools
> Architecture rationale (why provider-specific config keys instead of a generic `IMAGE_API_KEY`, why permissive license filter with strict-mode escape hatch, why external refs in dev but two divergent embedding strategies for delivery): see [docs/technical-design.md "Image Acquisition & Embedding"](../../../../docs/technical-design.md#image-acquisition--embedding).
> **Design boundary**: keep provider credentials explicit, keep in-pipeline
> acquisition manifest-driven, and treat external image references as authoring
> inputs while delivery writes self-contained SVG previews and native PPTX
> media.
Image tools cover formula rendering, prompt-based AI generation, web image search, image inspection, and Gemini watermark removal.
@@ -15,7 +15,7 @@ defaults to `none`, exactly as before this validation upgrade.
| Generated PPTX resolution and validation | `svg_to_pptx/pptx_package/builder.py` |
| Narration timing merge | `svg_to_pptx/pptx_package/narration.py` |
| Public authoring contract | `references/animations.md` |
| Customization workflow | `workflows/customize-animations.md` |
| Customization stage | `workflows/stages/customize-animations.md` |
**Hard rule**: only the generated SVG-to-PPTX route writes object entrance
animations. Direct-PPTX routes preserve source animations and run structural
@@ -12,7 +12,7 @@ read-back validation for every PPTX route.
| In-slide object animation | scripts/pptx_animations.py |
| Generated PPTX adapter | svg_to_pptx/pptx_package/builder.py |
| Template Fill adapter | template_fill_pptx/transitions.py |
| Native Enhance adapter | native_narration_pptx.py |
| Native Enhance adapter | native_enhance_pptx_core.py |
| Public workflow | references/animations.md |
**Hard rule**: adapters resolve route policy, then call the shared core. They
@@ -1,6 +1,8 @@
# Project Tools
> Architecture rationale (why `import-sources` defaults are asymmetric for in-repo vs out-of-repo files): see [docs/technical-design.md "Project Structure & Lifecycle"](../../../../docs/technical-design.md#project-structure--lifecycle).
> **Import boundary**: copy out-of-repository sources by default to protect user
> files; move in-repository sources by default to avoid leaving accidental
> commit artifacts. Explicit `--copy` / `--move` flags override the default.
Project tools create, validate, and inspect the standard PPT Master workspace.
@@ -11,8 +13,12 @@ Main entry point for project setup and validation.
```bash
python3 scripts/project_manager.py init <project_name> --format ppt169
python3 scripts/project_manager.py import-sources <project_path> <source1_or_dir> [<source2_or_dir> ...]
python3 scripts/project_manager.py scaffold-spec <project_path>
python3 scripts/project_manager.py scaffold-lock <project_path>
python3 scripts/project_manager.py validate <project_path>
python3 scripts/project_manager.py info <project_path>
python3 scripts/project_manager.py page-context <project_path> P07 [--pretty] [--record-usage]
python3 scripts/project_manager.py page-context-report <project_path>
```
Notes:
@@ -22,16 +28,87 @@ Notes:
pass the source file/directory once when generated Markdown lives beside the
original source. If Step 1 used `-o` to write Markdown elsewhere, pass both
the original source path/directory and the Markdown output path/directory.
- Under move semantics, a supplied source directory left strictly empty after
import (or empty from the start) is removed; a directory that still holds any
file or subdirectory is left untouched. `--copy` never removes directories.
- Files already inside the repo are moved into `sources/` by default (with a stderr
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.
- `scaffold-spec` creates `design_spec.md` from
`templates/scaffolds/design_spec.md`; `scaffold-lock` creates `spec_lock.md`
from `templates/scaffolds/spec_lock.md`. Both substitute project/canvas
metadata deterministically and refuse to overwrite an existing artifact.
- `validate` parses the existing Markdown artifacts against
`templates/schemas/design_spec.schema.json` and
`templates/schemas/spec_lock.schema.json`. It reports missing sections and
fields, illegal enums, malformed page keys, and unmet conditional sections;
it does not rewrite either artifact or compare their values for textual
equality. It also does not prove final-confirmation → Design Spec fidelity or
Design Spec → lock semantic projection; Generate Step 4 owns those two gates
before this structural validation. One slice is enforced mechanically: when
`confirm_ui/result.json` records a final confirmed stage, every confirmed
non-`none` `image_usage` source must appear in at least one `## images` row
of the lock (`provided` maps to `user`; `ai` is also satisfied by `slice`).
The design schema is structural lint for
the human-readable brief; the lock schema owns machine execution values. For
structured template use, strict input prototypes must match their assigned
Master/Layout; adaptive input prototypes retain the assigned Master while a
new output Layout is validated only after its generated SVG exists. Versioned
scaffolds carry the schema marker. Markerless legacy artifacts are left on
their prior validation path with a warning;
malformed or unsupported markers are errors.
- PPTX-family inputs are enriched automatically under `analysis/` with
per-deck `<stem>.identity.json` / `<stem>.slide_library.json` plus the shared
multi-deck index `source_profile.json` (`decks[]`).
Multi-deck per project: several PPTX imports each get their own `<stem>.*`
artifacts and a `decks[]` entry; re-importing the same stem replaces its entry.
### Per-page execution view
`page-context` projects `design_spec.md` and `spec_lock.md` into one compact
current-page view on stdout. The default command is read-only; `--pretty`
changes JSON formatting only. Before projection it revalidates the machine lock
and selected template-root identities; design-brief values are not treated as
a second lock. Slide headings at H3H6 remain readable by the projector.
The output deliberately repeats the bounded `global` lock projection on every
page as an anti-drift guard. `lock_source` binds that projection to the current
`spec_lock.md` SHA. `page_context` contains the current §IX brief, rhythm,
resources, and conditional template/chart assignment. `reference_set` contains
only `kind`, scoped path, SHA, and `once-per-execution-context` policy for the
project/template Design Specs and selected prototype/chart SVGs. A model reads
a referenced file only when that exact path + SHA is absent from its active
context or has changed, then reuses the retained understanding on later pages.
The deprecated `--bundle` flag remains accepted as a compatibility no-op. It
never appends a Design Spec, prototype SVG, chart SVG, manifest, or text-slot
sidecar to stdout.
The projection keeps project-specific forbidden rules; universal SVG and icon
rules remain in the always-loaded execution core. Image rows are selected from
the current §IX brief, explicit §VIII page assignments, and mirror prototype
references. When those sources assign images elsewhere but not to the current
page, the view excludes those assigned images. Any still-unassigned legacy
image remains in a compatibility subset; `confirmed-none` is emitted only when
all locked images have a deterministic assignment elsewhere.
Mirror materialization may publish deterministic
`ppt-master.template-text-slots.v2-min` diagnostics. They are not page-context
or model inputs. The complete SVG remains the sole template authority; checker
and structured export validate output attributes, text/tspan topology, and
referenced-resource hashes against it internally.
`--record-usage` writes a derived snapshot to
`analysis/page-context/P<NN>.usage.json`. It hashes every input, measures the
exact compact stdout, and records the reference fingerprints. `tiktoken` is
loaded lazily with `o200k_base`; when unavailable, the command still succeeds
and records bytes, characters, hashes, and `tokens: null`.
`page-context-report` summarizes only fresh snapshots and identifies stale or
token-unavailable pages plus unique referenced files. The telemetry does not
measure the once-loaded reference payloads, source-material reads, or other
session-level prompt references.
Common formats:
- `ppt169`
- `ppt43`
@@ -45,8 +122,12 @@ Examples:
```bash
python3 scripts/project_manager.py init my_presentation --format ppt169
python3 scripts/project_manager.py scaffold-spec projects/my_presentation_ppt169_20251116
python3 scripts/project_manager.py scaffold-lock projects/my_presentation_ppt169_20251116
python3 scripts/project_manager.py validate projects/my_presentation_ppt169_20251116
python3 scripts/project_manager.py info projects/my_presentation_ppt169_20251116
python3 scripts/project_manager.py page-context projects/my_presentation_ppt169_20251116 P07 --record-usage
python3 scripts/project_manager.py page-context-report projects/my_presentation_ppt169_20251116
```
## `project_utils.py`
@@ -106,15 +187,16 @@ Notes:
- Extracts reusable media assets from `ppt/media/`
- Summarizes slide size, theme colors, font metadata, and per-master theme metadata
- Resolves slide / layout / master relationships from OOXML relationships; every master and layout is included even when no sample slide currently references it
- Generates `manifest.json` (single source of truth for slide size, theme, per-master themes, assets, layouts, masters, placeholders, slides, SVG file paths, and page-type candidates), `summary.md` (short orientation digest), `assets/`, and shape-level SVGs under `svg/`
- **SVG output emits two views by default** (`--inheritance-mode both`):
- `svg/` — layered template view for designers: every master and layout in the deck rendered once as `svg/master_*.svg` / `svg/layout_*.svg` (including ones no sample slide currently references); `svg/slide_NN.svg` contains only that slide's own shapes; `svg/inheritance.json` records which layout / master each slide consumes.
- `svg-flat/`companion view: each `slide_NN.svg` is self-contained (master + layout + slide painted into one file), so opening any slide in isolation shows the full page like PowerPoint would. Useful for previews, screenshots, and "did this slide actually render correctly" sanity checks.
- Generates `manifest.json` (single source of truth for slide size, theme, per-master themes, assets, layouts, masters, placeholders, slides, SVG file paths, and page-type candidates), `native_structure.json`, `source_template.pptx`, `assets/`, `conversion-report.json`, and shape-level SVGs under `svg/`
- **SVG output defaults to the layered authoring source** (`--inheritance-mode layered`):
- `svg/` — layered template view for designers: every master and layout in the deck rendered once as `svg/master_*.svg` / `svg/layout_*.svg` (including ones no sample slide currently references); `svg/slide_NN.svg` contains only that slide's own shapes; `svg/inheritance.json` records parentage plus source-owned `showInheritedShapes` / `showMasterShapes` booleans.
- `svg-flat/`optional verification view emitted only by `--inheritance-mode both`: each `slide_NN.svg` is self-contained (the effective visible Master/Layout contributions plus Slide-local content painted into one file), so opening any slide in isolation shows the full page like PowerPoint would. Background inheritance remains independent of inherited-shape visibility. Useful for previews, screenshots, and "did this slide actually render correctly" sanity checks.
- `manifest.json` records `svgFile` for slides / layouts / masters, `flatSvgFile` for slides when `svg-flat/` exists, placeholder type / index / geometry / base style, an asset map used by SVG `href` values, and common assets reused through slide / layout / master inheritance. Placeholder semantics keep `subTitle`, `obj`, `media`, and `dt` distinct as `subtitle`, `object`, `media`, and `date`.
- `conversion-report.json` owns tolerant source-recovery diagnostics; it is not a cache or a duplicate of the structural manifests
- Layered slide SVGs keep only the slide's own background; inherited master / layout backgrounds stay in the corresponding master / layout SVGs
- Placeholder guides are intentionally lightweight in `svg/` master / layout files; `svg-flat/` hides those guides and is the visual preview source
- Charts, SmartArt, diagrams, and OLE objects become typed placeholders in `svg/`; `svg-flat/` shows a preview image with a corner badge when one exists, otherwise a visible placeholder. Tables are converted into real SVG content.
- Pass `--inheritance-mode layered` to skip `svg-flat/`, or `--inheritance-mode flat` for the legacy round-trip view (single self-contained `svg/` tree without master/layout/inheritance files).
- Pass `--inheritance-mode both` to add `svg-flat/`, or `--inheritance-mode flat` for the legacy round-trip view (single self-contained `svg/` tree without master/layout/inheritance files).
- SVG export reads OOXML directly via `pptx_to_svg` — no PowerPoint or Keynote dependency, runs on any platform
- `<image>` elements in `svg/` reference files in `assets/` directly; pass `--embed-images` to inline as data URIs instead
- External linked images and missing media are strict failures. Office vector media such as EMF / WMF are converted to PNG previews when the local toolchain can do so; otherwise the import fails instead of silently dropping content.
@@ -0,0 +1,39 @@
# Prompt Audit — Budget and Governance Lint
> Maintainer-only, read-only. Audits the agent-facing Markdown corpus without modifying it and is intentionally not wired into CI or pre-commit hooks. Generation roles never load this doc, the tool, or its manifest.
## Run
```bash
python3 skills/ppt-master/scripts/prompt_audit.py # text summary
python3 skills/ppt-master/scripts/prompt_audit.py --json # stable JSON report
```
Requires `tiktoken` (not part of `requirements.txt` — end users never need it):
```bash
pip install 'tiktoken>=0.7.0'
```
Exit code `1` on any deterministic error; advisory duplicate/schema candidates stay warnings. With `--json`, setup failures also use a stable `AUDIT_SETUP_ERROR` JSON envelope instead of a traceback or plain-text error.
## What It Checks
| Area | Failure class |
|---|---|
| Corpus and hot-file token ceilings | error on budget overflow |
| Declared load sets (route/stage scenarios) | error on budget overflow, unknown files, selector/registry drift |
| Load coverage | error when a corpus file is in no load set and has no `coverage.exempt` entry |
| Registry claims (layout patterns, modes, styles, renderings, types, charts) | error on ID/count/index drift |
| Markdown references and declared authority edges | error on broken links or unreferenced edges |
| Cross-file exact/near duplicates | warning; intentional cases are adjudicated via `duplicates.accepted` |
| Schema multi-definition | warning when an owner field also has grammar-like text in any non-owner file |
## Manifest Maintenance — `prompt_audit_manifest.json`
The manifest is audit-only (`audit_only: true`, `runtime_consumed: false`); it is a lint fixture, never prompt context. It hand-transcribes the load rules stated in `SKILL.md` and the role/workflow docs, so **every change to read instructions in those docs must update the matching load set in the same change** — the coverage check catches unclassified files, but only humans can catch a changed read rule for an existing file.
- **New corpus file** → when no existing category exemption matches it, the audit fails with `LOAD_COVERAGE_GAP` until you add it to the load sets that read it or exempt it with a one-line reason. Exempt only material that never enters role context (for example, a legacy tombstone, generated maintenance asset, maintainer-only doc, or license notice); represent conditional runtime reads as incremental load sets.
- **Intentional duplicate** → run `--json`, copy the finding's `kind`, `fingerprint`, and `paths` into `duplicates.accepted` with a reason. The acceptance identity is all three values, so separate path pairs with identical prose remain independently reviewable. Editing either reported raw block changes its fingerprint; stale acceptance fails with `DUPLICATE_ACCEPTED_STALE`. `--skip-near-duplicates` deliberately leaves accepted near pairs unchecked because that scan did not run.
- **Schema owner** → every configured field must have a definition signal in its declared owner. One grammar-like non-owner is enough to surface a candidate; split fields into separate owner entries when they belong to different artifacts.
- **Raising a budget** (`budget_policy: current_growth_ceiling` — ceilings ratchet against the current state): legitimate only when a deliberate content addition or a corrected load-set membership moves a file or scenario past its ceiling. Bump to the new actual value plus minimal slack in the same change that causes the increase, and say why in that change's message. Never pre-raise ceilings to make room.
@@ -1,6 +1,8 @@
# SVG Pipeline Tools
> Architecture rationale (why each artifact / step exists, deletion impact, two-consumer relationship between `svg_finalize/` and native pptx conversion): see [docs/technical-design.md "Post-Processing Pipeline"](../../../../docs/technical-design.md#post-processing-pipeline).
> **Maintenance boundary**: post-processing modules serve both the on-disk
> `svg_final/` preview and in-memory native PPTX conversion. Check both
> consumers before changing or removing a step.
These tools cover post-processing, SVG validation, speaker notes, recorded narration, and PPTX export.
@@ -8,16 +10,19 @@ The supported delivery contract has one PPTX path: `svg_output/` → the project
## `svg_authoring_view.py`
Create a lightweight inspection/authoring projection of one PPTX-imported SVG
or a directory of imported SVGs:
Create a lightweight editable authoring IR bundle from one PPTX-imported SVG or
a directory of imported SVGs:
```bash
python3 scripts/svg_authoring_view.py <svg-file-or-directory> -o <output-dir>
python3 scripts/svg_authoring_view.py <svg-file-or-directory> -o <output-dir> \
--projection-kind layered
```
The operation is non-destructive and refuses existing output files unless
`--force` is explicit. It never writes back to the source SVG. The JSON report
on stdout records original/projected byte counts and removals by category.
on stdout records original/projected byte counts and removals by category. The
output directory contains the editable SVGs, one model-readable
`authoring_summary.json`, and one tool-only `authoring_manifest.json`.
The projected copy:
@@ -27,26 +32,237 @@ The projected copy:
- removes source-object identity/style/hash attributes that are only useful to
an exact import round trip;
- keeps visible paths, text, images, stable ids, Master/Layout root markers,
and supported compact `data-pptx-object` / `data-pptx-prst` /
`data-pptx-frame` intent; and
- rewrites relative local asset references for the projection's new location.
selected native-shape intent, and a document-local `data-pptx-source-ref` on
each imported logical object;
- rewrites relative local asset references for the projection's new location;
- compacts imported model-facing frames and safe transform page coordinates to
at most two decimals.
The complete imported SVG remains the evidence source for mirror restoration.
The exporter does not read the import workspace or the projection. The
projection is deliberately not a template generator, not a replacement for
the explicit Master/Layout restoration workflow, and not a supported release
input to `svg_to_pptx.py`.
The summary stores the current SVG roster plus compact per-file canvas, size,
text, image, vector, placeholder, icon, and source-ref counts. Models read the
summary and editable SVGs; they do not read the machine manifest. The manifest
stores relative source/authoring filenames, source and initial authoring hashes,
and source element paths. It deliberately does not copy the opaque payload.
The authoring bundle is the editable source for template creation; the complete
imported SVG remains immutable native-payload backing. Final
`templates/*.svg` files are materialized and validated from that pair. The IR
directory itself is not a supported direct input to `svg_to_pptx.py`.
Regenerate the summary after direct edits that do not pass through one of the
in-place normalization tools:
```bash
python3 scripts/svg_authoring_view.py <authoring-dir> --refresh-summary
```
This projection is separate from canonical preset authoring. New project SVGs
and project-owned templates use the compact authored form: one atomic
`<g data-pptx-authoring="preset">` owns the preset intent and base paint, with
the registry-generated visible `<path>` layers as direct children. Quality
check and export rerender the locked registry to validate that group, so the
compact form has no hidden carrier, preview wrapper, or serialized preview
fingerprint. `pptx_to_svg.py` continues to emit the expanded carrier/preview
evidence required for import and round-trip decisions. The normative boundary
is owned by [`shared-standards-core.md`](../../references/shared-standards-core.md) §1.5, with
authoring guidance in
[`native-shape-authoring.md`](../../references/native-shape-authoring.md).
## `compact_svg_coordinates.py`
Compact safe model-facing page-space coordinates without rewriting unrelated
SVG formatting:
```bash
python3 scripts/compact_svg_coordinates.py <svg-file-or-directory>
python3 scripts/compact_svg_coordinates.py <template-directory> \
--inplace --keep-native-frames
```
The default run is a dry-run JSON report. `--inplace` atomically replaces only
changed SVG files. The shared create-template final pass uses
`--keep-native-frames`: it compacts `data-pptx-bounds`, translation values,
rotation centers, and
matrix `e/f`, while preserving canonical
authored-preset or inline native frames. `svg_authoring_view.py` separately
compacts imported model-facing frames because unchanged mirror refs can recover
their exact coordinates from immutable lossless backing.
The compactor never rounds path/points geometry, normalized crop or nested
`viewBox` ratios, gradient offsets, opacity, scale arguments, rotation angles,
or matrix `a/b/c/d` coefficients. Type A mirror materialization invokes the
same compactor before native-record externalization; `standard` and `fidelity`
use the shared final pass before template validation.
## `extract_svg_assets.py`
Factor large vector subtrees out of lightweight authoring IR documents and
replace them with compact `<use data-icon>` references:
```bash
python3 scripts/extract_svg_assets.py <layered_svg_dir> \
--icons-dir <icons_dir> --icon-namespace imported \
--inplace --id-prefix layered
python3 scripts/extract_svg_assets.py <flat_svg_dir> \
--icons-dir <icons_dir> --icon-namespace imported \
--reuse-inventory <layered_inventory.json> \
--inplace --id-prefix flat
```
The first pass records a source fingerprint before namespacing each extracted
asset's internal ids. The second pass reuses a fingerprint-matched asset and
writes no duplicate SVG file. Unmatched flat-only subtrees still extract
normally. Use `--clean-stale` on both import-workspace passes to remove stale
generated files for their respective prefixes. In create-template workspaces,
`imported` is the fixed namespace: assets live once under `icons/imported/`, and
the working SVGs reference them as `data-icon="imported/<name>"`. Inventory
entries retain source refs from each extracted subtree, allowing expansion to
reconnect the authoring-manifest mapping. A rerun on an
already rewritten namespaced projection inventories those references and does
not progressively extract their remaining parent or sibling geometry. An
in-place pass over an authoring bundle refreshes `authoring_summary.json`
automatically.
## `mirror_template_materialize.py`
Compile one Type A PPTX import workspace into a deterministic structured mirror
template after the layered authoring IR has been reviewed and edited:
```bash
python3 scripts/mirror_template_materialize.py \
<import_workspace> <empty_template_workspace>
```
The command treats `<import_workspace>/authoring-svg/` as the sole editable
source. It reads the tool-only layered authoring manifest internally and
validates it against immutable lossless SVG
hashes, source PPTX hash, complete Master/Layout/Slide graph, inheritance
visibility facts, source-ref closure, and extracted-vector inventory before it
writes anything. It refuses a non-empty destination and stages the whole result
before atomic publication, so a failed preflight cannot leave a partial
template.
Materialization preserves source page order and emits one definition-only
`layout_<layout_key>.svg` for every source Layout unused by all source Slides.
It mechanically expands fixed Master/Layout group wrappers into direct atoms,
rehydrates only unchanged converter-supported Slide-local/slot refs, keeps the
current SVG fallback for edited refs, preserves explicit text hard breaks, and
removes every IR-only source ref. Imported axis-flipped groups retain their
geometry reflection while descendant SVG text receives a matching
counter-reflection, preserving PowerPoint's upright glyph appearance in browser
previews. Supported opaque `p:txBody`,
relationship-free `p:style`, and `a:custGeom` payloads are deduplicated into
`templates/native_payloads.json.gz`. Repeated native restoration attributes
are stored there as short `data-pptx-native-ref` records; page and
imported-vector SVGs retain only those record ids and content-hash payload
references. The native record referenced by an imported text placeholder
carrier owns its authoritative source frame, so the Slide-local frame can
differ from reusable Layout bounds without restoring long exact coordinates
inline. Structural Master/Layout, placeholder, layer, and editable-object
fields remain inline. Source `p:sldLayout@showMasterSp` and
`p:sld@showMasterSp` facts become canonical root
`data-pptx-show-master-shapes` and
`data-pptx-show-inherited-shapes` booleans.
Checker, template-structure validation, and export hydrate both store layers in
memory; legacy inline payload and v1 payload-only stores remain readable.
The published `ppt-master.template-execution-manifest.v1` roster points to one
compact `ppt-master.template-text-slots.v2-min` sidecar per prototype. Each text
slot contains only `selector`, `role`, `current_text`, `text_segments`, and
`tspan_count`; a top-level tool hash covers its selectors and immutable
text/tspan topology and attributes. These records are deterministic tool
diagnostics, not page-authoring inputs. Page-context emits only the complete
prototype's path and SHA for that reference, so the model reads the SVG once
per execution context and reuses it until the SHA changes. The model chooses
semantics and edits only existing visible text values, while checker and
structured export validate output attributes, text/tspan topology, and
referenced-resource hashes against
the prototype.
The output routes reusable vectors once to `icons/imported/`, bitmaps to
`images/`, and other referenced files to `templates/assets/`. The JSON report
reports payload occurrence, native-record, unique-byte, and compressed-store
counts and is written to stdout only. The command intentionally does not create
`templates/design_spec.md`; Template_Designer writes the package-specific rules
and page roster after materialization. This compiler is for Type A mirror materialization,
not `standard` / `fidelity`, loose Type B SVGs, ordinary generation, finalize,
or export.
## `extract_svg_pictures.py`
Normalize one deliberately selected complex SVG object into one PowerPoint
picture. The command accepts exact `<g id>` values only, writes each group as a
tight standalone SVG asset, embeds its local image/CSS dependencies, and
replaces the source group at the same parent index with one `<image>`. Native
export therefore emits one `p:pic` backed by SVG media.
```bash
python3 scripts/extract_svg_pictures.py \
"<workspace>/authoring-svg/<layered_svg_file>.svg" \
--select "<group_id>" \
--resource-root "<workspace>" \
--images-dir "<workspace>/picture-assets" \
--inplace
```
Imported PowerPoint groups normally provide `data-pptx-frame`, which is used
as the picture bounds. For a large standalone SVG without frame metadata, the
tool measures the selected group with Playwright; use repeated
`--bounds ID=x,y,width,height` values when browser measurement is unavailable
or when effect overflow needs an explicit frame. `--padding` expands the
chosen bounds. The generated `*_picture_asset_inventory.json` records the
bounds source, asset hash, copied definition ids, and embedded local resources.
Nested selections are accepted only through metadata-only `<g>` ancestors.
When an ancestor carries a transform, style, clip, opacity, or other visual
attribute, select that outer group instead; this prevents applying the ancestor
effect once inside the SVG asset and again to the replacement `<image>`.
Scripts, `foreignObject`, SVG animation, remote resources, and external SVG
fragment references fail closed; local image/CSS resources must stay inside
the declared `--resource-root` and are embedded into the asset.
An in-place rewrite inside an authoring bundle refreshes
`authoring_summary.json` automatically.
This operation belongs only to an explicit `create-template` normalization
decision in `standard` or `fidelity` mode. It does not choose groups, detect
repetition, infer a Master/Layout, or run during ordinary import, free
generation, mirror materialization, finalize, or export. Placeholder, native
single-shape, table/chart, icon-placeholder, and authored-preset groups are
rejected because they already own a different semantic route.
Do not confuse this tool with `extract_svg_assets.py`:
- `extract_svg_assets.py` is a model-readability optimization. It replaces
heuristic vector runs with `<use data-icon>`, then re-inlines them before
export so the PPTX still contains native shapes.
- `extract_svg_pictures.py` is an explicit representation change. It replaces
only named groups with `<image>`, so each result intentionally remains one
editable PowerPoint picture rather than individually editable paths.
## Recommended Pipeline
Run these steps in order:
Run these steps one at a time. Wait for each command to exit successfully before
starting the next command.
```bash
python3 scripts/total_md_split.py <project_path>
```
After `total_md_split.py` exits successfully, run:
```bash
python3 scripts/finalize_svg.py <project_path>
```
After `finalize_svg.py` exits successfully, run:
```bash
python3 scripts/svg_to_pptx.py <project_path>
```
Do not start another post-processing command while the current command is still
running. The canonical gates and success criteria are owned by
[`generate-pptx.md`](../../workflows/generate-pptx.md) Step 7.
## `finalize_svg.py`
Unified post-processing entry point. This is the preferred way to run SVG cleanup.
@@ -65,7 +281,7 @@ Convert project SVGs into PPTX.
```bash
python3 scripts/svg_to_pptx.py <project_path>
python3 scripts/svg_to_pptx.py <project_path> --native-objects
python3 scripts/svg_to_pptx.py <project_path> --native-charts-and-tables
python3 scripts/svg_to_pptx.py <project_path> --pptx-structure structured # deck/layout template override
python3 scripts/svg_to_pptx.py <project_path> --pptx-structure flat # free-design/brand-only override
# Template-import visual round-trip diagnostic only:
@@ -77,16 +293,29 @@ python3 scripts/svg_to_pptx.py <project_path> -t none
python3 scripts/svg_to_pptx.py <project_path> --auto-advance 3
python3 scripts/svg_to_pptx.py <project_path> --animation mixed --animation-duration 0.8
python3 scripts/svg_to_pptx.py <project_path> --no-merge # strict line-fidelity mode (see below)
python3 scripts/notes_to_audio.py <project_path> --voice zh-CN-XiaoxiaoNeural
python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio
```
For generated-project narration, follow the
[`generate-audio`](../../workflows/stages/generate-audio.md) stage. It owns voice
selection, audio generation, and the narrated re-export workflow.
Behavior:
- Default output (default-flow mode, no `-o`):
- `exports/<project_name>_<timestamp>.pptx` — native editable pptx (canonical output)
- `validation/<project_name>_<timestamp>.report.json` — package postflight, quality-gate linkage, unresolved resource audit, and published part counts
- `backup/<timestamp>/svg_output/` — copy of Executor SVG source, always written so the pptx can be rebuilt via `finalize_svg → svg_to_pptx` without re-running the LLM
- `exports/` contains only final PPTX deliverables; machine-readable quality and postflight reports belong in `validation/`.
- `finalize_svg.py` always creates `svg_final/` before export. This directory is the self-contained SVG visual preview; it is not packaged as a second PPTX.
- Explicit `-o/--output` changes the native PPTX destination and skips `backup/`.
- Explicit `-o/--output` changes the native PPTX destination and skips `backup/`; its postflight report still uses the output stem under the project `validation/` directory.
- Postflight reruns ZIP integrity and published Slide count. Internal relationships,
structured-package validation, transitions, and animations are enforced before the
builder publishes the PPTX and are reported as `enforced-at-build`, not as repeated
postflight checks.
- `font_portability` warns only when a complete font stack contains generic CSS families
and no concrete family name. A recommended stack such as
`"Microsoft YaHei", Arial, sans-serif` does not warn merely because it ends with a
generic fallback.
- Paragraph merging is enabled by default and trades some SVG line-layout fidelity for PowerPoint editability:
- Default: mergeable paragraph blocks (same x, dy clustered around one base line-height) collapse into one editable text frame. Equal effective font sizes may join as flowing prose; a font-size change, list marker, or accepted larger gap starts a new `<a:p>` with precise `<a:lnSpc>` / `<a:spcBef>`. Resizing the box reflows text inside it without erasing those paragraph boundaries.
- With `--no-merge`: every dy-stacked `<tspan>` becomes its own text frame — exact SVG line layout is preserved but a 12-line paragraph is 12 separate textboxes
@@ -101,15 +330,19 @@ Behavior:
inputs and package-level processing.
- For PPTX template-import workspaces, use `-s svg-flat` when you need a visual round-trip check. The layered `svg/` tree is the machine-readable template source and intentionally does not inline inherited master / layout decoration into each slide.
- Native mode is strict about unsupported visual SVG elements: if a visual element cannot be represented or safely preserved, export fails with the SVG file, element tag, and position instead of silently dropping content.
- Omitting `--pptx-structure` reads `spec_lock.md`. Free-design and brand-only releases declare `mode: flat`, omit Master/Layout mappings and SVG structure metadata, and materialize one clean project-owned Master plus one Blank Layout from the current lock. Deck/layout template releases declare `mode: structured` with complete unique `pptx_masters` / `pptx_layouts` rosters and one `page_pptx_layouts` assignment per page. A template-backed Layout definition may remain unused by pages and still register in the final package.
- On structured template routes, every page root repeats Master/Layout keys and picker names. Master/Layout fixed visuals are direct atomic children; layer `<g>` elements are invalid.
- Omitting `--pptx-structure` reads `spec_lock.md`. Free-design, brand-only, and `template_reuse_scope: style` releases declare `mode: flat`, omit Master/Layout mappings and SVG structure metadata, and materialize one clean project-owned Master plus one Blank Layout from the current lock. Deck/layout templates use `mode: structured` only for `template_reuse_scope: mirror|layout`, with complete unique `pptx_masters` / `pptx_layouts` rosters and one `page_pptx_layouts` assignment per page. A template-backed Layout definition may remain unused by pages and still register in the final package.
- On structured template routes, every page root repeats Master/Layout keys and picker names. Master/Layout fixed visuals are direct semantic atoms. Ordinary layer `<g>` elements are invalid; one validated compact authored-preset `<g>` emitted by `preset_shape_svg.py` is the sole group exception because it compiles to one native shape.
- Every visible direct root `<g>` requires root-coordinate `data-pptx-bounds`; nested bounds are ignored. Frame/native metadata never replaces it; placeholder bounds also define the slot frame. Checker compares root bounds with `viewBox` and only descendant text with that module. Images, shapes, paths, `<use>`, effects, and object frames are excluded. Per side: ≤`1px` ignored, ≤`5%` warns, >`5%` fails. Bounds never clip/reflow.
- Missing root bounds fails on final pages/templates and under `--template-mode`; references warn until adapted.
- On structured template routes, each normal slot is a direct root `<g id>` with semantic type, positive design-zone bounds, and exactly one compatible carrier. Composite `object` slots use explicit proxy binding; zero-slot Layouts are valid. Flat pages keep all SVG objects Slide-local.
- Flat export maps locked typography/colors into a clean project-owned theme/Master, removes stock content placeholders and unused built-in Layouts, retains only the standard date/footer/slide-number capability hooks, and keeps one Blank Layout without promoting Slide content. Structured export additionally creates one reusable Layout per declared key and reopens the package to verify the full Presentation → Master → Layout → Slide graph, fixed-object order, placeholder identities/bounds, carrier bindings, hidden proxies, and zero-slot Layouts.
- Template `page_layouts` remains input provenance. Strict preserves the prototype contract; adaptive retains its Master and may use a new Layout identity only when fixed Layout atoms or slot topology/bounds change.
- Legacy structured/template contracts using `baseline`, `template`, `preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled`/`utility`, direct atomic placeholders, or incomplete Master identity are rejected with a pointer to [`restore-pptx-structure`](../../workflows/restore-pptx-structure.md). Explicit flat free-design/brand-only projects intentionally omit Master identity.
- Legacy structured/template contracts using `baseline`, `template`, `preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled`/`utility`, direct atomic placeholders, or incomplete Master identity are rejected with a pointer to [`create-template`](../../workflows/create-template.md). Create a new workspace and generate new structured SVG pages; do not upgrade the existing project in place. Explicit flat free-design/brand-only projects intentionally omit Master identity.
- Native output uses content-hash media filenames, so identical images are reused and different images cannot overwrite each other by sharing a basename.
- `[Content_Types].xml` is generated from the actual media extensions written into the PPTX. Unknown media extensions fail unless Python's `mimetypes` can identify them.
- Native export writes to a temporary file first and publishes the requested PPTX only after conversion succeeds. A failed conversion does not replace the main output file.
- After publication, native export writes `validation/<output_stem>.report.json`. The report distinguishes authored Slides from internal Layout definitions, reruns ZIP integrity and published Slide-count checks, records slide/layout/master/notes part counts, labels relationship/structured/transition/animation validation as enforced at build time, links the final SVG quality report only when its SHA-256 source fingerprint matches the exact export inputs, and surfaces stale/unverified gates, unresolved template tokens, generic-only font stacks, and external image references. A matching final quality report with introduced warnings yields `passed-with-warnings` and a `quality_introduced_warnings=<N>` receipt instead of a clean `passed` claim.
- By default, a successful command also prints a compact receipt instead of requiring a report read: `[POSTFLIGHT] status=<...> quality_gate=<...> slides=<N> warning_categories=<N>`, followed by one compact line per warning category and the `[PPTX]` / `[REPORT]` paths. Resource-warning lines carry counts; a non-passing quality gate carries its status. Routine agents use this receipt and do not load either complete validation JSON into model context. Full reports remain cold audit artifacts; failure investigation and explicit audits extract only the required fields. `--quiet` keeps suppressing successful-run output.
- Before publishing structured template output, export reopens the temporary PPTX and validates the Slide → Layout → Master graph and registrations, Layout identity, placeholder identity, reusable bounds, and prompt/level-one sizes. A mismatch aborts publication. Flat release instead validates its single referenced Master/Layout shell and exact date/footer/slide-number hook roster before packaging.
- SVG clip paths are still restricted for authored SVGs, but nested crop wrappers generated by PPTX import are mapped back to native picture crop / geometry when possible.
- Speaker notes are embedded automatically unless `--no-notes` is used
@@ -173,6 +406,8 @@ Validate SVG technical compliance.
python3 scripts/svg_quality_checker.py examples/project/svg_output/01_cover.svg
python3 scripts/svg_quality_checker.py examples/project/svg_output
python3 scripts/svg_quality_checker.py examples/project
python3 scripts/svg_quality_checker.py examples/project --stage first-page
python3 scripts/svg_quality_checker.py examples/project --stage final --json
python3 scripts/svg_quality_checker.py examples/project --format ppt169
python3 scripts/svg_quality_checker.py --all examples
python3 scripts/svg_quality_checker.py examples/project --export
@@ -182,13 +417,32 @@ python3 scripts/svg_quality_checker.py path/to/template/templates --template-mod
Checks include:
- `viewBox`
- banned elements
- paint compatibility: unsupported values error; supported non-default spellings such as `rgba()` receive non-blocking recommendations for `#RRGGBB` plus explicit alpha
- line-break structure
- explicit Master/Layout/slot structure for reusable templates
- duplicate empty Layout contracts under different keys
Template mode accepts compact canonical preset shapes marked with
`data-pptx-authoring="preset"`. It validates the explicit structured SVG
contract; it does not implement a separate source-payload opt-in marker.
Warnings are advisory: they require no modification or acknowledgement and do
not affect the command's zero exit status. Only errors block the quality gate.
`--stage first-page` resolves only the first authored SVG and permits an incomplete
future page roster. `--stage final` checks the complete project. With `--json`,
the final stage writes `validation/svg_quality_report.json`, while the first-page
stage writes `validation/svg_quality_first_page_report.json` so it cannot overwrite
the release gate (or use `--json-output`). The report separates
release failures (`blocking`), changed/new advisories (`introduced`),
prototype-identical diagnostics (`inherited`), and source-conversion losses
(`source-import`). It also fingerprints every checked SVG so postflight cannot
mistake a stale report for the current export gate. On a successful run, use the
checker exit status and terminal summary; do not load the complete JSON unless a
failure investigation or explicit audit requires targeted fields.
Template mode accepts the same compact canonical preset groups as generated
pages: one atomic `<g data-pptx-authoring="preset">` with direct visible paths.
It validates those paths dynamically against the locked registry and does not
require an import-style carrier, preview wrapper, fingerprint, or a separate
source-payload opt-in marker. Exact syntax remains owned by the linked
standards rather than this pipeline overview.
## `svg_position_calculator.py`
@@ -255,9 +509,11 @@ Replaces `<use data-icon="chunk-filled/name" .../>`, `<use data-icon="tabler-fil
## SVG Compatibility Contract
The canonical SVG authoring and native-mapping contract lives exclusively in
[`shared-standards.md`](../../references/shared-standards.md). This tool guide
does not repeat accepted syntax, rejected constructs, or conditional limits.
The always-on SVG authoring contract lives in
[`shared-standards-core.md`](../../references/shared-standards-core.md), with
advanced effects, native data objects, and structured PPTX metadata owned by
their conditionally loaded modules. This tool guide does not repeat accepted
syntax, rejected constructs, or conditional limits.
`svg_quality_checker.py` validates source SVG before finalization.
`finalize_svg.py` and native export apply the preprocessing required by that
@@ -1,6 +1,8 @@
# update_spec.py
> Architecture rationale (why narrow scope, why no backups, the spec_lock as execution contract): see [docs/technical-design.md "Spec Propagation"](../../../../docs/technical-design.md#spec-propagation-spec_lockmd-as-execution-contract).
> **Scope boundary**: this tool updates only deterministic global color and font
> substitutions, writes the authoritative `spec_lock.md` first, and relies on
> version control for rollback instead of creating parallel backups.
Propagate a `spec_lock.md` value change to both the lock file and every `svg_output/*.svg`. The single edit surface for bulk style tweaks after generation.
@@ -22,8 +22,8 @@ class ErrorHelper:
'message': 'Missing README.md file',
'solutions': [
'Create a README.md file with project description, usage instructions, etc.',
'Reference template: examples/google_annual_report_ppt169_20251116/README.md',
'Or use command: cp examples/google_annual_report_ppt169_20251116/README.md <your_project>/'
'Document the project goal, source material, canvas, generated artifacts, and export path',
'Keep project-specific instructions local instead of copying a repository example'
],
'severity': 'error'
},
@@ -112,7 +112,7 @@ class ErrorHelper:
'Remove <foreignObject> elements',
'Use <text> + <tspan> for manual line wrapping',
'This is a project technical specification requirement',
'Reference: references/shared-standards.md'
'Reference: references/shared-standards-core.md'
],
'severity': 'error'
},
@@ -121,7 +121,7 @@ class ErrorHelper:
'solutions': [
'Remove clip-path from shapes / groups / text',
'Draw the target geometry directly with the matching native element: <circle> / <ellipse> / <rect rx="..."> / <polygon> / <path>. A rect clipped to a circle is just a <circle>.',
'clip-path on <image> is conditionally allowed — see references/shared-standards.md §1.2'
'clip-path on <image> is conditionally allowed — see references/shared-standards-core.md §1.2'
],
'severity': 'error'
},
@@ -130,7 +130,7 @@ class ErrorHelper:
'solutions': [
'Define the referenced <clipPath id="..."> inside <defs>',
'The clipPath must contain exactly one shape child (circle / ellipse / rect with rx,ry / path / polygon)',
'Reference: references/shared-standards.md §1.2'
'Reference: references/shared-standards-core.md §1.2'
],
'severity': 'error'
},
@@ -181,14 +181,14 @@ class ErrorHelper:
'severity': 'error'
},
# Note: <marker> and marker-end are NO LONGER forbidden — they are
# conditionally allowed (see references/shared-standards.md §1.1).
# conditionally allowed (see references/shared-standards-core.md §1.1).
# The converter maps qualifying markers to native DrawingML arrow heads.
'marker_orphan_ref': {
'message': 'marker-start/marker-end references a marker id, but no <marker> element is defined',
'solutions': [
'Define the <marker> inside <defs>',
'Or remove the marker-start/marker-end attribute',
'See shared-standards.md §1.1 for marker constraints',
'See shared-standards-core.md §1.1 for marker constraints',
],
'severity': 'error'
},
@@ -3,7 +3,7 @@
PPT Master - Large Vector Asset Extractor
Factor large inline vector groups (complex illustrations) out of working SVGs
into project icon assets, leaving a one-line `<use data-icon="id"/>`
into project icon assets, leaving a one-line `<use data-icon="namespace/id"/>`
placeholder behind so the working SVG stays readable (structure, not a wall of
`<path>`). Visually lossless and reversible: the existing icon embedding path
re-inlines each asset before export, so the exported PPTX remains native shapes,
@@ -14,10 +14,16 @@ threshold is a readability convenience — it changes which blobs are factored
out, not whether the export stays editable.
Usage:
python3 scripts/extract_svg_assets.py <svg_dir> [--icons-dir <icons_dir>] [--min-drawables N] [--min-bytes N] [--min-decoration-bytes N] [--inplace] [--clean-stale]
python3 scripts/extract_svg_assets.py <svg_dir> [options]
Examples:
python3 scripts/extract_svg_assets.py import_ws/svg --icons-dir import_ws/icons --inplace --id-prefix layered --clean-stale
python3 scripts/extract_svg_assets.py import_ws/authoring-svg \
--icons-dir import_ws/icons --icon-namespace imported \
--inplace --id-prefix layered --clean-stale
python3 scripts/extract_svg_assets.py import_ws/authoring-svg-flat \
--icons-dir import_ws/icons --icon-namespace imported \
--reuse-inventory import_ws/authoring-svg_vector_asset_inventory.json \
--inplace --id-prefix flat --clean-stale
python3 scripts/extract_svg_assets.py project/svg_output --inplace --min-drawables 40
Dependencies:
@@ -30,6 +36,7 @@ from __future__ import annotations
import argparse
import copy
import hashlib
import json
import re
import sys
@@ -38,6 +45,10 @@ from typing import Optional
from xml.etree import ElementTree as ET
from console_encoding import configure_utf8_stdio
from svg_authoring_view import (
AUTHORING_MANIFEST_NAME,
write_authoring_summary,
)
configure_utf8_stdio()
@@ -47,6 +58,8 @@ SEMANTIC_CONTENT = {"text", "tspan", "foreignObject"}
DEFAULT_MIN_DRAWABLES = 20
DEFAULT_MIN_BYTES = 3000
DEFAULT_MIN_DECORATION_BYTES = 3000
SOURCE_REF_ATTRIBUTE = "data-pptx-source-ref"
ICON_NAMESPACE_RE = re.compile(r"^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$")
URL_REF_RE = re.compile(r"url\(\s*(['\"]?)#([^)'\"]\S*?)\1\s*\)")
@@ -59,7 +72,12 @@ def _drawable_count(elem: ET.Element) -> int:
def _xml_size(elem: ET.Element) -> int:
if not any(item.get(SOURCE_REF_ATTRIBUTE) for item in elem.iter()):
return len(ET.tostring(elem, encoding="utf-8"))
measured = copy.deepcopy(elem)
for item in measured.iter():
item.attrib.pop(SOURCE_REF_ATTRIBUTE, None)
return len(ET.tostring(measured, encoding="utf-8"))
def _large_enough(elem: ET.Element, min_drawables: int, min_bytes: int) -> bool:
@@ -75,9 +93,13 @@ def _is_existing_placeholder(elem: ET.Element) -> bool:
return _local(elem.tag) == "use" and elem.get("data-icon") is not None
def _has_icon_placeholder(elem: ET.Element) -> bool:
return any(_is_existing_placeholder(item) for item in elem.iter())
def _is_extractable_subtree(elem: ET.Element) -> bool:
"""Pure vector subtrees can be moved; semantic content must stay inline."""
if _is_existing_placeholder(elem) or _is_chart_group(elem) or _has_semantic_content(elem):
if _has_icon_placeholder(elem) or _is_chart_group(elem) or _has_semantic_content(elem):
return False
return _drawable_count(elem) > 0
@@ -99,6 +121,14 @@ def _tag_histogram(elem: ET.Element) -> dict[str, int]:
return hist
def _source_references(elem: ET.Element) -> list[str]:
return sorted({
source_ref
for item in elem.iter()
if (source_ref := item.get(SOURCE_REF_ATTRIBUTE))
})
def _is_descendant(container: ET.Element, candidate: ET.Element) -> bool:
return any(elem is candidate for elem in container.iter())
@@ -128,7 +158,7 @@ def _dependency_elements(root: ET.Element, asset_group: ET.Element) -> list[ET.E
by_id = _id_index(root)
dependencies: list[ET.Element] = []
seen: set[str] = set()
queue = list(_referenced_ids(asset_group))
queue = sorted(_referenced_ids(asset_group))
while queue:
ref_id = queue.pop(0)
@@ -189,7 +219,7 @@ def _find_extractable(root: ET.Element, min_drawables: int, min_bytes: int) -> l
if (
not _is_chart_group(child)
and not _has_semantic_content(child)
and child.get("data-icon") is None
and not _has_icon_placeholder(child)
and _large_enough(child, min_drawables, min_bytes)
):
found.append(child) # outermost qualifying — do not descend
@@ -262,6 +292,24 @@ def _asset_svg(
return ET.tostring(svg, encoding="utf-8", xml_declaration=True)
def _source_sha256(
group: ET.Element,
dependencies: list[ET.Element],
view_box: str | None,
width: str | None,
height: str | None,
) -> str:
"""Fingerprint an extracted subtree before asset-id namespacing."""
payload = _asset_svg(
copy.deepcopy(group),
[copy.deepcopy(dependency) for dependency in dependencies],
view_box,
width,
height,
)
return hashlib.sha256(payload).hexdigest()
def _asset_group(nodes: list[ET.Element]) -> ET.Element:
group = ET.Element(f"{{{SVG_NS}}}g")
for node in nodes:
@@ -274,6 +322,40 @@ def _asset_id(svg_path: Path, index: int, id_prefix: str) -> str:
return f"{prefix}{svg_path.stem}_ill{index:02d}"
def _icon_reference(icon_namespace: str, asset_id: str) -> str:
return f"{icon_namespace}/{asset_id}" if icon_namespace else asset_id
def _asset_relative_path(icon_namespace: str, asset_id: str) -> str:
return f"{_icon_reference(icon_namespace, asset_id)}.svg"
def _icon_asset_for_namespace(icon_name: str, icon_namespace: str) -> str | None:
"""Map one local placeholder to its asset path, excluding other libraries."""
if icon_namespace:
prefix = f"{icon_namespace}/"
if not icon_name.startswith(prefix):
return None
asset_id = icon_name[len(prefix):]
if not asset_id or "/" in asset_id:
return None
return f"{icon_name}.svg"
if "/" in icon_name:
return None
return f"{icon_name}.svg"
def _has_namespace_placeholder(root: ET.Element, icon_namespace: str) -> bool:
if not icon_namespace:
return False
return any(
_local(elem.tag) == "use"
and (icon_name := elem.get("data-icon")) is not None
and _icon_asset_for_namespace(icon_name, icon_namespace) is not None
for elem in root.iter()
)
def _generated_asset_re(svg_stems: list[str], id_prefix: str) -> re.Pattern[str] | None:
if not svg_stems:
return None
@@ -282,21 +364,29 @@ def _generated_asset_re(svg_stems: list[str], id_prefix: str) -> re.Pattern[str]
return re.compile(rf"^{prefix}(?:{stems})_ill\d+\.svg$")
def _clean_stale_assets(icons_dir: Path, svg_paths: list[Path], id_prefix: str, keep_assets: set[str]) -> list[str]:
def _clean_stale_assets(
icons_dir: Path,
icon_namespace: str,
svg_paths: list[Path],
id_prefix: str,
keep_assets: set[str],
) -> list[str]:
pattern = _generated_asset_re([path.stem for path in svg_paths], id_prefix)
if pattern is None:
return []
removed: list[str] = []
for asset_path in sorted(icons_dir.glob("*.svg")):
if asset_path.name in keep_assets or not pattern.match(asset_path.name):
asset_dir = icons_dir / icon_namespace if icon_namespace else icons_dir
for asset_path in sorted(asset_dir.glob("*.svg")):
relative_asset = asset_path.relative_to(icons_dir).as_posix()
if relative_asset in keep_assets or not pattern.match(asset_path.name):
continue
asset_path.unlink()
removed.append(asset_path.name)
removed.append(relative_asset)
return removed
def _referenced_icon_assets(svg_paths: list[Path]) -> set[str]:
def _referenced_icon_assets(svg_paths: list[Path], icon_namespace: str) -> set[str]:
assets: set[str] = set()
for svg_path in svg_paths:
try:
@@ -307,14 +397,15 @@ def _referenced_icon_assets(svg_paths: list[Path]) -> set[str]:
if _local(elem.tag) != "use":
continue
icon_name = elem.get("data-icon")
if icon_name and "/" not in icon_name:
assets.add(f"{icon_name}.svg")
if icon_name and (asset := _icon_asset_for_namespace(icon_name, icon_namespace)):
assets.add(asset)
return assets
def _existing_placeholder_entries(
svg_paths: list[Path],
icons_dir: Path,
icon_namespace: str,
known_assets: set[str],
) -> list[dict]:
by_asset: dict[str, dict] = {}
@@ -328,10 +419,12 @@ def _existing_placeholder_entries(
if _local(elem.tag) != "use":
continue
icon_name = elem.get("data-icon")
if not icon_name or "/" in icon_name:
if not icon_name:
continue
asset = f"{icon_name}.svg"
asset = _icon_asset_for_namespace(icon_name, icon_namespace)
if asset is None:
continue
if asset in known_assets:
continue
@@ -354,6 +447,9 @@ def _existing_placeholder_entries(
for asset, entry in sorted(by_asset.items()):
asset_path = icons_dir / asset
if asset_path.exists():
entry["asset_sha256"] = hashlib.sha256(
asset_path.read_bytes()
).hexdigest()
try:
root = ET.parse(asset_path).getroot()
except ET.ParseError:
@@ -362,6 +458,7 @@ def _existing_placeholder_entries(
entry["drawable_count"] = _drawable_count(root)
entry["byte_count"] = _xml_size(root)
entry["elements"] = _tag_histogram(root)
entry["source_refs"] = _source_references(root)
entry["dependencies"] = sorted(
elem_id
for elem in root.iter()
@@ -373,6 +470,57 @@ def _existing_placeholder_entries(
return entries
def _load_reusable_assets(inventory_path: Path, icons_dir: Path) -> dict[str, dict]:
"""Load fingerprinted assets from an earlier extraction inventory."""
try:
payload = json.loads(inventory_path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise ValueError(f"reuse inventory not found: {inventory_path}") from exc
except json.JSONDecodeError as exc:
raise ValueError(f"invalid reuse inventory JSON: {inventory_path}: {exc}") from exc
entries = payload.get("assets")
if not isinstance(entries, list):
raise ValueError(f"reuse inventory has no assets list: {inventory_path}")
reusable: dict[str, dict] = {}
fingerprinted = 0
for entry in entries:
if not isinstance(entry, dict):
continue
source_sha256 = entry.get("source_sha256")
asset_sha256 = entry.get("asset_sha256")
asset = entry.get("asset")
icon = entry.get("icon")
if not all(
isinstance(value, str) and value
for value in (source_sha256, asset_sha256, asset, icon)
):
continue
fingerprinted += 1
asset_path = icons_dir / asset
if not asset_path.is_file():
raise ValueError(
f"reusable asset is missing from the target icons directory: {asset_path}"
)
actual_asset_sha256 = hashlib.sha256(asset_path.read_bytes()).hexdigest()
if actual_asset_sha256 != asset_sha256:
raise ValueError(
f"reusable asset hash does not match its inventory: {asset_path}"
)
current = reusable.get(source_sha256)
if current is None or asset < str(current["asset"]):
reusable[source_sha256] = entry
extracted_count = payload.get("extracted_count", 0)
if isinstance(extracted_count, int) and extracted_count > fingerprinted:
raise ValueError(
"reuse inventory predates source fingerprints; rerun the source extraction "
f"with the current tool: {inventory_path}"
)
return reusable
def _rewritten_path(svg_path: Path, rewritten_dir: Path | None, inplace: bool) -> Path:
if inplace:
return svg_path
@@ -384,12 +532,14 @@ def _rewritten_path(svg_path: Path, rewritten_dir: Path | None, inplace: bool) -
def extract_file(
svg_path: Path,
icons_dir: Path,
icon_namespace: str,
min_drawables: int,
min_bytes: int,
min_decoration_bytes: int,
inplace: bool,
id_prefix: str = "",
rewritten_dir: Path | None = None,
reusable_assets: dict[str, dict] | None = None,
) -> list[dict]:
"""Extract qualifying groups from one SVG. Returns inventory entries."""
ET.register_namespace("", SVG_NS)
@@ -399,6 +549,16 @@ def extract_file(
width = root.get("width")
height = root.get("height")
# A namespaced projection is an all-at-once readability pass. Once it owns
# an asset reference, reruns inventory the existing placeholders instead of
# progressively factoring their remaining parent/sibling geometry.
if _has_namespace_placeholder(root, icon_namespace):
if not inplace:
rewritten = _rewritten_path(svg_path, rewritten_dir, inplace)
rewritten.parent.mkdir(parents=True, exist_ok=True)
tree.write(rewritten, encoding="utf-8", xml_declaration=True)
return []
targets: list[tuple[ET.Element, list[ET.Element]]] = []
parents = {child: parent for parent in root.iter() for child in parent}
group_targets = _find_extractable(root, min_drawables, min_bytes)
@@ -433,13 +593,16 @@ def extract_file(
tree.write(rewritten, encoding="utf-8", xml_declaration=True)
return []
icons_dir.mkdir(parents=True, exist_ok=True)
asset_dir = icons_dir / icon_namespace if icon_namespace else icons_dir
asset_dir.mkdir(parents=True, exist_ok=True)
entries = []
for index, (parent, nodes) in enumerate(targets, start=1):
if not nodes or not all(node in parent for node in nodes):
continue
asset_id = _asset_id(svg_path, index, id_prefix)
icon_reference = _icon_reference(icon_namespace, asset_id)
asset = _asset_relative_path(icon_namespace, asset_id)
pos = list(parent).index(nodes[0])
group = nodes[0] if len(nodes) == 1 and _local(nodes[0].tag) == "g" else _asset_group(nodes)
@@ -450,6 +613,35 @@ def extract_file(
for elem in dependency.iter()
if (elem_id := elem.get("id"))
})
source_sha256 = _source_sha256(group, dependencies, view_box, width, height)
source_refs = _source_references(group)
reusable = (reusable_assets or {}).get(source_sha256)
if reusable is not None:
reused_icon = str(reusable["icon"])
reused_asset = str(reusable["asset"])
placeholder = ET.Element(f"{{{SVG_NS}}}use")
placeholder.set("data-icon", reused_icon)
for node in nodes:
if node in parent:
parent.remove(node)
parent.insert(pos, placeholder)
entries.append({
"svg": svg_path.name,
"id": reused_icon,
"icon": reused_icon,
"asset": reused_asset,
"source": "reused-inventory",
"source_sha256": source_sha256,
"asset_sha256": reusable["asset_sha256"],
"reused_from_svg": reusable.get("svg"),
"drawable_count": _drawable_count(group),
"byte_count": _xml_size(group),
"source_refs": source_refs,
"dependencies": dependency_source_ids,
"elements": _tag_histogram(group),
})
continue
id_mapping = _collect_id_mapping(asset_id, group, dependencies)
_rewrite_references(group, id_mapping)
for dependency in dependencies:
@@ -457,10 +649,10 @@ def extract_file(
# Asset keeps the group in original page coordinates and carries its defs.
asset_bytes = _asset_svg(group, dependencies, view_box, width, height)
(icons_dir / f"{asset_id}.svg").write_bytes(asset_bytes)
(icons_dir / asset).write_bytes(asset_bytes)
placeholder = ET.Element(f"{{{SVG_NS}}}use")
placeholder.set("data-icon", asset_id)
placeholder.set("data-icon", icon_reference)
for node in nodes:
if node in parent:
parent.remove(node)
@@ -468,11 +660,15 @@ def extract_file(
entries.append({
"svg": svg_path.name,
"id": asset_id,
"icon": asset_id,
"asset": f"{asset_id}.svg",
"id": icon_reference,
"icon": icon_reference,
"asset": asset,
"source": "extracted",
"source_sha256": source_sha256,
"asset_sha256": hashlib.sha256(asset_bytes).hexdigest(),
"drawable_count": _drawable_count(group),
"byte_count": _xml_size(group),
"source_refs": source_refs,
"dependencies": [id_mapping.get(elem_id, elem_id) for elem_id in dependency_source_ids],
"elements": _tag_histogram(group),
})
@@ -491,6 +687,14 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("svg_dir", help="Directory of working SVGs (e.g. import_ws/svg or project/svg_output)")
parser.add_argument("-o", "--output", dest="icons_dir", help="Project icon dir (default: <svg_dir>/../icons)")
parser.add_argument("--icons-dir", dest="icons_dir", help="Project icon dir (default: <svg_dir>/../icons)")
parser.add_argument(
"--icon-namespace",
default="",
help=(
"Optional lower-case subdirectory and data-icon prefix for extracted "
"assets (create-template uses: imported)"
),
)
parser.add_argument(
"--rewritten-dir",
help="Directory for rewritten SVGs when not using --inplace (default: <svg_dir>/../<svg_dir-name>-rewritten)",
@@ -499,10 +703,20 @@ def build_parser() -> argparse.ArgumentParser:
"--inventory",
help="Inventory JSON path (default: <svg_dir>/../<svg_dir-name>_vector_asset_inventory.json)",
)
parser.add_argument(
"--reuse-inventory",
help=(
"Reuse fingerprint-matched assets from an earlier extraction inventory; "
"only unmatched vector subtrees create new assets"
),
)
parser.add_argument(
"--id-prefix",
default="",
help="Optional prefix for generated asset IDs, useful when processing layered and flat SVG dirs into one icons dir",
help=(
"Optional prefix for generated asset IDs, useful when processing "
"layered and flat SVG dirs into one icons dir"
),
)
parser.add_argument(
"--min-drawables", type=int, default=DEFAULT_MIN_DRAWABLES,
@@ -543,8 +757,28 @@ def main(argv: Optional[list[str]] = None) -> int:
icons_dir = Path(args.icons_dir) if args.icons_dir else svg_dir.parent / "icons"
icons_dir.mkdir(parents=True, exist_ok=True)
icon_namespace = args.icon_namespace.strip()
if icon_namespace and not ICON_NAMESPACE_RE.fullmatch(icon_namespace):
print(
"[ERROR] --icon-namespace must be one lower-case ASCII directory name "
"using only letters, digits, '_' or '-'",
file=sys.stderr,
)
return 1
reusable_assets: dict[str, dict] = {}
reuse_inventory_path = Path(args.reuse_inventory) if args.reuse_inventory else None
if reuse_inventory_path is not None:
try:
reusable_assets = _load_reusable_assets(reuse_inventory_path, icons_dir)
except ValueError as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
return 1
rewritten_dir = Path(args.rewritten_dir) if args.rewritten_dir else None
inventory_path = Path(args.inventory) if args.inventory else svg_dir.parent / f"{svg_dir.name}_vector_asset_inventory.json"
inventory_path = (
Path(args.inventory)
if args.inventory
else svg_dir.parent / f"{svg_dir.name}_vector_asset_inventory.json"
)
svg_paths = sorted(svg_dir.glob("*.svg"))
inventory: list[dict] = []
@@ -554,27 +788,38 @@ def main(argv: Optional[list[str]] = None) -> int:
extract_file(
svg_path,
icons_dir,
icon_namespace,
args.min_drawables,
args.min_bytes,
args.min_decoration_bytes,
args.inplace,
args.id_prefix,
rewritten_dir,
reusable_assets,
)
)
except ET.ParseError as exc:
print(f"[WARN] skip unparseable {svg_path.name}: {exc}", file=sys.stderr)
extracted_count = len(inventory)
extracted_count = sum(entry.get("source") == "extracted" for entry in inventory)
reused_count = sum(entry.get("source") == "reused-inventory" for entry in inventory)
known_assets = {str(entry["asset"]) for entry in inventory}
inventory.extend(_existing_placeholder_entries(svg_paths, icons_dir, known_assets))
inventory.extend(
_existing_placeholder_entries(
svg_paths,
icons_dir,
icon_namespace,
known_assets,
)
)
stale_removed: list[str] = []
if args.clean_stale:
keep_assets = {str(entry["asset"]) for entry in inventory}
keep_assets.update(_referenced_icon_assets(svg_paths))
keep_assets.update(_referenced_icon_assets(svg_paths, icon_namespace))
stale_removed = _clean_stale_assets(
icons_dir,
icon_namespace,
svg_paths,
args.id_prefix,
keep_assets,
@@ -584,20 +829,45 @@ def main(argv: Optional[list[str]] = None) -> int:
"schema": "vector_asset_inventory.v1",
"svg_dir": str(svg_dir),
"icons_dir": str(icons_dir),
"rewritten_dir": None if args.inplace else str(_rewritten_path(svg_dir / "_sample.svg", rewritten_dir, False).parent),
"icon_namespace": icon_namespace or None,
"rewritten_dir": (
None
if args.inplace
else str(_rewritten_path(svg_dir / "_sample.svg", rewritten_dir, False).parent)
),
"reuse_inventory": str(reuse_inventory_path) if reuse_inventory_path is not None else None,
"min_drawables": args.min_drawables,
"min_bytes": args.min_bytes,
"min_decoration_bytes": args.min_decoration_bytes,
"extracted_count": extracted_count,
"reused_count": reused_count,
"asset_count": len(inventory),
"stale_removed": stale_removed,
"assets": inventory,
}
inventory_path.parent.mkdir(parents=True, exist_ok=True)
inventory_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"[OK] extracted {extracted_count} new asset(s), inventoried {len(inventory)} asset(s) -> {icons_dir}", file=sys.stderr)
summary_path: Path | None = None
if args.inplace and (svg_dir / AUTHORING_MANIFEST_NAME).is_file():
try:
summary_path = write_authoring_summary(svg_dir)
except (OSError, ValueError) as exc:
print(
f"[ERROR] vector extraction succeeded but authoring summary "
f"refresh failed: {exc}",
file=sys.stderr,
)
return 1
print(
f"[OK] extracted {extracted_count} new asset(s), reused {reused_count} asset(s), "
f"inventoried {len(inventory)} asset reference(s) -> "
f"{icons_dir / icon_namespace if icon_namespace else icons_dir}",
file=sys.stderr,
)
if stale_removed:
print(f"[OK] removed {len(stale_removed)} stale generated asset(s)", file=sys.stderr)
if summary_path is not None:
print(f"[OK] refreshed model-readable summary: {summary_path}", file=sys.stderr)
return 0
@@ -13,7 +13,7 @@ Architecture note: this module's outputs feed svg_final/ on disk AND its
sub-modules (svg_finalize.embed_icons, svg_finalize.flatten_tspan, ...)
are memory-reused by svg_to_pptx during native conversion. Deleting any
step here may also break native pptx output, not just svg_final/.
See docs/technical-design.md "Post-Processing Pipeline" before modifying.
See scripts/docs/svg-pipeline.md before modifying the shared pipeline.
Usage:
# Execute all processing steps (recommended)
@@ -9,10 +9,11 @@ Usage:
python3 scripts/generate_examples_index.py examples
"""
import os
import sys
from pathlib import Path
from datetime import datetime
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from console_encoding import configure_utf8_stdio
@@ -37,11 +38,18 @@ def generate_examples_index(examples_dir: str = 'examples') -> str:
Generated README.md content
"""
examples_path = Path(examples_dir)
skill_dir = Path(__file__).resolve().parent.parent
if not examples_path.exists():
print(f"[ERROR] Directory not found: {examples_dir}")
return ""
def skill_link(target: Path) -> str:
"""Return a link from the generated index to a packaged Skill resource."""
return Path(
os.path.relpath(target, start=examples_path.resolve())
).as_posix()
print(f"[SCAN] Scanning directory: {examples_dir}")
# Find all projects
@@ -70,7 +78,7 @@ def generate_examples_index(examples_dir: str = 'examples') -> str:
# Generate README content
content = []
content.append("# PPT Master Example Projects Index\n")
content.append("> This file is auto-generated by `skills/ppt-master/scripts/generate_examples_index.py`\n")
content.append("> This file is auto-generated by the packaged `scripts/generate_examples_index.py`\n")
content.append(f"> Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
# Overview statistics
@@ -190,11 +198,13 @@ def generate_examples_index(examples_dir: str = 'examples') -> str:
# Related resources
content.append("## [Resources] Related Resources\n")
content.append("- [Quick Start](../README.md)")
content.append("- [Workflow Tutorial](../../AGENTS.md)")
content.append("- [Canvas Formats](../references/canvas-formats.md)")
content.append("- [Role Definitions](../references/)")
content.append("- [Chart Templates](../templates/charts/README.md)\n")
content.append(f"- [Workflow]({skill_link(skill_dir / 'SKILL.md')})")
content.append(
f"- [Canvas Formats]({skill_link(skill_dir / 'references' / 'canvas-formats.md')})")
content.append(
f"- [Role Definitions]({skill_link(skill_dir / 'references')})")
content.append(
f"- [Chart Templates]({skill_link(skill_dir / 'templates' / 'charts' / 'README.md')})\n")
# Footer
content.append("---\n")
@@ -18,7 +18,7 @@ Examples:
python3 scripts/native_enhance_pptx.py apply projects/fire_station_native_enhance_20260626
Dependencies:
Same as native_narration_pptx.py.
Same as native_enhance_pptx_core.py.
"""
from __future__ import annotations
@@ -31,7 +31,7 @@ if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from console_encoding import configure_utf8_stdio # noqa: E402
from native_narration_pptx import main # noqa: E402
from native_enhance_pptx_core import main # noqa: E402
configure_utf8_stdio()
@@ -0,0 +1,969 @@
#!/usr/bin/env python3
"""
PPT Master - Native Existing PPTX Enhancer
Implementation core for the public native enhancement CLI and its legacy
narration compatibility entrypoint. It enhances an existing PPTX without
entering the SVG generation pipeline or modifying the original file.
V1 enhancement modules: speaker notes, narration audio, slide auto-advance
timings, and optional page transitions.
Usage:
python3 scripts/native_enhance_pptx.py init <source.pptx> [--name project_name]
python3 scripts/native_enhance_pptx.py apply <project_path> [--output output.pptx]
python3 scripts/native_enhance_pptx.py validate <project_path>
Examples:
python3 scripts/native_enhance_pptx.py init projects/source.pptx --name fire_station
python3 scripts/native_enhance_pptx.py apply projects/fire_station_native_enhance_20260626
python3 scripts/native_enhance_pptx.py validate projects/fire_station_native_enhance_20260626
Dependencies:
ffprobe for audio-duration-based auto-advance timings.
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from xml.etree import ElementTree as ET
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from console_encoding import configure_utf8_stdio # noqa: E402
from pptx_animations import ( # noqa: E402
object_animation_fingerprint,
validate_pptx_animation_package,
)
from pptx_transitions import ( # noqa: E402
AdvanceUpdate,
EnterUpdate,
TRANSITIONS,
apply_slide_motion_xml,
set_directory_use_timings,
validate_pptx_transition_package,
validate_seconds,
)
from svg_to_pptx.pptx_package.builder import ( # noqa: E402
_add_default_content_type,
_append_relationship,
_ensure_notes_master,
)
from svg_to_pptx.pptx_package.narration import ( # noqa: E402
AUDIO_CONTENT_TYPES,
AUDIO_MARKER_PNG_BYTES,
AUDIO_REL_TYPE,
IMAGE_REL_TYPE,
MEDIA_REL_TYPE,
NARRATION_EXTENSIONS,
inject_narration,
next_shape_id,
probe_audio_duration,
)
from svg_to_pptx.pptx_package.notes import ( # noqa: E402
create_notes_slide_rels_xml,
create_notes_slide_xml,
markdown_to_plain_text,
)
configure_utf8_stdio()
PROJECT_SCHEMA = "native_pptx_enhancement_project.v1"
LEGACY_PROJECT_SCHEMAS = {"native_narration_pptx_project.v1"}
NOTES_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"
PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"
REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
CONTENT_TYPE_NOTES_SLIDE = (
"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
)
CONTENT_TYPE_NOTES_MASTER = (
"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"
)
CONTENT_TYPE_THEME = "application/vnd.openxmlformats-officedocument.theme+xml"
@dataclass(frozen=True)
class SlidePart:
index: int
part_name: str
slide_number: int
def _sanitize_slug(value: str) -> str:
slug = re.sub(r"[^0-9A-Za-z_-]+", "_", value).strip("_")
return slug or "native_enhance"
def _positive_seconds_arg(value: str) -> float:
try:
return validate_seconds(value, "transition duration", allow_zero=False)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def _non_negative_seconds_arg(value: str) -> float:
try:
return validate_seconds(value, "narration padding", allow_zero=True)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def _read_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def _write_json(path: Path, data: dict) -> None:
path.write_text(
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def _is_relative_to(path: Path, parent: Path) -> bool:
try:
path.resolve().relative_to(parent.resolve())
return True
except ValueError:
return False
def _archive_source_pptx(source_pptx: Path, archived_pptx: Path, projects_root: Path) -> str:
"""Move project-local sources into the project; copy external sources."""
archived_pptx.parent.mkdir(parents=True, exist_ok=True)
if source_pptx.resolve() == archived_pptx.resolve():
return "reuse"
if _is_relative_to(source_pptx, projects_root):
shutil.move(str(source_pptx), str(archived_pptx))
return "move"
shutil.copy2(source_pptx, archived_pptx)
return "copy"
def _relationship_file_for_part(extract_dir: Path, part_name: str) -> Path:
part = Path(part_name)
return extract_dir / part.parent / "_rels" / f"{part.name}.rels"
def _ensure_rels_file(path: Path) -> None:
if path.exists():
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
f'<Relationships xmlns="{PACKAGE_REL_NS}">\n</Relationships>',
encoding="utf-8",
)
def _remove_relationships_by_type(rels_path: Path, rel_type: str) -> None:
if not rels_path.exists():
return
content = rels_path.read_text(encoding="utf-8")
content = re.sub(
rf'\s*<Relationship\b[^>]*\bType="{re.escape(rel_type)}"[^>]*/>',
"",
content,
)
rels_path.write_text(content, encoding="utf-8")
def _target_to_part(target: str) -> str:
target = target.lstrip("/")
if target.startswith("ppt/"):
return target
return f"ppt/{target}"
def _slide_number_from_part(part_name: str) -> int:
match = re.search(r"slide(\d+)\.xml$", part_name)
if not match:
raise ValueError(f"Unsupported slide part name: {part_name}")
return int(match.group(1))
def read_slide_parts(extract_dir: Path) -> list[SlidePart]:
presentation_path = extract_dir / "ppt" / "presentation.xml"
rels_path = extract_dir / "ppt" / "_rels" / "presentation.xml.rels"
if not presentation_path.exists() or not rels_path.exists():
raise RuntimeError("PPTX package is missing presentation.xml or its relationships")
rels_root = ET.parse(rels_path).getroot()
rels: dict[str, str] = {}
for rel in rels_root.findall(f"{{{PACKAGE_REL_NS}}}Relationship"):
rel_id = rel.attrib.get("Id")
target = rel.attrib.get("Target")
if rel_id and target:
rels[rel_id] = target
presentation_root = ET.parse(presentation_path).getroot()
slide_parts: list[SlidePart] = []
for index, slide_id in enumerate(
presentation_root.findall(f".//{{{PRESENTATION_NS}}}sldId"),
1,
):
rel_id = slide_id.attrib.get(f"{{{REL_NS}}}id")
if not rel_id or rel_id not in rels:
continue
part_name = _target_to_part(rels[rel_id])
slide_parts.append(
SlidePart(
index=index,
part_name=part_name,
slide_number=_slide_number_from_part(part_name),
)
)
if not slide_parts:
raise RuntimeError("No slides found in presentation.xml")
return slide_parts
def _zip_dir(source_dir: Path, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
for path in sorted(source_dir.rglob("*")):
if path.is_file():
zf.write(path, path.relative_to(source_dir).as_posix())
def _extract_pptx(source_pptx: Path, extract_dir: Path) -> None:
with zipfile.ZipFile(source_pptx, "r") as zf:
zf.extractall(extract_dir)
def _note_path(notes_dir: Path, index: int) -> Path | None:
candidates = [
notes_dir / f"{index:03d}.md",
notes_dir / f"{index:02d}.md",
notes_dir / f"{index}.md",
notes_dir / f"slide{index:03d}.md",
notes_dir / f"slide{index:02d}.md",
notes_dir / f"slide{index}.md",
]
for candidate in candidates:
if candidate.exists():
return candidate
return None
def _audio_path(audio_dir: Path, index: int) -> Path | None:
stems = [
f"{index:03d}",
f"{index:02d}",
str(index),
f"slide{index:03d}",
f"slide{index:02d}",
f"slide{index}",
]
for stem in stems:
for ext in NARRATION_EXTENSIONS:
candidate = audio_dir / f"{stem}{ext}"
if candidate.exists():
return candidate
return None
def _add_override(content_types: str, part_name: str, content_type: str) -> str:
if re.search(
rf'<Override\b[^>]*\bPartName="/{re.escape(part_name)}"[^>]*/>',
content_types,
):
return content_types
override = f' <Override PartName="/{part_name}" ContentType="{content_type}"/>'
return content_types.replace("</Types>", override + "\n</Types>")
def _add_notes_content_types(content_types: str, note_indices: set[int]) -> str:
content_types = _add_override(content_types, "ppt/theme/theme2.xml", CONTENT_TYPE_THEME)
content_types = _add_override(
content_types,
"ppt/notesMasters/notesMaster1.xml",
CONTENT_TYPE_NOTES_MASTER,
)
for index in sorted(note_indices):
content_types = _add_override(
content_types,
f"ppt/notesSlides/notesSlide{index}.xml",
CONTENT_TYPE_NOTES_SLIDE,
)
return content_types
def _apply_notes(extract_dir: Path, slide: SlidePart, note_md: Path) -> None:
notes_text = markdown_to_plain_text(note_md.read_text(encoding="utf-8"))
if not notes_text:
return
_ensure_notes_master(extract_dir)
notes_dir = extract_dir / "ppt" / "notesSlides"
notes_dir.mkdir(parents=True, exist_ok=True)
notes_xml_path = notes_dir / f"notesSlide{slide.index}.xml"
notes_xml_path.write_text(
create_notes_slide_xml(slide.slide_number, notes_text),
encoding="utf-8",
)
notes_rels_dir = notes_dir / "_rels"
notes_rels_dir.mkdir(parents=True, exist_ok=True)
notes_rels_path = notes_rels_dir / f"notesSlide{slide.index}.xml.rels"
notes_rels_path.write_text(
create_notes_slide_rels_xml(slide.slide_number),
encoding="utf-8",
)
slide_rels = _relationship_file_for_part(extract_dir, slide.part_name)
_ensure_rels_file(slide_rels)
_remove_relationships_by_type(slide_rels, NOTES_REL_TYPE)
_append_relationship(
slide_rels,
NOTES_REL_TYPE,
f"../notesSlides/notesSlide{slide.index}.xml",
)
def _apply_audio(
extract_dir: Path,
slide: SlidePart,
audio_path: Path,
*,
enter: EnterUpdate,
timings_enabled: bool,
narration_padding: float,
) -> bool:
media_dir = extract_dir / "ppt" / "media"
media_dir.mkdir(parents=True, exist_ok=True)
ext = audio_path.suffix.lower()
media_name = f"native_enhance_audio_{slide.index:03d}{ext}"
shutil.copy2(audio_path, media_dir / media_name)
poster_name = "native_enhance_audio_poster.png"
poster_path = media_dir / poster_name
if not poster_path.exists():
poster_path.write_bytes(AUDIO_MARKER_PNG_BYTES)
slide_rels = _relationship_file_for_part(extract_dir, slide.part_name)
_ensure_rels_file(slide_rels)
media_rid = _append_relationship(slide_rels, MEDIA_REL_TYPE, f"../media/{media_name}")
audio_rid = _append_relationship(slide_rels, AUDIO_REL_TYPE, f"../media/{media_name}")
poster_rid = _append_relationship(slide_rels, IMAGE_REL_TYPE, f"../media/{poster_name}")
slide_xml_path = extract_dir / slide.part_name
slide_xml = slide_xml_path.read_text(encoding="utf-8")
source_animation_fingerprint = object_animation_fingerprint(slide_xml)
shape_id = next_shape_id(slide_xml)
slide_xml = inject_narration(
slide_xml,
shape_id=shape_id,
shape_name=media_name,
audio_rid=audio_rid,
media_rid=media_rid,
poster_rid=poster_rid,
)
advance = AdvanceUpdate(mode="preserve")
if timings_enabled:
duration = probe_audio_duration(audio_path)
if duration is None:
raise RuntimeError(f"Unable to read narration duration with ffprobe: {audio_path}")
advance = AdvanceUpdate(
mode="narration",
after=duration + narration_padding,
)
wrote_advance = False
if enter.policy != "preserve" or timings_enabled:
slide_xml, wrote_advance = apply_slide_motion_xml(
slide_xml,
enter=enter,
advance=advance,
)
if object_animation_fingerprint(slide_xml) != source_animation_fingerprint:
raise RuntimeError(
f"Slide {slide.index} object animations changed while adding narration"
)
slide_xml_path.write_text(slide_xml, encoding="utf-8")
return timings_enabled and wrote_advance
def _update_content_types(extract_dir: Path, note_indices: set[int], audio_exts: set[str]) -> None:
content_types_path = extract_dir / "[Content_Types].xml"
content_types = content_types_path.read_text(encoding="utf-8")
if note_indices:
content_types = _add_notes_content_types(content_types, note_indices)
for ext in sorted(audio_exts):
content_type = AUDIO_CONTENT_TYPES.get(ext)
if content_type:
content_types = _add_default_content_type(content_types, ext, content_type)
if audio_exts:
content_types = _add_default_content_type(content_types, "png", "image/png")
content_types_path.write_text(content_types, encoding="utf-8")
def _project_paths(project_path: Path) -> tuple[Path, Path, Path, Path]:
project = _read_json(project_path / "project.json")
source_pptx = project_path / project["source_pptx"]
notes_dir = project_path / project["notes_dir"]
audio_dir = project_path / project["audio_dir"]
exports_dir = project_path / project["exports_dir"]
return source_pptx, notes_dir, audio_dir, exports_dir
def _plan_path(project_path: Path) -> Path:
return project_path / "analysis" / "enhancement_plan.json"
def _load_enhancement_plan(project_path: Path) -> dict:
path = _plan_path(project_path)
if not path.exists():
return {}
return _read_json(path)
def _enabled_modules(plan: dict) -> set[str]:
modules = plan.get("modules")
if not isinstance(modules, dict):
return {"notes", "audio", "timings", "transitions"}
enabled: set[str] = set()
for name, config in modules.items():
if isinstance(config, dict) and config.get("enabled") is True:
enabled.add(str(name))
return enabled
def _resolve_enter_update(
*,
cli_effect: str | None,
configured_effect: object,
transitions_enabled: bool,
duration: float,
) -> EnterUpdate:
if cli_effect is None and not transitions_enabled:
if configured_effect == "none":
return EnterUpdate(policy="none", effect=None, duration=duration)
return EnterUpdate(policy="preserve", duration=duration)
effect = cli_effect if cli_effect is not None else configured_effect
if not isinstance(effect, str):
raise ValueError(f"transition effect must be a string: {effect!r}")
if effect == "none":
return EnterUpdate(policy="none", effect=None, duration=duration)
if effect not in TRANSITIONS:
valid = ", ".join(sorted(TRANSITIONS))
raise ValueError(
f"unknown transition effect {effect!r}; valid effects: {valid}, none"
)
return EnterUpdate(policy="replace", effect=effect, duration=duration)
def _plan_confirmed(plan: dict) -> bool:
return plan.get("status") == "confirmed"
def _build_enhancement_plan(
project: dict,
*,
slide_count: int,
notes_count: int,
audio_count: int,
transition: str,
transition_duration: float,
narration_padding: float,
apply_transition_without_audio: bool,
) -> dict:
return {
"schema": "native_pptx_enhancement_plan.v1",
"status": "draft",
"source_pptx": project.get("source_pptx"),
"slide_count": slide_count,
"modules": {
"notes": {
"enabled": True,
"requires_confirmation": True,
"status": "ready" if notes_count == slide_count else "needs_notes",
"coverage": {"ready": notes_count, "total": slide_count},
},
"audio": {
"enabled": True,
"requires_confirmation": True,
"status": "ready" if audio_count == slide_count else "needs_audio",
"coverage": {"ready": audio_count, "total": slide_count},
},
"timings": {
"enabled": True,
"requires_confirmation": True,
"status": "ready" if audio_count == slide_count else "blocked_until_audio",
"source": "audio_duration",
"narration_padding": narration_padding,
},
"transitions": {
"enabled": transition != "none",
"requires_confirmation": True,
"status": "ready",
"effect": transition,
"duration": transition_duration,
"apply_without_audio": apply_transition_without_audio,
},
},
"not_in_v1": [
"object_animation",
"visible_watermark",
"footer_or_logo_insertion",
"background_music",
"media_compression",
],
}
def init_project(args: argparse.Namespace) -> int:
source_pptx = Path(args.source_pptx).expanduser().resolve()
if not source_pptx.exists() or source_pptx.suffix.lower() != ".pptx":
print(f"error: expected an existing .pptx file: {source_pptx}", file=sys.stderr)
return 1
stem = _sanitize_slug(args.name or source_pptx.stem)
date = datetime.now().strftime("%Y%m%d")
project_path = (
Path(args.project_dir).expanduser().resolve()
if args.project_dir
else Path(args.projects_root).expanduser().resolve() / f"{stem}_native_enhance_{date}"
)
if project_path.exists() and any(project_path.iterdir()):
print(f"error: project directory already exists and is not empty: {project_path}", file=sys.stderr)
return 1
for dirname in ("sources", "analysis", "notes", "audio", "exports", "validation"):
(project_path / dirname).mkdir(parents=True, exist_ok=True)
archived_pptx = project_path / "sources" / source_pptx.name
projects_root = Path(args.projects_root).expanduser().resolve()
source_import_mode = _archive_source_pptx(source_pptx, archived_pptx, projects_root)
source_md = project_path / "sources" / f"{source_pptx.stem}.md"
ppt_to_md = _SCRIPTS_DIR / "source_to_md" / "ppt_to_md.py"
result = subprocess.run(
[sys.executable, str(ppt_to_md), str(archived_pptx), "-o", str(source_md)],
check=False,
text=True,
capture_output=True,
)
if result.returncode != 0:
print(result.stderr or result.stdout, file=sys.stderr)
return result.returncode
with tempfile.TemporaryDirectory(prefix="native-enhance-intake-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(archived_pptx, extract_dir)
slide_parts = read_slide_parts(extract_dir)
slide_index = {
"schema": "native_pptx_enhancement_slide_index.v1",
"source_pptx": f"sources/{source_pptx.name}",
"slide_count": len(slide_parts),
"slides": [
{
"index": slide.index,
"note_file": f"notes/{slide.index:03d}.md",
"audio_stem": f"{slide.index:03d}",
"part_name": slide.part_name,
"slide_number": slide.slide_number,
}
for slide in slide_parts
],
}
_write_json(project_path / "analysis" / "slide_index.json", slide_index)
project = {
"schema": PROJECT_SCHEMA,
"kind": "native_pptx_enhancement",
"modules": ["notes", "audio", "timings", "transitions"],
"source_pptx": f"sources/{source_pptx.name}",
"source_markdown": f"sources/{source_pptx.stem}.md",
"source_import": {
"mode": source_import_mode,
"original_path": str(source_pptx),
},
"slide_count": len(slide_parts),
"notes_dir": "notes",
"audio_dir": "audio",
"exports_dir": "exports",
"transition": {
"effect": args.transition,
"duration": args.transition_duration,
},
"audio": {
"provider": "",
"voice": "",
"rate": "",
},
}
_write_json(project_path / "project.json", project)
plan = _build_enhancement_plan(
project,
slide_count=len(slide_parts),
notes_count=0,
audio_count=0,
transition=args.transition,
transition_duration=args.transition_duration,
narration_padding=args.narration_padding,
apply_transition_without_audio=args.apply_transition_without_audio,
)
_write_json(_plan_path(project_path), plan)
print(f"Project: {project_path}", file=sys.stderr)
print(f"Slides: {len(slide_parts)}", file=sys.stderr)
print(f"Source import: {source_import_mode}", file=sys.stderr)
print(f"Source markdown: {source_md}", file=sys.stderr)
print(f"Draft enhancement plan: {_plan_path(project_path)}", file=sys.stderr)
print(
"Review the plan with the user and set status to \"confirmed\" before generating notes/audio/applying.",
file=sys.stderr,
)
return 0
def plan_project(args: argparse.Namespace) -> int:
project_path = Path(args.project_path).expanduser().resolve()
project = _read_json(project_path / "project.json")
if project.get("schema") not in {PROJECT_SCHEMA, *LEGACY_PROJECT_SCHEMAS}:
print(f"error: not a native PPTX enhancement project: {project_path}", file=sys.stderr)
return 1
source_pptx, notes_dir, audio_dir, _exports_dir = _project_paths(project_path)
with tempfile.TemporaryDirectory(prefix="native-enhance-plan-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(source_pptx, extract_dir)
slides = read_slide_parts(extract_dir)
notes_count = sum(1 for slide in slides if _note_path(notes_dir, slide.index) is not None)
audio_count = sum(1 for slide in slides if _audio_path(audio_dir, slide.index) is not None)
plan = _build_enhancement_plan(
project,
slide_count=len(slides),
notes_count=notes_count,
audio_count=audio_count,
transition=args.transition,
transition_duration=args.transition_duration,
narration_padding=args.narration_padding,
apply_transition_without_audio=args.apply_transition_without_audio,
)
_write_json(_plan_path(project_path), plan)
print(json.dumps(plan, ensure_ascii=False, indent=2))
print(f"Plan written: {_plan_path(project_path)}", file=sys.stderr)
print(
"Confirm by editing status to \"confirmed\" after user approval, then run apply.",
file=sys.stderr,
)
return 0
def apply_project(args: argparse.Namespace) -> int:
project_path = Path(args.project_path).expanduser().resolve()
project = _read_json(project_path / "project.json")
if project.get("schema") not in {PROJECT_SCHEMA, *LEGACY_PROJECT_SCHEMAS}:
print(f"error: not a native PPTX enhancement project: {project_path}", file=sys.stderr)
return 1
source_pptx, notes_dir, audio_dir, exports_dir = _project_paths(project_path)
transition_cfg = project.get("transition", {}) if isinstance(project.get("transition"), dict) else {}
plan = _load_enhancement_plan(project_path)
if not _plan_confirmed(plan) and not args.force:
print(
f"error: enhancement plan is not confirmed: {_plan_path(project_path)} "
"(run plan, get user confirmation, set status to \"confirmed\", or pass --force)",
file=sys.stderr,
)
return 1
modules = _enabled_modules(plan)
modules_cfg = plan.get("modules") if isinstance(plan.get("modules"), dict) else {}
transitions_cfg = modules_cfg.get("transitions", {})
if not isinstance(transitions_cfg, dict):
transitions_cfg = {}
timings_cfg = modules_cfg.get("timings", {})
if not isinstance(timings_cfg, dict):
timings_cfg = {}
if "effect" in transitions_cfg:
configured_effect = transitions_cfg["effect"]
elif "effect" in transition_cfg:
configured_effect = transition_cfg["effect"]
else:
configured_effect = "fade"
if args.transition_duration is not None:
raw_transition_duration = args.transition_duration
elif "duration" in transitions_cfg:
raw_transition_duration = transitions_cfg["duration"]
elif "duration" in transition_cfg:
raw_transition_duration = transition_cfg["duration"]
else:
raw_transition_duration = 0.5
if args.narration_padding is not None:
raw_narration_padding = args.narration_padding
elif "narration_padding" in timings_cfg:
raw_narration_padding = timings_cfg["narration_padding"]
else:
raw_narration_padding = 0.4
try:
if args.transition is not None or "transitions" in modules:
transition_duration = validate_seconds(
raw_transition_duration,
"transition duration",
allow_zero=False,
)
else:
transition_duration = 0.5
if "timings" in modules:
narration_padding = validate_seconds(
raw_narration_padding,
"narration padding",
allow_zero=True,
)
else:
narration_padding = 0.4
enter_update = _resolve_enter_update(
cli_effect=args.transition,
configured_effect=configured_effect,
transitions_enabled="transitions" in modules,
duration=transition_duration,
)
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
apply_transition_without_audio = (
args.apply_transition_without_audio
or bool(transitions_cfg.get("apply_without_audio"))
)
output_path = (
Path(args.output).expanduser().resolve()
if args.output
else exports_dir / f"{source_pptx.stem}_enhanced.pptx"
)
if output_path.exists() and not args.overwrite:
print(f"error: output already exists, pass --overwrite: {output_path}", file=sys.stderr)
return 1
with tempfile.TemporaryDirectory(prefix="native-enhance-pptx-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(source_pptx, extract_dir)
slides = read_slide_parts(extract_dir)
note_indices: set[int] = set()
audio_exts: set[str] = set()
audio_count = 0
transition_only_count = 0
wrote_auto_advance = False
for slide in slides:
note = _note_path(notes_dir, slide.index)
if "notes" in modules and note:
_apply_notes(extract_dir, slide, note)
note_indices.add(slide.index)
audio = _audio_path(audio_dir, slide.index)
if "audio" in modules and audio:
wrote_auto_advance = _apply_audio(
extract_dir,
slide,
audio,
enter=enter_update,
timings_enabled="timings" in modules,
narration_padding=narration_padding,
) or wrote_auto_advance
audio_exts.add(audio.suffix.lower())
audio_count += 1
continue
if (
apply_transition_without_audio
and enter_update.policy != "preserve"
):
slide_xml_path = extract_dir / slide.part_name
slide_xml = slide_xml_path.read_text(encoding="utf-8")
source_animation_fingerprint = object_animation_fingerprint(
slide_xml
)
slide_xml, _uses_timings = apply_slide_motion_xml(
slide_xml,
enter=enter_update,
advance=AdvanceUpdate(mode="preserve"),
)
if (
object_animation_fingerprint(slide_xml)
!= source_animation_fingerprint
):
raise RuntimeError(
f"Slide {slide.index} object animations changed while "
"updating the transition"
)
slide_xml_path.write_text(
slide_xml,
encoding="utf-8",
)
transition_only_count += 1
_update_content_types(extract_dir, note_indices, audio_exts)
if wrote_auto_advance:
set_directory_use_timings(extract_dir)
output_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(
prefix="native-enhance-output-",
dir=output_path.parent,
) as output_tmp:
candidate_path = Path(output_tmp) / output_path.name
_zip_dir(extract_dir, candidate_path)
try:
validate_pptx_transition_package(
candidate_path,
require_use_timings=wrote_auto_advance,
)
except ValueError as exc:
raise RuntimeError(
f"PPTX transition package validation failed: {exc}"
) from exc
try:
validate_pptx_animation_package(
candidate_path,
require_supported_effects=False,
)
except ValueError as exc:
raise RuntimeError(
f"PPTX animation/timing package validation failed: {exc}"
) from exc
candidate_path.replace(output_path)
print(f"Output: {output_path}", file=sys.stderr)
print(f"Notes applied: {len(note_indices)}", file=sys.stderr)
print(f"Audio embedded: {audio_count}", file=sys.stderr)
if transition_only_count:
print(f"Transition-only slides: {transition_only_count}", file=sys.stderr)
return 0
def validate_project(args: argparse.Namespace) -> int:
project_path = Path(args.project_path).expanduser().resolve()
project = _read_json(project_path / "project.json")
if project.get("schema") not in {PROJECT_SCHEMA, *LEGACY_PROJECT_SCHEMAS}:
print(f"error: not a native PPTX enhancement project: {project_path}", file=sys.stderr)
return 1
source_pptx, notes_dir, audio_dir, _exports_dir = _project_paths(project_path)
with tempfile.TemporaryDirectory(prefix="native-enhance-validate-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(source_pptx, extract_dir)
slides = read_slide_parts(extract_dir)
plan = _load_enhancement_plan(project_path)
modules = _enabled_modules(plan)
notes_count = sum(1 for slide in slides if _note_path(notes_dir, slide.index) is not None)
audio_count = sum(1 for slide in slides if _audio_path(audio_dir, slide.index) is not None)
missing_notes = (
[slide.index for slide in slides if _note_path(notes_dir, slide.index) is None]
if "notes" in modules
else []
)
missing_audio = (
[slide.index for slide in slides if _audio_path(audio_dir, slide.index) is None]
if "audio" in modules
else []
)
report = {
"schema": "native_pptx_enhancement_validation.v1",
"slide_count": len(slides),
"plan_status": plan.get("status") or "missing",
"enabled_modules": sorted(modules),
"notes_required": "notes" in modules,
"audio_required": "audio" in modules,
"notes_count": notes_count,
"audio_count": audio_count,
"missing_notes": missing_notes,
"missing_audio": missing_audio,
}
validation_dir = project_path / "validation"
validation_dir.mkdir(exist_ok=True)
_write_json(validation_dir / "report.json", report)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if not missing_notes and not missing_audio else 2
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Create/apply a native existing-PPTX enhancement project without SVG conversion.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
init = subparsers.add_parser("init", help="create a native PPTX enhancement project")
init.add_argument("source_pptx", help="source .pptx file")
init.add_argument("--name", default=None, help="ASCII project name slug")
init.add_argument("--project-dir", default=None, help="explicit project directory")
init.add_argument("--projects-root", default="projects", help="projects root (default: projects)")
init.add_argument("--transition", default="fade", choices=sorted(TRANSITIONS.keys()))
init.add_argument("--transition-duration", type=_positive_seconds_arg, default=0.5)
init.add_argument("--narration-padding", type=_non_negative_seconds_arg, default=0.4)
init.add_argument(
"--apply-transition-without-audio",
action="store_true",
help="draft the plan with page transitions for slides without audio",
)
init.set_defaults(func=init_project)
plan = subparsers.add_parser("plan", help="draft an enhancement module plan")
plan.add_argument("project_path", help="native enhancement project directory")
plan.add_argument("--transition", default="fade", choices=sorted(TRANSITIONS.keys()) + ["none"])
plan.add_argument("--transition-duration", type=_positive_seconds_arg, default=0.5)
plan.add_argument("--narration-padding", type=_non_negative_seconds_arg, default=0.4)
plan.add_argument(
"--apply-transition-without-audio",
action="store_true",
help="include page transitions for slides without audio",
)
plan.set_defaults(func=plan_project)
apply = subparsers.add_parser("apply", help="patch notes/audio/timings into a copied PPTX")
apply.add_argument("project_path", help="native enhancement project directory")
apply.add_argument("-o", "--output", default=None, help="output .pptx path")
apply.add_argument("--overwrite", action="store_true", help="overwrite output if it exists")
apply.add_argument("--transition", default=None, choices=sorted(TRANSITIONS.keys()) + ["none"])
apply.add_argument("--transition-duration", type=_positive_seconds_arg, default=None)
apply.add_argument("--narration-padding", type=_non_negative_seconds_arg, default=None)
apply.add_argument("--force", action="store_true", help="apply without a confirmed enhancement plan")
apply.add_argument(
"--apply-transition-without-audio",
action="store_true",
help="also write page transitions on slides that do not have audio",
)
apply.set_defaults(func=apply_project)
validate = subparsers.add_parser("validate", help="check notes/audio coverage")
validate.add_argument("project_path", help="native enhancement project directory")
validate.set_defaults(func=validate_project)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,968 +1,37 @@
#!/usr/bin/env python3
"""
PPT Master - Native Existing PPTX Enhancer
PPT Master - Native Narration PPTX Compatibility Entrypoint
Create and apply a lightweight project for enhancing an existing PPTX without
entering the SVG generation pipeline or modifying the original file.
V1 enhancement modules: speaker notes, narration audio, slide auto-advance
timings, and optional page transitions.
Backward-compatible CLI for callers that still use the retired narration
script name. New calls use native_enhance_pptx.py.
Usage:
python3 scripts/native_enhance_pptx.py init <source.pptx> [--name project_name]
python3 scripts/native_enhance_pptx.py apply <project_path> [--output output.pptx]
python3 scripts/native_enhance_pptx.py validate <project_path>
python3 scripts/native_narration_pptx.py init <source.pptx> [--name project_name]
python3 scripts/native_narration_pptx.py plan <project_path>
python3 scripts/native_narration_pptx.py validate <project_path>
python3 scripts/native_narration_pptx.py apply <project_path>
Examples:
python3 scripts/native_enhance_pptx.py init projects/source.pptx --name fire_station
python3 scripts/native_enhance_pptx.py apply projects/fire_station_native_enhance_20260626
python3 scripts/native_enhance_pptx.py validate projects/fire_station_native_enhance_20260626
python3 scripts/native_narration_pptx.py validate projects/native_enhance_project
Dependencies:
ffprobe for audio-duration-based auto-advance timings.
Same as native_enhance_pptx_core.py.
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from xml.etree import ElementTree as ET
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from console_encoding import configure_utf8_stdio # noqa: E402
from pptx_animations import ( # noqa: E402
object_animation_fingerprint,
validate_pptx_animation_package,
)
from pptx_transitions import ( # noqa: E402
AdvanceUpdate,
EnterUpdate,
TRANSITIONS,
apply_slide_motion_xml,
set_directory_use_timings,
validate_pptx_transition_package,
validate_seconds,
)
from svg_to_pptx.pptx_package.builder import ( # noqa: E402
_add_default_content_type,
_append_relationship,
_ensure_notes_master,
)
from svg_to_pptx.pptx_package.narration import ( # noqa: E402
AUDIO_CONTENT_TYPES,
AUDIO_MARKER_PNG_BYTES,
AUDIO_REL_TYPE,
IMAGE_REL_TYPE,
MEDIA_REL_TYPE,
NARRATION_EXTENSIONS,
inject_narration,
next_shape_id,
probe_audio_duration,
)
from svg_to_pptx.pptx_package.notes import ( # noqa: E402
create_notes_slide_rels_xml,
create_notes_slide_xml,
markdown_to_plain_text,
)
from native_enhance_pptx_core import main # noqa: E402
configure_utf8_stdio()
PROJECT_SCHEMA = "native_pptx_enhancement_project.v1"
LEGACY_PROJECT_SCHEMAS = {"native_narration_pptx_project.v1"}
NOTES_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"
PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"
REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
CONTENT_TYPE_NOTES_SLIDE = (
"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
)
CONTENT_TYPE_NOTES_MASTER = (
"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"
)
CONTENT_TYPE_THEME = "application/vnd.openxmlformats-officedocument.theme+xml"
@dataclass(frozen=True)
class SlidePart:
index: int
part_name: str
slide_number: int
def _sanitize_slug(value: str) -> str:
slug = re.sub(r"[^0-9A-Za-z_-]+", "_", value).strip("_")
return slug or "native_enhance"
def _positive_seconds_arg(value: str) -> float:
try:
return validate_seconds(value, "transition duration", allow_zero=False)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def _non_negative_seconds_arg(value: str) -> float:
try:
return validate_seconds(value, "narration padding", allow_zero=True)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def _read_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def _write_json(path: Path, data: dict) -> None:
path.write_text(
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def _is_relative_to(path: Path, parent: Path) -> bool:
try:
path.resolve().relative_to(parent.resolve())
return True
except ValueError:
return False
def _archive_source_pptx(source_pptx: Path, archived_pptx: Path, projects_root: Path) -> str:
"""Move project-local sources into the project; copy external sources."""
archived_pptx.parent.mkdir(parents=True, exist_ok=True)
if source_pptx.resolve() == archived_pptx.resolve():
return "reuse"
if _is_relative_to(source_pptx, projects_root):
shutil.move(str(source_pptx), str(archived_pptx))
return "move"
shutil.copy2(source_pptx, archived_pptx)
return "copy"
def _relationship_file_for_part(extract_dir: Path, part_name: str) -> Path:
part = Path(part_name)
return extract_dir / part.parent / "_rels" / f"{part.name}.rels"
def _ensure_rels_file(path: Path) -> None:
if path.exists():
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
f'<Relationships xmlns="{PACKAGE_REL_NS}">\n</Relationships>',
encoding="utf-8",
)
def _remove_relationships_by_type(rels_path: Path, rel_type: str) -> None:
if not rels_path.exists():
return
content = rels_path.read_text(encoding="utf-8")
content = re.sub(
rf'\s*<Relationship\b[^>]*\bType="{re.escape(rel_type)}"[^>]*/>',
"",
content,
)
rels_path.write_text(content, encoding="utf-8")
def _target_to_part(target: str) -> str:
target = target.lstrip("/")
if target.startswith("ppt/"):
return target
return f"ppt/{target}"
def _slide_number_from_part(part_name: str) -> int:
match = re.search(r"slide(\d+)\.xml$", part_name)
if not match:
raise ValueError(f"Unsupported slide part name: {part_name}")
return int(match.group(1))
def read_slide_parts(extract_dir: Path) -> list[SlidePart]:
presentation_path = extract_dir / "ppt" / "presentation.xml"
rels_path = extract_dir / "ppt" / "_rels" / "presentation.xml.rels"
if not presentation_path.exists() or not rels_path.exists():
raise RuntimeError("PPTX package is missing presentation.xml or its relationships")
rels_root = ET.parse(rels_path).getroot()
rels: dict[str, str] = {}
for rel in rels_root.findall(f"{{{PACKAGE_REL_NS}}}Relationship"):
rel_id = rel.attrib.get("Id")
target = rel.attrib.get("Target")
if rel_id and target:
rels[rel_id] = target
presentation_root = ET.parse(presentation_path).getroot()
slide_parts: list[SlidePart] = []
for index, slide_id in enumerate(
presentation_root.findall(f".//{{{PRESENTATION_NS}}}sldId"),
1,
):
rel_id = slide_id.attrib.get(f"{{{REL_NS}}}id")
if not rel_id or rel_id not in rels:
continue
part_name = _target_to_part(rels[rel_id])
slide_parts.append(
SlidePart(
index=index,
part_name=part_name,
slide_number=_slide_number_from_part(part_name),
)
)
if not slide_parts:
raise RuntimeError("No slides found in presentation.xml")
return slide_parts
def _zip_dir(source_dir: Path, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
for path in sorted(source_dir.rglob("*")):
if path.is_file():
zf.write(path, path.relative_to(source_dir).as_posix())
def _extract_pptx(source_pptx: Path, extract_dir: Path) -> None:
with zipfile.ZipFile(source_pptx, "r") as zf:
zf.extractall(extract_dir)
def _note_path(notes_dir: Path, index: int) -> Path | None:
candidates = [
notes_dir / f"{index:03d}.md",
notes_dir / f"{index:02d}.md",
notes_dir / f"{index}.md",
notes_dir / f"slide{index:03d}.md",
notes_dir / f"slide{index:02d}.md",
notes_dir / f"slide{index}.md",
]
for candidate in candidates:
if candidate.exists():
return candidate
return None
def _audio_path(audio_dir: Path, index: int) -> Path | None:
stems = [
f"{index:03d}",
f"{index:02d}",
str(index),
f"slide{index:03d}",
f"slide{index:02d}",
f"slide{index}",
]
for stem in stems:
for ext in NARRATION_EXTENSIONS:
candidate = audio_dir / f"{stem}{ext}"
if candidate.exists():
return candidate
return None
def _add_override(content_types: str, part_name: str, content_type: str) -> str:
if re.search(
rf'<Override\b[^>]*\bPartName="/{re.escape(part_name)}"[^>]*/>',
content_types,
):
return content_types
override = f' <Override PartName="/{part_name}" ContentType="{content_type}"/>'
return content_types.replace("</Types>", override + "\n</Types>")
def _add_notes_content_types(content_types: str, note_indices: set[int]) -> str:
content_types = _add_override(content_types, "ppt/theme/theme2.xml", CONTENT_TYPE_THEME)
content_types = _add_override(
content_types,
"ppt/notesMasters/notesMaster1.xml",
CONTENT_TYPE_NOTES_MASTER,
)
for index in sorted(note_indices):
content_types = _add_override(
content_types,
f"ppt/notesSlides/notesSlide{index}.xml",
CONTENT_TYPE_NOTES_SLIDE,
)
return content_types
def _apply_notes(extract_dir: Path, slide: SlidePart, note_md: Path) -> None:
notes_text = markdown_to_plain_text(note_md.read_text(encoding="utf-8"))
if not notes_text:
return
_ensure_notes_master(extract_dir)
notes_dir = extract_dir / "ppt" / "notesSlides"
notes_dir.mkdir(parents=True, exist_ok=True)
notes_xml_path = notes_dir / f"notesSlide{slide.index}.xml"
notes_xml_path.write_text(
create_notes_slide_xml(slide.slide_number, notes_text),
encoding="utf-8",
)
notes_rels_dir = notes_dir / "_rels"
notes_rels_dir.mkdir(parents=True, exist_ok=True)
notes_rels_path = notes_rels_dir / f"notesSlide{slide.index}.xml.rels"
notes_rels_path.write_text(
create_notes_slide_rels_xml(slide.slide_number),
encoding="utf-8",
)
slide_rels = _relationship_file_for_part(extract_dir, slide.part_name)
_ensure_rels_file(slide_rels)
_remove_relationships_by_type(slide_rels, NOTES_REL_TYPE)
_append_relationship(
slide_rels,
NOTES_REL_TYPE,
f"../notesSlides/notesSlide{slide.index}.xml",
)
def _apply_audio(
extract_dir: Path,
slide: SlidePart,
audio_path: Path,
*,
enter: EnterUpdate,
timings_enabled: bool,
narration_padding: float,
) -> bool:
media_dir = extract_dir / "ppt" / "media"
media_dir.mkdir(parents=True, exist_ok=True)
ext = audio_path.suffix.lower()
media_name = f"native_enhance_audio_{slide.index:03d}{ext}"
shutil.copy2(audio_path, media_dir / media_name)
poster_name = "native_enhance_audio_poster.png"
poster_path = media_dir / poster_name
if not poster_path.exists():
poster_path.write_bytes(AUDIO_MARKER_PNG_BYTES)
slide_rels = _relationship_file_for_part(extract_dir, slide.part_name)
_ensure_rels_file(slide_rels)
media_rid = _append_relationship(slide_rels, MEDIA_REL_TYPE, f"../media/{media_name}")
audio_rid = _append_relationship(slide_rels, AUDIO_REL_TYPE, f"../media/{media_name}")
poster_rid = _append_relationship(slide_rels, IMAGE_REL_TYPE, f"../media/{poster_name}")
slide_xml_path = extract_dir / slide.part_name
slide_xml = slide_xml_path.read_text(encoding="utf-8")
source_animation_fingerprint = object_animation_fingerprint(slide_xml)
shape_id = next_shape_id(slide_xml)
slide_xml = inject_narration(
slide_xml,
shape_id=shape_id,
shape_name=media_name,
audio_rid=audio_rid,
media_rid=media_rid,
poster_rid=poster_rid,
)
advance = AdvanceUpdate(mode="preserve")
if timings_enabled:
duration = probe_audio_duration(audio_path)
if duration is None:
raise RuntimeError(f"Unable to read narration duration with ffprobe: {audio_path}")
advance = AdvanceUpdate(
mode="narration",
after=duration + narration_padding,
)
wrote_advance = False
if enter.policy != "preserve" or timings_enabled:
slide_xml, wrote_advance = apply_slide_motion_xml(
slide_xml,
enter=enter,
advance=advance,
)
if object_animation_fingerprint(slide_xml) != source_animation_fingerprint:
raise RuntimeError(
f"Slide {slide.index} object animations changed while adding narration"
)
slide_xml_path.write_text(slide_xml, encoding="utf-8")
return timings_enabled and wrote_advance
def _update_content_types(extract_dir: Path, note_indices: set[int], audio_exts: set[str]) -> None:
content_types_path = extract_dir / "[Content_Types].xml"
content_types = content_types_path.read_text(encoding="utf-8")
if note_indices:
content_types = _add_notes_content_types(content_types, note_indices)
for ext in sorted(audio_exts):
content_type = AUDIO_CONTENT_TYPES.get(ext)
if content_type:
content_types = _add_default_content_type(content_types, ext, content_type)
if audio_exts:
content_types = _add_default_content_type(content_types, "png", "image/png")
content_types_path.write_text(content_types, encoding="utf-8")
def _project_paths(project_path: Path) -> tuple[Path, Path, Path, Path]:
project = _read_json(project_path / "project.json")
source_pptx = project_path / project["source_pptx"]
notes_dir = project_path / project["notes_dir"]
audio_dir = project_path / project["audio_dir"]
exports_dir = project_path / project["exports_dir"]
return source_pptx, notes_dir, audio_dir, exports_dir
def _plan_path(project_path: Path) -> Path:
return project_path / "analysis" / "enhancement_plan.json"
def _load_enhancement_plan(project_path: Path) -> dict:
path = _plan_path(project_path)
if not path.exists():
return {}
return _read_json(path)
def _enabled_modules(plan: dict) -> set[str]:
modules = plan.get("modules")
if not isinstance(modules, dict):
return {"notes", "audio", "timings", "transitions"}
enabled: set[str] = set()
for name, config in modules.items():
if isinstance(config, dict) and config.get("enabled") is True:
enabled.add(str(name))
return enabled
def _resolve_enter_update(
*,
cli_effect: str | None,
configured_effect: object,
transitions_enabled: bool,
duration: float,
) -> EnterUpdate:
if cli_effect is None and not transitions_enabled:
if configured_effect == "none":
return EnterUpdate(policy="none", effect=None, duration=duration)
return EnterUpdate(policy="preserve", duration=duration)
effect = cli_effect if cli_effect is not None else configured_effect
if not isinstance(effect, str):
raise ValueError(f"transition effect must be a string: {effect!r}")
if effect == "none":
return EnterUpdate(policy="none", effect=None, duration=duration)
if effect not in TRANSITIONS:
valid = ", ".join(sorted(TRANSITIONS))
raise ValueError(
f"unknown transition effect {effect!r}; valid effects: {valid}, none"
)
return EnterUpdate(policy="replace", effect=effect, duration=duration)
def _plan_confirmed(plan: dict) -> bool:
return plan.get("status") == "confirmed"
def _build_enhancement_plan(
project: dict,
*,
slide_count: int,
notes_count: int,
audio_count: int,
transition: str,
transition_duration: float,
narration_padding: float,
apply_transition_without_audio: bool,
) -> dict:
return {
"schema": "native_pptx_enhancement_plan.v1",
"status": "draft",
"source_pptx": project.get("source_pptx"),
"slide_count": slide_count,
"modules": {
"notes": {
"enabled": True,
"requires_confirmation": True,
"status": "ready" if notes_count == slide_count else "needs_notes",
"coverage": {"ready": notes_count, "total": slide_count},
},
"audio": {
"enabled": True,
"requires_confirmation": True,
"status": "ready" if audio_count == slide_count else "needs_audio",
"coverage": {"ready": audio_count, "total": slide_count},
},
"timings": {
"enabled": True,
"requires_confirmation": True,
"status": "ready" if audio_count == slide_count else "blocked_until_audio",
"source": "audio_duration",
"narration_padding": narration_padding,
},
"transitions": {
"enabled": transition != "none",
"requires_confirmation": True,
"status": "ready",
"effect": transition,
"duration": transition_duration,
"apply_without_audio": apply_transition_without_audio,
},
},
"not_in_v1": [
"object_animation",
"visible_watermark",
"footer_or_logo_insertion",
"background_music",
"media_compression",
],
}
def init_project(args: argparse.Namespace) -> int:
source_pptx = Path(args.source_pptx).expanduser().resolve()
if not source_pptx.exists() or source_pptx.suffix.lower() != ".pptx":
print(f"error: expected an existing .pptx file: {source_pptx}", file=sys.stderr)
return 1
stem = _sanitize_slug(args.name or source_pptx.stem)
date = datetime.now().strftime("%Y%m%d")
project_path = (
Path(args.project_dir).expanduser().resolve()
if args.project_dir
else Path(args.projects_root).expanduser().resolve() / f"{stem}_native_enhance_{date}"
)
if project_path.exists() and any(project_path.iterdir()):
print(f"error: project directory already exists and is not empty: {project_path}", file=sys.stderr)
return 1
for dirname in ("sources", "analysis", "notes", "audio", "exports", "validation"):
(project_path / dirname).mkdir(parents=True, exist_ok=True)
archived_pptx = project_path / "sources" / source_pptx.name
projects_root = Path(args.projects_root).expanduser().resolve()
source_import_mode = _archive_source_pptx(source_pptx, archived_pptx, projects_root)
source_md = project_path / "sources" / f"{source_pptx.stem}.md"
ppt_to_md = _SCRIPTS_DIR / "source_to_md" / "ppt_to_md.py"
result = subprocess.run(
[sys.executable, str(ppt_to_md), str(archived_pptx), "-o", str(source_md)],
check=False,
text=True,
capture_output=True,
)
if result.returncode != 0:
print(result.stderr or result.stdout, file=sys.stderr)
return result.returncode
with tempfile.TemporaryDirectory(prefix="native-enhance-intake-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(archived_pptx, extract_dir)
slide_parts = read_slide_parts(extract_dir)
slide_index = {
"schema": "native_pptx_enhancement_slide_index.v1",
"source_pptx": f"sources/{source_pptx.name}",
"slide_count": len(slide_parts),
"slides": [
{
"index": slide.index,
"note_file": f"notes/{slide.index:03d}.md",
"audio_stem": f"{slide.index:03d}",
"part_name": slide.part_name,
"slide_number": slide.slide_number,
}
for slide in slide_parts
],
}
_write_json(project_path / "analysis" / "slide_index.json", slide_index)
project = {
"schema": PROJECT_SCHEMA,
"kind": "native_pptx_enhancement",
"modules": ["notes", "audio", "timings", "transitions"],
"source_pptx": f"sources/{source_pptx.name}",
"source_markdown": f"sources/{source_pptx.stem}.md",
"source_import": {
"mode": source_import_mode,
"original_path": str(source_pptx),
},
"slide_count": len(slide_parts),
"notes_dir": "notes",
"audio_dir": "audio",
"exports_dir": "exports",
"transition": {
"effect": args.transition,
"duration": args.transition_duration,
},
"audio": {
"provider": "",
"voice": "",
"rate": "",
},
}
_write_json(project_path / "project.json", project)
plan = _build_enhancement_plan(
project,
slide_count=len(slide_parts),
notes_count=0,
audio_count=0,
transition=args.transition,
transition_duration=args.transition_duration,
narration_padding=args.narration_padding,
apply_transition_without_audio=args.apply_transition_without_audio,
)
_write_json(_plan_path(project_path), plan)
print(f"Project: {project_path}", file=sys.stderr)
print(f"Slides: {len(slide_parts)}", file=sys.stderr)
print(f"Source import: {source_import_mode}", file=sys.stderr)
print(f"Source markdown: {source_md}", file=sys.stderr)
print(f"Draft enhancement plan: {_plan_path(project_path)}", file=sys.stderr)
print(
"Review the plan with the user and set status to \"confirmed\" before generating notes/audio/applying.",
file=sys.stderr,
)
return 0
def plan_project(args: argparse.Namespace) -> int:
project_path = Path(args.project_path).expanduser().resolve()
project = _read_json(project_path / "project.json")
if project.get("schema") not in {PROJECT_SCHEMA, *LEGACY_PROJECT_SCHEMAS}:
print(f"error: not a native PPTX enhancement project: {project_path}", file=sys.stderr)
return 1
source_pptx, notes_dir, audio_dir, _exports_dir = _project_paths(project_path)
with tempfile.TemporaryDirectory(prefix="native-enhance-plan-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(source_pptx, extract_dir)
slides = read_slide_parts(extract_dir)
notes_count = sum(1 for slide in slides if _note_path(notes_dir, slide.index) is not None)
audio_count = sum(1 for slide in slides if _audio_path(audio_dir, slide.index) is not None)
plan = _build_enhancement_plan(
project,
slide_count=len(slides),
notes_count=notes_count,
audio_count=audio_count,
transition=args.transition,
transition_duration=args.transition_duration,
narration_padding=args.narration_padding,
apply_transition_without_audio=args.apply_transition_without_audio,
)
_write_json(_plan_path(project_path), plan)
print(json.dumps(plan, ensure_ascii=False, indent=2))
print(f"Plan written: {_plan_path(project_path)}", file=sys.stderr)
print(
"Confirm by editing status to \"confirmed\" after user approval, then run apply.",
file=sys.stderr,
)
return 0
def apply_project(args: argparse.Namespace) -> int:
project_path = Path(args.project_path).expanduser().resolve()
project = _read_json(project_path / "project.json")
if project.get("schema") not in {PROJECT_SCHEMA, *LEGACY_PROJECT_SCHEMAS}:
print(f"error: not a native PPTX enhancement project: {project_path}", file=sys.stderr)
return 1
source_pptx, notes_dir, audio_dir, exports_dir = _project_paths(project_path)
transition_cfg = project.get("transition", {}) if isinstance(project.get("transition"), dict) else {}
plan = _load_enhancement_plan(project_path)
if not _plan_confirmed(plan) and not args.force:
print(
f"error: enhancement plan is not confirmed: {_plan_path(project_path)} "
"(run plan, get user confirmation, set status to \"confirmed\", or pass --force)",
file=sys.stderr,
)
return 1
modules = _enabled_modules(plan)
modules_cfg = plan.get("modules") if isinstance(plan.get("modules"), dict) else {}
transitions_cfg = modules_cfg.get("transitions", {})
if not isinstance(transitions_cfg, dict):
transitions_cfg = {}
timings_cfg = modules_cfg.get("timings", {})
if not isinstance(timings_cfg, dict):
timings_cfg = {}
if "effect" in transitions_cfg:
configured_effect = transitions_cfg["effect"]
elif "effect" in transition_cfg:
configured_effect = transition_cfg["effect"]
else:
configured_effect = "fade"
if args.transition_duration is not None:
raw_transition_duration = args.transition_duration
elif "duration" in transitions_cfg:
raw_transition_duration = transitions_cfg["duration"]
elif "duration" in transition_cfg:
raw_transition_duration = transition_cfg["duration"]
else:
raw_transition_duration = 0.5
if args.narration_padding is not None:
raw_narration_padding = args.narration_padding
elif "narration_padding" in timings_cfg:
raw_narration_padding = timings_cfg["narration_padding"]
else:
raw_narration_padding = 0.4
try:
if args.transition is not None or "transitions" in modules:
transition_duration = validate_seconds(
raw_transition_duration,
"transition duration",
allow_zero=False,
)
else:
transition_duration = 0.5
if "timings" in modules:
narration_padding = validate_seconds(
raw_narration_padding,
"narration padding",
allow_zero=True,
)
else:
narration_padding = 0.4
enter_update = _resolve_enter_update(
cli_effect=args.transition,
configured_effect=configured_effect,
transitions_enabled="transitions" in modules,
duration=transition_duration,
)
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
apply_transition_without_audio = (
args.apply_transition_without_audio
or bool(transitions_cfg.get("apply_without_audio"))
)
output_path = (
Path(args.output).expanduser().resolve()
if args.output
else exports_dir / f"{source_pptx.stem}_enhanced.pptx"
)
if output_path.exists() and not args.overwrite:
print(f"error: output already exists, pass --overwrite: {output_path}", file=sys.stderr)
return 1
with tempfile.TemporaryDirectory(prefix="native-enhance-pptx-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(source_pptx, extract_dir)
slides = read_slide_parts(extract_dir)
note_indices: set[int] = set()
audio_exts: set[str] = set()
audio_count = 0
transition_only_count = 0
wrote_auto_advance = False
for slide in slides:
note = _note_path(notes_dir, slide.index)
if "notes" in modules and note:
_apply_notes(extract_dir, slide, note)
note_indices.add(slide.index)
audio = _audio_path(audio_dir, slide.index)
if "audio" in modules and audio:
wrote_auto_advance = _apply_audio(
extract_dir,
slide,
audio,
enter=enter_update,
timings_enabled="timings" in modules,
narration_padding=narration_padding,
) or wrote_auto_advance
audio_exts.add(audio.suffix.lower())
audio_count += 1
continue
if (
apply_transition_without_audio
and enter_update.policy != "preserve"
):
slide_xml_path = extract_dir / slide.part_name
slide_xml = slide_xml_path.read_text(encoding="utf-8")
source_animation_fingerprint = object_animation_fingerprint(
slide_xml
)
slide_xml, _uses_timings = apply_slide_motion_xml(
slide_xml,
enter=enter_update,
advance=AdvanceUpdate(mode="preserve"),
)
if (
object_animation_fingerprint(slide_xml)
!= source_animation_fingerprint
):
raise RuntimeError(
f"Slide {slide.index} object animations changed while "
"updating the transition"
)
slide_xml_path.write_text(
slide_xml,
encoding="utf-8",
)
transition_only_count += 1
_update_content_types(extract_dir, note_indices, audio_exts)
if wrote_auto_advance:
set_directory_use_timings(extract_dir)
output_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(
prefix="native-enhance-output-",
dir=output_path.parent,
) as output_tmp:
candidate_path = Path(output_tmp) / output_path.name
_zip_dir(extract_dir, candidate_path)
try:
validate_pptx_transition_package(
candidate_path,
require_use_timings=wrote_auto_advance,
)
except ValueError as exc:
raise RuntimeError(
f"PPTX transition package validation failed: {exc}"
) from exc
try:
validate_pptx_animation_package(
candidate_path,
require_supported_effects=False,
)
except ValueError as exc:
raise RuntimeError(
f"PPTX animation/timing package validation failed: {exc}"
) from exc
candidate_path.replace(output_path)
print(f"Output: {output_path}", file=sys.stderr)
print(f"Notes applied: {len(note_indices)}", file=sys.stderr)
print(f"Audio embedded: {audio_count}", file=sys.stderr)
if transition_only_count:
print(f"Transition-only slides: {transition_only_count}", file=sys.stderr)
return 0
def validate_project(args: argparse.Namespace) -> int:
project_path = Path(args.project_path).expanduser().resolve()
project = _read_json(project_path / "project.json")
if project.get("schema") not in {PROJECT_SCHEMA, *LEGACY_PROJECT_SCHEMAS}:
print(f"error: not a native PPTX enhancement project: {project_path}", file=sys.stderr)
return 1
source_pptx, notes_dir, audio_dir, _exports_dir = _project_paths(project_path)
with tempfile.TemporaryDirectory(prefix="native-enhance-validate-") as tmp:
extract_dir = Path(tmp) / "pptx"
_extract_pptx(source_pptx, extract_dir)
slides = read_slide_parts(extract_dir)
plan = _load_enhancement_plan(project_path)
modules = _enabled_modules(plan)
notes_count = sum(1 for slide in slides if _note_path(notes_dir, slide.index) is not None)
audio_count = sum(1 for slide in slides if _audio_path(audio_dir, slide.index) is not None)
missing_notes = (
[slide.index for slide in slides if _note_path(notes_dir, slide.index) is None]
if "notes" in modules
else []
)
missing_audio = (
[slide.index for slide in slides if _audio_path(audio_dir, slide.index) is None]
if "audio" in modules
else []
)
report = {
"schema": "native_pptx_enhancement_validation.v1",
"slide_count": len(slides),
"plan_status": plan.get("status") or "missing",
"enabled_modules": sorted(modules),
"notes_required": "notes" in modules,
"audio_required": "audio" in modules,
"notes_count": notes_count,
"audio_count": audio_count,
"missing_notes": missing_notes,
"missing_audio": missing_audio,
}
validation_dir = project_path / "validation"
validation_dir.mkdir(exist_ok=True)
_write_json(validation_dir / "report.json", report)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if not missing_notes and not missing_audio else 2
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Create/apply a native existing-PPTX enhancement project without SVG conversion.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
init = subparsers.add_parser("init", help="create a native PPTX enhancement project")
init.add_argument("source_pptx", help="source .pptx file")
init.add_argument("--name", default=None, help="ASCII project name slug")
init.add_argument("--project-dir", default=None, help="explicit project directory")
init.add_argument("--projects-root", default="projects", help="projects root (default: projects)")
init.add_argument("--transition", default="fade", choices=sorted(TRANSITIONS.keys()))
init.add_argument("--transition-duration", type=_positive_seconds_arg, default=0.5)
init.add_argument("--narration-padding", type=_non_negative_seconds_arg, default=0.4)
init.add_argument(
"--apply-transition-without-audio",
action="store_true",
help="draft the plan with page transitions for slides without audio",
)
init.set_defaults(func=init_project)
plan = subparsers.add_parser("plan", help="draft an enhancement module plan")
plan.add_argument("project_path", help="native enhancement project directory")
plan.add_argument("--transition", default="fade", choices=sorted(TRANSITIONS.keys()) + ["none"])
plan.add_argument("--transition-duration", type=_positive_seconds_arg, default=0.5)
plan.add_argument("--narration-padding", type=_non_negative_seconds_arg, default=0.4)
plan.add_argument(
"--apply-transition-without-audio",
action="store_true",
help="include page transitions for slides without audio",
)
plan.set_defaults(func=plan_project)
apply = subparsers.add_parser("apply", help="patch notes/audio/timings into a copied PPTX")
apply.add_argument("project_path", help="native narration project directory")
apply.add_argument("-o", "--output", default=None, help="output .pptx path")
apply.add_argument("--overwrite", action="store_true", help="overwrite output if it exists")
apply.add_argument("--transition", default=None, choices=sorted(TRANSITIONS.keys()) + ["none"])
apply.add_argument("--transition-duration", type=_positive_seconds_arg, default=None)
apply.add_argument("--narration-padding", type=_non_negative_seconds_arg, default=None)
apply.add_argument("--force", action="store_true", help="apply without a confirmed enhancement plan")
apply.add_argument(
"--apply-transition-without-audio",
action="store_true",
help="also write page transitions on slides that do not have audio",
)
apply.set_defaults(func=apply_project)
validate = subparsers.add_parser("validate", help="check notes/audio coverage")
validate.add_argument("project_path", help="native narration project directory")
validate.set_defaults(func=validate_project)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,655 @@
#!/usr/bin/env python3
"""
PPT Master - Native SVG Metadata Store
Deduplicate opaque PowerPoint-native payloads and repeated restoration
attributes into one deterministic gzip-compressed workspace store, then
hydrate legacy inline metadata on demand.
Usage:
Imported by mirror materialization, SVG validation, and SVG-to-PPTX export.
Examples:
from native_payloads import hydrate_native_payload_refs
Dependencies:
None (standard library only).
"""
from __future__ import annotations
import base64
import binascii
import gzip
import hashlib
import json
import re
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from xml.etree import ElementTree as ET
LEGACY_PAYLOAD_STORE_SCHEMA = "ppt-master.native-payload-store.v1"
PAYLOAD_STORE_SCHEMA = "ppt-master.native-payload-store.v2"
PAYLOAD_STORE_FILENAME = "native_payloads.json.gz"
PAYLOAD_STORE_RELATIVE_PATH = Path("templates") / PAYLOAD_STORE_FILENAME
PAYLOAD_STORE_REFERENCE_PREFIX = (
f"project:{PAYLOAD_STORE_RELATIVE_PATH.as_posix()}#sha256:"
)
NATIVE_RECORD_REF_ATTRIBUTE = "data-pptx-native-ref"
TXBODY_REF_ATTRIBUTE = "data-pptx-ref"
SHAPE_STYLE_ATTRIBUTE = "data-pptx-shape-style"
SHAPE_STYLE_REF_ATTRIBUTE = "data-pptx-shape-style-ref"
CUSTOM_GEOMETRY_ATTRIBUTE = "data-pptx-custgeom"
CUSTOM_GEOMETRY_REF_ATTRIBUTE = "data-pptx-custgeom-ref"
_SHA256_RE = re.compile(r"[0-9a-f]{64}")
_NATIVE_RECORD_ID_RE = re.compile(r"r(?:0|[1-9][0-9]*)")
_NATIVE_RECORD_ATTRIBUTES = frozenset({
"data-pptx-custgeom-ref",
"data-pptx-frame",
"data-pptx-geometry-kind",
"data-pptx-geometry-sha256",
"data-pptx-object",
"data-pptx-part",
"data-pptx-preview-sha256",
"data-pptx-prst",
"data-pptx-ref",
"data-pptx-shape-id",
"data-pptx-shape-name",
"data-pptx-shape-scope",
"data-pptx-shape-style-ref",
"data-pptx-text-sha256",
})
_NATIVE_RECORD_PREFIXES = (
"data-pptx-av-",
"data-pptx-end-",
"data-pptx-start-",
)
class NativePayloadError(ValueError):
"""Reject malformed, missing, or contradictory native metadata transport."""
@dataclass
class NativePayloadStats:
"""Count externalized native metadata and its original inline bytes."""
txbody_count: int = 0
shape_style_count: int = 0
custom_geometry_count: int = 0
inline_bytes: int = 0
native_record_count: int = 0
native_attribute_bytes: int = 0
def merge(self, other: "NativePayloadStats") -> None:
self.txbody_count += other.txbody_count
self.shape_style_count += other.shape_style_count
self.custom_geometry_count += other.custom_geometry_count
self.inline_bytes += other.inline_bytes
self.native_record_count += other.native_record_count
self.native_attribute_bytes += other.native_attribute_bytes
def as_dict(self) -> dict[str, int]:
return {
"txbody_count": self.txbody_count,
"shape_style_count": self.shape_style_count,
"custom_geometry_count": self.custom_geometry_count,
"inline_bytes": self.inline_bytes,
"native_record_count": self.native_record_count,
"native_attribute_bytes": self.native_attribute_bytes,
}
@dataclass(frozen=True)
class NativePayloadStore:
"""Validated payload and native-attribute records loaded from one store."""
payloads: dict[str, bytes]
native_records: dict[str, dict[str, str]]
_STORE_CACHE: dict[Path, tuple[int, int, NativePayloadStore]] = {}
def _local_name(name: object) -> str:
return name.rsplit("}", 1)[-1] if isinstance(name, str) else ""
def _sha256(payload: bytes) -> str:
return hashlib.sha256(payload).hexdigest()
def _decode_base64(value: str, *, context: str) -> bytes:
try:
return base64.b64decode(value.strip(), validate=True)
except (ValueError, binascii.Error) as exc:
raise NativePayloadError(f"{context} is not valid base64: {exc}") from exc
def _register_payload(payloads: dict[str, bytes], raw: bytes) -> str:
digest = _sha256(raw)
existing = payloads.get(digest)
if existing is not None and existing != raw:
raise NativePayloadError(
f"SHA-256 collision while registering native payload {digest}"
)
payloads[digest] = raw
return PAYLOAD_STORE_REFERENCE_PREFIX + digest
def _is_native_record_attribute(name: str) -> bool:
return (
name in _NATIVE_RECORD_ATTRIBUTES
or name.startswith(_NATIVE_RECORD_PREFIXES)
)
def _native_record_attributes(element: ET.Element) -> dict[str, str]:
return {
name: value
for name, value in element.attrib.items()
if _is_native_record_attribute(name)
}
def _native_record_key(attributes: dict[str, str]) -> str:
return json.dumps(
attributes,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
)
def collect_native_attribute_record_keys(root: ET.Element) -> set[str]:
"""Return canonical record keys for compressible native attributes."""
keys: set[str] = set()
for element in root.iter():
if NATIVE_RECORD_REF_ATTRIBUTE in element.attrib:
raise NativePayloadError(
"native metadata is already externalized; source attributes "
"must be hydrated before building a new store"
)
attributes = _native_record_attributes(element)
if attributes:
keys.add(_native_record_key(attributes))
return keys
def build_native_attribute_records(
record_keys: set[str],
) -> tuple[dict[str, str], dict[str, dict[str, str]]]:
"""Assign deterministic short ids to canonical native-attribute records."""
ids_by_key: dict[str, str] = {}
records: dict[str, dict[str, str]] = {}
for index, key in enumerate(sorted(record_keys)):
try:
attributes = json.loads(key)
except json.JSONDecodeError as exc:
raise NativePayloadError(
f"Cannot decode canonical native-attribute record: {exc}"
) from exc
if not isinstance(attributes, dict) or not attributes:
raise NativePayloadError(
"Canonical native-attribute record must be a non-empty object"
)
if any(
not isinstance(name, str)
or not isinstance(value, str)
or not _is_native_record_attribute(name)
for name, value in attributes.items()
):
raise NativePayloadError(
"Canonical native-attribute record contains an unsupported field"
)
record_id = f"r{index}"
ids_by_key[key] = record_id
records[record_id] = attributes
return ids_by_key, records
def externalize_native_attribute_records(
root: ET.Element,
ids_by_key: dict[str, str],
) -> NativePayloadStats:
"""Replace supported native-attribute groups with deterministic short ids."""
stats = NativePayloadStats()
for element in root.iter():
if NATIVE_RECORD_REF_ATTRIBUTE in element.attrib:
raise NativePayloadError(
"native metadata is already externalized; source attributes "
"must be hydrated before building a new store"
)
attributes = _native_record_attributes(element)
if not attributes:
continue
record_id = ids_by_key.get(_native_record_key(attributes))
if record_id is None:
raise NativePayloadError(
"Native-attribute record was not registered before externalization"
)
for name in attributes:
element.attrib.pop(name)
element.set(NATIVE_RECORD_REF_ATTRIBUTE, record_id)
stats.native_record_count += 1
stats.native_attribute_bytes += sum(
len(name) + len(value) + 4
for name, value in attributes.items()
)
return stats
def externalize_native_payloads(
root: ET.Element,
payloads: dict[str, bytes],
) -> NativePayloadStats:
"""Move supported large inline native payloads into ``payloads``.
Repeated restoration attributes are handled separately by
``externalize_native_attribute_records``. The supported opaque payload
classes are:
- ``metadata[data-pptx-part="txbody"]``
- ``data-pptx-shape-style``
- ``data-pptx-custgeom``
"""
stats = NativePayloadStats()
for element in root.iter():
if (
_local_name(element.tag) == "metadata"
and element.get("data-pptx-part") == "txbody"
):
has_reference = TXBODY_REF_ATTRIBUTE in element.attrib
reference = element.get(TXBODY_REF_ATTRIBUTE) or ""
encoded = (element.text or "").strip()
encoding = element.get("data-pptx-encoding")
if has_reference:
if not reference:
raise NativePayloadError(
"txbody metadata has an empty payload reference"
)
raise NativePayloadError(
"txbody metadata is already externalized; source payload "
"must be hydrated before building a new store"
)
elif encoded:
if encoding != "base64":
raise NativePayloadError(
"txbody metadata must use base64 before externalization"
)
raw = _decode_base64(encoded, context="txbody metadata")
element.text = None
element.attrib.pop("data-pptx-encoding", None)
element.set(TXBODY_REF_ATTRIBUTE, _register_payload(payloads, raw))
stats.txbody_count += 1
stats.inline_bytes += len(encoded)
else:
raise NativePayloadError(
"txbody metadata requires inline base64 data or a payload reference"
)
has_shape_reference = SHAPE_STYLE_REF_ATTRIBUTE in element.attrib
shape_reference = element.get(SHAPE_STYLE_REF_ATTRIBUTE) or ""
shape_encoded = element.get(SHAPE_STYLE_ATTRIBUTE)
if has_shape_reference and not shape_reference:
raise NativePayloadError("shape-style metadata has an empty payload reference")
if has_shape_reference:
raise NativePayloadError(
"shape-style metadata is already externalized; source payload "
"must be hydrated before building a new store"
)
if shape_encoded:
raw = _decode_base64(shape_encoded, context="shape-style metadata")
element.attrib.pop(SHAPE_STYLE_ATTRIBUTE, None)
element.set(
SHAPE_STYLE_REF_ATTRIBUTE,
_register_payload(payloads, raw),
)
stats.shape_style_count += 1
stats.inline_bytes += len(shape_encoded)
has_geometry_reference = CUSTOM_GEOMETRY_REF_ATTRIBUTE in element.attrib
geometry_reference = element.get(CUSTOM_GEOMETRY_REF_ATTRIBUTE) or ""
geometry_encoded = element.get(CUSTOM_GEOMETRY_ATTRIBUTE)
if has_geometry_reference and not geometry_reference:
raise NativePayloadError(
"custom-geometry metadata has an empty payload reference"
)
if has_geometry_reference:
raise NativePayloadError(
"custom-geometry metadata is already externalized; source payload "
"must be hydrated before building a new store"
)
if geometry_encoded:
raw = _decode_base64(geometry_encoded, context="custom-geometry metadata")
element.attrib.pop(CUSTOM_GEOMETRY_ATTRIBUTE, None)
element.set(
CUSTOM_GEOMETRY_REF_ATTRIBUTE,
_register_payload(payloads, raw),
)
stats.custom_geometry_count += 1
stats.inline_bytes += len(geometry_encoded)
return stats
def serialize_native_payload_store(
payloads: dict[str, bytes],
native_records: dict[str, dict[str, str]] | None = None,
) -> bytes:
"""Return one deterministic gzip-compressed native metadata store."""
encoded_payloads: dict[str, str] = {}
for digest, raw in sorted(payloads.items()):
if _SHA256_RE.fullmatch(digest) is None or _sha256(raw) != digest:
raise NativePayloadError(
f"Native payload store key does not match its content: {digest!r}"
)
encoded_payloads[digest] = base64.b64encode(raw).decode("ascii")
encoded_records: dict[str, dict[str, str]] = {}
for record_id, attributes in sorted((native_records or {}).items()):
if _NATIVE_RECORD_ID_RE.fullmatch(record_id) is None:
raise NativePayloadError(
f"Native metadata store contains an invalid record id: {record_id!r}"
)
if not isinstance(attributes, dict) or not attributes:
raise NativePayloadError(
f"Native metadata record {record_id} must be a non-empty object"
)
if any(
not isinstance(name, str)
or not isinstance(value, str)
or not _is_native_record_attribute(name)
for name, value in attributes.items()
):
raise NativePayloadError(
f"Native metadata record {record_id} contains an unsupported field"
)
encoded_records[record_id] = dict(sorted(attributes.items()))
document = {
"schema": PAYLOAD_STORE_SCHEMA,
"hash": "sha256",
"payloads": encoded_payloads,
"native_records": encoded_records,
}
raw_json = json.dumps(
document,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
return gzip.compress(raw_json, compresslevel=9, mtime=0)
def _parse_reference(value: str) -> tuple[PurePosixPath, str]:
prefix = "project:"
marker = "#sha256:"
if not value.startswith(prefix) or marker not in value:
raise NativePayloadError(
f"Unsupported native payload reference: {value!r}"
)
path_text, digest = value[len(prefix):].split(marker, 1)
relative = PurePosixPath(path_text)
if (
not path_text
or relative.is_absolute()
or any(part in {"", ".", ".."} for part in relative.parts)
):
raise NativePayloadError(
f"Native payload reference must use a safe project-relative path: {value!r}"
)
if _SHA256_RE.fullmatch(digest) is None:
raise NativePayloadError(
f"Native payload reference has an invalid SHA-256 digest: {value!r}"
)
return relative, digest
def _resolve_store_path(svg_path: Path, relative: PurePosixPath) -> Path:
start = Path(svg_path).expanduser().resolve().parent
relative_path = Path(*relative.parts)
for base in (start, *start.parents):
candidate = base / relative_path
if candidate.is_file():
return candidate.resolve()
raise NativePayloadError(
f"Native payload store not found for {svg_path}: {relative.as_posix()}"
)
def _load_store(path: Path) -> NativePayloadStore:
resolved = path.resolve()
try:
stat = resolved.stat()
except OSError as exc:
raise NativePayloadError(
f"Cannot inspect native payload store {resolved}: {exc}"
) from exc
cached = _STORE_CACHE.get(resolved)
if cached and cached[0] == stat.st_mtime_ns and cached[1] == stat.st_size:
return cached[2]
try:
document = json.loads(gzip.decompress(resolved.read_bytes()).decode("utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise NativePayloadError(
f"Cannot decode native payload store {resolved}: {exc}"
) from exc
if not isinstance(document, dict) or document.get("schema") not in {
LEGACY_PAYLOAD_STORE_SCHEMA,
PAYLOAD_STORE_SCHEMA,
}:
raise NativePayloadError(
f"Unsupported native payload store schema in {resolved}"
)
if document.get("hash") != "sha256":
raise NativePayloadError(
f"Unsupported native payload store hash algorithm in {resolved}"
)
encoded_payloads = document.get("payloads")
if not isinstance(encoded_payloads, dict):
raise NativePayloadError(
f"Native payload store must contain a payload object: {resolved}"
)
payloads: dict[str, bytes] = {}
for digest, encoded in encoded_payloads.items():
if not isinstance(digest, str) or _SHA256_RE.fullmatch(digest) is None:
raise NativePayloadError(
f"Native payload store contains an invalid digest key: {digest!r}"
)
if not isinstance(encoded, str):
raise NativePayloadError(
f"Native payload {digest} must be a base64 string"
)
raw = _decode_base64(
encoded,
context=f"native payload {digest} in {resolved}",
)
if _sha256(raw) != digest:
raise NativePayloadError(
f"Native payload {digest} failed content-hash validation"
)
payloads[digest] = raw
schema = document.get("schema")
if schema == PAYLOAD_STORE_SCHEMA and "native_records" not in document:
raise NativePayloadError(
f"Native payload store is missing native_records: {resolved}"
)
native_records_value = document.get("native_records", {})
if schema == PAYLOAD_STORE_SCHEMA and not isinstance(native_records_value, dict):
raise NativePayloadError(
f"Native payload store must contain a native_records object: {resolved}"
)
if schema == LEGACY_PAYLOAD_STORE_SCHEMA:
native_records_value = {}
native_records: dict[str, dict[str, str]] = {}
for record_id, attributes in native_records_value.items():
if (
not isinstance(record_id, str)
or _NATIVE_RECORD_ID_RE.fullmatch(record_id) is None
):
raise NativePayloadError(
f"Native payload store contains an invalid record id: {record_id!r}"
)
if not isinstance(attributes, dict) or not attributes:
raise NativePayloadError(
f"Native metadata record {record_id} must be a non-empty object"
)
validated: dict[str, str] = {}
for name, value in attributes.items():
if (
not isinstance(name, str)
or not isinstance(value, str)
or not _is_native_record_attribute(name)
):
raise NativePayloadError(
f"Native metadata record {record_id} contains an unsupported field"
)
validated[name] = value
native_records[record_id] = validated
store = NativePayloadStore(
payloads=payloads,
native_records=native_records,
)
_STORE_CACHE[resolved] = (stat.st_mtime_ns, stat.st_size, store)
return store
def _payload_for_reference(value: str, svg_path: Path) -> bytes:
relative, digest = _parse_reference(value)
store_path = _resolve_store_path(svg_path, relative)
payload = _load_store(store_path).payloads.get(digest)
if payload is None:
raise NativePayloadError(
f"Native payload {digest} is missing from {store_path}"
)
return payload
def _native_record_for_reference(
value: str,
svg_path: Path,
) -> dict[str, str]:
if _NATIVE_RECORD_ID_RE.fullmatch(value) is None:
raise NativePayloadError(
f"Native metadata reference has an invalid record id: {value!r}"
)
store_path = _resolve_store_path(
svg_path,
PurePosixPath(PAYLOAD_STORE_RELATIVE_PATH.as_posix()),
)
record = _load_store(store_path).native_records.get(value)
if record is None:
raise NativePayloadError(
f"Native metadata record {value} is missing from {store_path}"
)
return record
def hydrate_native_payload_refs(root: ET.Element, svg_path: Path) -> int:
"""Restore compact native records and payloads as legacy inline metadata.
The operation preflights every reference before mutating the tree, so an
invalid store leaves the caller's parsed SVG unchanged.
"""
record_operations: list[tuple[ET.Element, dict[str, str]]] = []
payload_operations: list[tuple[str, ET.Element, bytes]] = []
for element in root.iter():
effective_attributes = dict(element.attrib)
has_native_record = NATIVE_RECORD_REF_ATTRIBUTE in element.attrib
native_record_ref = element.get(NATIVE_RECORD_REF_ATTRIBUTE) or ""
if has_native_record:
if not native_record_ref:
raise NativePayloadError(
"native metadata has an empty record reference"
)
record = _native_record_for_reference(native_record_ref, svg_path)
conflicts = sorted(set(record) & set(element.attrib))
if conflicts:
raise NativePayloadError(
"native metadata cannot carry both inline fields and a record: "
+ ", ".join(conflicts)
)
effective_attributes.update(record)
record_operations.append((element, record))
if (
_local_name(element.tag) == "metadata"
and effective_attributes.get("data-pptx-part") == "txbody"
):
has_reference = TXBODY_REF_ATTRIBUTE in effective_attributes
reference = effective_attributes.get(TXBODY_REF_ATTRIBUTE) or ""
if has_reference:
if not reference:
raise NativePayloadError(
"txbody metadata has an empty payload reference"
)
if (element.text or "").strip() or element.get("data-pptx-encoding"):
raise NativePayloadError(
"txbody metadata cannot carry both inline data and a reference"
)
payload_operations.append(
("txbody", element, _payload_for_reference(reference, svg_path))
)
has_shape_reference = SHAPE_STYLE_REF_ATTRIBUTE in effective_attributes
shape_reference = effective_attributes.get(SHAPE_STYLE_REF_ATTRIBUTE) or ""
if has_shape_reference:
if not shape_reference:
raise NativePayloadError(
"shape-style metadata has an empty payload reference"
)
if effective_attributes.get(SHAPE_STYLE_ATTRIBUTE):
raise NativePayloadError(
"shape-style metadata cannot carry both inline data and a reference"
)
payload_operations.append(
(
"shape-style",
element,
_payload_for_reference(shape_reference, svg_path),
)
)
has_geometry_reference = CUSTOM_GEOMETRY_REF_ATTRIBUTE in effective_attributes
geometry_reference = (
effective_attributes.get(CUSTOM_GEOMETRY_REF_ATTRIBUTE) or ""
)
if has_geometry_reference:
if not geometry_reference:
raise NativePayloadError(
"custom-geometry metadata has an empty payload reference"
)
if effective_attributes.get(CUSTOM_GEOMETRY_ATTRIBUTE):
raise NativePayloadError(
"custom-geometry metadata cannot carry both inline data and a reference"
)
payload_operations.append(
(
"custom-geometry",
element,
_payload_for_reference(geometry_reference, svg_path),
)
)
for element, record in record_operations:
element.attrib.pop(NATIVE_RECORD_REF_ATTRIBUTE)
element.attrib.update(record)
for kind, element, raw in payload_operations:
encoded = base64.b64encode(raw).decode("ascii")
if kind == "txbody":
element.text = encoded
element.set("data-pptx-encoding", "base64")
element.attrib.pop(TXBODY_REF_ATTRIBUTE, None)
elif kind == "shape-style":
element.set(SHAPE_STYLE_ATTRIBUTE, encoded)
element.attrib.pop(SHAPE_STYLE_REF_ATTRIBUTE, None)
else:
element.set(CUSTOM_GEOMETRY_ATTRIBUTE, encoded)
element.attrib.pop(CUSTOM_GEOMETRY_REF_ATTRIBUTE, None)
return len(record_operations) + len(payload_operations)
@@ -236,7 +236,7 @@ def main() -> int:
if args.provider == "edge" and not args.voice:
parser.error(
"--voice is required for --provider edge. Run --list-voices --locale <locale> to discover voices "
"(e.g. --locale zh-CN), or follow skills/ppt-master/workflows/generate-audio.md "
"(e.g. --locale zh-CN), or follow skills/ppt-master/workflows/stages/generate-audio.md "
"for an AI-curated recommendation."
)
raise AssertionError("unreachable")
@@ -0,0 +1,911 @@
#!/usr/bin/env python3
"""
PPT Master - Page Context Projection
Build deterministic per-page execution views and optional token telemetry.
Usage:
Imported by project_manager.py.
Examples:
build_page_context(Path("projects/demo"), "P07")
Dependencies:
None for projection; tiktoken is optional for exact usage counts.
"""
from __future__ import annotations
import hashlib
import json
import math
import re
import statistics
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Iterable
from project_specs import (
default_spec_lock_forbidden,
parse_markdown_artifact,
validate_project_artifacts,
)
from svg_to_pptx.pptx_package.template_structure import (
PptxStructureLock,
TemplateStructureError,
load_pptx_structure_lock,
)
PAGE_CONTEXT_SCHEMA = "ppt-master.page-context.v2"
PAGE_CONTEXT_USAGE_SCHEMA = "ppt-master.page-context-usage.v2"
PAGE_CONTEXT_REPORT_SCHEMA = "ppt-master.page-context-usage-report.v2"
TOKEN_ENCODING = "o200k_base"
PAGE_CONTEXT_TOKEN_TARGET = 2000
LOCK_PROJECTION_TOKEN_TARGET = 1000
_SKILL_DIR = Path(__file__).resolve().parent.parent
_CHARTS_DIR = _SKILL_DIR / "templates" / "charts"
_PAGE_RE = re.compile(r"^(?:P)?([0-9]+)$", re.IGNORECASE)
_SLIDE_HEADING_RE = re.compile(
r"^#{3,6}[ \t]+Slide[ \t]+0*([0-9]+)(?:[ \t]*(?:[-:–—]).*)?$",
re.IGNORECASE | re.MULTILINE,
)
_BLOCK_BOUNDARY_RE = re.compile(r"^#{2,6}[ \t]+", re.MULTILINE)
_PART_HEADING_RE = re.compile(r"^###[ \t]+(?!#)(.+?)[ \t]*$", re.MULTILINE)
_PAGE_TOKEN_RE = re.compile(
r"(?<![A-Za-z0-9_])P0*([1-9][0-9]*)(?![A-Za-z0-9_])",
re.IGNORECASE,
)
class PageContextError(RuntimeError):
"""Reject an incomplete or ambiguous page-context request."""
@dataclass(frozen=True)
class PageRead:
"""One exact model-visible page payload."""
kind: str
path: str
payload: str
@dataclass(frozen=True)
class PageContextResult:
"""One projected page view plus the files that make it current."""
project_path: Path
page: str
context: dict[str, object]
inputs: tuple[Path, ...]
def normalize_page_key(raw_page: str) -> tuple[str, int]:
"""Normalize a positive page identifier to the schema's P<NN> form."""
match = _PAGE_RE.fullmatch(raw_page.strip())
if match is None or int(match.group(1)) <= 0:
raise PageContextError("page must be a positive P<NN> identifier")
number = int(match.group(1))
return f"P{number:02d}", number
def _section_index(
sections: Iterable[dict[str, object]],
) -> dict[str, dict[str, object]]:
return {
str(section["heading"]).strip().casefold(): section
for section in sections
}
def _section_fields(
sections: dict[str, dict[str, object]],
heading: str,
) -> dict[str, str]:
section = sections.get(heading.casefold())
if section is None:
return {}
fields = section.get("fields", {})
if not isinstance(fields, dict):
return {}
return {str(key): str(value) for key, value in fields.items()}
def _forbidden_items(
sections: dict[str, dict[str, object]],
) -> list[str]:
section = sections.get("forbidden")
if section is None:
return []
items: list[str] = []
default_items = default_spec_lock_forbidden()
for raw_line in str(section.get("body", "")).splitlines():
line = raw_line.strip()
if not line:
continue
item = re.sub(r"^-[ \t]+", "", line)
if item not in default_items:
items.append(item)
return items
def _outline_section(
sections: Iterable[dict[str, object]],
) -> dict[str, object] | None:
for section in sections:
heading = str(section.get("heading", "")).strip().casefold()
if heading == "content outline" or heading.endswith(". content outline"):
return section
return None
def _page_image_filenames(
design_sections: Iterable[dict[str, object]],
page_number: int,
) -> tuple[set[str], set[str]]:
"""Read explicit P<NN> usage from the canonical image-resource table."""
section = next(
(
item
for item in design_sections
if (
(heading := str(item.get("heading", "")).strip().casefold())
== "image resource list"
or heading.startswith("viii. image resource list")
)
),
None,
)
if section is None:
return set(), set()
table_rows = [
[
cell.strip().replace(r"\|", "|")
for cell in re.split(r"(?<!\\)\|", line.strip().strip("|"))
]
for line in str(section.get("body", "")).splitlines()
if line.strip().startswith("|") and line.strip().endswith("|")
]
if not table_rows:
return set(), set()
header = {
name.casefold(): index
for index, name in enumerate(table_rows[0])
}
filename_index = header.get("filename")
purpose_index = header.get("purpose")
if filename_index is None or purpose_index is None:
return set(), set()
assigned: set[str] = set()
selected: set[str] = set()
for row in table_rows[1:]:
if len(row) <= max(filename_index, purpose_index):
continue
purpose = row[purpose_index]
pages = {int(match.group(1)) for match in _PAGE_TOKEN_RE.finditer(purpose)}
if not pages:
continue
filename = row[filename_index].strip().strip("`")
if not filename:
continue
basename = Path(filename).name
assigned.add(basename)
if page_number in pages:
selected.add(basename)
return assigned, selected
def _locked_image_basename(value: str) -> str:
return Path(value.split("|", 1)[0].strip()).name
def _outline_image_assignments(
design_sections: Iterable[dict[str, object]],
locked_images: dict[str, str],
) -> set[str]:
outline = _outline_section(design_sections)
if outline is None:
return set()
body = str(outline.get("body", ""))
return {
_locked_image_basename(value)
for key, value in locked_images.items()
if any(
_contains_token(body, token)
for token in (
key,
value.split("|", 1)[0].strip(),
_locked_image_basename(value),
)
)
}
def _slide_block(
design_sections: Iterable[dict[str, object]],
page_number: int,
) -> tuple[str | None, str]:
outline = _outline_section(design_sections)
if outline is None:
raise PageContextError("design_spec.md has no Content Outline section")
body = str(outline.get("body", ""))
matches = [
match
for match in _SLIDE_HEADING_RE.finditer(body)
if int(match.group(1)) == page_number
]
if not matches:
raise PageContextError(
f"design_spec.md Content Outline has no Slide {page_number:02d} block"
)
if len(matches) > 1:
raise PageContextError(
f"design_spec.md Content Outline repeats Slide {page_number:02d}"
)
match = matches[0]
next_boundary = _BLOCK_BOUNDARY_RE.search(body, match.end())
block_end = next_boundary.start() if next_boundary else len(body)
block = body[match.start():block_end].strip()
part_matches = list(_PART_HEADING_RE.finditer(body, 0, match.start()))
part = part_matches[-1].group(1).strip() if part_matches else None
return part, block
def _relative_project_path(project_path: Path, path: Path) -> str:
try:
return path.resolve().relative_to(project_path).as_posix()
except ValueError as exc:
raise PageContextError(f"path escapes project: {path}") from exc
def _prototype_image_refs(svg_path: Path) -> list[str]:
try:
root = ET.parse(svg_path).getroot()
except (OSError, ET.ParseError) as exc:
raise PageContextError(f"cannot read prototype SVG {svg_path}: {exc}") from exc
refs: set[str] = set()
for element in root.iter():
if element.tag.rsplit("}", 1)[-1] != "image":
continue
for name, value in element.attrib.items():
if name.rsplit("}", 1)[-1] != "href":
continue
normalized = value.strip()
if normalized and not normalized.startswith(("data:", "#")):
refs.add(normalized)
return sorted(refs)
def _contains_token(text: str, token: str) -> bool:
if not token:
return False
if re.fullmatch(r"[A-Za-z0-9_]+", token):
return re.search(
rf"(?<![A-Za-z0-9_]){re.escape(token)}(?![A-Za-z0-9_])",
text,
) is not None
return token in text
def _page_images(
locked_images: dict[str, str],
brief: str,
prototype_refs: list[str],
assigned_filenames: set[str],
resolved_filenames: set[str],
) -> tuple[str, dict[str, str]]:
if not locked_images:
return "none", {}
ref_basenames = {Path(ref).name for ref in prototype_refs}
selected: dict[str, str] = {}
unresolved: dict[str, str] = {}
for key, value in locked_images.items():
basename = _locked_image_basename(value)
if (
_contains_token(brief, key)
or _contains_token(brief, value)
or _contains_token(brief, basename)
or basename in ref_basenames
or basename in assigned_filenames
):
selected[key] = value
elif basename not in resolved_filenames:
unresolved[key] = value
if selected and unresolved:
return "explicit+unassigned", {**selected, **unresolved}
if selected:
return "explicit", selected
if unresolved:
return "unassigned", unresolved
return "confirmed-none", {}
def _page_template(
project_path: Path,
structure_lock: PptxStructureLock | None,
page_number: int,
) -> tuple[dict[str, object] | None, Path | None]:
if structure_lock is None or structure_lock.mode != "structured":
return None, None
prototype = next(
(item for item in structure_lock.prototypes if item.slide_num == page_number),
None,
)
assignment = next(
(item for item in structure_lock.layouts if item.slide_num == page_number),
None,
)
if prototype is None or assignment is None:
raise PageContextError(
f"structured lock has no complete mapping for P{page_number:02d}"
)
definition = next(
(
item
for item in structure_lock.layout_definitions
if item.layout_key == assignment.layout_key
),
None,
)
if definition is None:
raise PageContextError(
f"structured lock has no definition for Layout {assignment.layout_key!r}"
)
master = next(
(
item
for item in structure_lock.masters
if item.master_key == definition.master_key
),
None,
)
if master is None:
raise PageContextError(
f"structured lock has no definition for Master {definition.master_key!r}"
)
template = {
"reuse_scope": structure_lock.template_reuse_scope,
"adherence": structure_lock.template_adherence,
"prototype": prototype.template_basename,
"prototype_path": _relative_project_path(project_path, prototype.svg_path),
"layout": {
"key": definition.layout_key,
"name": definition.layout_name,
"source": (
f"P{definition.prototype_slide_num:02d}"
if definition.prototype_slide_num is not None
else _relative_project_path(
project_path,
definition.prototype_svg_path,
)
),
},
"master": {
"key": master.master_key,
"name": master.master_name,
},
}
return template, prototype.svg_path
def _reference_payload(
kind: str,
path: Path,
*,
scope: str,
display_path: str,
) -> dict[str, str]:
"""Describe one large reference without injecting its contents per page."""
return {
"kind": kind,
"scope": scope,
"path": display_path,
"sha256": _file_sha256(path),
"load_policy": "once-per-execution-context",
}
def _chart_reference(chart_key: str) -> tuple[dict[str, str], Path]:
"""Resolve one locked chart key to the shared Skill catalog."""
if Path(chart_key).name != chart_key or not chart_key:
raise PageContextError(f"invalid page_charts key: {chart_key!r}")
chart_path = (_CHARTS_DIR / f"{chart_key}.svg").resolve()
if not chart_path.is_file():
raise PageContextError(
f"page_charts key {chart_key!r} has no shared SVG reference"
)
return (
_reference_payload(
"chart-svg",
chart_path,
scope="skill",
display_path=f"templates/charts/{chart_path.name}",
),
chart_path,
)
def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
"""Build one current per-page projection without writing the project."""
project_path = Path(project).resolve()
if not project_path.is_dir():
raise PageContextError(f"project directory not found: {project_path}")
page, page_number = normalize_page_key(raw_page)
lock_path = project_path / "spec_lock.md"
design_path = project_path / "design_spec.md"
for required in (lock_path, design_path):
if not required.is_file():
raise PageContextError(f"required artifact not found: {required.name}")
preflight_errors, _preflight_warnings = validate_project_artifacts(
project_path,
include_design=False,
)
if preflight_errors:
preview = "; ".join(preflight_errors[:8])
suffix = (
""
if len(preflight_errors) <= 8
else f"; +{len(preflight_errors) - 8} more"
)
raise PageContextError(
"spec_lock/template preflight failed before page generation: "
f"{preview}{suffix}"
)
try:
lock_sections_raw = parse_markdown_artifact(
lock_path,
report_duplicate_fields=True,
)
design_sections = parse_markdown_artifact(design_path)
except (OSError, ValueError) as exc:
raise PageContextError(str(exc)) from exc
lock_sections = _section_index(lock_sections_raw)
part, brief = _slide_block(design_sections, page_number)
warnings: list[str] = []
rhythm_fields = _section_fields(lock_sections, "page_rhythm")
rhythm = rhythm_fields.get(page)
if rhythm is None:
rhythm = "dense"
warnings.append(f"page_rhythm has no {page}; using compatibility default dense")
chart_key = _section_fields(lock_sections, "page_charts").get(page)
try:
structure_lock = load_pptx_structure_lock(project_path)
except TemplateStructureError as exc:
raise PageContextError(str(exc)) from exc
template, prototype_path = _page_template(
project_path,
structure_lock,
page_number,
)
prototype_refs = (
_prototype_image_refs(prototype_path)
if prototype_path is not None
else []
)
table_assigned_filenames, assigned_filenames = _page_image_filenames(
design_sections,
page_number,
)
locked_images = _section_fields(lock_sections, "images")
resolved_filenames = (
{_locked_image_basename(value) for value in locked_images.values()}
if structure_lock is not None
and structure_lock.template_reuse_scope == "mirror"
else table_assigned_filenames
| _outline_image_assignments(design_sections, locked_images)
)
image_selection, selected_images = _page_images(
locked_images,
brief,
(
prototype_refs
if structure_lock is not None
and structure_lock.template_reuse_scope == "mirror"
else []
),
assigned_filenames,
resolved_filenames,
)
inputs = [lock_path, design_path]
reference_set: list[dict[str, str]] = [
_reference_payload(
"design-spec",
design_path,
scope="project",
display_path="design_spec.md",
),
]
template_design_path = project_path / "templates" / "design_spec.md"
if template_design_path.is_file():
inputs.append(template_design_path)
reference_set.append(
_reference_payload(
"template-design-spec",
template_design_path,
scope="project",
display_path="templates/design_spec.md",
)
)
if prototype_path is not None:
inputs.append(prototype_path)
reference_set.append(
_reference_payload(
"prototype-svg",
prototype_path,
scope="project",
display_path=_relative_project_path(project_path, prototype_path),
)
)
if chart_key is not None:
chart_reference, chart_path = _chart_reference(chart_key)
inputs.append(chart_path)
reference_set.append(chart_reference)
mode_fields = _section_fields(lock_sections, "mode")
visual_style_fields = _section_fields(lock_sections, "visual_style")
# Repeat this bounded projection per page intentionally: exact lock values
# are the anti-drift guard; large reference payloads use reference_set.
global_context = {
"communication": _section_fields(lock_sections, "communication"),
"canvas": _section_fields(lock_sections, "canvas"),
"mode": mode_fields.get("mode"),
"mode_behavior": mode_fields.get("mode_behavior"),
"visual_style": visual_style_fields.get("visual_style"),
"visual_style_behavior": visual_style_fields.get(
"visual_style_behavior"
),
"colors": _section_fields(lock_sections, "colors"),
"typography": _section_fields(lock_sections, "typography"),
"icons": _section_fields(lock_sections, "icons"),
"pptx_structure": _section_fields(lock_sections, "pptx_structure"),
"forbidden": _forbidden_items(lock_sections),
}
global_context = {
key: value
for key, value in global_context.items()
if value not in ({}, [], None, "")
}
current_page: dict[str, object] = {
"part": part,
"brief_markdown": brief,
"rhythm": rhythm,
"image_selection": image_selection,
}
if chart_key is not None:
current_page["chart"] = chart_key
if selected_images:
current_page["images"] = selected_images
if template is not None:
current_page["template"] = template
context: dict[str, object] = {
"schema": PAGE_CONTEXT_SCHEMA,
"page": page,
"lock_source": {
"path": "spec_lock.md",
"sha256": _file_sha256(lock_path),
"load_policy": "per-page-drift-guard",
},
"global": global_context,
"page_context": current_page,
"reference_set": reference_set,
}
if warnings:
context["warnings"] = warnings
unique_inputs = tuple(dict.fromkeys(path.resolve() for path in inputs))
return PageContextResult(
project_path=project_path,
page=page,
context=context,
inputs=unique_inputs,
)
def _compact_json(payload: object) -> str:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
def _pretty_json(payload: object) -> str:
return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
def render_page_context(
result: PageContextResult,
*,
bundle: bool = False,
pretty: bool = False,
) -> tuple[str, tuple[PageRead, ...]]:
"""Render compact stdout; ``bundle`` remains a compatibility no-op."""
context_payload = (
_pretty_json(result.context) if pretty else _compact_json(result.context)
)
context_read = PageRead(
kind="page-context",
path="stdout:page-context",
payload=context_payload,
)
return context_payload, (context_read,)
def _sha256_bytes(payload: bytes) -> str:
return hashlib.sha256(payload).hexdigest()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _input_location(project_path: Path, path: Path) -> tuple[str, str]:
"""Return a stable project- or Skill-relative locator for telemetry."""
resolved = path.resolve()
for scope, root in (("project", project_path), ("skill", _SKILL_DIR)):
try:
return scope, resolved.relative_to(root.resolve()).as_posix()
except ValueError:
continue
raise PageContextError(f"input escapes project and Skill roots: {path}")
def _resolve_input_location(
project_path: Path,
scope: str,
relative_path: str,
) -> Path | None:
"""Resolve one recorded input without accepting arbitrary filesystem roots."""
roots = {"project": project_path, "skill": _SKILL_DIR}
root = roots.get(scope)
if root is None:
return None
resolved = (root / relative_path).resolve()
try:
resolved.relative_to(root.resolve())
except ValueError:
return None
return resolved
def _token_counter() -> tuple[Callable[[str], int] | None, str]:
try:
import tiktoken
except ImportError:
return None, "unavailable"
try:
encoder = tiktoken.get_encoding(TOKEN_ENCODING)
except Exception:
return None, "unavailable"
return (
lambda text: len(encoder.encode(text, disallowed_special=())),
"exact",
)
def _payload_measurement(
read: PageRead,
count_tokens: Callable[[str], int] | None,
) -> dict[str, object]:
payload = read.payload.encode("utf-8")
measurement: dict[str, object] = {
"kind": read.kind,
"scope": "component" if read.kind == "lock-projection" else "page",
"path": read.path,
"sha256": _sha256_bytes(payload),
"utf8_bytes": len(payload),
"characters": len(read.payload),
"tokens": count_tokens(read.payload) if count_tokens else None,
}
return measurement
def record_page_context_usage(
result: PageContextResult,
output: str,
measured_reads: tuple[PageRead, ...],
) -> tuple[Path, str]:
"""Write one deterministic, derived token snapshot for the current page."""
count_tokens, token_status = _token_counter()
lock_read = PageRead(
kind="lock-projection",
path="stdout:global",
payload=_compact_json(result.context["global"]),
)
documents = [
_payload_measurement(read, count_tokens)
for read in (*measured_reads, lock_read)
]
output_bytes = output.encode("utf-8")
input_records: list[dict[str, object]] = []
for path in result.inputs:
scope, relative_path = _input_location(result.project_path, path)
input_records.append(
{
"scope": scope,
"path": relative_path,
"exists": True,
"sha256": _file_sha256(path),
}
)
by_kind = {
str(item["kind"]): item.get("tokens")
for item in documents
}
route = dict(result.context["global"].get("pptx_structure", {}))
template = result.context["page_context"].get("template")
if isinstance(template, dict):
if isinstance(value := template.get("reuse_scope"), str):
route["template_reuse_scope"] = value
usage = {
"schema": PAGE_CONTEXT_USAGE_SCHEMA,
"page": result.page,
"output_mode": "compact",
"route": route,
"encoding": TOKEN_ENCODING,
"token_status": token_status,
"image_selection": result.context["page_context"]["image_selection"],
"inputs": input_records,
"references": result.context.get("reference_set", []),
"documents": documents,
"controlled_output": {
"sha256": _sha256_bytes(output_bytes),
"utf8_bytes": len(output_bytes),
"characters": len(output),
"tokens": count_tokens(output) if count_tokens else None,
},
"totals": {
"page_context": by_kind.get("page-context"),
"lock_projection": by_kind.get("lock-projection"),
},
"targets": {
"page_context_max_tokens": PAGE_CONTEXT_TOKEN_TARGET,
"lock_projection_max_tokens": LOCK_PROJECTION_TOKEN_TARGET,
},
"untracked": [
"source-material reads",
"once-per-execution-context reference payloads",
"other session-level prompt references",
],
}
usage_dir = result.project_path / "analysis" / "page-context"
usage_dir.mkdir(parents=True, exist_ok=True)
usage_path = usage_dir / f"{result.page}.usage.json"
temporary_path = usage_path.with_suffix(".usage.json.tmp")
temporary_path.write_text(_pretty_json(usage), encoding="utf-8")
temporary_path.replace(usage_path)
return usage_path, token_status
def _nearest_rank(values: list[int], percentile: float) -> int:
rank = max(1, math.ceil(percentile * len(values)))
return sorted(values)[rank - 1]
def _metric(values: list[int], *, target: int | None = None) -> dict[str, object]:
if not values:
return {
"count": 0,
"sum": 0,
"min": None,
"p50": None,
"p95": None,
"max": None,
**({"over_target_count": 0, "target": target} if target else {}),
}
metric: dict[str, object] = {
"count": len(values),
"sum": sum(values),
"min": min(values),
"p50": round(statistics.median(values)),
"p95": _nearest_rank(values, 0.95),
"max": max(values),
}
if target is not None:
metric.update({
"target": target,
"over_target_count": sum(value > target for value in values),
})
return metric
def page_context_usage_report(project: str | Path) -> dict[str, object]:
"""Summarize fresh per-page telemetry without changing recorded history."""
project_path = Path(project).resolve()
usage_dir = project_path / "analysis" / "page-context"
records: list[dict[str, object]] = []
stale_pages: list[str] = []
unavailable_pages: list[str] = []
if usage_dir.is_dir():
for usage_path in sorted(usage_dir.glob("P*.usage.json")):
try:
record = json.loads(usage_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
stale_pages.append(usage_path.stem.split(".", 1)[0])
continue
page = str(record.get("page", usage_path.stem.split(".", 1)[0]))
if record.get("schema") != PAGE_CONTEXT_USAGE_SCHEMA:
stale_pages.append(page)
continue
if record.get("output_mode") != "compact":
stale_pages.append(page)
continue
stale = False
for item in record.get("inputs", []):
if not isinstance(item, dict):
stale = True
break
source_path = _resolve_input_location(
project_path,
str(item.get("scope", "project")),
str(item.get("path", "")),
)
if source_path is None:
stale = True
break
expected_exists = item.get("exists", True)
if expected_exists is False:
if source_path.exists():
stale = True
break
elif (
not source_path.is_file()
or _file_sha256(source_path) != item.get("sha256")
):
stale = True
break
if stale:
stale_pages.append(page)
continue
if record.get("token_status") != "exact":
unavailable_pages.append(page)
records.append(record)
def tokens_for(kind: str) -> list[int]:
values: list[int] = []
for record in records:
for document in record.get("documents", []):
if not isinstance(document, dict) or document.get("kind") != kind:
continue
value = document.get("tokens")
if isinstance(value, int):
values.append(value)
return values
controlled = [
value
for record in records
if isinstance(
value := record.get("controlled_output", {}).get("tokens"),
int,
)
]
unique_references = sorted({
f"{reference.get('scope', 'project')}:{reference.get('path', '')}"
for record in records
for reference in record.get("references", [])
if isinstance(reference, dict) and reference.get("path")
})
return {
"schema": PAGE_CONTEXT_REPORT_SCHEMA,
"project": project_path.name,
"record_count": len(records),
"pages": sorted(str(record["page"]) for record in records),
"stale_pages": sorted(set(stale_pages)),
"token_unavailable_pages": sorted(set(unavailable_pages)),
"unique_reference_count": len(unique_references),
"unique_references": unique_references,
"metrics": {
"page_context": _metric(
tokens_for("page-context"),
target=PAGE_CONTEXT_TOKEN_TARGET,
),
"lock_projection": _metric(
tokens_for("lock-projection"),
target=LOCK_PROJECTION_TOKEN_TARGET,
),
"controlled_output": _metric(controlled),
},
}
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Shared diagnostic contract for unsupported imported object effects."""
from __future__ import annotations
import json
from xml.etree import ElementTree as ET
EFFECT_STATUS_ATTR = "data-pptx-effect-status"
EFFECT_REASON_ATTR = "data-pptx-effect-reason"
UNSUPPORTED_EFFECT_STATUS = "unsupported"
_EFFECT_OBJECT_IDENTITY_ATTRS = (
"data-pptx-object",
"data-pptx-shape-id",
"data-pptx-shape-scope",
)
_DML_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/main"
_TEXT_PROPERTY_TAGS = frozenset({
f"{{{_DML_NAMESPACE}}}defRPr",
f"{{{_DML_NAMESPACE}}}endParaRPr",
f"{{{_DML_NAMESPACE}}}rPr",
})
_RUN_EFFECT_CONTAINER_TAGS = frozenset({
f"{{{_DML_NAMESPACE}}}effectLst",
f"{{{_DML_NAMESPACE}}}effectDag",
})
def project_effect_status_errors(root: ET.Element) -> list[str]:
"""Return blocking diagnostics for invalid or unsupported effect metadata."""
errors: set[str] = set()
parents = {
child: parent
for parent in root.iter()
for child in parent
}
for elem in root.iter():
raw_status = elem.get(EFFECT_STATUS_ATTR)
raw_reason = elem.get(EFFECT_REASON_ATTR)
if raw_status is None and raw_reason is None:
continue
parent = parents.get(elem)
if (
parent is not None
and parent.get(EFFECT_STATUS_ATTR) == raw_status
and parent.get(EFFECT_REASON_ATTR) == raw_reason
and _same_source_object(parent, elem)
):
# Import duplicates the marker on the logical object and carrier
# so stripping either copy cannot erase the block. Report it once.
continue
label = _element_label(elem)
status = (raw_status or "").strip()
if status != UNSUPPORTED_EFFECT_STATUS:
errors.add(
f'{label} {EFFECT_STATUS_ATTR} must equal '
f'{UNSUPPORTED_EFFECT_STATUS!r}; got {raw_status!r}'
)
continue
reason = (raw_reason or "").strip()
if not reason:
errors.add(
f'{label} {EFFECT_REASON_ATTR} requires a non-empty reason'
)
continue
errors.add(f'{label} has unsupported source PPTX effect: {reason}')
return sorted(errors)
def unsupported_effect_metadata(*reasons: str) -> dict[str, str]:
"""Build one canonical import marker without dropping compound reasons."""
normalized: set[str] = set()
for reason in reasons:
reason = reason.strip()
if not reason:
raise ValueError("Unsupported PPTX effect reason must not be empty")
items: object = reason
if reason.startswith("["):
try:
items = json.loads(reason)
except json.JSONDecodeError:
pass
if not isinstance(items, list):
items = [reason]
if not all(isinstance(item, str) and item.strip() for item in items):
raise ValueError("Unsupported PPTX effect reasons must be strings")
normalized.update(item.strip() for item in items)
if not normalized:
raise ValueError("Unsupported PPTX effect reason must not be empty")
ordered = sorted(normalized)
encoded = (
ordered[0]
if len(ordered) == 1
else json.dumps(ordered, separators=(",", ":"))
)
return {
EFFECT_STATUS_ATTR: UNSUPPORTED_EFFECT_STATUS,
EFFECT_REASON_ATTR: encoded,
}
def txbody_has_run_effects(*text_style_roots: ET.Element | None) -> bool:
"""Return whether rebuilding any supplied text style would lose an effect."""
for root in text_style_roots:
if root is None:
continue
for properties in root.iter():
if properties.tag not in _TEXT_PROPERTY_TAGS:
continue
for child in properties:
if child.tag in _RUN_EFFECT_CONTAINER_TAGS and any(
isinstance(effect.tag, str)
for effect in child
):
return True
return False
def _element_label(elem: ET.Element) -> str:
tag = elem.tag.rsplit("}", 1)[-1]
elem_id = elem.get("id") or elem.get("data-name")
if elem_id:
return f'<{tag} id="{elem_id}">'
shape_id = elem.get("data-pptx-shape-id")
if shape_id:
object_kind = elem.get("data-pptx-object") or "object"
scope = elem.get("data-pptx-shape-scope") or "unknown"
return (
f'<{tag} data-pptx-object="{object_kind}" '
f'data-pptx-shape-id="{shape_id}" '
f'data-pptx-shape-scope="{scope}">'
)
return f"<{tag}>"
def _same_source_object(parent: ET.Element, child: ET.Element) -> bool:
"""Return whether a parent/child marker describes one imported object."""
return all(
child.get(attr) is not None
and child.get(attr) == parent.get(attr)
for attr in _EFFECT_OBJECT_IDENTITY_ATTRS
)
@@ -154,7 +154,7 @@ def build_source_profile(
"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."
"unless the user selected the beautify profile or Fill Native PPTX route."
),
"beautify": (
"Promote source text, page order, page count, colors, fonts, and font sizes "
@@ -7,15 +7,16 @@ Reads OOXML directly via `pptx_to_svg` and writes a reusable reference workspace
asset inventory, and per-slide / per-layout / per-master metadata
- `native_structure.json` + `source_template.pptx` source-structure facts and
a byte-identical analysis copy used to rebuild explicit SVG structure
- `summary.md` short human-readable digest derived from manifest.json
- `assets/` extracted reusable image assets
- `svg/` primary view: by default the layered template view (every master
- `conversion-report.json` source-recovery and fidelity diagnostics emitted
with SVG conversion
- `svg/` canonical layered template view (every master
and layout in the deck rendered once each as `master_*.svg` /
`layout_*.svg`, slides contain only their own shapes, and an
`inheritance.json` describes the reuse graph)
- `svg-flat/` companion view (default mode "both"): each `slide_NN.svg`
is self-contained master/layout decoration is inlined so opening any
one slide shows the full page like PowerPoint would
- `svg-flat/` optional verification view (`--inheritance-mode both`): each
`slide_NN.svg` is self-contained, so opening one slide shows the full page
like PowerPoint would
"""
from __future__ import annotations
@@ -23,6 +24,8 @@ from __future__ import annotations
import argparse
import json
from pathlib import Path
from xml.etree import ElementTree as ET
from zipfile import BadZipFile
from console_encoding import configure_utf8_stdio
from template_import.manifest import build_manifest
@@ -54,8 +57,8 @@ def parse_args() -> argparse.Namespace:
"--manifest-only",
action="store_true",
help=(
"Only extract manifest.json + summary.md + reusable assets + the "
"native structure/source pair, without exporting slides to SVG"
"Only extract manifest.json + reusable assets + the native "
"structure/source pair, without exporting slides to SVG"
),
)
parser.add_argument(
@@ -66,16 +69,16 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--inheritance-mode",
choices=("both", "layered", "flat"),
default="both",
default="layered",
help=(
"How to render master/layout shapes for slide SVGs. "
"'both' (default): emit both views — svg/ holds the layered "
"'layered' (default): emit the canonical svg/ tree with master, "
"layout, and slide-local files plus svg/inheritance.json. "
"'both': also emit svg-flat/ with self-contained per-slide "
"verification files. In this mode svg/ still holds the layered "
"renderings (template designers see master/layout/slide as "
"separate files plus svg/inheritance.json) and svg-flat/ holds "
"self-contained per-slide SVGs (each one renders correctly when "
"opened on its own). 'layered': only the svg/ tree, useful when "
"you don't need the flat view. 'flat': only self-contained slide "
"SVGs in svg/, the round-trip view used by svg_to_pptx."
"separate files). 'flat': emit only self-contained slide SVGs "
"in svg/, the round-trip view used by svg_to_pptx."
),
)
return parser.parse_args()
@@ -108,7 +111,13 @@ def main() -> int:
manifest_path = output_dir / "manifest.json"
if not args.skip_manifest:
try:
manifest = build_manifest(pptx_path, output_dir)
manifest = build_manifest(
pptx_path,
output_dir,
include_flat_svg=(
not args.manifest_only and args.inheritance_mode == "both"
),
)
except (RuntimeError, OSError, ValueError) as exc:
print(f"Error: failed to extract PPTX metadata: {exc}")
return 1
@@ -139,7 +148,6 @@ def main() -> int:
f"{native_structure['strategy']['recommendedMode']}"
)
print("Template output mode: explicit SVG structure")
print("Summary: summary.md")
print(f"Assets exported: {len(manifest['assets']['allAssets'])}")
print(f"Common assets: {len(manifest['assets']['commonAssets'])}")
print(f"Slides analyzed: {len(manifest['slides'])}")
@@ -157,7 +165,11 @@ def main() -> int:
inheritance_mode=args.inheritance_mode,
asset_name_map=manifest.get("assets", {}).get("assetMap", {}) if manifest else {},
)
try:
result = convert_pptx_to_svg(pptx_path, output_dir, options)
except (BadZipFile, ET.ParseError, OSError, RuntimeError, ValueError) as exc:
print(f"Error: failed to convert PPTX template source: {exc}")
return 1
total_bytes = sum(len(art.svg.encode("utf-8")) for art in result.slides)
print(f"Inheritance mode: {args.inheritance_mode}")
@@ -168,6 +180,11 @@ def main() -> int:
print("Inheritance graph: svg/inheritance.json")
if result.flat_slides:
print(f"Flat companion slides: {len(result.flat_slides)} (svg-flat/)")
if result.diagnostics:
print(
f"Source recovery warnings: {len(result.diagnostics)} "
"(conversion-report.json)"
)
print(f"SVG bytes (primary): {total_bytes}")
print(f"Output directory: {output_dir}")
if native_structure is not None:
@@ -5,6 +5,7 @@ Usage:
python3 pptx_to_svg.py <pptx_file> [-o <output_dir>] [--embed-images]
[--media-subdir <name>] [--keep-hidden]
[--inheritance-mode {both,layered,flat}]
[--strict]
Output structure (default --inheritance-mode both):
<output_dir>/
@@ -27,6 +28,7 @@ import argparse
import sys
from pathlib import Path
from xml.etree import ElementTree as ET
from zipfile import BadZipFile
# Allow running this script from anywhere
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -48,7 +50,11 @@ def _reconstruction_only_graphics(result: object) -> list[tuple[int, str]]:
except ET.ParseError:
continue
for elem in root.iter():
if elem.get("data-pptx-route-status") != "reconstruction-only":
fallback_kind = (
elem.get("data-pptx-fallback-kind")
or elem.get("data-pptx-visual-status")
)
if fallback_kind != "placeholder":
continue
marker_id = elem.get("id") or elem.get("data-name") or "<unnamed>"
diagnostics.append((artifact.index, marker_id))
@@ -91,6 +97,14 @@ def parse_args() -> argparse.Namespace:
"self-contained slides under svg/ for backward compatibility."
),
)
parser.add_argument(
"--strict",
action="store_true",
help=(
"Stop on the first unsupported/malformed source construct instead "
"of the default tolerant conversion with diagnostics"
),
)
return parser.parse_args()
@@ -115,9 +129,14 @@ def main() -> int:
embed_images=args.embed_images,
keep_hidden=args.keep_hidden,
inheritance_mode=args.inheritance_mode,
strict=args.strict,
)
try:
result = convert_pptx_to_svg(pptx_path, output_dir, options)
except (BadZipFile, ET.ParseError, OSError, RuntimeError, ValueError) as exc:
print(f"Error: PPTX-to-SVG conversion failed: {exc}", file=sys.stderr)
return 1
print(f"Source: {pptx_path.name}")
print(f"Canvas: {result.canvas_px[0]:.0f} x {result.canvas_px[1]:.0f} px")
@@ -128,12 +147,37 @@ def main() -> int:
fonts = ", ".join(f"{k}={v}" for k, v in result.theme_fonts.items())
print(f"Theme fonts: {fonts}")
print(f"Slides converted: {len(result.slides)}")
if result.diagnostics:
print(
f"Warning: {len(result.diagnostics)} source construct(s) were "
"normalized, omitted, or replaced; see conversion-report.json.",
file=sys.stderr,
)
for item in result.diagnostics[:20]:
location = (
f"slide {item.slide_index}"
if item.slide_index
else item.part_path
)
shape = item.shape_name or item.shape_id
if shape:
location = f"{location}, {shape}" if location else shape
print(
f" {location or 'package'}: {item.code}: {item.message}",
file=sys.stderr,
)
if len(result.diagnostics) > 20:
print(
f" ... and {len(result.diagnostics) - 20} more",
file=sys.stderr,
)
reconstruction_only = _reconstruction_only_graphics(result)
if reconstruction_only:
print(
"Warning: chart placeholder(s) without a baked preview are "
"reconstruction-only. Default export keeps the placeholder; "
"--native-objects may reconstruct entries with a valid active marker:",
"--native-charts-and-tables may reconstruct entries with a valid "
"replacement marker:",
file=sys.stderr,
)
for slide_index, marker_id in reconstruction_only[:20]:
@@ -144,6 +188,7 @@ def main() -> int:
file=sys.stderr,
)
print(f"Output: {output_dir}")
print(f"Conversion report: {output_dir / 'conversion-report.json'}")
return 0
@@ -1,8 +1,8 @@
"""Extract editable native chart metadata from PPTX chart parts.
"""Extract native Chart replacement metadata from PPTX chart parts.
The visual chart preview still comes from the existing graphicFrame fallback.
This module only builds a conservative ``data-pptx-native="chart"`` payload
when the chart XML cache can be mapped to the current native chart schema.
This module only builds a conservative ``data-pptx-replace-with="chart"``
payload when the chart XML cache can be mapped to the current chart schema.
"""
from __future__ import annotations
@@ -51,8 +51,9 @@ def extract_native_chartex_payload(
) -> dict[str, Any]:
"""Return a writer-valid payload for one supported ChartEx reference.
``UnsupportedChartEx.status`` is suitable for ``data-pptx-native-status``
when the reference, relationship, data topology, or cache is unusable.
``UnsupportedChartEx.status`` is suitable for
``data-pptx-replacement-status`` when the reference, relationship, data
topology, or cache is unusable.
Style-part failures never reject an otherwise valid data payload.
"""
if graphic_data is None:

Some files were not shown because too many files have changed in this diff Show More