slotdeck 0.1.0 → 0.1.2
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 +16 -1
- package/dist/deck/validate.js +12 -5
- package/dist/deck/validate.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/gemini.d.ts +4 -0
- package/dist/llm/gemini.js +43 -7
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/provider.d.ts +12 -0
- package/dist/plan/catalog.d.ts +11 -1
- package/dist/plan/catalog.js +20 -7
- package/dist/plan/catalog.js.map +1 -1
- package/dist/plan/edit.js +9 -5
- package/dist/plan/edit.js.map +1 -1
- package/dist/plan/plan.d.ts +16 -2
- package/dist/plan/plan.js +50 -17
- package/dist/plan/plan.js.map +1 -1
- package/dist/plan/write.d.ts +8 -1
- package/dist/plan/write.js +29 -3
- package/dist/plan/write.js.map +1 -1
- package/dist/render/render.js +4 -4
- package/dist/render/render.js.map +1 -1
- package/dist/template/change.d.ts +1 -0
- package/dist/template/change.js +12 -0
- package/dist/template/change.js.map +1 -1
- package/dist/template/instruct.js +4 -2
- package/dist/template/instruct.js.map +1 -1
- package/dist/template/manifest.d.ts +16 -0
- package/dist/template/manifest.js +26 -0
- package/dist/template/manifest.js.map +1 -1
- package/dist/template/sample.js +4 -3
- package/dist/template/sample.js.map +1 -1
- package/dist/tools.js +6 -0
- package/dist/tools.js.map +1 -1
- package/docs/README.md +77 -0
- package/docs/api-reference.md +680 -0
- package/docs/changelog.md +107 -0
- package/docs/chat-tools.md +257 -0
- package/docs/concepts.md +126 -0
- package/docs/deck-format.md +225 -0
- package/docs/generating.md +254 -0
- package/docs/getting-started.md +139 -0
- package/docs/languages.md +83 -0
- package/docs/layout-freedom.md +136 -0
- package/docs/manifest-format.md +208 -0
- package/docs/providers.md +281 -0
- package/docs/rendering.md +172 -0
- package/docs/template-studio.md +379 -0
- package/docs/troubleshooting.md +174 -0
- package/package.json +3 -2
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
maxOutputTokens?: number; // a ceiling on the answer's size in tokens, reasoning not counted; see "Runaway answers"
|
|
24
|
+
effort?: 'low'; // set on short structural requests: use light reasoning if your model has a setting for it
|
|
25
|
+
images?: Array<{ mimeType: string; data: Uint8Array }>; // pictures to look at; only draftManifest sends them
|
|
26
|
+
signal?: AbortSignal; // pass it to your HTTP call so work can be cancelled
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The contract:
|
|
31
|
+
|
|
32
|
+
- Resolve with the **parsed** value (an object), not a string of JSON.
|
|
33
|
+
- The value should follow `schema`. slotdeck checks what it gets back against the template's manifest and asks
|
|
34
|
+
again when content breaks a limit, but it expects the overall shape the schema describes.
|
|
35
|
+
- Reject (throw) when the call fails. The planner does not retry failed calls; retries belong in the provider,
|
|
36
|
+
which knows which failures pass by themselves (see "Runaway answers" and the Gemini adapter below).
|
|
37
|
+
- One call is one request and one answer. There is no conversation state: every request carries everything it
|
|
38
|
+
needs.
|
|
39
|
+
|
|
40
|
+
### What the schemas contain
|
|
41
|
+
|
|
42
|
+
Only a small, widely supported subset of JSON Schema:
|
|
43
|
+
|
|
44
|
+
- `type`: `object`, `array`, `string`, `integer`, `number`, `boolean`
|
|
45
|
+
- `properties`, `required`, `items`, `enum`, `description`
|
|
46
|
+
- in a few places `minItems`, `maxItems`, `minimum`, `maximum`
|
|
47
|
+
|
|
48
|
+
There is no `anyOf`, `oneOf`, `$ref`, `pattern` or `format`. If your backend rejects the length or range
|
|
49
|
+
keywords, your provider can delete them from the schema before sending it: the result is checked by code in any
|
|
50
|
+
case.
|
|
51
|
+
|
|
52
|
+
### A provider for a backend with structured output
|
|
53
|
+
|
|
54
|
+
Most hosted models and gateways accept a JSON Schema and return JSON that follows it. The adapter is then a
|
|
55
|
+
single HTTP call. The request and response fields below are placeholders: use the ones your gateway documents.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { LlmProvider } from 'slotdeck';
|
|
59
|
+
|
|
60
|
+
export function gatewayModel(options: { url: string; model: string; token: () => Promise<string> }): LlmProvider {
|
|
61
|
+
return {
|
|
62
|
+
async generateJson({ system, prompt, schema, signal }) {
|
|
63
|
+
const response = await fetch(options.url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${await options.token()}` },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
model: options.model,
|
|
68
|
+
messages: [
|
|
69
|
+
{ role: 'system', content: system },
|
|
70
|
+
{ role: 'user', content: prompt },
|
|
71
|
+
],
|
|
72
|
+
// However your gateway asks for JSON that follows a schema:
|
|
73
|
+
response_format: { type: 'json_schema', json_schema: { name: 'answer', schema } },
|
|
74
|
+
}),
|
|
75
|
+
signal,
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) throw new Error(`Model gateway answered ${response.status}: ${(await response.text()).slice(0, 300)}`);
|
|
78
|
+
const body = await response.json();
|
|
79
|
+
const text = body.choices[0].message.content; // wherever your gateway puts the answer text
|
|
80
|
+
return JSON.parse(text);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### A provider for a model without structured output
|
|
87
|
+
|
|
88
|
+
Put the schema into the prompt and parse what comes back. Models often wrap JSON in a code fence or add a
|
|
89
|
+
sentence around it, so extract the JSON before parsing.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
export function plainTextModel(complete: (system: string, prompt: string, signal?: AbortSignal) => Promise<string>): LlmProvider {
|
|
93
|
+
return {
|
|
94
|
+
async generateJson({ system, prompt, schema, signal }) {
|
|
95
|
+
const instruction =
|
|
96
|
+
`${prompt}\n\nAnswer with one JSON value that follows this JSON Schema, and nothing else:\n${JSON.stringify(schema)}`;
|
|
97
|
+
const text = await complete(system, instruction, signal);
|
|
98
|
+
const start = text.indexOf('{');
|
|
99
|
+
const end = text.lastIndexOf('}');
|
|
100
|
+
if (start < 0 || end <= start) throw new Error(`The model did not answer with JSON: ${text.slice(0, 200)}`);
|
|
101
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Every top-level answer slotdeck asks for is a JSON object, so looking for the outermost braces is safe.
|
|
108
|
+
|
|
109
|
+
### Choosing a model
|
|
110
|
+
|
|
111
|
+
- The requests are small. Measured on the demo template (15 patterns): the outline request is a system prompt
|
|
112
|
+
of about 600 characters, a prompt of about 3,600 characters and a schema of about 700 characters. The writing
|
|
113
|
+
request for a 15-slide deck is a prompt of about 1,500 characters and a schema of about 9,800 characters. Any
|
|
114
|
+
current general-purpose model handles this.
|
|
115
|
+
- Writing a deck is a single call that returns all slides, so the model needs enough output room for the whole
|
|
116
|
+
deck. The JSON of a nine-slide deck is about 6 KB.
|
|
117
|
+
- `editManifest` sends the whole manifest with the measured shapes of every pattern's slide: about 58,000
|
|
118
|
+
characters for a template with 24 patterns.
|
|
119
|
+
- `draftManifest` can send a picture of each slide (`images`). A provider for a model that cannot see may ignore
|
|
120
|
+
them; the draft then relies on measurements alone.
|
|
121
|
+
|
|
122
|
+
### Runaway answers: always set an output limit
|
|
123
|
+
|
|
124
|
+
Now and then a model runs away inside one text field of its answer: it keeps adding sentence fragments to a
|
|
125
|
+
single string until something stops it. This was seen with a planning request whose proper answer is about 200
|
|
126
|
+
tokens: the model wrote more than 27,000 tokens into one field. Without a limit such a call takes minutes, costs
|
|
127
|
+
tens of thousands of output tokens, and ends in a timeout. It is a matter of chance: the same request usually
|
|
128
|
+
works the next time.
|
|
129
|
+
|
|
130
|
+
It was tied to reasoning. With the model's default reasoning, a request for a text-only plan ran away in 6 of
|
|
131
|
+
24 calls. With light reasoning it did not run away in 18 calls, and each took 4 seconds instead of about 35.
|
|
132
|
+
That is why planning requests carry `effort: 'low'`.
|
|
133
|
+
|
|
134
|
+
So a provider should do three things:
|
|
135
|
+
|
|
136
|
+
1. **Use light reasoning when a request says `effort: 'low'`**, if your model has such a setting.
|
|
137
|
+
2. **Limit the output of every call.** The planner passes `maxOutputTokens` with its requests: a generous
|
|
138
|
+
ceiling for that answer (for an outline of six slides, 1,360 tokens). Pass it to your model as its output
|
|
139
|
+
limit. If your model counts reasoning tokens against the same limit, add room for them. When a request
|
|
140
|
+
carries no estimate, use a default of your own rather than no limit.
|
|
141
|
+
3. **Ask once more when an answer is cut off at the limit.** A cut-off answer is not valid JSON. Retry the same
|
|
142
|
+
request once, and throw if it is cut off again.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
async generateJson({ system, prompt, schema, maxOutputTokens, effort, signal }) {
|
|
146
|
+
const limit = (maxOutputTokens ?? 16_000) + 8_000; // room for reasoning, if your model counts it here
|
|
147
|
+
const reasoning = effort === 'low' ? 'low' : undefined; // whatever your model calls its light setting
|
|
148
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
149
|
+
const answer = await callModel({ system, prompt, schema, limit, reasoning, signal });
|
|
150
|
+
if (!answer.cutOffAtLimit) return JSON.parse(answer.text);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`The model's answer was cut off at ${limit} tokens twice`);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The included Gemini adapter does all three. Its limit is the request's estimate plus 8,192 tokens for reasoning
|
|
157
|
+
(24,576 when there is no estimate, or its own `maxOutputTokens` option when set). It sends a cut-off request
|
|
158
|
+
once more. It uses thinking level `low` for requests with `effort: 'low'`, unless `thinkingLevel` is set on the
|
|
159
|
+
adapter, which then applies to every request. It also sends a request once more after a server error (500 to
|
|
160
|
+
504) or when the service's recitation filter blocks an ordinary answer, which happens at random; a bad key, a
|
|
161
|
+
quota or a bad request is not repeated.
|
|
162
|
+
|
|
163
|
+
### Timeouts and cancellation
|
|
164
|
+
|
|
165
|
+
slotdeck passes `signal` through when the caller gave one. Your provider should pass it to its HTTP call, and
|
|
166
|
+
should have a time limit of its own: a model call can hang without failing.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const limit = AbortSignal.timeout(180_000);
|
|
170
|
+
const combined = signal ? AbortSignal.any([signal, limit]) : limit;
|
|
171
|
+
await fetch(url, { ...init, signal: combined });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Logging and auditing
|
|
175
|
+
|
|
176
|
+
A provider is the one place every model call passes through, so it is where to count calls, log prompts or
|
|
177
|
+
enforce a budget:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const counted: LlmProvider = {
|
|
181
|
+
async generateJson(request) {
|
|
182
|
+
const started = Date.now();
|
|
183
|
+
try {
|
|
184
|
+
return await inner.generateJson(request);
|
|
185
|
+
} finally {
|
|
186
|
+
log({ promptChars: request.prompt.length, ms: Date.now() - started });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## `ImageProvider`: the picture source
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
interface ImageProvider {
|
|
196
|
+
/** Resolves with PNG, JPEG or GIF bytes. */
|
|
197
|
+
generateImage(request: ImageRequest): Promise<Uint8Array>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface ImageRequest {
|
|
201
|
+
prompt: string; // a description of the picture, written by the planning model
|
|
202
|
+
kind: 'photo' | 'icon' | 'graphic'; // what the slot holds; generated pictures are photos
|
|
203
|
+
aspectRatio: number; // width divided by height of the frame the picture goes into
|
|
204
|
+
signal?: AbortSignal;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
It does not have to be an image model. A stock library, an internal picture search or a fixed set of brand
|
|
209
|
+
photos all fit: return the bytes of the picture that best matches `prompt`.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const brandPhotos: ImageProvider = {
|
|
213
|
+
async generateImage({ prompt, aspectRatio }) {
|
|
214
|
+
const hit = await photoLibrary.search(prompt, { orientation: aspectRatio >= 1 ? 'landscape' : 'portrait' });
|
|
215
|
+
return new Uint8Array(await hit.download());
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The picture is cropped to the frame when the slot's fit is `cover`, so any proportions work; matching
|
|
221
|
+
`aspectRatio` just loses less of the picture.
|
|
222
|
+
|
|
223
|
+
Without an `ImageProvider`, leave `images` out. `generateDeck` and the chat tools then plan decks with no
|
|
224
|
+
generated photos (patterns that need a photo are not offered), unless you supply photos as `assets`.
|
|
225
|
+
|
|
226
|
+
## The included Gemini adapters
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { gemini, geminiImages, vertexEndpoint, GeminiError, GEMINI_DEVELOPER_ENDPOINT } from 'slotdeck';
|
|
230
|
+
|
|
231
|
+
const llm = gemini({ model: 'gemini-3.8-flash', apiKey });
|
|
232
|
+
const images = geminiImages({ model: 'gemini-3.1-flash-image', apiKey });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
They call Google's Interactions API with plain `fetch`, so they need network access to that API. Inside a
|
|
236
|
+
network without it, write a provider for your own gateway as shown above.
|
|
237
|
+
|
|
238
|
+
Options of both:
|
|
239
|
+
|
|
240
|
+
| Option | Meaning |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `model` | Model id. Required. |
|
|
243
|
+
| `apiKey` | Gemini Developer API key, sent as the `x-goog-api-key` header. |
|
|
244
|
+
| `endpoint` | Interactions endpoint. Defaults to `GEMINI_DEVELOPER_ENDPOINT`. Use `vertexEndpoint(project, location?)` for Vertex AI, or the address of a proxy. |
|
|
245
|
+
| `headers` | Extra headers, or a function returning them (may be async), for example a fresh `Authorization: Bearer` token. |
|
|
246
|
+
| `fetch` | A custom `fetch`, for proxies and tests. |
|
|
247
|
+
| `timeoutMs` | Gives up on a request after this long. Default 180000 (3 minutes). `0` waits for ever. |
|
|
248
|
+
|
|
249
|
+
Only for `gemini`: `thinkingLevel` (`minimal`, `low`, `medium`, `high`) and `maxOutputTokens`, the output limit
|
|
250
|
+
of a request with reasoning included. By default the limit is the request's own estimate plus 8,192 tokens, or
|
|
251
|
+
24,576 when the request has none.
|
|
252
|
+
|
|
253
|
+
Only for `geminiImages`: `imageSize` (`512`, `1K`, `2K`, `4K`; default `1K`) and `style`, a sentence appended to
|
|
254
|
+
every prompt, such as a house photo style.
|
|
255
|
+
|
|
256
|
+
Failures throw `GeminiError`, which has the HTTP `status` when there was one.
|
|
257
|
+
|
|
258
|
+
Vertex AI:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const llm = gemini({
|
|
262
|
+
model: 'gemini-3.8-flash',
|
|
263
|
+
endpoint: vertexEndpoint('my-project'), // location defaults to 'global'
|
|
264
|
+
headers: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The adapters were run live against the Gemini Developer API. The Vertex endpoint is supported by the adapter
|
|
269
|
+
but has not been run live.
|
|
270
|
+
|
|
271
|
+
## Testing without a model
|
|
272
|
+
|
|
273
|
+
A scripted provider makes tests deterministic and free:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const scripted: LlmProvider = {
|
|
277
|
+
async generateJson({ prompt, schema }) {
|
|
278
|
+
return cannedAnswers.shift(); // objects shaped like the schema
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
```
|
|
@@ -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
|
+
```
|