Pptx Builder
Two-skill plugin: pptx-builder (python-pptx primary, pptxgenjs polish) and diagram-mapper. Calm-precision design, theme-portability gates, host-portable.
Install
claude plugin marketplace add tyroneross/RossLabs-AI-Toolkit
claude plugin install pptx-builder@rosslabs-ai-toolkit
The Problem
Generated PowerPoint decks look fine on the machine that made them, then fall apart when opened elsewhere — fonts substitute, theme colors shift, layouts reflow. The cause is OOXML theme inheritance: a deck that does not fully define its theme inherits the opener’s, and the design breaks. Separate problem: drawing diagrams of a codebase, a workflow, or a state machine usually ends in a hand-built image that goes stale the moment the underlying thing changes.
What I Built
A Claude Code plugin that hosts two skills:
pptx-builder — PowerPoint deck builder built around the calm-precision-pptx hybrid model. python-pptx is the primary structural build engine and the canonical theme owner (typed structure.json → tokens → helpers → .pptx, following Calm Precision design philosophy and a Gestalt decision framework). The pptxgenjs polish layer is wired (v0.6.0+) for slides where pixel-precise composition earns its weight — the cover slide is the first consumer, composed in Node and merged into the python-pptx base by a from-scratch cross-deck slide merge engine at the OOXML/ZIP-package level. Opt in via --polish-cover; default OFF — zero regression to the python-pptx primary path.
diagram-mapper — turns any source artifact (codebase, workflow engine, state machine, API, process doc, org structure, financial model) into an audit-grade diagram with explicit node types, decision branches, and exception paths. Renders to Mermaid (default), SVG, HTML/interactive, or PowerPoint via the same plugin QA stack. When a diagram is embedded into a calm-precision deck, its palette resolves to the deck’s tokens so the diagram matches.
Architecture
Neither skill runs a model of its own. Both are host-LLM-driven: the LLM already running the calling session — Claude Code, Codex, or Claude cowork — reads the request, authors the intermediate artifact (structure.json for a deck, a node/edge grammar for a diagram), and hands it to this plugin’s code. Everything downstream of that handoff — layout selection, rendering, merging, linting, rendering to PNG — is deterministic Python and Node with no LLM or vendor API call in the loop. structure/schema.json states this directly: “the host LLM emits this from content; downstream stages … read it.” plugin.json states the same for both skills: “no vendor API calls in the build or diagram path.”
flowchart LR
A["Host LLM (Claude Code / Codex / cowork)<br/>authors structure.json"] --> B["validate_structure.py<br/>jsonschema"]
B --> C["build_deck.py<br/>dispatch by intent + content.kind"]
C --> D["auto_layout.select_layout()<br/>detect / classify / match / rank"]
D --> E["slides_*.py helpers<br/>python-pptx + calm-precision tokens"]
E --> F{"--polish-cover?"}
F -->|"default: no"| G[".pptx base deck"]
F -->|"opt-in"| H["polish/cover.mjs<br/>Node + pptxgenjs"]
H --> I["merge_pptx.py<br/>OOXML/ZIP splice + slideLayout remap"]
I --> G
G --> J["QA gauntlet, 9 gates<br/>typography / lint / contrast / drift / theme-portability"]
J --> K["render_pptx.py --png<br/>PowerPoint macOS (ground truth)<br/>LibreOffice fallback"]
K --> L["Shipped .pptx"]
Models: none. The plugin ships zero model or API calls of its own — every build, merge, and QA script (build_deck.py, merge_pptx.py, select_layout(), verify_*.py, the lints) is deterministic Python (python-pptx, lxml, jsonschema) or Node (pptxgenjs). The only “model” in the system is the LLM hosting the calling session, and it operates entirely upstream of the plugin’s own runtime — it authors structure.json or extracts a diagram’s node/edge grammar, then the plugin takes over and produces byte-identical output for the same input on every run. This is deliberate: a slide-composition and OOXML-repair pipeline needs to be reproducible and auditable, not creative, so nothing in it depends on model output at build time.
Tools & infra — which, why:
| Tool | Why |
|---|---|
python-pptx (>=1.0,<2) | Primary structural build engine and canonical theme owner; composes the typed structure.json into OOXML slides via build/helpers/* |
lxml (>=4.9,<7) | Direct XML manipulation for the cross-deck slide merge engine (merge_pptx.py) and theme-portability repair (fix_theme_portable.py) — operations python-pptx doesn’t expose |
jsonschema (>=4.0,<5) | Validates structure.json against structure/schema.json before any slide renders, so a malformed request fails at the input boundary, not mid-build |
pptxgenjs (^4.0.1, Node ≥18) | Polish layer for pixel-precise composition; wired for the cover slide only, opt-in via --polish-cover |
| Mermaid | Default diagram-mapper output — renders natively in Markdown, GitHub/GitLab, Notion, and most docs tooling with no extra render step |
uv | Python dependency/venv management for the skill’s scripts (uv run --project skills/pptx-builder ...) |
| Microsoft PowerPoint (macOS) | Primary visual-render ground truth, via render_pptx.py --png — the renderer the audience actually opens the deck in decides pass/fail |
| LibreOffice | Fallback renderer (render_qa.sh) when PowerPoint is unavailable or blocked; checks coverage, doesn’t get veto power |
| Databases | None — the plugin is stateless. structure.json, the tokens JSON, and the output .pptx are the only persistent artifacts, all local files |
matplotlib/Pillow (chart-as-image) and pandas/openpyxl (Excel-driven slide data) are declared as reserved optional-dependency groups in pyproject.toml but are not wired into the default build path — native, Excel-editable charts are the shipped path instead.
How it works
- A user (or an upstream skill such as
tyrone-writing-system/deck-structure) hands the host LLM a locked argument; the host LLM writesstructure.json— deck-levelgoverning_thought, per-slideintent(cover,section,big-number,bullets,two-column,compare,quote,table,chart,closer,custom), and per-slidetakeaway— againstskills/pptx-builder/structure/schema.json. structure/validate_structure.pychecks the JSON against that schema viajsonschemabefore any rendering starts; a schema violation fails here, not mid-render.build/build_deck.pywalks the slide list and resolves a handler by(intent, content.kind). An intent that isn’t wired raisesNotImplementedErrorbefore anycontent[...]field is read, so an unsupported request fails loud instead of crashing on a missing key.build/auto_layout/select_layout()runs first for every slide (detect → classify → match → rank against known templates) and returns aLayoutDecision; the dispatchedslides_*.pyhelper (bignum, chart, compare, quote, text, framework, scorecard) may consult it but currently falls back to its own in-helper geometry, which already matches the decision’s coordinates.- Each helper composes the slide with
python-pptx, pulling colors and fonts fromtokens/calm-precision.tokens.json(or a project’s.deck-prefs.jsonoverride) — the single source of visual truth that also drives the typography and drift-detection QA gates downstream. - If
--polish-cover(orpolish: trueon the cover slide) is set,polish/cover.mjscomposes that one slide in Node withpptxgenjs, andbuild/merge/merge_pptx.pysplices it into the python-pptx base at the OOXML/ZIP-package level, remapping the polish slide’sslideLayoutrelationship onto the base deck’s theme so the pixel-precise slide doesn’t carry a divergent theme. Default is off; the rest of the deck is unaffected either way. - The QA gauntlet runs nine gates in order — schema conformance, Inter-only typography, em-dash/italic/fractional-EMU policy, sentence-shape and length lints, WCAG 4.5:1 contrast, token-vs-output drift (
--tokenscanonical,--skinlegacy for back-compat), OOXML theme-inheritance repair (closes six known cross-machine drift gaps), and a strict portability pass/fail gate. Every gate must exit 0 before the deck is considered shippable. - An optional visual-render step drives Microsoft PowerPoint on macOS (
render_pptx.py --png) as ground truth, with LibreOffice (render_qa.sh) as fallback when PowerPoint is unavailable, producing a PNG a human or an audit pass can inspect. diagram-mapper, the plugin’s sibling skill, follows the same “source first, grammar first, render last” loop for non-deck diagrams and reuses this same QA/render stack whenever the chosen output format is PPTX; when a diagram is embedded into a calm-precision deck, its palette resolves to the deck’s tokens so it matches.
Tech stack
Python (python-pptx, lxml, jsonschema) for the primary build, merge, and QA path, managed with uv. Node (pptxgenjs ^4.0.1) for the opt-in polish layer. Mermaid as the default diagram-mapper render target, with SVG, HTML/interactive, and PowerPoint as progressively heavier fallbacks. No database, queue, or hosted service — the plugin runs entirely inside the calling agent host (Claude Code, Codex, or Claude cowork) against local files.
Hard Rules (Mechanically Enforced)
Calm Precision base, enforced as lints:
- Action titles only — full sentences, sentence case, no terminal period, ≤95 chars
- One assertion per slide
- Footnote every numeric claim
- Semantic color only —
BRAND_PRIMARYappears once per slide on the dominant element - Whitespace is content; outer margins ≥ 0.4”
- Slide background set explicitly on every slide (
#FAFBFC)
User overrides also mechanically enforced:
- No em-dashes / en-dashes — use colons, semicolons, commas, “to”
- No horizontal row lines in tables — vertical column separators only
- No decorative lines — table header underline is the only allowed horizontal rule
- No panels for grouping — whitespace and the 12-column grid carry grouping
- No bottom takeaway bar — the action title carries the takeaway
- Native tables and charts — Excel-editable, no images of tables
- Inter only (FONT_PRIMARY = “Inter”; serif fails)
- High-contrast light background mandatory
The QA Gauntlet
Every gate must exit 0 before a deck is considered shippable:
| Gate | What it catches |
|---|---|
validate_structure.py | structure.json schema conformance |
verify_typography.py | Inter only; 3-tier ladder for body / chart-axis / chart-legend |
verify_deck.py | Em-dash / italic / fractional-EMU policy |
text_lint.py | Sentence shape, terminal period, length caps |
contrast_lint.py | WCAG 4.5:1 minimum |
drift_check.py | Tokens-vs-output mismatch (canonical --tokens mode reads the python-primary JSON; legacy --skin mode reads JS skins for back-compat) |
fix_theme_portable.py | Closes the six OOXML inheritance gaps that cause cross-machine drift |
verify_portable.py --strict | Pass/fail gate run before delivery |
render_pptx.py --png | Microsoft PowerPoint on macOS (primary ground truth); LibreOffice via render_qa.sh is the fallback |
Style-Input Modes
Three modes, no mode-switching ceremony:
- No style guidance — built-in default house skin (Calm Precision tokens).
- Copy an existing deck — ingest a sample
.pptx, extract tokens viaextract_theme_colors.py, build new slides that match;fix_theme_portable.py --theme-map <extracted>for paste compatibility. - User-provided style — explicit
.deck-prefs.jsonat the repo root or a named skin.
Repo Layout (folder-per-capability)
skills/pptx-builder/
├── design/references/ vendored calm-precision + user overrides + palette library
├── build/ python-pptx PRIMARY build path + cross-deck slide merge
├── polish/ pptxgenjs polish layer (cover slide wired v0.6.0)
├── structure/ schema.json + validator
├── qa/ typography, lints, contrast, drift, portability, render
├── tokens/ single source of truth
├── samples/ fixtures (cx-premium-kickoff for the slide-1-3 proof)
└── legacy/ demoted v0.2.0 fixed-renderer + v0.3.0 PptxGenJS guided pipeline
Lessons
-
Polish as an opt-in layer, not a replacement. python-pptx stayed the canonical theme owner and primary build engine; pptxgenjs was wired in as a polish layer scoped to the cover slide only, gated behind
--polish-coverand OFF by default. The judgment call: pixel-precise composition was worth the added cross-deck slide merge engine for one slide type, not worth risking regression across the whole build path by making it the default. -
Two render engines, one ranked as ground truth.
render_pptx.py --pngtreats Microsoft PowerPoint on macOS as primary ground truth and LibreOffice (viarender_qa.sh) as fallback, rather than treating both as equally authoritative. The reasoning: the renderer the audience actually opens the deck in should decide pass/fail; the open-source renderer checks coverage but doesn’t get veto power. -
Legacy paths demoted, not deleted.
drift_check.pykept a--skinmode that reads JS skins for back-compat alongside the canonical--tokensmode, and the earlier v0.2.0 fixed-renderer and v0.3.0 PptxGenJS guided pipeline moved tolegacy/instead of being removed. After two prior pipeline shifts, the call was to preserve rollback paths and old-skin compatibility rather than force a clean break a third time. -
No model in the build path, by design. The plugin’s own code — build, merge, and every QA gate — is fully deterministic; the only LLM involved is whichever session is hosting the request, and it stops being involved the moment
structure.jsonis handed off. That boundary is what makes the QA gates meaningful: they can assert “this exact output” rather than “output within tolerance of a model’s mood.”
Pairs With
- tyrone-writing-system / deck-structure — lock the argument before drafting
- pyramid-principle / pyramid-presentation — deeper Pyramid storylining
- design plugin —
design-critiqueandaccessibility-reviewpasses