Open intermediate representation (IR) → open-source compiler → editable PowerPoint (.pptx).
OpenPPT fills the open-source gap left by proprietary “YAML deck + closed WASM exporter” stacks: agents write a declarative, schema-checked deck, and a deterministic OSS path (pptxgenjs) produces a real, text-editable PPTX. No Kimi / neo-ppt frontend mirror and no official WASM are required for the default export path.
| Version | 1.5.0 |
| License | Apache-2.0 (see LICENSE + NOTICE) |
| Runtime | Bun ≥ 1.1 (preferred; scripts and shebang use Bun) |
| Default exporter | pptxgenjs (MIT) |
| Repo | https://github.com/bbingz/OpenPPT |
git clone https://github.com/bbingz/OpenPPT.git
cd OpenPPT
bun install# scaffold a project (themes: default|dark|magazine|report)
bun bin/openppt.js init my-deck --theme magazine --title "Hello"
# markdown outline → deck (# title, ## section, - bullets)
bun bin/openppt.js from-outline fixtures/outline-sample.md -o out/from-md --force
# validate IR
bun bin/openppt.js validate fixtures/golden/deck.json
# export editable PPTX
bun bin/openppt.js export fixtures/golden/deck.json -o out/deck.pptx --force
# lossy import PPTX → IR project (text/shapes/images/tables)
bun bin/openppt.js import out/deck.pptx -o recovered/ --force
# structural layout QA (JSON report; --fail-on low|med|high|critical)
bun bin/openppt.js qa fixtures/golden/deck.json
bun bin/openppt.js qa fixtures/golden/deck.json --fail-on med
# offline HTML preview
bun bin/openppt.js preview fixtures/golden/deck.json -o out/preview.html
# pitch skeleton (cover + TOC + body + final)
bun bin/openppt.js export templates/pitch-skeleton/deck.json -o out/pitch.pptx --forceAfter linking, the binary name is openppt (shebang: #!/usr/bin/env bun).
Thin skill for Claude Code / Codex / Cursor (and any SKILL.md host):
bun run install:skill
# or: bash scripts/install-skill.shInstalls openppt under ~/.agents/skills (and ~/.claude / ~/.codex / ~/.cursor / ~/.grok when those trees exist). Source of truth: skills/openppt/SKILL.md.
- Schema:
schema/openppt-ir.schema.json - Version marker:
"version": "openppt-1" - Canvas:
size: [width, height]in CSS pixels (default fixture uses 960×540) - Theme tokens:
"$primary"style references undertheme.colors - Elements:
text(+ optionalhref) ·shape·image·chart·table - Layout groups (v1.4):
type: "group"withlayout: stack|row|grid|layer→ absolute bounds at load - Multi-file:
pagesmay list relative page files (e.g."pages/cover.json") - Bounds: absolute
[x, y, width, height]— must fit inside the canvas - IDs: page ids and element ids must each be unique across the whole deck
(element ids are not scoped per page — don't reuse
titleon every slide) - Media:
media/only; extension + magic-byte check; no remote URLs; no symlink escape
Golden fixture: fixtures/golden/deck.json (2 pages: cover + body, text/shape/image).
Templates: templates/pitch-skeleton/deck.json (cover · TOC · body · final) and page fragments under templates/pages/. See templates/README.md.
Charts demo: fixtures/chart-demo/deck.json.
Layout demo: fixtures/layout-demo/deck.json (stack · nested row · grid · layer).
Table demo: fixtures/table-demo/deck.json.
Agents: start with docs/AGENT.md.
{
"version": "openppt-1",
"title": "Hello",
"size": [960, 540],
"theme": {
"colors": {
"primary": "#2563EB",
"text": "#111827",
"background": "#FFFFFF"
}
},
"pages": [
{
"id": "p1",
"background": { "type": "solid", "color": "$background" },
"elements": [
{
"id": "t1",
"type": "text",
"bounds": [60, 200, 840, 60],
"text": "Hello OpenPPT",
"fontSize": 32,
"color": "$primary",
"align": "center"
}
]
}
]
}import { loadDeck, validateDeck, compileToPptx, exportDeckFile } from "openppt";
const { deck, projectRoot } = loadDeck("./deck.json");
validateDeck(deck, { projectRoot, checkMedia: true });
await compileToPptx(deck, "./out.pptx", { projectRoot, force: true });
// or one-shot:
await exportDeckFile("./deck.json", "./out.pptx", { force: true });Default export refuses to write a PPTX when:
| Condition | Error code |
|---|---|
| JSON Schema mismatch | SCHEMA_INVALID |
| Element outside canvas | BOUNDS_OUT_OF_RANGE |
| Missing local image | MEDIA_MISSING |
Unresolved $token |
THEME_COLOR_UNRESOLVED |
Duplicate page id |
SCHEMA_INVALID |
Duplicate element id (deck-wide) |
SCHEMA_INVALID |
The last two are enforced by validateDeck, not by the JSON Schema — schema
alone cannot express cross-document uniqueness.
- Read
schema/openppt-ir.schema.json. Optionally copy colors fromthemes/default.jsoninto your deck'stheme.colors(it is a template only — not auto-loaded at runtime). - Write a self-contained project:
deck.json+media/*next to it. - Run
bun bin/openppt.js validate <deck.json>and fix errors. - Run
bun bin/openppt.js export <deck.json> -o <deck.pptx> --force. - Deliver both the IR project folder and the
.pptx.
Do not call any Kimi WASM, neo-ppt mirror, or www.kimi.com export path.
Use Bun, not Node, for install/test/export in this project.
In scope
- Versioned open IR + machine-checkable schema
- Open compiler to real OOXML PPTX (editable text)
- Structural validation (bounds + media)
- CLI + library entry points + tests
Not in v1.0 (backlog)
- Charts, animations, font embedding parity
- Browser WYSIWYG editor
- Lossless PPTX → IR round-trip
- Full consulting design-system packs
- npm registry publish
See docs/BACKLOG.md.
- Default export uses only
pptxgenjs+ our validate/load code. - Package
files/ dependencies do not includeeditor/neo-ppt/**orpptd_wasm*.wasm. - Developer checkouts may keep
upstream/orbackups/as gitignored research references; they are not required to export.
bun test ./test/The ./ prefix matters: bun test positional args are path filters, so a bare
test/ (or no argument) also collects any gitignored upstream/test/**.
Apache-2.0 — Copyright 2026 OpenPPT contributors. Third-party notices in NOTICE.