slotdeck 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +495 -2
  3. package/dist/deck/numbers.d.ts +14 -0
  4. package/dist/deck/numbers.js +48 -0
  5. package/dist/deck/numbers.js.map +1 -0
  6. package/dist/deck/spec.d.ts +73 -0
  7. package/dist/deck/spec.js +21 -0
  8. package/dist/deck/spec.js.map +1 -0
  9. package/dist/deck/validate.d.ts +49 -0
  10. package/dist/deck/validate.js +194 -0
  11. package/dist/deck/validate.js.map +1 -0
  12. package/dist/generate.d.ts +53 -0
  13. package/dist/generate.js +44 -0
  14. package/dist/generate.js.map +1 -0
  15. package/dist/index.d.ts +42 -0
  16. package/dist/index.js +27 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/llm/gemini.d.ts +39 -0
  19. package/dist/llm/gemini.js +147 -0
  20. package/dist/llm/gemini.js.map +1 -0
  21. package/dist/llm/provider.d.ts +38 -0
  22. package/dist/llm/provider.js +2 -0
  23. package/dist/llm/provider.js.map +1 -0
  24. package/dist/node.d.ts +3 -0
  25. package/dist/node.js +31 -0
  26. package/dist/node.js.map +1 -0
  27. package/dist/plan/assets.d.ts +28 -0
  28. package/dist/plan/assets.js +80 -0
  29. package/dist/plan/assets.js.map +1 -0
  30. package/dist/plan/catalog.d.ts +106 -0
  31. package/dist/plan/catalog.js +302 -0
  32. package/dist/plan/catalog.js.map +1 -0
  33. package/dist/plan/edit.d.ts +31 -0
  34. package/dist/plan/edit.js +364 -0
  35. package/dist/plan/edit.js.map +1 -0
  36. package/dist/plan/plan.d.ts +106 -0
  37. package/dist/plan/plan.js +243 -0
  38. package/dist/plan/plan.js.map +1 -0
  39. package/dist/plan/write.d.ts +39 -0
  40. package/dist/plan/write.js +179 -0
  41. package/dist/plan/write.js.map +1 -0
  42. package/dist/pptx/package.d.ts +48 -0
  43. package/dist/pptx/package.js +202 -0
  44. package/dist/pptx/package.js.map +1 -0
  45. package/dist/pptx/presentation.d.ts +20 -0
  46. package/dist/pptx/presentation.js +31 -0
  47. package/dist/pptx/presentation.js.map +1 -0
  48. package/dist/pptx/xml.d.ts +23 -0
  49. package/dist/pptx/xml.js +67 -0
  50. package/dist/pptx/xml.js.map +1 -0
  51. package/dist/render/chart.d.ts +18 -0
  52. package/dist/render/chart.js +158 -0
  53. package/dist/render/chart.js.map +1 -0
  54. package/dist/render/extract.d.ts +11 -0
  55. package/dist/render/extract.js +65 -0
  56. package/dist/render/extract.js.map +1 -0
  57. package/dist/render/picture.d.ts +20 -0
  58. package/dist/render/picture.js +114 -0
  59. package/dist/render/picture.js.map +1 -0
  60. package/dist/render/render.d.ts +32 -0
  61. package/dist/render/render.js +247 -0
  62. package/dist/render/render.js.map +1 -0
  63. package/dist/render/repeat.d.ts +15 -0
  64. package/dist/render/repeat.js +118 -0
  65. package/dist/render/repeat.js.map +1 -0
  66. package/dist/render/table.d.ts +12 -0
  67. package/dist/render/table.js +84 -0
  68. package/dist/render/table.js.map +1 -0
  69. package/dist/render/text.d.ts +31 -0
  70. package/dist/render/text.js +210 -0
  71. package/dist/render/text.js.map +1 -0
  72. package/dist/store.d.ts +18 -0
  73. package/dist/store.js +23 -0
  74. package/dist/store.js.map +1 -0
  75. package/dist/template/change.d.ts +138 -0
  76. package/dist/template/change.js +406 -0
  77. package/dist/template/change.js.map +1 -0
  78. package/dist/template/check.d.ts +15 -0
  79. package/dist/template/check.js +97 -0
  80. package/dist/template/check.js.map +1 -0
  81. package/dist/template/draft.d.ts +47 -0
  82. package/dist/template/draft.js +276 -0
  83. package/dist/template/draft.js.map +1 -0
  84. package/dist/template/inspect.d.ts +53 -0
  85. package/dist/template/inspect.js +157 -0
  86. package/dist/template/inspect.js.map +1 -0
  87. package/dist/template/instruct.d.ts +26 -0
  88. package/dist/template/instruct.js +278 -0
  89. package/dist/template/instruct.js.map +1 -0
  90. package/dist/template/manifest.d.ts +226 -0
  91. package/dist/template/manifest.js +116 -0
  92. package/dist/template/manifest.js.map +1 -0
  93. package/dist/template/measure.d.ts +80 -0
  94. package/dist/template/measure.js +212 -0
  95. package/dist/template/measure.js.map +1 -0
  96. package/dist/template/sample.d.ts +17 -0
  97. package/dist/template/sample.js +96 -0
  98. package/dist/template/sample.js.map +1 -0
  99. package/dist/template/view.d.ts +46 -0
  100. package/dist/template/view.js +38 -0
  101. package/dist/template/view.js.map +1 -0
  102. package/dist/tools.d.ts +68 -0
  103. package/dist/tools.js +266 -0
  104. package/dist/tools.js.map +1 -0
  105. package/dist/util/image.d.ts +11 -0
  106. package/dist/util/image.js +58 -0
  107. package/dist/util/image.js.map +1 -0
  108. package/dist/util/png.d.ts +2 -0
  109. package/dist/util/png.js +50 -0
  110. package/dist/util/png.js.map +1 -0
  111. package/docs/README.md +76 -0
  112. package/docs/api-reference.md +668 -0
  113. package/docs/chat-tools.md +253 -0
  114. package/docs/concepts.md +126 -0
  115. package/docs/deck-format.md +225 -0
  116. package/docs/generating.md +218 -0
  117. package/docs/getting-started.md +139 -0
  118. package/docs/languages.md +83 -0
  119. package/docs/layout-freedom.md +113 -0
  120. package/docs/manifest-format.md +207 -0
  121. package/docs/providers.md +236 -0
  122. package/docs/rendering.md +172 -0
  123. package/docs/template-studio.md +369 -0
  124. package/docs/troubleshooting.md +166 -0
  125. package/package.json +64 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 renanbazinin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,496 @@
