@jokerized/decksmith 0.3.1 → 0.3.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.
@@ -0,0 +1,250 @@
1
+ import { type Source } from "../types.js";
2
+ export interface HarvestOptions {
3
+ /** Cap on the page's own HTML. Generous: a real article ships megabytes of it. */
4
+ maxBytes?: number;
5
+ /** Cap on one asset, matching what `fetchFigures` allows a figure to be. */
6
+ maxAssetBytes?: number;
7
+ /** Whole-call budget for each fetch, and for the browser's own steps. */
8
+ timeoutMs?: number;
9
+ /** How many assets are downloaded before the rest are named in `warnings`. */
10
+ maxAssets?: number;
11
+ /**
12
+ * How many VIDEOS are downloaded, counted separately from `maxAssets`.
13
+ *
14
+ * Separate because the two are different sizes of mistake. A page with forty
15
+ * images costs a few megabytes; a page with forty videos costs a gigabyte and
16
+ * an hour, and one of them was probably an advertisement. A clip earns its
17
+ * place by being the thing a beat is planned around, and a deck does not have
18
+ * room for four of those, let alone forty.
19
+ */
20
+ maxClips?: number;
21
+ /**
22
+ * Seconds of a downloaded clip that are kept. Longer is TRUNCATED, and the
23
+ * trim is warned about rather than performed quietly.
24
+ *
25
+ * A cap on SECONDS rather than on bytes because seconds are what the render
26
+ * spends: hyperframes pre-decodes a clip to one still per output frame before
27
+ * capture begins, so a five-minute video inside a four-minute deck is four
28
+ * minutes of full-size stills written to disk for a beat that can be sixty
29
+ * seconds at most (`beatSchema` in src/types.ts caps it there). The default
30
+ * lives in ./transcode.ts beside the rest of the encode.
31
+ *
32
+ * Ignored when `transcode` is false: trimming is something ffmpeg does, and
33
+ * there is no ffmpeg in that path to do it.
34
+ */
35
+ maxClipSeconds?: number;
36
+ /**
37
+ * Whether a downloaded clip is re-encoded to a slide-sized VP9 webm at all.
38
+ * True unless stated, and stating `false` ships the page's own file.
39
+ *
40
+ * The reason to turn it off is that the encode is the one part of a harvest
41
+ * that costs CPU rather than network: a caller re-ingesting the same page ten
42
+ * times while tuning a plan pays for it ten times, and the deck it is looking
43
+ * at does not care. The reason to leave it on is everything ./transcode.ts
44
+ * says — the bytes shipped, and the stills the render writes.
45
+ */
46
+ transcode?: boolean;
47
+ /**
48
+ * Total bytes across every asset this harvest downloads.
49
+ *
50
+ * `maxAssetBytes` bounds ONE file; nothing bounded the sum, so forty assets
51
+ * one byte under the per-file cap was a legal harvest of 1.2 GB. Charged from
52
+ * what actually arrived, and only for a fetch that succeeded — the counter
53
+ * that charges before the check is how a refused figure still costs the
54
+ * budget it was refused for (`guardFigures` in src/server/pipeline.ts does
55
+ * exactly that, deliberately not copied here).
56
+ */
57
+ maxTotalBytes?: number;
58
+ /**
59
+ * Wall clock for the whole harvest, checked BEFORE each fetch is started.
60
+ *
61
+ * BE HONEST ABOUT WHAT THIS BOUNDS: it stops the NEXT fetch, never one already
62
+ * in flight, so the real ceiling is this plus one `timeoutMs`. Bounding it
63
+ * exactly would mean an abort signal threaded through `fetchGuarded`, and a
64
+ * harvest that overruns by twenty seconds is not the failure this is for — a
65
+ * page whose forty images each take fifteen seconds is.
66
+ */
67
+ maxWallMs?: number;
68
+ /**
69
+ * How the markdown SPELLS its asset references. Absolute paths by default,
70
+ * which is what a caller reading the document in place needs.
71
+ *
72
+ * `"relative"` writes the bare filename instead, for a caller that is about to
73
+ * move the directory somewhere this process cannot see — the MCP zips the
74
+ * harvest and hands it to the server, where an absolute path out of this
75
+ * machine's temp directory is refused by `guardFigures` and the figure is
76
+ * dropped. `assets` stays absolute either way: it names files on THIS disk.
77
+ */
78
+ refs?: "absolute" | "relative";
79
+ /**
80
+ * TEST SEAM, passed straight through to `fetchGuarded`, where it is documented.
81
+ *
82
+ * It is the only way to point this at a `node:http` server on loopback, which
83
+ * is what test/harvest.test.ts needs to drive the real code path — including
84
+ * the interception proof, which requires a second server this process can see
85
+ * the request count of. It relaxes nothing else: the private ranges, the
86
+ * schemes, the caps and the timeout all still apply. Nothing in src passes it.
87
+ */
88
+ allowLoopback?: boolean;
89
+ }
90
+ export interface Harvested {
91
+ /** The document, in the dialect src/source/markdown.ts reads. */
92
+ markdown: string;
93
+ /**
94
+ * Absolute paths of every file the MARKDOWN references, in document order. A
95
+ * clip's video is written into `dir` too and is deliberately not one of them:
96
+ * the markdown cannot reference it, and a caller shipping the document
97
+ * elsewhere (the MCP zips this list) would otherwise carry megabytes nothing
98
+ * in the document points at. It is named in `clips` instead.
99
+ */
100
+ assets: string[];
101
+ /**
102
+ * The videos, which the markdown CANNOT carry — see `HarvestedClip`. Hand
103
+ * these to `attachClips` with the parsed source to get them back.
104
+ */
105
+ clips: HarvestedClip[];
106
+ /** Everything left out, and why. One dead image is not a failed harvest. */
107
+ warnings: string[];
108
+ /** The page's title, also emitted as the document's opening `#` heading. */
109
+ title: string;
110
+ }
111
+ /**
112
+ * One video, carried BESIDE the markdown because the dialect has no word for it.
113
+ *
114
+ * `parseMarkdown` produces figures out of images and nothing else, so a clip —
115
+ * `kind: "clip"`, a poster, a duration, and either a file or a page to watch it
116
+ * on — cannot be spelled in the document at all. Rather than invent a dialect
117
+ * extension that only this module writes and only `parseMarkdown` would have to
118
+ * learn, the clip travels alongside and `attachClips` puts it back afterwards.
119
+ *
120
+ * `poster` is also the JOIN: when there is one, the markdown references it as an
121
+ * ordinary image, so `parseMarkdown` gives that figure the id, the section and
122
+ * the sentence that mentions it — everything the planner uses to decide where a
123
+ * picture belongs — and `attachClips` upgrades that same figure in place. A clip
124
+ * with no poster has nothing to join to and is appended as a new figure, which
125
+ * costs it exactly those three facts.
126
+ */
127
+ export interface HarvestedClip {
128
+ /** Absolute path of the downloaded video, or "" for one we hold no file for. */
129
+ file: string;
130
+ /** Absolute path of the still, or "" when the page offered none. */
131
+ poster: string;
132
+ /** Where a viewer watches it, when there is no file. "" when there is one. */
133
+ href: string;
134
+ /**
135
+ * The VIDEO's own pixels when we hold the file, and the poster's when we do
136
+ * not. types.ts says this box is the video's rather than the still's, and it
137
+ * is right — every annotation downstream is a fraction of it. A clip we could
138
+ * not download has no other box to offer, and the deck shows the still.
139
+ */
140
+ width: number;
141
+ height: number;
142
+ /** Measured off the container, never guessed. Absent for a link-only clip. */
143
+ seconds?: number;
144
+ /** The figcaption, link text or iframe title the page gave it. */
145
+ caption: string;
146
+ }
147
+ /**
148
+ * One block of the page, as the DOM walker sees it and as `toMarkdown` writes it.
149
+ *
150
+ * `src`, `poster` and `href` hold the PAGE's URLs when `readDom` returns them and
151
+ * LOCAL absolute paths once `localise` has rewritten them. One type rather than
152
+ * two because the two differ in nothing but that, and a second near-identical
153
+ * union is the kind of thing that grows a third.
154
+ */
155
+ export type Block = {
156
+ kind: "heading";
157
+ depth: number;
158
+ text: string;
159
+ } | {
160
+ kind: "paragraph";
161
+ text: string;
162
+ } | {
163
+ kind: "code";
164
+ text: string;
165
+ } | {
166
+ kind: "list";
167
+ ordered: boolean;
168
+ items: string[];
169
+ } | {
170
+ kind: "table";
171
+ columns: string[];
172
+ rows: string[][];
173
+ } | {
174
+ kind: "image";
175
+ src: string;
176
+ alt: string;
177
+ caption: string;
178
+ } | {
179
+ kind: "video";
180
+ src: string;
181
+ poster: string;
182
+ href: string;
183
+ caption: string;
184
+ };
185
+ export declare function harvest(url: string, dir: string, opts?: HarvestOptions): Promise<Harvested>;
186
+ /** What a container declares about the picture inside it. */
187
+ export interface Measured {
188
+ width: number;
189
+ height: number;
190
+ /** Absent when the container declares no usable duration — a live capture does. */
191
+ seconds?: number;
192
+ }
193
+ /**
194
+ * A video's display box and its length, read out of its own header.
195
+ *
196
+ * WHY NOT `ffprobe`, WHICH WOULD BE FOUR LINES. Because it would make ingest —
197
+ * the one verb that has to work on a laptop with a browser and nothing else —
198
+ * depend on a binary this project otherwise needs only to RENDER. A machine
199
+ * without ffmpeg would then harvest a page and silently come back with the video
200
+ * demoted to a link, which is the shape of failure this file exists to avoid. A
201
+ * width, a height and a duration are four integers in a header; reading them is
202
+ * cheaper than the dependency, and it is the same trade `imageSize` already
203
+ * makes for PNG, JPEG, WebP and AVIF.
204
+ *
205
+ * Exported because it is pure, and because the half of test/harvest.test.ts that
206
+ * runs on CI has no browser — measuring a hand-built header is testable there
207
+ * and driving a real page is not.
208
+ */
209
+ export declare function videoSize(b: Buffer): Measured & {
210
+ container: "mp4" | "webm";
211
+ };
212
+ /**
213
+ * Put the harvest's clips back into the parsed source, and their files beside it.
214
+ *
215
+ * CALL THIS BEFORE `fetchFigures`, not after. `fetchFigures` passes a clip
216
+ * through untouched — it says so in as many words, because a clip carries the
217
+ * video's dimensions rather than an image's and its first bytes are an `ftyp`
218
+ * box the image sniffer is right to refuse — so a clip that arrives after it has
219
+ * run is a figure nothing ever localises, whose `src` is an absolute path into a
220
+ * temp directory that will not exist on the machine that opens the deck.
221
+ *
222
+ * WHERE A CLIP LANDS. One with a poster REPLACES the figure `parseMarkdown` made
223
+ * out of that poster, keeping its id, its section and the sentence that mentions
224
+ * it — the three facts the planner uses to decide which point a picture belongs
225
+ * to, and the reason the poster is written into the markdown at all. One without
226
+ * a poster has nothing to replace and is appended, which costs it exactly those
227
+ * three facts and is why a page that gives its videos posters harvests better.
228
+ */
229
+ export declare function attachClips(source: Source, clips: readonly HarvestedClip[], dir: string): Promise<Source>;
230
+ /**
231
+ * Blocks become THIS PROJECT'S markdown dialect, which is narrower than markdown.
232
+ *
233
+ * Three rules from src/source/markdown.ts, each of which loses content silently
234
+ * when broken — no error, no gate, just a text-only deck:
235
+ *
236
+ * - A figure is lifted only from a paragraph whose children are ALL images
237
+ * (`onlyImages`, around :82 and :158). So an image is always alone in its
238
+ * paragraph; `readDom` has already broken sentences around inline ones.
239
+ * - A caption is read only from a FOLLOWING paragraph that is a single run of
240
+ * emphasis (`captionOf`, :166). So a caption is `*text*` on its own, directly
241
+ * after the image, with every `*` inside it escaped.
242
+ * - Raw HTML returns the empty string in BOTH walkers (:243 and :264). So
243
+ * nothing here emits raw HTML, ever — not a `<figure>`, not a `<br>`, not an
244
+ * HTML comment.
245
+ *
246
+ * Exported because test/harvest.test.ts runs `parseMarkdown` back over its output
247
+ * and asserts the figures survive, and that test must run on a machine with no
248
+ * browser — which is every CI runner this project has.
249
+ */
250
+ export declare function toMarkdown(blocks: readonly Block[]): string;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * WHICH PART OF A PAGE IS THE ARTICLE — the Readability scoring algorithm,
3
+ * implemented here rather than depended on.
4
+ *
5
+ * `src/source/harvest.ts` picks its content region with three rules: believe
6
+ * `<main>`, believe `<article>`, else take the deepest element whose block text
7
+ * minus its link text is maximal. Its own comment says that is deliberately not
8
+ * a Readability implementation, and it holds up on the shapes it was written
9
+ * against. It loses on the shapes it was not: a `<main>` that also wraps the
10
+ * comment thread is believed whole and short-circuits before the loop ever runs;
11
+ * a `<div role="dialog">` cookie banner matches nothing in the walker's SKIP set,
12
+ * so it is neither scored down nor skipped; and the score counts text inside
13
+ * `nav`/`aside`/`footer` subtrees that the walker will then throw away, so a
14
+ * wrapper can be chosen on the strength of text that never reaches the markdown.
15
+ *
16
+ * This is Mozilla Readability's `grabArticle`, in the shape that fits here:
17
+ * strip the elements whose tag, role, class or id say chrome; score every
18
+ * paragraph by its length and its comma count; propagate that score to the
19
+ * parent, the grandparent and up to five levels of ancestor, dividing as it
20
+ * climbs; discount each candidate by its LINK DENSITY, which is the term that
21
+ * tells a rail of headlines from prose; take the highest, climb to a parent that
22
+ * scores higher still, then merge in the siblings that score close to it.
23
+ *
24
+ * WHAT THIS IS NOT. It is not a semantic understanding of the page. Nothing here
25
+ * reads the article; it is arithmetic over text length, punctuation, and the
26
+ * words site authors happen to put in class names. It is much better than one
27
+ * subtraction and it is wrong in ways that are structural rather than accidental,
28
+ * so they are worth naming:
29
+ *
30
+ * - A PAGINATED ARTICLE harvests as page one and nothing says so. Readability
31
+ * proper follows "next page" links; this cannot, because `harvest` fetches
32
+ * exactly one document and every subresource is aborted.
33
+ * - A PAGE WHOSE CONTENT IS A LIST OF LINKS — a link blog, a search result, a
34
+ * documentation index — is scored down by the very term that makes the pass
35
+ * work. Link density cannot tell a nav from a page that is a nav on purpose.
36
+ * - A GALLERY loses. Pictures carry no text, `<li>` is not a scored tag, and a
37
+ * page of captioned images scores near zero everywhere, so the winner is
38
+ * whichever caption block happened to be longest.
39
+ * - CLASS NAMES ARE A GUESS, and the same word list that rescues
40
+ * `<div class="article-body">` rescues `<div class="comment-body">`.
41
+ * - AN ELEMENT THE STRIP REMOVES TAKES ITS `<video>` WITH IT. That matters more
42
+ * here than in a reader: harvest's poster is the join that lets `attachClips`
43
+ * keep a clip's id, section and mention, so a silently dropped player demotes
44
+ * a clip with every gate green. Hence `mediaDropped` in the report — this pass
45
+ * counts what it lost rather than leaving the caller to find out later.
46
+ *
47
+ * When it declines it says so, PUTS BACK EVERY NODE IT REMOVED, and changes
48
+ * nothing else, so `harvest`'s own heuristic runs against the document it would
49
+ * have seen. Declining is the retry Readability performs with its flags off;
50
+ * here the simpler pass IS that retry, and it already exists.
51
+ *
52
+ * IT RUNS INSIDE THE PAGE. `harvest` ships code into the browser as a function
53
+ * reference — `page.evaluate(readDom)` — which puppeteer serialises with
54
+ * `Function.prototype.toString()` and evaluates as source text. `readContentRegion`
55
+ * is shipped the same way and carries the same rule: it may not close over
56
+ * anything in this module, so every constant and every helper it uses is declared
57
+ * inside it, INCLUDING the marker attribute that `CONTENT_MARKER` below spells a
58
+ * second time. A closed-over constant does not fail here, where a test would see
59
+ * it; it fails in the page, as a `ReferenceError`, on somebody's ingest — so
60
+ * test/readability.test.ts asserts the two spellings agree and that the source
61
+ * names nothing from Node. The node bundle is built with `minify: false`
62
+ * (scripts/build.mjs), which is what keeps the serialised text intact.
63
+ */
64
+ /**
65
+ * The attribute the pass puts on the region it chose.
66
+ *
67
+ * A marker rather than a return value because an `Element` cannot cross the
68
+ * `page.evaluate` boundary — what comes back is structured-clone data. Marking
69
+ * is also what lets this stay a second self-contained function beside `readDom`
70
+ * rather than something `readDom` has to grow a second job for: the walker's
71
+ * `pickRoot` need only look for this attribute first and keep its own three
72
+ * rules as the fallback for when this pass declines.
73
+ */
74
+ export declare const CONTENT_MARKER = "data-ds-content";
75
+ /** One element the scorer considered, in the terms it was judged on. */
76
+ export interface Candidate {
77
+ /** Up to three levels, `div#page > div#content > article.post`, to name it. */
78
+ path: string;
79
+ /** Paragraph scores propagated up, plus this element's tag and class weight. */
80
+ content: number;
81
+ /** Anchor text over all text, 0..1 — a rail of headlines is near 1. */
82
+ linkDensity: number;
83
+ /** `content * (1 - linkDensity)`. This is what the winner is chosen on. */
84
+ score: number;
85
+ /** Characters of text held, whitespace collapsed. */
86
+ text: number;
87
+ }
88
+ /** What the pass hands back across the `page.evaluate` boundary. */
89
+ export interface ContentPick {
90
+ /** True when some element in the document now carries `CONTENT_MARKER`. */
91
+ marked: boolean;
92
+ /**
93
+ * Why, in a sentence a `warnings` entry can carry verbatim. Populated on
94
+ * every path, including the ones that change nothing.
95
+ */
96
+ reason: string;
97
+ /** The best few candidates, best first, so a failure names which part broke. */
98
+ candidates: Candidate[];
99
+ /** Siblings merged in beside the winner, the winner itself not counted. */
100
+ merged: number;
101
+ /** Elements the strip removed. Zero when the pass declined and put them back. */
102
+ stripped: number;
103
+ /** Characters of text in the marked region. */
104
+ text: number;
105
+ /**
106
+ * `<video>` and `<iframe>` elements the page had that the region does not.
107
+ * Not an error — a related-videos rail SHOULD be lost — but the clip path is
108
+ * load-bearing enough downstream that the number is reported rather than
109
+ * discovered.
110
+ */
111
+ mediaDropped: number;
112
+ }
113
+ /**
114
+ * Choose the content region, mark it, and report what that cost.
115
+ *
116
+ * Evaluated INSIDE THE PAGE, so nothing outside this function body exists at
117
+ * run time. Type annotations are erased by the build and are the only thing
118
+ * here that refers to anything above.
119
+ */
120
+ export declare function readContentRegion(): ContentPick;
@@ -0,0 +1,83 @@
1
+ export interface TranscodeOptions {
2
+ /** Longest edge of the box to fit inside, in pixels. Default 1280. */
3
+ maxEdgePx?: number;
4
+ /** Seconds to keep. A longer video is TRUNCATED, and the trim is warned. */
5
+ maxSeconds?: number;
6
+ /** Wall clock for the ffmpeg run before it is killed. Default 10 minutes. */
7
+ timeoutMs?: number;
8
+ /**
9
+ * TEST SEAM: the binary to run. Only ever "ffmpeg" in src.
10
+ *
11
+ * It exists so test/transcode.test.ts can prove the skip path by naming a
12
+ * binary that is not installed, which is the one branch here that cannot be
13
+ * exercised on a machine where the feature works.
14
+ */
15
+ ffmpeg?: string;
16
+ }
17
+ export interface Transcoded {
18
+ /** The `out` given, or the `input` given back when nothing was done to it. */
19
+ path: string;
20
+ /** Measured off the file `path` names — never the arithmetic that asked for it. */
21
+ width: number;
22
+ height: number;
23
+ /** Absent when the container declares no usable duration, as `Measured` says. */
24
+ seconds?: number;
25
+ /** False when `path` is still the original's bytes. */
26
+ transcoded: boolean;
27
+ /** What was skipped or traded, and why. Empty when nothing was. */
28
+ warnings: string[];
29
+ }
30
+ /**
31
+ * The even box `width`x`height` fits inside, never larger than it started.
32
+ *
33
+ * FLOOR to even rather than round, on both edges. Even because VP9 in yuv420p
34
+ * refuses odd dimensions and says so in a way nobody reads as "your width is
35
+ * odd"; floor because rounding UP a 1919-wide source that needs no scaling at
36
+ * all would enlarge it by a pixel, and "never upscale" is easier to keep than to
37
+ * qualify.
38
+ *
39
+ * Rounding each edge independently moves the aspect ratio by up to a pixel's
40
+ * worth, and there is one place downstream where that is visible: claim-figure
41
+ * picks a full-width layout at exactly `width / height >= 3` (claim-figure.ts:197),
42
+ * so a source sitting on that boundary can land either side of it afterwards.
43
+ * That is a layout choice moving one step, not a deck disagreeing with its file
44
+ * — the figure carries the measured box, so the planner and the emitter still
45
+ * describe the same rectangle.
46
+ */
47
+ export declare function fitBox(width: number, height: number, maxEdgePx: number): {
48
+ width: number;
49
+ height: number;
50
+ };
51
+ /**
52
+ * The command, built where it can be read and tested without running anything.
53
+ *
54
+ * The house pattern from src/render/ffmpeg.ts: an exported pure args builder and
55
+ * a one-line `runTool` at the use site, so the flags are assertable on a machine
56
+ * with no ffmpeg on it.
57
+ *
58
+ * `-an` drops the audio track outright. A clip is emitted muted on purpose — the
59
+ * deck already spends its single audio track on narration (claim-figure.ts:121)
60
+ * — so carrying the audio is pure bytes for something nothing will ever unmute.
61
+ *
62
+ * `-crf 32 -b:v 0` is libvpx-vp9's constant-quality mode; `-deadline good
63
+ * -cpu-used 4 -row-mt 1` is the speed setting that makes this affordable at
64
+ * ingest time (4 seconds of 1080p in 0.8s of wall clock, measured) rather than
65
+ * the several-minutes-per-clip the encoder's defaults cost. The three bitexact
66
+ * and metadata flags are the determinism story in the header.
67
+ */
68
+ export declare function transcodeArgs(input: string, out: string, plan: {
69
+ width: number;
70
+ height: number;
71
+ seconds?: number;
72
+ }): string[];
73
+ /**
74
+ * Shrink `input` into `out`, and say what the result actually is.
75
+ *
76
+ * Throws for exactly one thing: an `input` this cannot measure. That is a bug at
77
+ * the call site rather than a machine missing a tool — `grabVideo` in harvest
78
+ * measures the same bytes with the same function before it writes them, and
79
+ * refuses what it cannot read — and there is no honest box to hand back for a
80
+ * file whose header says nothing. Everything else that can go wrong comes back
81
+ * as the original plus a warning.
82
+ */
83
+ export declare function transcode(input: string, out: string, opts?: TranscodeOptions): Promise<Transcoded>;
@@ -23,12 +23,19 @@ export declare const refSchema: z.ZodObject<{
23
23
  }, z.core.$strip>;
