A topic goes in. A themed deck and a graded report come out.
Local-first presentation and report generation for academic work.
| Glassmorphism — a frosted plate, rendered in Chrome | Swiss International — a dense KPI slide |
|---|---|
![]() |
![]() |
Every image in this README is a real render from the repository, produced by the same code path a generated deck goes through — not a mockup. The left one is a plate: a background Chrome renders as an image because PowerPoint cannot draw frosted glass, with the text still native and editable on top of it.
| A real briefing session — topic → choices → optional thesis |
|---|
![]() |
Captured from a fresh private Docker install. The session enters a real topic, chooses presentation settings, and opens the actual optional-detail questions. It stops before generation because no model was available; it does not pretend a deck was produced. Download the MP4.
| A real local app session — project workspace → slide viewer |
|---|
![]() |
Captured from the local Docker bundle using the public Green Hydrogen deck in this repository. It shows opening and inspecting existing work, not a fabricated live-generation run. Download the MP4.
Install Docker, then:
git clone https://github.com/Deepnar/presentation-forge.git
cd presentation-forge
docker compose up -d --build --waitOpen http://localhost:8090. The first visit creates one local owner and signs you in; after that, public signup is closed. There is no email verification, Google login or password-reset ceremony in this private-machine mode.
Forge and its private SearXNG research service now run. To generate, choose one of two model paths:
- Bring your own key: add an OpenAI-compatible provider under Settings → Cloud. Only model requests go to that provider. Forge starts with a 180,000-token rolling 24-hour safety budget, caps each BYOK response at 12,000 tokens, and allows at most one transport retry; the key owner can change the daily guard in Profile. Your provider bills your account directly, so also set a hard spending limit in its dashboard.
- Keep inference local: install Ollama, run
ollama pull qwen3:4b, and leave Forge on Auto. This is a lightweight starting model; use a stronger model or BYOK when output quality matters.
# Optional install check; Node runs inside the container.
docker compose exec forge env FORGE_CHECK_URL=http://localhost:5174 node tools/local-check.mjsWindows and macOS work through Docker Desktop. Linux may need Ollama exposed to
Docker's host gateway when using local inference. LOCAL_SETUP.md
has that command, platform notes, data locations and troubleshooting. This
private bundle binds only to localhost; use docs/DEPLOY.md
for an internet-facing multi-user service.
The model never writes layout. Asking a language model for coordinates and font sizes fails the moment content changes length. So it cannot express them:
| Layer | Owns | Written by |
|---|---|---|
| chrome | crest, banner, presenter line, slide number | locked code in src/chrome.js |
| theme | palette, type, spacing, shape | human-authored themes/*.yaml |
| content | what the slides say | the model, as schema-validated YAML |
The model picks a semantic slide type and writes its content. It cannot supply a coordinate, a hex value, or a font name — those keys do not exist in the schema it is decoding against.
Which is what makes this possible. The same slide, same words, four themes:
| One slide, four themes — 6-second loop |
|---|
![]() |
This is a real render loop, not a product simulation. Nothing in the content
changes between frames; switching theme is one field in deck.yaml, and 34 of
them ship. Download the MP4.
brief → research → outline → HUMAN GATE → content → validate → render → preview → critique
notes.md plan.yaml deck.yaml .pptx PNGs
.docx
The gate is deliberate: nothing renders until a person has read the outline and approved it. Everything after it is deterministic or checked.
- Research from SearXNG, arXiv, Crossref, or your own uploaded documents. Claims are checked back against the saved notes before a deck is finalised.
- Per-slide retrieval. Each slide is shown the research that slide needs rather than the whole corpus — which is both cheaper and better writing.
- Rendered, then read back. A written
.pptxproves only that the file parsed. Slides are rasterised and the text read off the image, becausepres.writeFile()succeeds happily for text running off the canvas. - A vision critic looks at the rendered PNGs and fixes what it sees.
74 slide types: 73 native layouts plus the deliberate freeform exception.
The model chooses among them by what the content is — a comparison becomes a
comparison layout, not a bulleted list about comparing.
timeline |
compare |
chart |
|---|---|---|
![]() |
![]() |
![]() |
pyramid |
matrix |
flow |
![]() |
![]() |
![]() |
table |
funnel |
big-number |
![]() |
![]() |
![]() |
Text stays editable. freeform is the one type rasterised whole — it is the
deliberate exception, where the model writes the slide as HTML. A plate theme
is not the same thing: Chrome renders only the decorative background to a PNG,
and every word on top of it is still native PowerPoint text you can select and
retype.
Decks and reports made with this app, committed so you can open them without running anything:
| Presentation | Size | Files |
|---|---|---|
| Recent Trends in Mixed-Mode Programming | 21 slides + report | PPTX · DOCX |
| First Impressions & Networking | 20 slides + report | PDF · PPTX · DOCX |
| First Impressions & Networking, a second run | 20 slides | PPTX |
Browse published/. Private decks live in decks/<slug>/ and
are gitignored.
The deck is free and themed; the report is rigid and graded against an
institutional template. Your department's .docx is the donor: the renderer
strips only its body and keeps everything else byte-identical — the watermark
in the header, the footer's page field, the styles, the media.
The section list is read from the donor itself, so a different college's template is an upload, not a code change. Table-of-contents page numbers are real: the render is two-pass, converting once to find where each heading lands.
Use this only when developing Forge itself rather than using it. It exposes the Vite UI on :5173 and API on :5174, and expects the renderer prerequisites on your host:
npm install
npm run fonts
npm run brand
cp config/identity.example.yaml config/identity.yaml
ollama pull qwen3:4b
npm run devLOCAL_SETUP.md covers the source path, platform-specific
prerequisites and troubleshooting. docs/DEPLOY.md is the
separate hardened production route — hosted mode, Caddy/TLS, mail, persistent
secrets, administrator controls and tenant limits.
npm run dev # API :5174 + UI :5173
npm test # automated test suite
npm run render decks/<slug>/deck.yaml # content -> .pptx
npm run preview decks/<slug>/out/deck.pptx # .pptx -> PNGs
npm run themematrix # every theme x every type, fit verdicts
npm run textcheck # did every word survive onto the page
npm run drawcheck # did every field reach the page at all
npm run deckscore <slug> # a deck, scored /100
npm run deckscore -- --history # what previous runs scored
npm run forge -- new "<topic>" --research # headless: outline
npm run forge -- generate <slug> --critic # headless: deckThe CLI is not a wrapper around the app — both call the same src/. The API
layer holds no presentation logic, because the pipeline has to run headless.
A written .pptx proves the file parsed and nothing else. pres.writeFile()
succeeds for decks with text off the canvas and invisible-on-invisible colour
pairs. So the checks ask four different questions, and each was added because
the ones before it were clean on a defect that shipped:
- Is the box on the slide? A geometry watcher runs inside every render.
- Was the field drawn at all?
drawcheckwrites a marker into each field and reads back what the layout emitted. - Does the text fit?
themematrixacross 34 themes × 74 types. - Did it survive the render?
textcheckrasterises and reads it back.
None of them replaces looking at the image.
app/server/ Express transport over the shared core — no logic of its own
app/web/ Vite + React browser application
src/ renderer, pipeline, research, validation, previews, metering
themes/ 34 design languages
styles/ reusable token overrides layered over a theme
schema/ the content contracts for decks and reports
templates/ starting content templates
config/ models, identity, accounts, keys
brand/ institutional marks, per operator and per account
app/gallery/ committed theme specimens — the images above
decks/<slug>/ a portable workspace: content, plan, research, output
docker/ image, Compose stack, TLS, SearXNG
docs/ architecture, blockers, economics, roadmap, traps
No external database service is required. A deck workspace is ordinary files and copies to another installation.
| Data | Location | Committed? |
|---|---|---|
| Deck content, outline, research, output | decks/<slug>/ |
definitions yes, output no |
| Institution identity | config/identity.yaml, config/identities/ |
no |
| Accounts, sessions, keys, usage | config/forge.db |
no — keys encrypted under FORGE_KEY_PEPPER |
| Provider keys (install-wide) | config/local.yaml |
no |
| Brand assets | brand/ |
no — they may carry protected marks |
With Ollama, model calls never leave the machine. Research providers and an explicitly chosen hosted provider involve network requests; the deck, the research and the account data stay local.
Retention. Nothing is deleted unless FORGE_SWEEP_DAYS is set, and then the
clock is inactivity, not age. GET /api/policy reports what the running
install actually enforces, so the notice in the app cannot drift from the
scheduler behind it.
The internet-facing stack is intentionally separate: TLS, SMTP, secure cookies,
operator secrets, tenant limits and retention. It still uses the same image and
builds for amd64 and arm64. See docs/DEPLOY.md; the root
Compose file is private local software and must not be exposed as a public
service.
| Doc | Answers |
|---|---|
| ARCHITECTURE | how it is built, as built |
| ROADMAP | all work — done, planned, and why |
| BLOCKED | what cannot be worked on, and who can unblock it |
| ECONOMICS | what a deck costs to produce and what it can be sold for |
| PRODUCTION | what stands between here and real users |
| TRAPS | failure modes that have already bitten |
| LOCAL_SETUP | prerequisites and troubleshooting |
Questions and early ideas belong in GitHub Discussions. Reproducible defects and scoped feature requests use the repository issue templates. Before contributing, read CONTRIBUTING.md and the Code of Conduct. Report vulnerabilities privately through the process in SECURITY.md; never put keys or private deck content in a public issue.
MIT. Institutional marks, donor templates and assets you add remain yours and may carry their own restrictions.













