slotdeck 0.0.0-stage → 0.1.0

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +486 -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 +189 -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/package.json +63 -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,487 @@
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
+ ## How it works
34
+
35
+ A template is an ordinary deck of sample slides, the kind a designer hands over:
36
+
37
+ ![The demo template: thirteen sample slides](docs/template.png)
38
+
39
+ A **manifest** (JSON) says which slides are patterns and which shapes on them are slots, with a character
40
+ budget for each text slot. A **deck** (JSON) lists slides, each naming a pattern and giving values for its
41
+ slots:
42
+
43
+ ```json
44
+ {
45
+ "version": 1,
46
+ "fields": { "organizationDepartment": "Riverbend Bike Share | Season review" },
47
+ "slides": [
48
+ { "pattern": "cover", "slots": { "title": "A year of Riverbend Bike Share", "subtitle": "What the 2025 season taught us" } },
49
+ {
50
+ "pattern": "icon-cards",
51
+ "items": 3,
52
+ "slots": {
53
+ "title": "Three changes for next season",
54
+ "card1Icon": { "asset": "clock" }, "card1Heading": "Longer hours", "card1Text": "Bikes unlock from five in the morning.",
55
+ "card2Icon": { "asset": "shield" }, "card2Heading": "Safer docks", "card2Text": "Every station gets lighting.",
56
+ "card3Icon": { "asset": "people" }, "card3Heading": "Group passes", "card3Text": "One pass for up to five riders."
57
+ }
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ ```ts
64
+ import { loadTemplate, renderDeck } from 'slotdeck';
65
+
66
+ const template = loadTemplate(pptxBytes, manifest); // unzip once, reuse for every deck
67
+ const { pptx, warnings } = renderDeck(template, deck, { assets: { clock, shield, people } });
68
+ ```
69
+
70
+ That is the whole renderer: no model, no network, about 100 ms for a 15-slide deck.
71
+
72
+ ## Generate a deck with a model
73
+
74
+ ```ts
75
+ import { gemini, geminiImages, generateDeck, loadTemplate } from 'slotdeck';
76
+
77
+ const template = loadTemplate(pptxBytes, manifest);
78
+
79
+ const { pptx, deck, warnings } = await generateDeck(
80
+ template,
81
+ {
82
+ prompt: 'A deck for new adopters: why adopt a rescue dog, the first month at home, what it costs',
83
+ language: 'en',
84
+ slideCount: 9,
85
+ imageDensity: 'medium', // none | low | medium | high
86
+ fields: { organizationDepartment: 'Harbor Street Shelter | Adoption team' },
87
+ },
88
+ {
89
+ llm: gemini({ model: textModel, apiKey }),
90
+ images: geminiImages({ model: imageModel, apiKey }),
91
+ icons: [{ id: 'clock', description: 'a clock: time, schedule', data: clockPng }],
92
+ assets: [{ id: 'flow', kind: 'graphic', description: 'adoption process diagram', data: mermaidPng }],
93
+ layout: 'adaptive', // or 'strict'
94
+ onProgress: (event) => console.log(event.stage),
95
+ },
96
+ );
97
+ ```
98
+
99
+ `generateDeck` is three steps you can also call yourself:
100
+
101
+ | Step | Function | Model calls |
102
+ | --- | --- | --- |
103
+ | Brief to deck JSON | `planDeck` | 2, plus up to 2 repair rounds |
104
+ | Deck JSON and instruction to deck JSON | `planEdit` | 1 or 2, plus up to 2 repair rounds |
105
+ | Deck JSON to picture bytes | `resolveAssets` | one per generated photo |
106
+ | Deck JSON and pictures to pptx | `renderDeck` | none |
107
+
108
+ Keep the returned `deck`. It is plain JSON with no binary data, so it can be stored, changed and rendered again.
109
+ Pass the same `cache` map to later calls and photos whose description did not change are not generated twice.
110
+
111
+ **Icons** come from a set your app registers, and the model picks among them by description. **Photos** come
112
+ from an image source you plug in. **Supplied pictures** (`assets`) are for things your app already has, such as
113
+ a rendered Mermaid diagram.
114
+
115
+ ### How planning works
116
+
117
+ 1. **Outline.** The model sees the template's patterns with their slot limits and picks one per slide.
118
+ 2. **Content.** The model fills a JSON schema built for exactly that sequence of slides, so it cannot invent
119
+ patterns or slots.
120
+ 3. **Repair.** The draft is checked against the manifest. Text over a slot's budget or a missing slot goes back
121
+ to the model with the list of problems.
122
+
123
+ Patterns the caller cannot fill are never offered: photo patterns when images are off, icon patterns without an
124
+ icon set, patterns that need a deck field that was not given.
125
+
126
+ Patterns built around numbers (`"figures": true` in the manifest) are offered only when the brief itself
127
+ contains figures. In a live run without this guard the model filled such a slide with an invented statistic. Set
128
+ `numberPatterns: 'always'` to offer them regardless, or `'never'` to rule them out. Chart values the brief does
129
+ not state come back as warnings (`chart values not stated in the brief: 89`).
130
+
131
+ ### Editing a deck
132
+
133
+ ```ts
134
+ import { editDeck, extractAssets } from 'slotdeck';
135
+
136
+ // The deck JSON and the pptx rendered from it are all that has to be stored between turns:
137
+ // the pictures are read back from the file instead of being generated again.
138
+ const cache = new Map(Object.entries(extractAssets(template, deck, previousPptx)));
139
+
140
+ const { pptx, deck: next, summary, rewritten } = await editDeck(template, deck, 'Shorten slide 3 to two bullets', {
141
+ llm,
142
+ images,
143
+ icons, // the same icon set and assets the deck was made with
144
+ cache,
145
+ });
146
+ ```
147
+
148
+ An edit is two model calls. The first decides what the deck looks like after the change: which slides stay,
149
+ move, go, get rewritten or are new. The second writes only the slides that change. Reordering and deleting need
150
+ no second call.
151
+
152
+ - Slides the instruction does not touch are copied by code, so their text cannot drift.
153
+ - A rewritten slide keeps its pictures unless the instruction is about them.
154
+ - A slide given a different layout takes its photo along.
155
+ - A change of language rewrites every slide, sets the reading direction and translates deck-level texts.
156
+ - `summary` is one sentence from the model on what it did, to show the person who asked.
157
+
158
+ Changes made by hand in PowerPoint are not read back: the deck JSON is the source of truth.
159
+
160
+ ### Chat tools
161
+
162
+ For a chat app, the package provides the four tools a model needs to make a deck with a person in the loop:
163
+
164
+ | Tool | What it does | Model calls |
165
+ | --- | --- | --- |
166
+ | `plan_presentation` | Decides the slides, the layout of each and the point each makes | 1 |
167
+ | `edit_presentation_plan` | Changes a plan by instruction | 1 |
168
+ | `create_presentation` | Writes the approved plan's content, makes its pictures, renders the file | 1, plus repairs and pictures |
169
+ | `edit_presentation` | Changes a created deck by instruction | 1 or 2, plus repairs |
170
+
171
+ ```ts
172
+ import { presentationTools } from 'slotdeck';
173
+ import { directoryStore } from 'slotdeck/node';
174
+
175
+ const tools = presentationTools({
176
+ template,
177
+ llm,
178
+ images, // optional: without it decks are planned with no photos
179
+ icons,
180
+ store: directoryStore('/mnt/presentations'), // a volume, or any object with get(key) and set(key, bytes)
181
+ fields: { organizationDepartment: 'Harbor Street Shelter | Adoption team' },
182
+ thumbnail: (pattern) => `/static/thumbnails/${pattern}.png`,
183
+ });
184
+
185
+ // Declare the tools to the chat model (Interactions format shown):
186
+ const declarations = tools.definitions.map((tool) => ({ type: 'function', ...tool }));
187
+
188
+ // Run a tool the model called:
189
+ const { output, plan, deck, pptx } = await tools.call(call.name, call.arguments);
190
+ // `output` goes back to the model; `plan`, `deck` and `pptx` are for the app's own UI.
191
+ ```
192
+
193
+ The model only ever handles ids and short summaries. Everything else is kept in the store:
194
+
195
+ | Key | Content | Typical size |
196
+ | --- | --- | --- |
197
+ | `plan/<id>.json` | A plan: the brief and the slide list | 1 KB |
198
+ | `deck/<id>.json` | A deck: all text, and for each picture its id and description | 4 KB |
199
+ | `asset/<id>` | One picture | tens of KB to 1 MB |
200
+ | `pptx/<id>.pptx` | The rendered file | 1 MB and up |
201
+
202
+ Pictures are stored once, apart from the JSON and the file. A deck's JSON never holds image bytes, so no heavy
203
+ data reaches a model, and a pptx can be rebuilt from the JSON and the pictures at any time. Every change makes a
204
+ new plan or deck under a new id; earlier versions stay.
205
+
206
+ To show a plan as layout thumbnails, render the template once and name the pictures after the patterns
207
+ (`scripts/pattern-thumbnails.ts`). `examples/plan-preview.ts` turns a plan into a self-contained page.
208
+
209
+ ### Other models
210
+
211
+ A text model is anything with this method:
212
+
213
+ ```ts
214
+ const llm: LlmProvider = {
215
+ async generateJson({ system, prompt, schema, images, signal }) {
216
+ return myModel.json({ system, prompt, schema }); // resolve with the parsed JSON
217
+ },
218
+ };
219
+ ```
220
+
221
+ An image source is anything with `generateImage({ prompt, kind, aspectRatio })` that resolves with PNG, JPEG or
222
+ GIF bytes. It can be an image model, a stock library or a search.
223
+
224
+ The Gemini adapters call the Interactions API with plain `fetch`. For Vertex AI, pass
225
+ `endpoint: vertexEndpoint(project)` and a `headers` function that returns a bearer token. A request that has
226
+ not answered after three minutes is abandoned with an error; `timeoutMs` changes the limit.
227
+
228
+ ## Languages and direction
229
+
230
+ A deck states its language and reading direction; the template has its own. When they differ, text flips and
231
+ the layout stays:
232
+
233
+ ![A Hebrew deck on the left-to-right demo template](docs/hebrew.png)
234
+
235
+ - Every paragraph gets the direction of its own text, so a Hebrew deck may hold an English line.
236
+ - Alignment mirrors (`alignOnDirectionChange: 'flip'`, for the whole template or per slot). Templates whose text
237
+ boxes share one edge but not the other can `keep` it.
238
+ - Rows and grids of items are filled mirror-wise, so the deck's first card is where its readers start.
239
+ - Tables and charts run in the deck's direction.
240
+ - Right-to-left runs are marked with their language, because PowerPoint places punctuation by the language of a
241
+ run: without it, the colon in "contact: name@example.org" lands on the wrong side of the address.
242
+ - Shapes do not move. A photo on the right stays on the right.
243
+
244
+ ## Templates
245
+
246
+ - **Manifest** (`TemplateManifest`): which slides are patterns and which shapes are slots. See
247
+ [`templates/demo.manifest.json`](templates/demo.manifest.json).
248
+ - **Layout**: `strict` requires every slot to be filled. `adaptive` lets optional slots be left out, and removes
249
+ their shapes.
250
+ - **Empty slots**: each slot says what happens without a value. `error` makes it required, `remove` makes it
251
+ optional, `keep` leaves the template's content, and `drop` always removes it (for a sticker drawn onto a
252
+ particular sample photo).
253
+ - **Deck fields**: a text that is the same on every slide, such as the organization line beside the logo, is
254
+ one value for the whole deck (`fields`).
255
+ - **Tables**: a table slot takes `{ header, rows }`. The sample table's rows supply the styles (header, then the
256
+ body rows in turn), columns share its width, and it runs in the deck's reading direction.
257
+ - **Supplied pictures**: a `graphic` slot is filled from the `assets` the app passes. The picture keeps its
258
+ proportions inside the frame; `box` gives it a larger area than the sample picture's.
259
+ - **Charts**: a chart slot takes `{ type, categories, series, format }` and becomes a native PowerPoint chart
260
+ (`column`, `bar`, `line` or `pie`) with its data in an embedded workbook, so it can be changed with Edit Data.
261
+ The slot points at the sample picture of a chart, which the real chart replaces. Colours, font and line colour
262
+ come from the manifest. Bars start at zero.
263
+ - **Number patterns**: `figures: true` marks a pattern built around numbers.
264
+
265
+ ### Layout freedom
266
+
267
+ A deck can depart from the template slide in bounded ways. In each, the model chooses among options the
268
+ manifest allows and the code moves the shapes; the model never positions anything.
269
+
270
+ - **A different number of items.** A pattern with `repeat` names the shapes that make one card and the range it
271
+ accepts (in the demo template `icon-cards` takes 2 to 4 and `stat-cards` 2 to 6). A slide says how many with
272
+ `items`. Surplus cards are removed, extra ones are copies of the last, and the row is spaced out again around
273
+ its centre. A copied card has its own slots (`card5Icon`, `card5Heading`, ...).
274
+ - **Grids.** With `columns`, the items form a grid. They take as few rows as they need and spread evenly: four
275
+ figures in a three-column grid make two rows of two, five make three and two, three make one row.
276
+ - **Larger text.** `textScale` on a deck (or `textSize: 'large' | 'x-large'` on a brief) enlarges body text:
277
+ slots of 12 pt and under by default, not titles. Because the boxes stay the same size, character budgets fall
278
+ with the square of the scale, so the model writes less.
279
+ - **Derived patterns.** Several patterns can come from one slide. A text slot can be moved and resized (`box`),
280
+ given a font size (`fontSize`) and paragraph spacing, and any shape can be dropped. In the demo template
281
+ `text-only` is the photo slide with its photo dropped and its text widened, and `chart-native` is the chart
282
+ slide with a native chart in place of the picture.
283
+
284
+ ## Template studio
285
+
286
+ Writing a manifest by hand means reading shape ids out of a pptx. The studio functions do that work with a
287
+ model, and keep the model from breaking anything.
288
+
289
+ ```ts
290
+ import { checkManifest, draftManifest, gemini, loadTemplate, renderDeck, sampleDeck } from 'slotdeck';
291
+
292
+ // 1. Draft: one model call per slide, plus one for the deck.
293
+ const { manifest, skipped, issues } = await draftManifest(pptx, {
294
+ llm: gemini({ model: textModel, apiKey }),
295
+ name: 'marketing',
296
+ slideImage: (slide) => thumbnails.get(slide), // optional PNG or JPEG of each slide; may be async
297
+ onProgress: ({ done, total }) => report(done, total),
298
+ });
299
+
300
+ // 2. Check: after every change to the manifest, and whenever a template is loaded.
301
+ const template = loadTemplate(pptx, manifest);
302
+ const problems = checkManifest(template);
303
+
304
+ // 3. Look: every pattern once, every slot filled close to its budget.
305
+ const { deck, assets } = sampleDeck(template);
306
+ const sample = renderDeck(template, deck, { assets }).pptx;
307
+ ```
308
+
309
+ **`draftManifest`** splits the work so that the model cannot break the manifest. The code measures: every
310
+ shape's id, kind, position, font size and sample text; what sits in the same place on many slides (logo, page
311
+ number: left alone); texts that repeat across slides (deck fields); the reading direction; a character budget
312
+ for each text box from its size and font; a table's columns, rows and cell size; and rows or grids of like
313
+ items, from numbered slot names. The model answers two things for each slide: whether it is a reusable layout
314
+ at all, and what role each shape plays (`title`, `text`, `list`, `number`, `photo`, `icon`, `graphic`, `table`,
315
+ `decoration`, or `fixed`). A label for a shape that is not on the slide is discarded, and the result is run
316
+ through `checkManifest` before it is returned.
317
+
318
+ **`checkManifest`** reports slots that point at a missing shape or the wrong kind of shape, duplicate names,
319
+ rows whose items do not line up with their slots, and text slots with no budget. It reads only the pptx and the
320
+ manifest, so it is safe to run on every load.
321
+
322
+ **`sampleDeck`** uses the template's own filler words, so the result reads like the template with every box
323
+ full. Text that overflows in the sample will overflow in real decks. `items: 'max'` fills rows to their largest
324
+ count, `fill` sets how close to the budget each slot goes.
325
+
326
+ ### Changing a manifest
327
+
328
+ A manifest is changed through small operations. A studio screen sends them from its own controls, and
329
+ `editManifest` has a model turn an instruction into the same operations. Either way the code checks each
330
+ operation against the pptx, measures what follows from it, and refuses what cannot work.
331
+
332
+ ```ts
333
+ import { changeManifest, editManifest, studioView } from 'slotdeck';
334
+
335
+ // By hand: one operation or many.
336
+ const result = changeManifest(template, [
337
+ { op: 'copyPattern', pattern: 'chart-with-takeaway', id: 'chart-native' },
338
+ { op: 'addSlot', pattern: 'chart-native', shape: '6', role: 'chart' }, // the sample picture becomes a native chart
339
+ { op: 'setRepeat', pattern: 'stat-cards', min: 2, max: 6 },
340
+ { op: 'setSlot', pattern: 'table', slot: 'table', maxColumns: 5, bodyText: { size: 12 } },
341
+ ]);
342
+ result.manifest; // the new manifest; the template that was passed in is not touched
343
+ result.applied; // [{ index, change, message }]
344
+ result.rejected; // [{ index, change, reason }] "5 cards do not fit side by side: ... The most is 4"
345
+ result.issues; // checkManifest on the result
346
+
347
+ // By instruction, in any language.
348
+ const edited = await editManifest(template, 'Rename four-icon-cards to icon-cards and make the card icons required.', { llm });
349
+ edited.note; // what the model could not do, in the person's language; usually empty
350
+
351
+ // For a screen: every slide's shapes with their place on the slide, their slots, and the roles they can take.
352
+ const view = studioView(template);
353
+ ```
354
+
355
+ | Operation | What it changes |
356
+ | --- | --- |
357
+ | `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. |
358
+ | `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. |
359
+ | `removeSlot` | The shape goes back to being part of the design. |
360
+ | `setRepeat`, `removeRepeat` | How many items a row or grid takes. Finds the row from numbered slot names when the pattern has none. |
361
+ | `addPattern`, `copyPattern`, `setPattern`, `removePattern` | Which slides are patterns, their ids and descriptions, and variants of a pattern from the same slide. |
362
+ | `setManifest` | The template's name, language, direction and alignment policy. |
363
+
364
+ What the code adds to each change:
365
+
366
+ - **Budgets follow.** A new font size, text area, table text size or column count changes the character budget,
367
+ unless the change states one.
368
+ - **Rows are checked for room.** Asking for more items than the row draws widens it as far as the slide allows,
369
+ up to the margin or the next shape beside the row. Asking for more than fit is refused with the most that do.
370
+ - **Row items stay alike.** A change to `card1Icon` is made to that slot of every card.
371
+ - **Nothing is left broken.** A change that would make the manifest point at something the pptx does not have
372
+ is refused, and the rest of the list is still applied.
373
+
374
+ `editManifest` makes one model call, and a second when some of its operations were refused, so the model can
375
+ correct them. The model never sets a position: `box` and `span` exist only for changes made by hand.
376
+ `describeManifest(template)` returns the text the model reads, for an app that wants to show the manifest to its
377
+ own chat model.
378
+
379
+ ### How the demo manifest was made
380
+
381
+ `templates/demo.manifest.json` was made with these functions, not written by hand:
382
+
383
+ 1. `draftManifest` on the 13 slides, with a 480-pixel picture of each: 14 model calls, 29 seconds. All 13
384
+ slides became patterns, with the row of cards, the grid of figures, the two columns and the organization
385
+ line found, and no checker issue.
386
+ 2. One instruction to `editManifest` renamed five patterns, made the card icons and texts required and added the
387
+ native-chart variant. It also asked for five cards in a row; that was refused, because four fit.
388
+ 3. A few changes by hand with `changeManifest`: the text-only variant, chart colours, mirrored alignment for
389
+ right-to-left decks, and tighter budgets for figures and one-line captions.
390
+
391
+ A draft is a starting point. What it does not contain, and a person adds while reviewing: budgets tuned by eye,
392
+ derived patterns, wider rows, picture areas and chart colours. On a 36-slide corporate template (not included
393
+ here) the draft took 37 calls and 80 seconds, skipped the 12 slides that were not layouts, and matched a
394
+ hand-written manifest on 114 of 115 slots.
395
+
396
+ ## Try the examples
397
+
398
+ ```bash
399
+ git clone https://github.com/renanbazinin/slotdeck.git
400
+ ```
401
+
402
+ ```bash
403
+ npm install
404
+ ```
405
+
406
+ ```bash
407
+ npm test
408
+ ```
409
+
410
+ Render the example decks (no model, no key). They are written to `out/`:
411
+
412
+ ```bash
413
+ npm run demo
414
+ ```
415
+
416
+ Generate a deck with Gemini. Put `GEMINI_API_KEY=...` in a `.env` file first:
417
+
418
+ ```bash
419
+ node --env-file=.env examples/generate-demo.ts "A deck about adopting a rescue dog" --slides 6
420
+ ```
421
+
422
+ Photos are free drawn stand-ins unless you add `--real-images`, which calls the paid image model. `--lang he`
423
+ writes in another language, `--charts` supplies two sample charts the way an app would supply its own diagrams,
424
+ and `--dry-run` prints the prompts without calling anything.
425
+
426
+ Change a deck that was generated earlier:
427
+
428
+ ```bash
429
+ node --env-file=.env examples/edit-demo.ts out/a-deck-about-adopting-a-rescue-dog.deck.json "Shorten slide 3 to two bullets"
430
+ ```
431
+
432
+ Play a chat app that calls the four tools in turn:
433
+
434
+ ```bash
435
+ node --env-file=.env examples/tools-demo.ts "A deck about adopting a rescue dog"
436
+ ```
437
+
438
+ Draft a manifest for a template of your own, then change it by instruction:
439
+
440
+ ```bash
441
+ node --env-file=.env examples/studio-demo.ts draft path/to/template.pptx
442
+ ```
443
+
444
+ ```bash
445
+ node --env-file=.env examples/studio-demo.ts edit "make the subtitle optional on every slide"
446
+ ```
447
+
448
+ List the shapes of a template's slides, as the manifest addresses them:
449
+
450
+ ```bash
451
+ node scripts/inspect.ts templates/demo.pptx --slides 2,5
452
+ ```
453
+
454
+ On Windows with PowerPoint installed, this exports every slide of a deck to PNG, which is how the pictures on
455
+ this page were made:
456
+
457
+ ```bash
458
+ powershell -File scripts/render-pptx.ps1 -Path out/patterns-en.pptx -OutDir out/png/patterns-en
459
+ ```
460
+
461
+ ## What to know before relying on it
462
+
463
+ - **Shapes need their own position.** The engine measures the shapes on a slide. A shape that takes its position
464
+ and font size from a layout placeholder, without stating them on the slide, is filled correctly but measured
465
+ as empty, so its budget is only as long as its sample text. Decks exported from Google Slides and decks built
466
+ from free-standing text boxes work as they are.
467
+ - **Budgets are estimates.** A character budget cannot see lines lost to wrapping. Budgets for slots with several
468
+ paragraphs should be set with room to spare, and a sample deck is the way to see them.
469
+ - **Models invent details.** In the showcase deck the model made up an email address and a phone number for the
470
+ closing slide, which were replaced by hand. Figures are guarded (see How planning works); contact details and
471
+ names are not.
472
+ - **What has been run.** The whole path was run against the Gemini Developer API with `gemini-3.8-flash` for
473
+ text and `gemini-3.1-flash-image` for photos, in English and Hebrew. Vertex AI is supported by the adapter but
474
+ has not been run live. Output was checked by rendering in PowerPoint on Windows; other viewers were not tested.
475
+ - **Not built:** swapping the photo side, stacked or combined charts, speaker notes, and reading back changes
476
+ made in PowerPoint.
477
+
478
+ ## The demo template and pictures
479
+
480
+ `templates/demo.pptx` and the icons in `examples/icons` are drawn by
481
+ [`scripts/build-demo-template.py`](scripts/build-demo-template.py): no stock photo, logo or icon from anywhere
482
+ else is in them. The four photos of the showcase deck were generated once with an image model from the
483
+ descriptions in its JSON. All of it is covered by the license below and free to reuse.
484
+
485
+ ## License
486
+
487
+ 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[];