24
24
  export declare const figureSchema: z.ZodObject<{
25
25
  id: z.ZodString;
26
+ kind: z.ZodDefault<z.ZodEnum<{
27
+ image: "image";
28
+ clip: "clip";
29
+ }>>;
26
30
  src: z.ZodString;
27
31
  caption: z.ZodString;
28
32
  width: z.ZodInt;
29
33
  height: z.ZodInt;
30
34
  sectionId: z.ZodOptional<z.ZodString>;
31
35
  mention: z.ZodOptional<z.ZodString>;
36
+ poster: z.ZodOptional<z.ZodString>;
37
+ seconds: z.ZodOptional<z.ZodNumber>;
38
+ href: z.ZodOptional<z.ZodString>;
32
39
  }, z.core.$strip>;
33
40
  export declare const equationSchema: z.ZodObject<{
34
41
  id: z.ZodString;
@@ -59,12 +66,19 @@ export declare const sourceSchema: z.ZodObject<{
59
66
  }, z.core.$strip>>;
60
67
  figures: z.ZodArray<z.ZodObject<{
61
68
  id: z.ZodString;
69
+ kind: z.ZodDefault<z.ZodEnum<{
70
+ image: "image";
71
+ clip: "clip";
72
+ }>>;
62
73
  src: z.ZodString;
63
74
  caption: z.ZodString;
64
75
  width: z.ZodInt;
65
76
  height: z.ZodInt;
66
77
  sectionId: z.ZodOptional<z.ZodString>;
67
78
  mention: z.ZodOptional<z.ZodString>;
79
+ poster: z.ZodOptional<z.ZodString>;
80
+ seconds: z.ZodOptional<z.ZodNumber>;
81
+ href: z.ZodOptional<z.ZodString>;
68
82
  }, z.core.$strip>>;
69
83
  equations: z.ZodArray<z.ZodObject<{
70
84
  id: z.ZodString;
@@ -1720,12 +1734,19 @@ export declare const packSchema: z.ZodObject<{
1720
1734
  }, z.core.$strip>>;
1721
1735
  figures: z.ZodArray<z.ZodObject<{
1722
1736
  id: z.ZodString;
1737
+ kind: z.ZodDefault<z.ZodEnum<{
1738
+ image: "image";
1739
+ clip: "clip";
1740
+ }>>;
1723
1741
  src: z.ZodString;
1724
1742
  caption: z.ZodString;
1725
1743
  width: z.ZodInt;
1726
1744
  height: z.ZodInt;
1727
1745
  sectionId: z.ZodOptional<z.ZodString>;
1728
1746
  mention: z.ZodOptional<z.ZodString>;
1747
+ poster: z.ZodOptional<z.ZodString>;
1748
+ seconds: z.ZodOptional<z.ZodNumber>;
1749
+ href: z.ZodOptional<z.ZodString>;
1729
1750
  }, z.core.$strip>>;
1730
1751
  equations: z.ZodArray<z.ZodObject<{
1731
1752
  id: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",