1
- # Temporary Holding Version
1
+ # slotdeck
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Turn a PowerPoint template into new decks. A language model decides what goes on each slide; the code clones
4
+ the template's own slides and fills them, so the design is never redrawn.
5
+
6
+ ![A nine-slide deck made from a one-paragraph brief](docs/showcase.jpg)
7
+
8
+ *Planned, written and illustrated from a one-paragraph brief, on the demo template in this repository
9
+ ([deck JSON](examples/showcase/rescue-dog/deck.json)). The shelter and its numbers are invented.*
10
+
11
+ - **Template in, decks out.** Each sample slide of a pptx becomes a reusable **pattern**. A deck picks a
12
+ pattern per slide and supplies text and pictures for its **slots**. Everything that is not a slot (logo,
13
+ backgrounds, fonts, decorations) is copied untouched.
14
+ - **Two layers.** A renderer that needs no model (`template + deck JSON -> pptx`), and a planner that turns a
15
+ brief into that JSON with any model you plug in. Gemini adapters are included.
16
+ - **Editable results.** Decks are plain JSON without binary data, so they can be stored, changed by instruction
17
+ ("shorten slide 3") and rendered again. Charts are native PowerPoint charts with their data inside.
18
+ - **Any language, either direction.** A Hebrew or Arabic deck on a left-to-right template mirrors its text and
19
+ the order of its cards, and the other way round.
20
+ - **A studio for templates.** A model drafts the manifest that describes a template; you correct it by hand or
21
+ by instruction; the code checks every change against the pptx.
22
+
23
+ It has two runtime dependencies (`fflate`, `@xmldom/xmldom`), no native code and no headless office.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install slotdeck
29
+ ```
30
+
31
+ Node 22.18 or later. The package is ESM.
32
+
33
+ ## Documentation
34
+
35
+ The full documentation is in [`docs/`](docs/README.md): concepts, both JSON formats, every function and option,
36
+ the chat tools, connecting your own model, the template studio, and every error message with its fix.
37
+
38
+ It is plain Markdown with no links to the internet, and it ships inside the npm package
39
+ (`node_modules/slotdeck/docs/`), so a coding assistant without network access can read it where the package is
40
+ installed. This page is the overview.
41
+
42
+ ## How it works
43
+
44
+ A template is an ordinary deck of sample slides, the kind a designer hands over:
45
+
46
+ ![The demo template: thirteen sample slides](docs/template.png)
47
+
48
+ A **manifest** (JSON) says which slides are patterns and which shapes on them are slots, with a character
49
+ budget for each text slot. A **deck** (JSON) lists slides, each naming a pattern and giving values for its
50
+ slots:
51
+
52
+ ```json
53
+ {
54
+ "version": 1,
55
+ "fields": { "organizationDepartment": "Riverbend Bike Share | Season review" },
56
+ "slides": [
57
+ { "pattern": "cover", "slots": { "title": "A year of Riverbend Bike Share", "subtitle": "What the 2025 season taught us" } },
58
+ {
59
+ "pattern": "icon-cards",
60
+ "items": 3,
61
+ "slots": {
62
+ "title": "Three changes for next season",
63
+ "card1Icon": { "asset": "clock" }, "card1Heading": "Longer hours", "card1Text": "Bikes unlock from five in the morning.",
64
+ "card2Icon": { "asset": "shield" }, "card2Heading": "Safer docks", "card2Text": "Every station gets lighting.",
65
+ "card3Icon": { "asset": "people" }, "card3Heading": "Group passes", "card3Text": "One pass for up to five riders."
66
+ }
67
+ }
68
+ ]
69
+ }
70
+ ```
71
+
72
+ ```ts
73
+ import { loadTemplate, renderDeck } from 'slotdeck';
74
+
75
+ const template = loadTemplate(pptxBytes, manifest); // unzip once, reuse for every deck
76
+ const { pptx, warnings } = renderDeck(template, deck, { assets: { clock, shield, people } });
77
+ ```
78
+
79
+ That is the whole renderer: no model, no network, about 100 ms for a 15-slide deck.
80
+
81
+ ## Generate a deck with a model
82
+
83
+ ```ts
84
+ import { gemini, geminiImages, generateDeck, loadTemplate } from 'slotdeck';
85
+
86
+ const template = loadTemplate(pptxBytes, manifest);
87
+
88
+ const { pptx, deck, warnings } = await generateDeck(
89
+ template,
90
+ {
91
+ prompt: 'A deck for new adopters: why adopt a rescue dog, the first month at home, what it costs',
92
+ language: 'en',
93
+ slideCount: 9,
94
+ imageDensity: 'medium', // none | low | medium | high
95
+ fields: { organizationDepartment: 'Harbor Street Shelter | Adoption team' },
96
+ },
97
+ {
98
+ llm: gemini({ model: textModel, apiKey }),
99
+ images: geminiImages({ model: imageModel, apiKey }),
100
+ icons: [{ id: 'clock', description: 'a clock: time, schedule', data: clockPng }],
101
+ assets: [{ id: 'flow', kind: 'graphic', description: 'adoption process diagram', data: mermaidPng }],
102
+ layout: 'adaptive', // or 'strict'
103
+ onProgress: (event) => console.log(event.stage),
104
+ },
105
+ );
106
+ ```
107
+
108
+ `generateDeck` is three steps you can also call yourself:
109
+
110
+ | Step | Function | Model calls |
111
+ | --- | --- | --- |
112
+ | Brief to deck JSON | `planDeck` | 2, plus up to 2 repair rounds |
113
+ | Deck JSON and instruction to deck JSON | `planEdit` | 1 or 2, plus up to 2 repair rounds |
114
+ | Deck JSON to picture bytes | `resolveAssets` | one per generated photo |
115
+ | Deck JSON and pictures to pptx | `renderDeck` | none |
116
+
117
+ Keep the returned `deck`. It is plain JSON with no binary data, so it can be stored, changed and rendered again.
118
+ Pass the same `cache` map to later calls and photos whose description did not change are not generated twice.
119
+
120
+ **Icons** come from a set your app registers, and the model picks among them by description. **Photos** come
121
+ from an image source you plug in. **Supplied pictures** (`assets`) are for things your app already has, such as
122
+ a rendered Mermaid diagram.
123
+
124
+ ### How planning works
125
+
126
+ 1. **Outline.** The model sees the template's patterns with their slot limits and picks one per slide.
127
+ 2. **Content.** The model fills a JSON schema built for exactly that sequence of slides, so it cannot invent
128
+ patterns or slots.
129
+ 3. **Repair.** The draft is checked against the manifest. Text over a slot's budget or a missing slot goes back
130
+ to the model with the list of problems.
131
+
132
+ Patterns the caller cannot fill are never offered: photo patterns when images are off, icon patterns without an
133
+ icon set, patterns that need a deck field that was not given.
134
+
135
+ Patterns built around numbers (`"figures": true` in the manifest) are offered only when the brief itself
136
+ contains figures. In a live run without this guard the model filled such a slide with an invented statistic. Set
137
+ `numberPatterns: 'always'` to offer them regardless, or `'never'` to rule them out. Chart values the brief does
138
+ not state come back as warnings (`chart values not stated in the brief: 89`).
139
+
140
+ ### Editing a deck
141
+
142
+ ```ts
143
+ import { editDeck, extractAssets } from 'slotdeck';
144
+
145
+ // The deck JSON and the pptx rendered from it are all that has to be stored between turns:
146
+ // the pictures are read back from the file instead of being generated again.
147
+ const cache = new Map(Object.entries(extractAssets(template, deck, previousPptx)));
148
+
149
+ const { pptx, deck: next, summary, rewritten } = await editDeck(template, deck, 'Shorten slide 3 to two bullets', {
150
+ llm,
151
+ images,
152
+ icons, // the same icon set and assets the deck was made with
153
+ cache,
154
+ });
155
+ ```
156
+
157
+ An edit is two model calls. The first decides what the deck looks like after the change: which slides stay,
158
+ move, go, get rewritten or are new. The second writes only the slides that change. Reordering and deleting need
159
+ no second call.
160
+
161
+ - Slides the instruction does not touch are copied by code, so their text cannot drift.
162
+ - A rewritten slide keeps its pictures unless the instruction is about them.
163
+ - A slide given a different layout takes its photo along.
164
+ - A change of language rewrites every slide, sets the reading direction and translates deck-level texts.
165
+ - `summary` is one sentence from the model on what it did, to show the person who asked.
166
+
167
+ Changes made by hand in PowerPoint are not read back: the deck JSON is the source of truth.
168
+
169
+ ### Chat tools
170
+
171
+ For a chat app, the package provides the four tools a model needs to make a deck with a person in the loop:
172
+
173
+ | Tool | What it does | Model calls |
174
+ | --- | --- | --- |
175
+ | `plan_presentation` | Decides the slides, the layout of each and the point each makes | 1 |
176
+ | `edit_presentation_plan` | Changes a plan by instruction | 1 |
177
+ | `create_presentation` | Writes the approved plan's content, makes its pictures, renders the file | 1, plus repairs and pictures |
178
+ | `edit_presentation` | Changes a created deck by instruction | 1 or 2, plus repairs |
179
+
180
+ ```ts
181
+ import { presentationTools } from 'slotdeck';
182
+ import { directoryStore } from 'slotdeck/node';
183
+
184
+ const tools = presentationTools({
185
+ template,
186
+ llm,
187
+ images, // optional: without it decks are planned with no photos
188
+ icons,
189
+ store: directoryStore('/mnt/presentations'), // a volume, or any object with get(key) and set(key, bytes)
190
+ fields: { organizationDepartment: 'Harbor Street Shelter | Adoption team' },
191
+ thumbnail: (pattern) => `/static/thumbnails/${pattern}.png`,
192
+ });
193
+
194
+ // Declare the tools to the chat model (Interactions format shown):
195
+ const declarations = tools.definitions.map((tool) => ({ type: 'function', ...tool }));
196
+
197
+ // Run a tool the model called:
198
+ const { output, plan, deck, pptx } = await tools.call(call.name, call.arguments);
199
+ // `output` goes back to the model; `plan`, `deck` and `pptx` are for the app's own UI.
200
+ ```
201
+
202
+ The model only ever handles ids and short summaries. Everything else is kept in the store:
203
+
204
+ | Key | Content | Typical size |
205
+ | --- | --- | --- |
206
+ | `plan/<id>.json` | A plan: the brief and the slide list | 1 KB |
207
+ | `deck/<id>.json` | A deck: all text, and for each picture its id and description | 4 KB |
208
+ | `asset/<id>` | One picture | tens of KB to 1 MB |
209
+ | `pptx/<id>.pptx` | The rendered file | 1 MB and up |
210
+
211
+ Pictures are stored once, apart from the JSON and the file. A deck's JSON never holds image bytes, so no heavy
212
+ data reaches a model, and a pptx can be rebuilt from the JSON and the pictures at any time. Every change makes a
213
+ new plan or deck under a new id; earlier versions stay.
214
+
215
+ To show a plan as layout thumbnails, render the template once and name the pictures after the patterns
216
+ (`scripts/pattern-thumbnails.ts`). `examples/plan-preview.ts` turns a plan into a self-contained page.
217
+
218
+ ### Other models
219
+
220
+ A text model is anything with this method:
221
+
222
+ ```ts
223
+ const llm: LlmProvider = {
224
+ async generateJson({ system, prompt, schema, images, signal }) {
225
+ return myModel.json({ system, prompt, schema }); // resolve with the parsed JSON
226
+ },
227
+ };
228
+ ```
229
+
230
+ An image source is anything with `generateImage({ prompt, kind, aspectRatio })` that resolves with PNG, JPEG or
231
+ GIF bytes. It can be an image model, a stock library or a search.
232
+
233
+ The Gemini adapters call the Interactions API with plain `fetch`. For Vertex AI, pass
234
+ `endpoint: vertexEndpoint(project)` and a `headers` function that returns a bearer token. A request that has
235
+ not answered after three minutes is abandoned with an error; `timeoutMs` changes the limit.
236
+
237
+ ## Languages and direction
238
+
239
+ A deck states its language and reading direction; the template has its own. When they differ, text flips and
240
+ the layout stays:
241
+
242
+ ![A Hebrew deck on the left-to-right demo template](docs/hebrew.png)
243
+
244
+ - Every paragraph gets the direction of its own text, so a Hebrew deck may hold an English line.
245
+ - Alignment mirrors (`alignOnDirectionChange: 'flip'`, for the whole template or per slot). Templates whose text
246
+ boxes share one edge but not the other can `keep` it.
247
+ - Rows and grids of items are filled mirror-wise, so the deck's first card is where its readers start.
248
+ - Tables and charts run in the deck's direction.
249
+ - Right-to-left runs are marked with their language, because PowerPoint places punctuation by the language of a
250
+ run: without it, the colon in "contact: name@example.org" lands on the wrong side of the address.
251
+ - Shapes do not move. A photo on the right stays on the right.
252
+
253
+ ## Templates
254
+
255
+ - **Manifest** (`TemplateManifest`): which slides are patterns and which shapes are slots. See
256
+ [`templates/demo.manifest.json`](templates/demo.manifest.json).
257
+ - **Layout**: `strict` requires every slot to be filled. `adaptive` lets optional slots be left out, and removes
258
+ their shapes.
259
+ - **Empty slots**: each slot says what happens without a value. `error` makes it required, `remove` makes it
260
+ optional, `keep` leaves the template's content, and `drop` always removes it (for a sticker drawn onto a
261
+ particular sample photo).
262
+ - **Deck fields**: a text that is the same on every slide, such as the organization line beside the logo, is
263
+ one value for the whole deck (`fields`).
264
+ - **Tables**: a table slot takes `{ header, rows }`. The sample table's rows supply the styles (header, then the
265
+ body rows in turn), columns share its width, and it runs in the deck's reading direction.
266
+ - **Supplied pictures**: a `graphic` slot is filled from the `assets` the app passes. The picture keeps its
267
+ proportions inside the frame; `box` gives it a larger area than the sample picture's.
268
+ - **Charts**: a chart slot takes `{ type, categories, series, format }` and becomes a native PowerPoint chart
269
+ (`column`, `bar`, `line` or `pie`) with its data in an embedded workbook, so it can be changed with Edit Data.
270
+ The slot points at the sample picture of a chart, which the real chart replaces. Colours, font and line colour
271
+ come from the manifest. Bars start at zero.
272
+ - **Number patterns**: `figures: true` marks a pattern built around numbers.
273
+
274
+ ### Layout freedom
275
+
276
+ A deck can depart from the template slide in bounded ways. In each, the model chooses among options the
277
+ manifest allows and the code moves the shapes; the model never positions anything.
278
+
279
+ - **A different number of items.** A pattern with `repeat` names the shapes that make one card and the range it
280
+ accepts (in the demo template `icon-cards` takes 2 to 4 and `stat-cards` 2 to 6). A slide says how many with
281
+ `items`. Surplus cards are removed, extra ones are copies of the last, and the row is spaced out again around
282
+ its centre. A copied card has its own slots (`card5Icon`, `card5Heading`, ...).
283
+ - **Grids.** With `columns`, the items form a grid. They take as few rows as they need and spread evenly: four
284
+ figures in a three-column grid make two rows of two, five make three and two, three make one row.
285
+ - **Larger text.** `textScale` on a deck (or `textSize: 'large' | 'x-large'` on a brief) enlarges body text:
286
+ slots of 12 pt and under by default, not titles. Because the boxes stay the same size, character budgets fall
287
+ with the square of the scale, so the model writes less.
288
+ - **Derived patterns.** Several patterns can come from one slide. A text slot can be moved and resized (`box`),
289
+ given a font size (`fontSize`) and paragraph spacing, and any shape can be dropped. In the demo template
290
+ `text-only` is the photo slide with its photo dropped and its text widened, and `chart-native` is the chart
291
+ slide with a native chart in place of the picture.
292
+
293
+ ## Template studio
294
+
295
+ Writing a manifest by hand means reading shape ids out of a pptx. The studio functions do that work with a
296
+ model, and keep the model from breaking anything.
297
+
298
+ ```ts
299
+ import { checkManifest, draftManifest, gemini, loadTemplate, renderDeck, sampleDeck } from 'slotdeck';
300
+
301
+ // 1. Draft: one model call per slide, plus one for the deck.
302
+ const { manifest, skipped, issues } = await draftManifest(pptx, {
303
+ llm: gemini({ model: textModel, apiKey }),
304
+ name: 'marketing',
305
+ slideImage: (slide) => thumbnails.get(slide), // optional PNG or JPEG of each slide; may be async
306
+ onProgress: ({ done, total }) => report(done, total),
307
+ });
308
+
309
+ // 2. Check: after every change to the manifest, and whenever a template is loaded.
310
+ const template = loadTemplate(pptx, manifest);
311
+ const problems = checkManifest(template);
312
+
313
+ // 3. Look: every pattern once, every slot filled close to its budget.
314
+ const { deck, assets } = sampleDeck(template);
315
+ const sample = renderDeck(template, deck, { assets }).pptx;
316
+ ```
317
+
318
+ **`draftManifest`** splits the work so that the model cannot break the manifest. The code measures: every
319
+ shape's id, kind, position, font size and sample text; what sits in the same place on many slides (logo, page
320
+ number: left alone); texts that repeat across slides (deck fields); the reading direction; a character budget
321
+ for each text box from its size and font; a table's columns, rows and cell size; and rows or grids of like
322
+ items, from numbered slot names. The model answers two things for each slide: whether it is a reusable layout
323
+ at all, and what role each shape plays (`title`, `text`, `list`, `number`, `photo`, `icon`, `graphic`, `table`,
324
+ `decoration`, or `fixed`). A label for a shape that is not on the slide is discarded, and the result is run
325
+ through `checkManifest` before it is returned.
326
+
327
+ **`checkManifest`** reports slots that point at a missing shape or the wrong kind of shape, duplicate names,
328
+ rows whose items do not line up with their slots, and text slots with no budget. It reads only the pptx and the
329
+ manifest, so it is safe to run on every load.
330
+
331
+ **`sampleDeck`** uses the template's own filler words, so the result reads like the template with every box
332
+ full. Text that overflows in the sample will overflow in real decks. `items: 'max'` fills rows to their largest
333
+ count, `fill` sets how close to the budget each slot goes.
334
+
335
+ ### Changing a manifest
336
+
337
+ A manifest is changed through small operations. A studio screen sends them from its own controls, and
338
+ `editManifest` has a model turn an instruction into the same operations. Either way the code checks each
339
+ operation against the pptx, measures what follows from it, and refuses what cannot work.
340
+
341
+ ```ts
342
+ import { changeManifest, editManifest, studioView } from 'slotdeck';
343
+
344
+ // By hand: one operation or many.
345
+ const result = changeManifest(template, [
346
+ { op: 'copyPattern', pattern: 'chart-with-takeaway', id: 'chart-native' },
347
+ { op: 'addSlot', pattern: 'chart-native', shape: '6', role: 'chart' }, // the sample picture becomes a native chart
348
+ { op: 'setRepeat', pattern: 'stat-cards', min: 2, max: 6 },
349
+ { op: 'setSlot', pattern: 'table', slot: 'table', maxColumns: 5, bodyText: { size: 12 } },
350
+ ]);
351
+ result.manifest; // the new manifest; the template that was passed in is not touched
352
+ result.applied; // [{ index, change, message }]
353
+ result.rejected; // [{ index, change, reason }] "5 cards do not fit side by side: ... The most is 4"
354
+ result.issues; // checkManifest on the result
355
+
356
+ // By instruction, in any language.
357
+ const edited = await editManifest(template, 'Rename four-icon-cards to icon-cards and make the card icons required.', { llm });
358
+ edited.note; // what the model could not do, in the person's language; usually empty
359
+
360
+ // For a screen: every slide's shapes with their place on the slide, their slots, and the roles they can take.
361
+ const view = studioView(template);
362
+ ```
363
+
364
+ | Operation | What it changes |
365
+ | --- | --- |
366
+ | `setSlot` | A slot's budget, whether it is required, optional, kept or always removed (`empty`), font size, area (`box`), name, deck field, picture kind, and the settings of a table or chart. |
367
+ | `addSlot` | Makes a shape a slot in a role (`title`, `text`, `list`, `number`, `caption`, `photo`, `icon`, `graphic`, `chart`, `table`, `decoration`), with limits measured from the shape. On a shape that is already a slot it changes the role. |
368
+ | `removeSlot` | The shape goes back to being part of the design. |
369
+ | `setRepeat`, `removeRepeat` | How many items a row or grid takes. Finds the row from numbered slot names when the pattern has none. |
370
+ | `addPattern`, `copyPattern`, `setPattern`, `removePattern` | Which slides are patterns, their ids and descriptions, and variants of a pattern from the same slide. |
371
+ | `setManifest` | The template's name, language, direction and alignment policy. |
372
+
373
+ What the code adds to each change:
374
+
375
+ - **Budgets follow.** A new font size, text area, table text size or column count changes the character budget,
376
+ unless the change states one.
377
+ - **Rows are checked for room.** Asking for more items than the row draws widens it as far as the slide allows,
378
+ up to the margin or the next shape beside the row. Asking for more than fit is refused with the most that do.
379
+ - **Row items stay alike.** A change to `card1Icon` is made to that slot of every card.
380
+ - **Nothing is left broken.** A change that would make the manifest point at something the pptx does not have
381
+ is refused, and the rest of the list is still applied.
382
+
383
+ `editManifest` makes one model call, and a second when some of its operations were refused, so the model can
384
+ correct them. The model never sets a position: `box` and `span` exist only for changes made by hand.
385
+ `describeManifest(template)` returns the text the model reads, for an app that wants to show the manifest to its
386
+ own chat model.
387
+
388
+ ### How the demo manifest was made
389
+
390
+ `templates/demo.manifest.json` was made with these functions, not written by hand:
391
+
392
+ 1. `draftManifest` on the 13 slides, with a 480-pixel picture of each: 14 model calls, 29 seconds. All 13
393
+ slides became patterns, with the row of cards, the grid of figures, the two columns and the organization
394
+ line found, and no checker issue.
395
+ 2. One instruction to `editManifest` renamed five patterns, made the card icons and texts required and added the
396
+ native-chart variant. It also asked for five cards in a row; that was refused, because four fit.
397
+ 3. A few changes by hand with `changeManifest`: the text-only variant, chart colours, mirrored alignment for
398
+ right-to-left decks, and tighter budgets for figures and one-line captions.
399
+
400
+ A draft is a starting point. What it does not contain, and a person adds while reviewing: budgets tuned by eye,
401
+ derived patterns, wider rows, picture areas and chart colours. On a 36-slide corporate template (not included
402
+ here) the draft took 37 calls and 80 seconds, skipped the 12 slides that were not layouts, and matched a
403
+ hand-written manifest on 114 of 115 slots.
404
+
405
+ ## Try the examples
406
+
407
+ ```bash
408
+ git clone https://github.com/renanbazinin/slotdeck.git
409
+ ```
410
+
411
+ ```bash
412
+ npm install
413
+ ```
414
+
415
+ ```bash
416
+ npm test
417
+ ```
418
+
419
+ Render the example decks (no model, no key). They are written to `out/`:
420
+
421
+ ```bash
422
+ npm run demo
423
+ ```
424
+
425
+ Generate a deck with Gemini. Put `GEMINI_API_KEY=...` in a `.env` file first:
426
+
427
+ ```bash
428
+ node --env-file=.env examples/generate-demo.ts "A deck about adopting a rescue dog" --slides 6
429
+ ```
430
+
431
+ Photos are free drawn stand-ins unless you add `--real-images`, which calls the paid image model. `--lang he`
432
+ writes in another language, `--charts` supplies two sample charts the way an app would supply its own diagrams,
433
+ and `--dry-run` prints the prompts without calling anything.
434
+
435
+ Change a deck that was generated earlier:
436
+
437
+ ```bash
438
+ node --env-file=.env examples/edit-demo.ts out/a-deck-about-adopting-a-rescue-dog.deck.json "Shorten slide 3 to two bullets"
439
+ ```
440
+
441
+ Play a chat app that calls the four tools in turn:
442
+
443
+ ```bash
444
+ node --env-file=.env examples/tools-demo.ts "A deck about adopting a rescue dog"
445
+ ```
446
+
447
+ Draft a manifest for a template of your own, then change it by instruction:
448
+
449
+ ```bash
450
+ node --env-file=.env examples/studio-demo.ts draft path/to/template.pptx
451
+ ```
452
+
453
+ ```bash
454
+ node --env-file=.env examples/studio-demo.ts edit "make the subtitle optional on every slide"
455
+ ```
456
+
457
+ List the shapes of a template's slides, as the manifest addresses them:
458
+
459
+ ```bash
460
+ node scripts/inspect.ts templates/demo.pptx --slides 2,5
461
+ ```
462
+
463
+ On Windows with PowerPoint installed, this exports every slide of a deck to PNG, which is how the pictures on
464
+ this page were made:
465
+
466
+ ```bash
467
+ powershell -File scripts/render-pptx.ps1 -Path out/patterns-en.pptx -OutDir out/png/patterns-en
468
+ ```
469
+
470
+ ## What to know before relying on it
471
+
472
+ - **Shapes need their own position.** The engine measures the shapes on a slide. A shape that takes its position
473
+ and font size from a layout placeholder, without stating them on the slide, is filled correctly but measured
474
+ as empty, so its budget is only as long as its sample text. Decks exported from Google Slides and decks built
475
+ from free-standing text boxes work as they are.
476
+ - **Budgets are estimates.** A character budget cannot see lines lost to wrapping. Budgets for slots with several
477
+ paragraphs should be set with room to spare, and a sample deck is the way to see them.
478
+ - **Models invent details.** In the showcase deck the model made up an email address and a phone number for the
479
+ closing slide, which were replaced by hand. Figures are guarded (see How planning works); contact details and
480
+ names are not.
481
+ - **What has been run.** The whole path was run against the Gemini Developer API with `gemini-3.8-flash` for
482
+ text and `gemini-3.1-flash-image` for photos, in English and Hebrew. Vertex AI is supported by the adapter but
483
+ has not been run live. Output was checked by rendering in PowerPoint on Windows; other viewers were not tested.
484
+ - **Not built:** swapping the photo side, stacked or combined charts, speaker notes, and reading back changes
485
+ made in PowerPoint.
486
+
487
+ ## The demo template and pictures
488
+
489
+ `templates/demo.pptx` and the icons in `examples/icons` are drawn by
490
+ [`scripts/build-demo-template.py`](scripts/build-demo-template.py): no stock photo, logo or icon from anywhere
491
+ else is in them. The four photos of the showcase deck were generated once with an image model from the
492
+ descriptions in its JSON. All of it is covered by the license below and free to reuse.
493
+
494
+ ## License
495
+
496
+ MIT
@@ -0,0 +1,14 @@
1
+ import type { TemplateManifest } from '../template/manifest.ts';
2
+ import type { DeckSpec } from './spec.ts';
3
+ import type { Issue } from './validate.ts';
4
+ /**
5
+ * The numbers a text states. "3K" counts as both 3 and 3000, "1,200" as 1200, "45%" as 45.
6
+ * Only Western digits are read.
7
+ */
8
+ export declare function statedNumbers(text: string): Set<number>;
9
+ /**
10
+ * Chart values that the deck's brief does not state. A model asked for a chart can fill it with
11
+ * plausible numbers; this flags them so a person can check. Values worked out from the brief
12
+ * (a sum, a growth rate applied) are flagged too: the check cannot tell them from invented ones.
13
+ */
14
+ export declare function unstatedChartValues(manifest: TemplateManifest, deck: DeckSpec): Issue[];
@@ -0,0 +1,48 @@
1
+ import { isChartValue } from './spec.js';
2
+ /**
3
+ * The numbers a text states. "3K" counts as both 3 and 3000, "1,200" as 1200, "45%" as 45.
4
+ * Only Western digits are read.
5
+ */
6
+ export function statedNumbers(text) {
7
+ const numbers = new Set();
8
+ for (const match of text.matchAll(/(\d[\d,]*(?:\.\d+)?)\s*([KkMm])?/g)) {
9
+ const value = Number(match[1].replace(/,/g, ''));
10
+ if (!Number.isFinite(value))
11
+ continue;
12
+ numbers.add(value);
13
+ const suffix = match[2]?.toLowerCase();
14
+ if (suffix === 'k')
15
+ numbers.add(value * 1000);
16
+ if (suffix === 'm')
17
+ numbers.add(value * 1_000_000);
18
+ }
19
+ return numbers;
20
+ }
21
+ /**
22
+ * Chart values that the deck's brief does not state. A model asked for a chart can fill it with
23
+ * plausible numbers; this flags them so a person can check. Values worked out from the brief
24
+ * (a sum, a growth rate applied) are flagged too: the check cannot tell them from invented ones.
25
+ */
26
+ export function unstatedChartValues(manifest, deck) {
27
+ const stated = statedNumbers(deck.brief ?? '');
28
+ const issues = [];
29
+ deck.slides.forEach((slide, index) => {
30
+ const pattern = manifest.patterns.find((candidate) => candidate.id === slide.pattern);
31
+ for (const slot of pattern?.slots ?? []) {
32
+ const value = slide.slots[slot.name];
33
+ if (slot.kind !== 'chart' || !isChartValue(value))
34
+ continue;
35
+ const unknown = [...new Set(value.series.flatMap((series) => series.values))].filter((number) => !stated.has(number));
36
+ if (unknown.length > 0) {
37
+ issues.push({
38
+ severity: 'warning',
39
+ slide: index + 1,
40
+ slot: slot.name,
41
+ message: `chart values not stated in the brief: ${unknown.join(', ')}. Check them before using the deck.`,
42
+ });
43
+ }
44
+ }
45
+ });
46
+ return issues;
47
+ }
48
+ //# sourceMappingURL=numbers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbers.js","sourceRoot":"","sources":["../../src/deck/numbers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAIzC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,mCAAmC,CAAC,EAAE,CAAC;QACvE,MAAM,KAAK,GAAG,MAAM,CAAE,KAAK,CAAC,CAAC,CAAY,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,SAAS;QACtC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACvC,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;QAC9C,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAA0B,EAAE,IAAc;IAC5E,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC;QACtF,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;gBAAE,SAAS;YAC5D,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YACtH,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,CAAC,IAAI,CAAC;oBACV,QAAQ,EAAE,SAAS;oBACnB,KAAK,EAAE,KAAK,GAAG,CAAC;oBAChB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,OAAO,EAAE,yCAAyC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,qCAAqC;iBAC1G,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,73 @@
1
+ import type { Direction } from '../template/manifest.ts';
2
+ /**
3
+ * A deck as plain JSON: the source of truth that is rendered, stored and edited.
4
+ * It holds no binary data; images are referenced by asset id.
5
+ */
6
+ export interface DeckSpec {
7
+ version: 1;
8
+ title?: string;
9
+ /** The request the deck was first written from. Later edits use it as background. */
10
+ brief?: string;
11
+ /** BCP 47 tag of the deck's text. */
12
+ language?: string;
13
+ /** Reading direction of the deck. Defaults to the template's. */
14
+ direction?: Direction;
15
+ /** Values shared by every slide, e.g. the header text. */
16
+ fields?: Record<string, string>;
17
+ /** Size of body text as a factor on the template's: 1 as designed, 1.25 large, 1.5 extra large. */
18
+ textScale?: number;
19
+ slides: SlideSpec[];
20
+ }
21
+ export interface SlideSpec {
22
+ /** Id of a pattern in the template manifest. */
23
+ pattern: string;
24
+ /** For a pattern with a row of items: how many items this slide has. Defaults to the template's number. */
25
+ items?: number;
26
+ slots: Record<string, SlotValue>;
27
+ }
28
+ /** Reference to an image passed to the renderer alongside the deck. */
29
+ export interface ImageRef {
30
+ asset: string;
31
+ /** Alternative text for screen readers. */
32
+ alt?: string;
33
+ /**
34
+ * Description the image can be generated from when the asset is not supplied.
35
+ * Kept in the deck so a later re-render can recreate the picture.
36
+ */
37
+ prompt?: string;
38
+ }
39
+ /** Content of a table slot. */
40
+ export interface TableValue {
41
+ /** Column headings. Their number sets the number of columns. */
42
+ header: string[];
43
+ /** Body rows, each with one cell per column. */
44
+ rows: string[][];
45
+ }
46
+ export type ChartType = 'column' | 'bar' | 'line' | 'pie';
47
+ /** Content of a chart slot. */
48
+ export interface ChartValue {
49
+ /** `column` has vertical bars, `bar` horizontal ones. */
50
+ type: ChartType;
51
+ /** Labels along the axis, or the names of the pie slices. */
52
+ categories: string[];
53
+ /** One or more series, each with one value per category. A pie takes exactly one. */
54
+ series: Array<{
55
+ name: string;
56
+ values: number[];
57
+ }>;
58
+ /** `percent` shows values with a percent sign (45 is shown as 45%). */
59
+ format?: 'number' | 'percent';
60
+ }
61
+ /**
62
+ * - text slot: a string (newlines split paragraphs) or one string per paragraph
63
+ * - image slot: an {@link ImageRef}
64
+ * - table slot: a {@link TableValue}
65
+ * - chart slot: a {@link ChartValue}
66
+ * - `null`: explicitly leave the slot out (the shape is removed)
67
+ */
68
+ export type SlotValue = string | string[] | ImageRef | TableValue | ChartValue | null;
69
+ export declare function isImageRef(value: unknown): value is ImageRef;
70
+ export declare function isTableValue(value: unknown): value is TableValue;
71
+ export declare function isChartValue(value: unknown): value is ChartValue;
72
+ /** Normalises a text value to paragraphs. */
73
+ export declare function toParagraphs(value: string | string[]): string[];