slotdeck 0.1.0 → 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.
- package/README.md +9 -0
- package/dist/deck/validate.js +5 -0
- package/dist/deck/validate.js.map +1 -1
- package/docs/README.md +76 -0
- package/docs/api-reference.md +668 -0
- package/docs/chat-tools.md +253 -0
- package/docs/concepts.md +126 -0
- package/docs/deck-format.md +225 -0
- package/docs/generating.md +218 -0
- package/docs/getting-started.md +139 -0
- package/docs/languages.md +83 -0
- package/docs/layout-freedom.md +113 -0
- package/docs/manifest-format.md +207 -0
- package/docs/providers.md +236 -0
- package/docs/rendering.md +172 -0
- package/docs/template-studio.md +369 -0
- package/docs/troubleshooting.md +166 -0
- package/package.json +3 -2
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Manifest format
|
|
2
|
+
|
|
3
|
+
A manifest is a JSON object of type `TemplateManifest`. It describes one template: which of its slides are
|
|
4
|
+
reusable patterns and which shapes on them are slots.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import type { TemplateManifest, PatternDef, SlotDef, TextSlotDef, ImageSlotDef, TableSlotDef, ChartSlotDef, RepeatDef } from 'slotdeck';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
A manifest is written once per template. You can have a model draft it (`draftManifest`), change it with
|
|
11
|
+
operations (`changeManifest`) or by instruction (`editManifest`), and verify it (`checkManifest`): see
|
|
12
|
+
[template-studio.md](template-studio.md). This page is the reference for what the JSON means.
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"version": 1,
|
|
19
|
+
"name": "demo",
|
|
20
|
+
"language": "en",
|
|
21
|
+
"direction": "ltr",
|
|
22
|
+
"alignOnDirectionChange": "flip",
|
|
23
|
+
"patterns": [
|
|
24
|
+
{
|
|
25
|
+
"id": "cover",
|
|
26
|
+
"sourceSlide": 1,
|
|
27
|
+
"description": "Opening slide with the presentation's title and a subtitle.",
|
|
28
|
+
"slots": [
|
|
29
|
+
{ "name": "title", "kind": "text", "shapeId": "5", "size": 50, "maxChars": 36 },
|
|
30
|
+
{ "name": "subtitle", "kind": "text", "shapeId": "6", "size": 20, "maxChars": 86, "empty": "remove" },
|
|
31
|
+
{ "name": "organizationDepartment", "kind": "text", "shapeId": "7", "size": 13, "maxChars": 61, "deckField": "organizationDepartment", "alignOnDirectionChange": "keep" }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "icon-cards",
|
|
36
|
+
"sourceSlide": 5,
|
|
37
|
+
"description": "A row of cards, each with an icon, a heading and a short text.",
|
|
38
|
+
"slots": [
|
|
39
|
+
{ "name": "title", "kind": "text", "shapeId": "5", "size": 34, "maxChars": 41 },
|
|
40
|
+
{ "name": "card1Icon", "kind": "image", "imageKind": "icon", "shapeId": "9" },
|
|
41
|
+
{ "name": "card1Heading", "kind": "text", "shapeId": "10", "size": 19, "maxChars": 15 },
|
|
42
|
+
{ "name": "card1Text", "kind": "text", "shapeId": "11", "size": 15, "maxChars": 117 },
|
|
43
|
+
{ "name": "card2Icon", "kind": "image", "imageKind": "icon", "shapeId": "15" },
|
|
44
|
+
{ "name": "card2Heading", "kind": "text", "shapeId": "16", "size": 19, "maxChars": 15 },
|
|
45
|
+
{ "name": "card2Text", "kind": "text", "shapeId": "17", "size": 15, "maxChars": 117 }
|
|
46
|
+
],
|
|
47
|
+
"repeat": {
|
|
48
|
+
"item": "card",
|
|
49
|
+
"slots": ["card{n}Icon", "card{n}Heading", "card{n}Text"],
|
|
50
|
+
"shapes": [["6"], ["12"]],
|
|
51
|
+
"min": 2,
|
|
52
|
+
"max": 4
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## `TemplateManifest`
|
|
60
|
+
|
|
61
|
+
| Field | Type | Required | Meaning |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `version` | `1` | yes | Always 1. |
|
|
64
|
+
| `name` | string | yes | A name for the template. Used in messages. |
|
|
65
|
+
| `direction` | `"ltr"` or `"rtl"` | yes | The reading direction the template was designed for. |
|
|
66
|
+
| `patterns` | `PatternDef[]` | yes | The reusable layouts. |
|
|
67
|
+
| `language` | string | no | BCP 47 tag of the template's sample text. Decks default to it. |
|
|
68
|
+
| `alignOnDirectionChange` | `"flip"` or `"keep"` | no | What happens to left and right paragraph alignment in a deck that reads the other way. `flip` (default) mirrors it. `keep` leaves it as designed. A text slot can override this. |
|
|
69
|
+
|
|
70
|
+
## `PatternDef`
|
|
71
|
+
|
|
72
|
+
| Field | Type | Required | Meaning |
|
|
73
|
+
| --- | --- | --- | --- |
|
|
74
|
+
| `id` | string | yes | What decks call the pattern. Unique in the manifest. Lower-case words joined by hyphens is the convention. |
|
|
75
|
+
| `sourceSlide` | integer | yes | Number of the template slide the pattern is cloned from, counted from 1. |
|
|
76
|
+
| `description` | string | yes | What the pattern is for. The planning model reads it to choose patterns, so write it for an author: what the slide holds and when to use it. |
|
|
77
|
+
| `slots` | `SlotDef[]` | yes | The shapes a deck fills. |
|
|
78
|
+
| `figures` | boolean | no | The pattern is built around numbers. The planner offers it only when the brief contains figures, so a model is not led to invent statistics. |
|
|
79
|
+
| `repeat` | `RepeatDef` | no | A row or grid of like items whose number a deck may choose. See below. |
|
|
80
|
+
| `sequences` | string[][][] | no | Rows of items with a fixed count, for mirroring in decks that read the other way. Each row lists its items first to last in the template's direction, and each item lists its slot names. Not needed for slots covered by `repeat`. |
|
|
81
|
+
|
|
82
|
+
## Slots: properties every kind has
|
|
83
|
+
|
|
84
|
+
| Field | Type | Required | Meaning |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `name` | string | yes | What decks call the slot. Unique within the pattern. Letters and digits, starting with a lower-case letter. |
|
|
87
|
+
| `kind` | `"text"`, `"image"`, `"table"` or `"chart"` | yes | What the slot holds. |
|
|
88
|
+
| `shapeId` | string | yes | The id of the shape on the source slide (the `id` attribute of the shape's `p:cNvPr` element, as a string). `inspectTemplate` and `studioView` list them. |
|
|
89
|
+
| `description` | string | no | Guidance for the planning model about this slot, such as "One line". |
|
|
90
|
+
| `empty` | `"error"`, `"remove"`, `"keep"` or `"drop"` | no | What happens without a value. Default `error` (required). See [concepts.md](concepts.md). |
|
|
91
|
+
|
|
92
|
+
Slots with `empty` of `keep` or `drop` are never offered to the planning model.
|
|
93
|
+
|
|
94
|
+
## Text slot (`kind: "text"`)
|
|
95
|
+
|
|
96
|
+
| Field | Type | Meaning |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `maxChars` | integer | Soft limit on the total characters of the slot. Exceeding it gives a warning. |
|
|
99
|
+
| `maxParagraphs` | integer | Soft limit on the number of paragraphs. When it is more than 1, the planner asks the model for a list of paragraphs. |
|
|
100
|
+
| `deckField` | string | Take the value from `deck.fields[deckField]` when the slide does not set it. |
|
|
101
|
+
| `size` | number | Font size of the slot's text on the template slide, in points. Informational: it decides whether a deck's `textScale` applies (see `scales`). |
|
|
102
|
+
| `scales` | boolean | Whether a deck's `textScale` applies to this slot. Default: yes for text of 12 pt and under that is not a deck field. |
|
|
103
|
+
| `fontSize` | number | Sets the font size outright, in points. For a derived pattern that uses the shape differently. |
|
|
104
|
+
| `box` | `{ x, y, w, h }` | Moves and resizes the text box, as fractions of the slide (0 to 1). Only for a shape that is not inside a group. |
|
|
105
|
+
| `paragraphGap` | number | Space after each paragraph, in points. |
|
|
106
|
+
| `alignOnDirectionChange` | `"flip"` or `"keep"` | Overrides the manifest's setting for this slot. |
|
|
107
|
+
| `styleRun` | `"first"` or `"last"` | Which run of the template paragraph supplies the formatting, when the template paragraph mixes styles (a bold label followed by plain text). Default `first`. |
|
|
108
|
+
|
|
109
|
+
## Image slot (`kind: "image"`)
|
|
110
|
+
|
|
111
|
+
| Field | Type | Meaning |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `imageKind` | `"photo"`, `"icon"` or `"graphic"` | Required. `photo`: a generated or supplied photo. `icon`: one of the app's icon set. `graphic`: a picture the app supplies, such as a diagram. |
|
|
114
|
+
| `fit` | `"cover"` or `"contain"` | `cover` crops the picture to fill the frame. `contain` shows the whole picture inside the frame's area. Default: `cover` for photos, `contain` for icons and graphics. |
|
|
115
|
+
| `box` | `{ x, y, w, h }` | The area the picture may use, when other than the sample picture's frame. Fractions of the slide. Only for a picture that is not inside a group. |
|
|
116
|
+
|
|
117
|
+
## Table slot (`kind: "table"`)
|
|
118
|
+
|
|
119
|
+
The shape must be a table.
|
|
120
|
+
|
|
121
|
+
| Field | Type | Meaning |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `maxColumns` | integer | Soft limit on columns. |
|
|
124
|
+
| `maxRows` | integer | Soft limit on body rows (the header row is not counted). |
|
|
125
|
+
| `maxCellChars` | integer | Soft limit on the characters in one cell. |
|
|
126
|
+
| `headerText` | `{ size?, bold?, color?, font? }` | Formatting for header cells, where the sample table defines none or it should differ. `color` is hex without `#`. |
|
|
127
|
+
| `bodyText` | `{ size?, bold?, color?, font? }` | The same for body cells. |
|
|
128
|
+
|
|
129
|
+
## Chart slot (`kind: "chart"`)
|
|
130
|
+
|
|
131
|
+
The shape is the sample picture of a chart. At render time it is replaced by a native chart in the same place.
|
|
132
|
+
The shape must not be inside a group.
|
|
133
|
+
|
|
134
|
+
| Field | Type | Meaning |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `box` | `{ x, y, w, h }` | The area the chart may use, when other than the sample picture's frame. |
|
|
137
|
+
| `maxCategories` | integer | Soft limit on categories. |
|
|
138
|
+
| `maxSeries` | integer | Soft limit on series. |
|
|
139
|
+
| `colors` | string[] | Series colours in order, hex without `#`. They repeat when there are more series. |
|
|
140
|
+
| `text` | `{ size?, bold?, color?, font? }` | Formatting of axis labels, data labels and the legend. |
|
|
141
|
+
| `lineColor` | string | Colour of gridlines and axis lines, hex without `#`. |
|
|
142
|
+
|
|
143
|
+
## `RepeatDef`: a row or grid of items
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
"repeat": {
|
|
147
|
+
"item": "card",
|
|
148
|
+
"slots": ["card{n}Icon", "card{n}Heading", "card{n}Text"],
|
|
149
|
+
"shapes": [["6"], ["12"], ["18"], ["24"]],
|
|
150
|
+
"min": 2,
|
|
151
|
+
"max": 4,
|
|
152
|
+
"span": { "left": 0.04, "right": 0.96 },
|
|
153
|
+
"columns": 3
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Field | Type | Required | Meaning |
|
|
158
|
+
| --- | --- | --- | --- |
|
|
159
|
+
| `item` | string | yes | What one item is called in prompts, such as `card` or `figure`. |
|
|
160
|
+
| `slots` | string[] | yes | Slot name templates of one item. `{n}` stands for the item number, from 1. For every item drawn on the slide the pattern must have a slot with each of these names (`card1Icon`, `card2Icon`, ...). |
|
|
161
|
+
| `shapes` | string[][] | yes | For each item drawn on the template slide, first in reading order first: the ids of the shapes that move together as that item. Usually the group around a card. A background shape that sits outside the group belongs in the list too. |
|
|
162
|
+
| `min`, `max` | integer | no | Fewest and most items a deck may ask for. Both default to the number drawn. |
|
|
163
|
+
| `span` | `{ left, right }` | no | Left and right limits of the row, as fractions of the slide width. Defaults to the row as drawn. Needed when `max` is more than the drawn items can fit in the drawn width. Only applies to shapes placed directly on the slide. |
|
|
164
|
+
| `columns` | integer | no | For a grid: how many items one line holds on the template slide. Leave it out for a single row. A grid cannot hold more items than it draws. |
|
|
165
|
+
|
|
166
|
+
How the items are laid out for a given count is described in [layout-freedom.md](layout-freedom.md).
|
|
167
|
+
|
|
168
|
+
Slots of copied items do not appear in the manifest. They are derived: item 5 of a row that draws 4 gets copies
|
|
169
|
+
of item 4's slots, named with the next number.
|
|
170
|
+
|
|
171
|
+
## Rules a manifest must satisfy
|
|
172
|
+
|
|
173
|
+
`checkManifest(template)` reports violations of these:
|
|
174
|
+
|
|
175
|
+
- Every `sourceSlide` exists in the pptx.
|
|
176
|
+
- Every `shapeId` is a shape on that slide, and is the right kind of shape for the slot (a text slot points at
|
|
177
|
+
a shape with text, an image slot at a picture, a table slot at a table). A slot with `"empty": "drop"` may
|
|
178
|
+
point at any shape, because the shape is only ever removed.
|
|
179
|
+
- Pattern ids are unique. Slot names are unique within a pattern.
|
|
180
|
+
- A `box` is not set on a shape inside a group. A chart slot's shape is not inside a group.
|
|
181
|
+
- For a `repeat`: a slot exists for every name template and every drawn item; every shape id exists; all items
|
|
182
|
+
sit directly on the slide or all in the same group; `min` is at least 1 and not more than `max`.
|
|
183
|
+
|
|
184
|
+
It also warns about text slots without `maxChars` and about two slots that point at the same shape.
|
|
185
|
+
|
|
186
|
+
## Finding shape ids
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { inspectTemplate, PptxPackage } from 'slotdeck';
|
|
190
|
+
|
|
191
|
+
const info = inspectTemplate(PptxPackage.load(pptxBytes));
|
|
192
|
+
for (const shape of info.slides[4].shapes) {
|
|
193
|
+
// slide 5: index 4
|
|
194
|
+
console.log(shape.id, shape.kind, shape.box, shape.groupId ?? '', shape.paragraphs?.join(' / ') ?? '');
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`shape.box` is `{ x, y, w, h }` as fractions of the slide, already worked out through any groups. `shape.kind`
|
|
199
|
+
is `text`, `picture`, `shape` (a drawn shape without text), `line`, `group`, `table` or `other`.
|
|
200
|
+
|
|
201
|
+
## Writing good descriptions
|
|
202
|
+
|
|
203
|
+
The planning model chooses a pattern by its `description` and its slots' names and limits. A description should
|
|
204
|
+
say what the slide holds and when to use it, in one or two sentences, without numbers that the deck may change:
|
|
205
|
+
|
|
206
|
+
- Good: "A row of cards, each with an icon, a heading and a short text: features, steps or categories."
|
|
207
|
+
- Poor: "Four cards." (the count can vary, and it does not say what the cards are for)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Model providers
|
|
2
|
+
|
|
3
|
+
slotdeck does not know any model service. It defines two small interfaces, and you pass objects that implement
|
|
4
|
+
them. This is how it works with a public API, a cloud account, or a gateway inside a network with no internet
|
|
5
|
+
access.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import type { LlmProvider, JsonRequest, JsonSchema, ImageProvider, ImageRequest } from 'slotdeck';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## `LlmProvider`: the text model
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
interface LlmProvider {
|
|
15
|
+
/** Resolves with the parsed JSON value. */
|
|
16
|
+
generateJson(request: JsonRequest): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface JsonRequest {
|
|
20
|
+
system: string; // the system prompt: the role and the rules
|
|
21
|
+
prompt: string; // the user message: the brief, the manifest's patterns, what to answer
|
|
22
|
+
schema: JsonSchema; // the JSON Schema the answer must follow
|
|
23
|
+
images?: Array<{ mimeType: string; data: Uint8Array }>; // pictures to look at; only draftManifest sends them
|
|
24
|
+
signal?: AbortSignal; // pass it to your HTTP call so work can be cancelled
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The contract:
|
|
29
|
+
|
|
30
|
+
- Resolve with the **parsed** value (an object), not a string of JSON.
|
|
31
|
+
- The value should follow `schema`. slotdeck checks what it gets back against the template's manifest and asks
|
|
32
|
+
again when content breaks a limit, but it expects the overall shape the schema describes.
|
|
33
|
+
- Reject (throw) when the call fails. slotdeck does not retry failed calls; add retries in your provider if you
|
|
34
|
+
want them.
|
|
35
|
+
- One call is one request and one answer. There is no conversation state: every request carries everything it
|
|
36
|
+
needs.
|
|
37
|
+
|
|
38
|
+
### What the schemas contain
|
|
39
|
+
|
|
40
|
+
Only a small, widely supported subset of JSON Schema:
|
|
41
|
+
|
|
42
|
+
- `type`: `object`, `array`, `string`, `integer`, `number`, `boolean`
|
|
43
|
+
- `properties`, `required`, `items`, `enum`, `description`
|
|
44
|
+
- in a few places `minItems`, `maxItems`, `minimum`, `maximum`
|
|
45
|
+
|
|
46
|
+
There is no `anyOf`, `oneOf`, `$ref`, `pattern` or `format`. If your backend rejects the length or range
|
|
47
|
+
keywords, your provider can delete them from the schema before sending it: the result is checked by code in any
|
|
48
|
+
case.
|
|
49
|
+
|
|
50
|
+
### A provider for a backend with structured output
|
|
51
|
+
|
|
52
|
+
Most hosted models and gateways accept a JSON Schema and return JSON that follows it. The adapter is then a
|
|
53
|
+
single HTTP call. The request and response fields below are placeholders: use the ones your gateway documents.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import type { LlmProvider } from 'slotdeck';
|
|
57
|
+
|
|
58
|
+
export function gatewayModel(options: { url: string; model: string; token: () => Promise<string> }): LlmProvider {
|
|
59
|
+
return {
|
|
60
|
+
async generateJson({ system, prompt, schema, signal }) {
|
|
61
|
+
const response = await fetch(options.url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${await options.token()}` },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
model: options.model,
|
|
66
|
+
messages: [
|
|
67
|
+
{ role: 'system', content: system },
|
|
68
|
+
{ role: 'user', content: prompt },
|
|
69
|
+
],
|
|
70
|
+
// However your gateway asks for JSON that follows a schema:
|
|
71
|
+
response_format: { type: 'json_schema', json_schema: { name: 'answer', schema } },
|
|
72
|
+
}),
|
|
73
|
+
signal,
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) throw new Error(`Model gateway answered ${response.status}: ${(await response.text()).slice(0, 300)}`);
|
|
76
|
+
const body = await response.json();
|
|
77
|
+
const text = body.choices[0].message.content; // wherever your gateway puts the answer text
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### A provider for a model without structured output
|
|
85
|
+
|
|
86
|
+
Put the schema into the prompt and parse what comes back. Models often wrap JSON in a code fence or add a
|
|
87
|
+
sentence around it, so extract the JSON before parsing.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export function plainTextModel(complete: (system: string, prompt: string, signal?: AbortSignal) => Promise<string>): LlmProvider {
|
|
91
|
+
return {
|
|
92
|
+
async generateJson({ system, prompt, schema, signal }) {
|
|
93
|
+
const instruction =
|
|
94
|
+
`${prompt}\n\nAnswer with one JSON value that follows this JSON Schema, and nothing else:\n${JSON.stringify(schema)}`;
|
|
95
|
+
const text = await complete(system, instruction, signal);
|
|
96
|
+
const start = text.indexOf('{');
|
|
97
|
+
const end = text.lastIndexOf('}');
|
|
98
|
+
if (start < 0 || end <= start) throw new Error(`The model did not answer with JSON: ${text.slice(0, 200)}`);
|
|
99
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Every top-level answer slotdeck asks for is a JSON object, so looking for the outermost braces is safe.
|
|
106
|
+
|
|
107
|
+
### Choosing a model
|
|
108
|
+
|
|
109
|
+
- The requests are small. Measured on the demo template (15 patterns): the outline request is a system prompt
|
|
110
|
+
of about 600 characters, a prompt of about 3,600 characters and a schema of about 700 characters. The writing
|
|
111
|
+
request for a 15-slide deck is a prompt of about 1,500 characters and a schema of about 9,800 characters. Any
|
|
112
|
+
current general-purpose model handles this.
|
|
113
|
+
- Writing a deck is a single call that returns all slides, so the model needs enough output room for the whole
|
|
114
|
+
deck. The JSON of a nine-slide deck is about 6 KB.
|
|
115
|
+
- `editManifest` sends the whole manifest with the measured shapes of every pattern's slide: about 58,000
|
|
116
|
+
characters for a template with 24 patterns.
|
|
117
|
+
- `draftManifest` can send a picture of each slide (`images`). A provider for a model that cannot see may ignore
|
|
118
|
+
them; the draft then relies on measurements alone.
|
|
119
|
+
|
|
120
|
+
### Timeouts and cancellation
|
|
121
|
+
|
|
122
|
+
slotdeck passes `signal` through when the caller gave one. Your provider should pass it to its HTTP call, and
|
|
123
|
+
should have a time limit of its own: a model call can hang without failing.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const limit = AbortSignal.timeout(180_000);
|
|
127
|
+
const combined = signal ? AbortSignal.any([signal, limit]) : limit;
|
|
128
|
+
await fetch(url, { ...init, signal: combined });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Logging and auditing
|
|
132
|
+
|
|
133
|
+
A provider is the one place every model call passes through, so it is where to count calls, log prompts or
|
|
134
|
+
enforce a budget:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const counted: LlmProvider = {
|
|
138
|
+
async generateJson(request) {
|
|
139
|
+
const started = Date.now();
|
|
140
|
+
try {
|
|
141
|
+
return await inner.generateJson(request);
|
|
142
|
+
} finally {
|
|
143
|
+
log({ promptChars: request.prompt.length, ms: Date.now() - started });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## `ImageProvider`: the picture source
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
interface ImageProvider {
|
|
153
|
+
/** Resolves with PNG, JPEG or GIF bytes. */
|
|
154
|
+
generateImage(request: ImageRequest): Promise<Uint8Array>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface ImageRequest {
|
|
158
|
+
prompt: string; // a description of the picture, written by the planning model
|
|
159
|
+
kind: 'photo' | 'icon' | 'graphic'; // what the slot holds; generated pictures are photos
|
|
160
|
+
aspectRatio: number; // width divided by height of the frame the picture goes into
|
|
161
|
+
signal?: AbortSignal;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It does not have to be an image model. A stock library, an internal picture search or a fixed set of brand
|
|
166
|
+
photos all fit: return the bytes of the picture that best matches `prompt`.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const brandPhotos: ImageProvider = {
|
|
170
|
+
async generateImage({ prompt, aspectRatio }) {
|
|
171
|
+
const hit = await photoLibrary.search(prompt, { orientation: aspectRatio >= 1 ? 'landscape' : 'portrait' });
|
|
172
|
+
return new Uint8Array(await hit.download());
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The picture is cropped to the frame when the slot's fit is `cover`, so any proportions work; matching
|
|
178
|
+
`aspectRatio` just loses less of the picture.
|
|
179
|
+
|
|
180
|
+
Without an `ImageProvider`, leave `images` out. `generateDeck` and the chat tools then plan decks with no
|
|
181
|
+
generated photos (patterns that need a photo are not offered), unless you supply photos as `assets`.
|
|
182
|
+
|
|
183
|
+
## The included Gemini adapters
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { gemini, geminiImages, vertexEndpoint, GeminiError, GEMINI_DEVELOPER_ENDPOINT } from 'slotdeck';
|
|
187
|
+
|
|
188
|
+
const llm = gemini({ model: 'gemini-3.8-flash', apiKey });
|
|
189
|
+
const images = geminiImages({ model: 'gemini-3.1-flash-image', apiKey });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
They call Google's Interactions API with plain `fetch`, so they need network access to that API. Inside a
|
|
193
|
+
network without it, write a provider for your own gateway as shown above.
|
|
194
|
+
|
|
195
|
+
Options of both:
|
|
196
|
+
|
|
197
|
+
| Option | Meaning |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `model` | Model id. Required. |
|
|
200
|
+
| `apiKey` | Gemini Developer API key, sent as the `x-goog-api-key` header. |
|
|
201
|
+
| `endpoint` | Interactions endpoint. Defaults to `GEMINI_DEVELOPER_ENDPOINT`. Use `vertexEndpoint(project, location?)` for Vertex AI, or the address of a proxy. |
|
|
202
|
+
| `headers` | Extra headers, or a function returning them (may be async), for example a fresh `Authorization: Bearer` token. |
|
|
203
|
+
| `fetch` | A custom `fetch`, for proxies and tests. |
|
|
204
|
+
| `timeoutMs` | Gives up on a request after this long. Default 180000 (3 minutes). `0` waits for ever. |
|
|
205
|
+
|
|
206
|
+
Only for `gemini`: `thinkingLevel` (`minimal`, `low`, `medium`, `high`) and `maxOutputTokens`.
|
|
207
|
+
|
|
208
|
+
Only for `geminiImages`: `imageSize` (`512`, `1K`, `2K`, `4K`; default `1K`) and `style`, a sentence appended to
|
|
209
|
+
every prompt, such as a house photo style.
|
|
210
|
+
|
|
211
|
+
Failures throw `GeminiError`, which has the HTTP `status` when there was one.
|
|
212
|
+
|
|
213
|
+
Vertex AI:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const llm = gemini({
|
|
217
|
+
model: 'gemini-3.8-flash',
|
|
218
|
+
endpoint: vertexEndpoint('my-project'), // location defaults to 'global'
|
|
219
|
+
headers: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The adapters were run live against the Gemini Developer API. The Vertex endpoint is supported by the adapter
|
|
224
|
+
but has not been run live.
|
|
225
|
+
|
|
226
|
+
## Testing without a model
|
|
227
|
+
|
|
228
|
+
A scripted provider makes tests deterministic and free:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const scripted: LlmProvider = {
|
|
232
|
+
async generateJson({ prompt, schema }) {
|
|
233
|
+
return cannedAnswers.shift(); // objects shaped like the schema
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
```
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Rendering
|
|
2
|
+
|
|
3
|
+
Rendering turns a deck (JSON) into a `.pptx` using a template. It uses no model and no network, and it is
|
|
4
|
+
deterministic: the same template, deck and assets give the same bytes.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { loadTemplate, renderDeck, validateDeck, extractAssets, DeckValidationError } from 'slotdeck';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## `loadTemplate(pptx, manifest)`
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
function loadTemplate(pptx: Uint8Array, manifest: TemplateManifest): Template;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Unzips the template once. The result is `{ pkg, manifest }`. Keep it and reuse it for every render; rendering
|
|
17
|
+
does not change it. Loading reads the whole file, so do it once at start-up rather than per request. A render
|
|
18
|
+
is fast: a 15-slide deck from the demo template takes about 100 ms.
|
|
19
|
+
|
|
20
|
+
It does not verify the manifest. Call `checkManifest(template)` when a template or manifest is loaded if you
|
|
21
|
+
want to catch a manifest that no longer matches its file (see [template-studio.md](template-studio.md)).
|
|
22
|
+
|
|
23
|
+
## `renderDeck(template, deck, options?)`
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
function renderDeck(template: Template, deck: DeckSpec, options?: RenderOptions): RenderResult;
|
|
27
|
+
|
|
28
|
+
interface RenderOptions {
|
|
29
|
+
assets?: Record<string, Uint8Array>; // picture bytes by asset id
|
|
30
|
+
layout?: 'strict' | 'adaptive'; // default 'strict'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RenderResult {
|
|
34
|
+
pptx: Uint8Array;
|
|
35
|
+
warnings: Issue[]; // non-blocking findings, such as text over a slot's budget
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
What it does, for each slide of the deck:
|
|
40
|
+
|
|
41
|
+
1. Clones the template slide of the slide's pattern, with its layout, background and every shape.
|
|
42
|
+
2. For a pattern with a row or grid of items, removes or copies items to match `items` and spaces them out.
|
|
43
|
+
3. Fills each slot: text into text shapes, pictures into picture frames, rows into tables, a native chart in
|
|
44
|
+
place of a chart slot's sample picture.
|
|
45
|
+
4. Removes shapes whose slot is left out (optional slots in `adaptive` layout, and slots with `empty: "drop"`).
|
|
46
|
+
5. Sets the page number where the template has one.
|
|
47
|
+
|
|
48
|
+
Then it writes a new package that contains only what the new slides use: template slides, pictures and other
|
|
49
|
+
parts that are not referenced any more are left out, so the output is not bloated by the template's sample
|
|
50
|
+
content. Speaker notes, named sections and revision history of the template are not carried over, because they
|
|
51
|
+
describe the template's own slides.
|
|
52
|
+
|
|
53
|
+
### Assets
|
|
54
|
+
|
|
55
|
+
Every `ImageRef` in the deck names an asset id. `options.assets` must have bytes under that id, or the deck is
|
|
56
|
+
invalid (`asset "x" was not provided`). The same bytes may be used by several slides; they are stored in the
|
|
57
|
+
file once.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
renderDeck(template, deck, {
|
|
61
|
+
assets: {
|
|
62
|
+
clock: clockPngBytes,
|
|
63
|
+
'photo-commute': photoJpegBytes,
|
|
64
|
+
'gen-68db256a51fa6c83': generatedPhotoBytes,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When a deck was made by the planner, `resolveAssets` or `generateDeck` builds this map for you
|
|
70
|
+
(see [generating.md](generating.md)).
|
|
71
|
+
|
|
72
|
+
### Errors
|
|
73
|
+
|
|
74
|
+
`renderDeck` validates first. If the deck has any error-level issue it throws:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
try {
|
|
78
|
+
renderDeck(template, deck, { assets });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof DeckValidationError) {
|
|
81
|
+
for (const issue of error.issues) console.log(issue.severity, issue.slide, issue.slot, issue.message);
|
|
82
|
+
} else {
|
|
83
|
+
throw error; // the manifest does not match the template, or an image format is not supported
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Other exceptions mean the manifest and the pptx disagree (for example
|
|
89
|
+
`Pattern "x": slot "y" points at shape #12, which is not on template slide 3`). `checkManifest` finds these
|
|
90
|
+
ahead of time.
|
|
91
|
+
|
|
92
|
+
## `validateDeck(manifest, deck, options?)`
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
function validateDeck(manifest: TemplateManifest, deck: DeckSpec, options?: ValidateOptions): Issue[];
|
|
96
|
+
|
|
97
|
+
interface ValidateOptions {
|
|
98
|
+
layout?: 'strict' | 'adaptive';
|
|
99
|
+
assets?: Iterable<string>; // ids that will be available at render time; when given, image references are checked
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface Issue {
|
|
103
|
+
severity: 'error' | 'warning';
|
|
104
|
+
slide?: number; // 1-based; absent for deck-level issues
|
|
105
|
+
slot?: string;
|
|
106
|
+
message: string;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
It takes the manifest, not the loaded template, so it can run where the pptx is not available (in a browser,
|
|
111
|
+
or before a job is queued). It uses exactly the same rules as the renderer.
|
|
112
|
+
|
|
113
|
+
- **Errors** block rendering: unknown pattern, a required slot without a value, a value of the wrong kind, a
|
|
114
|
+
table whose rows do not match its header, a chart whose series do not match its categories, an item count
|
|
115
|
+
outside the pattern's range, an asset that was not provided.
|
|
116
|
+
- **Warnings** do not: text, tables or charts over a slot's soft limits; a slot name the pattern does not have;
|
|
117
|
+
`items` on a pattern with a fixed number of items; an unknown chart `format`.
|
|
118
|
+
|
|
119
|
+
All messages are listed in [troubleshooting.md](troubleshooting.md).
|
|
120
|
+
|
|
121
|
+
## `extractAssets(template, deck, pptx)`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
function extractAssets(template: Template, deck: DeckSpec, pptx: Uint8Array): Record<string, Uint8Array>;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Reads the pictures of a deck back out of a pptx that was rendered from that deck, keyed by asset id. With it,
|
|
128
|
+
the deck JSON and its pptx are all you have to store between edits: generated photos are read back instead of
|
|
129
|
+
being generated, and paid for, again.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const cache = new Map(Object.entries(extractAssets(template, deck, previousPptx)));
|
|
133
|
+
const edited = await editDeck(template, deck, 'Shorten slide 3', { llm, images, cache });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Only slides that still match the deck are used. If the file was changed by hand (slides added, removed or
|
|
137
|
+
reordered, or a title retyped), those slides are skipped.
|
|
138
|
+
|
|
139
|
+
## Layout modes in practice
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// The deck leaves out the optional "subtitle" slot.
|
|
143
|
+
renderDeck(template, deck, { layout: 'strict' }); // throws: leaving this slot empty needs layout: 'adaptive'
|
|
144
|
+
renderDeck(template, deck, { layout: 'adaptive' }); // renders; the subtitle shape is removed
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use `adaptive` unless you need a guarantee that every slide looks exactly like the template slide with
|
|
148
|
+
different words.
|
|
149
|
+
|
|
150
|
+
## What the output is
|
|
151
|
+
|
|
152
|
+
- A `.pptx` that PowerPoint opens without a repair prompt. Output was checked by rendering in PowerPoint on
|
|
153
|
+
Windows; other programs were not tested.
|
|
154
|
+
- Charts are native charts with their data in an embedded workbook.
|
|
155
|
+
- Text keeps the template's formatting. Text boxes are not resized: text that is longer than its box overflows
|
|
156
|
+
it, which is why budgets exist.
|
|
157
|
+
- The output has no dependency on slotdeck. It is an ordinary file.
|
|
158
|
+
|
|
159
|
+
## Serving a file from a web server
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
app.post('/decks', async (request, response) => {
|
|
163
|
+
const deck = request.body as DeckSpec;
|
|
164
|
+
const issues = validateDeck(template.manifest, deck, { layout: 'adaptive', assets: Object.keys(assets) });
|
|
165
|
+
if (issues.some((issue) => issue.severity === 'error')) return response.status(400).json({ issues });
|
|
166
|
+
const { pptx } = renderDeck(template, deck, { assets, layout: 'adaptive' });
|
|
167
|
+
response
|
|
168
|
+
.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.presentationml.presentation')
|
|
169
|
+
.setHeader('Content-Disposition', 'attachment; filename="deck.pptx"')
|
|
170
|
+
.send(Buffer.from(pptx));
|
|
171
|
+
});
|
|
172
|
+
```
|