Back to projects
Active Started May 2026

Pptx Builder

Two-skill plugin: pptx-builder (python-pptx primary, pptxgenjs polish) and diagram-mapper. Calm-precision design, theme-portability gates, host-portable.

Private Repo
Claude Code Plugin python-pptx lxml jsonschema pptxgenjs Python Node Mermaid

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:

ToolWhy
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
MermaidDefault diagram-mapper output — renders natively in Markdown, GitHub/GitLab, Notion, and most docs tooling with no extra render step
uvPython 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
LibreOfficeFallback renderer (render_qa.sh) when PowerPoint is unavailable or blocked; checks coverage, doesn’t get veto power
DatabasesNone — 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

  1. A user (or an upstream skill such as tyrone-writing-system/deck-structure) hands the host LLM a locked argument; the host LLM writes structure.json — deck-level governing_thought, per-slide intent (cover, section, big-number, bullets, two-column, compare, quote, table, chart, closer, custom), and per-slide takeaway — against skills/pptx-builder/structure/schema.json.
  2. structure/validate_structure.py checks the JSON against that schema via jsonschema before any rendering starts; a schema violation fails here, not mid-render.
  3. build/build_deck.py walks the slide list and resolves a handler by (intent, content.kind). An intent that isn’t wired raises NotImplementedError before any content[...] field is read, so an unsupported request fails loud instead of crashing on a missing key.
  4. build/auto_layout/select_layout() runs first for every slide (detect → classify → match → rank against known templates) and returns a LayoutDecision; the dispatched slides_*.py helper (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.
  5. Each helper composes the slide with python-pptx, pulling colors and fonts from tokens/calm-precision.tokens.json (or a project’s .deck-prefs.json override) — the single source of visual truth that also drives the typography and drift-detection QA gates downstream.
  6. If --polish-cover (or polish: true on the cover slide) is set, polish/cover.mjs composes that one slide in Node with pptxgenjs, and build/merge/merge_pptx.py splices it into the python-pptx base at the OOXML/ZIP-package level, remapping the polish slide’s slideLayout relationship 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.
  7. 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 (--tokens canonical, --skin legacy 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.
  8. 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.
  9. 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_PRIMARY appears 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:

GateWhat it catches
validate_structure.pystructure.json schema conformance
verify_typography.pyInter only; 3-tier ladder for body / chart-axis / chart-legend
verify_deck.pyEm-dash / italic / fractional-EMU policy
text_lint.pySentence shape, terminal period, length caps
contrast_lint.pyWCAG 4.5:1 minimum
drift_check.pyTokens-vs-output mismatch (canonical --tokens mode reads the python-primary JSON; legacy --skin mode reads JS skins for back-compat)
fix_theme_portable.pyCloses the six OOXML inheritance gaps that cause cross-machine drift
verify_portable.py --strictPass/fail gate run before delivery
render_pptx.py --pngMicrosoft PowerPoint on macOS (primary ground truth); LibreOffice via render_qa.sh is the fallback

Style-Input Modes

Three modes, no mode-switching ceremony:

  1. No style guidance — built-in default house skin (Calm Precision tokens).
  2. Copy an existing deck — ingest a sample .pptx, extract tokens via extract_theme_colors.py, build new slides that match; fix_theme_portable.py --theme-map <extracted> for paste compatibility.
  3. User-provided style — explicit .deck-prefs.json at 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-cover and 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 --png treats Microsoft PowerPoint on macOS as primary ground truth and LibreOffice (via render_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.py kept a --skin mode that reads JS skins for back-compat alongside the canonical --tokens mode, and the earlier v0.2.0 fixed-renderer and v0.3.0 PptxGenJS guided pipeline moved to legacy/ 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.json is 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-critique and accessibility-review passes