@jokerized/decksmith 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 (78) hide show
  1. package/README.md +794 -0
  2. package/dist/cli.js +8906 -0
  3. package/dist/deck-runtime.js +55 -0
  4. package/dist/index.js +8590 -0
  5. package/dist/mcp.js +7809 -0
  6. package/dist/server/errors.js +73 -0
  7. package/dist/server/http.js +504 -0
  8. package/dist/server/main.js +91 -0
  9. package/dist/server/options.js +198 -0
  10. package/dist/server/pipeline.js +356 -0
  11. package/dist/server/queue.js +195 -0
  12. package/dist/server/ui.js +1614 -0
  13. package/dist/server/upload.js +232 -0
  14. package/dist/types/cli.d.ts +1 -0
  15. package/dist/types/deck/runtime.d.ts +59 -0
  16. package/dist/types/deck/subtitles.d.ts +101 -0
  17. package/dist/types/emit/archetypes/annotated-figure.d.ts +103 -0
  18. package/dist/types/emit/archetypes/bar-compare.d.ts +30 -0
  19. package/dist/types/emit/archetypes/callout.d.ts +7 -0
  20. package/dist/types/emit/archetypes/claim-figure.d.ts +9 -0
  21. package/dist/types/emit/archetypes/data-table.d.ts +21 -0
  22. package/dist/types/emit/archetypes/equation-walk.d.ts +2 -0
  23. package/dist/types/emit/archetypes/grid.d.ts +16 -0
  24. package/dist/types/emit/archetypes/index.d.ts +19 -0
  25. package/dist/types/emit/archetypes/line-chart.d.ts +22 -0
  26. package/dist/types/emit/archetypes/pipeline.d.ts +81 -0
  27. package/dist/types/emit/archetypes/split-compare.d.ts +2 -0
  28. package/dist/types/emit/archetypes/stack.d.ts +93 -0
  29. package/dist/types/emit/archetypes/title.d.ts +188 -0
  30. package/dist/types/emit/camera.d.ts +397 -0
  31. package/dist/types/emit/composition.d.ts +191 -0
  32. package/dist/types/emit/island.d.ts +19 -0
  33. package/dist/types/emit/kit.d.ts +256 -0
  34. package/dist/types/emit/svg.d.ts +177 -0
  35. package/dist/types/emit/theme.d.ts +65 -0
  36. package/dist/types/emit/themes/index.d.ts +40 -0
  37. package/dist/types/emit/themes/ink.d.ts +12 -0
  38. package/dist/types/emit/themes/mono.d.ts +20 -0
  39. package/dist/types/emit/themes/paper.d.ts +18 -0
  40. package/dist/types/index.d.ts +200 -0
  41. package/dist/types/mcp/main.d.ts +2 -0
  42. package/dist/types/mcp/prereqs.d.ts +22 -0
  43. package/dist/types/mcp/tools.d.ts +212 -0
  44. package/dist/types/narrate/narrate.d.ts +87 -0
  45. package/dist/types/narrate/tts.d.ts +134 -0
  46. package/dist/types/narrate/voices.d.ts +29 -0
  47. package/dist/types/pack/media.d.ts +61 -0
  48. package/dist/types/pack/pack.d.ts +16 -0
  49. package/dist/types/plan/codex.d.ts +24 -0
  50. package/dist/types/plan/duration.d.ts +394 -0
  51. package/dist/types/plan/prompt.d.ts +47 -0
  52. package/dist/types/plan/refs.d.ts +22 -0
  53. package/dist/types/plan/select.d.ts +116 -0
  54. package/dist/types/prefs.d.ts +43 -0
  55. package/dist/types/render/captions.d.ts +108 -0
  56. package/dist/types/render/ffmpeg.d.ts +129 -0
  57. package/dist/types/render/render.d.ts +123 -0
  58. package/dist/types/render/timing.d.ts +290 -0
  59. package/dist/types/server/errors.d.ts +11 -0
  60. package/dist/types/server/http.d.ts +57 -0
  61. package/dist/types/server/main.d.ts +1 -0
  62. package/dist/types/server/options.d.ts +90 -0
  63. package/dist/types/server/pipeline.d.ts +21 -0
  64. package/dist/types/server/queue.d.ts +105 -0
  65. package/dist/types/server/ui.d.ts +9 -0
  66. package/dist/types/server/upload.d.ts +107 -0
  67. package/dist/types/source/assets.d.ts +11 -0
  68. package/dist/types/source/fonts.d.ts +15 -0
  69. package/dist/types/source/markdown.d.ts +8 -0
  70. package/dist/types/types.d.ts +1992 -0
  71. package/dist/types/verify/budget.d.ts +41 -0
  72. package/dist/types/verify/check.d.ts +78 -0
  73. package/dist/types/verify/drift.d.ts +158 -0
  74. package/dist/types/verify/fidelity.d.ts +247 -0
  75. package/dist/types/verify/index.d.ts +207 -0
  76. package/dist/types/verify/overprint.d.ts +133 -0
  77. package/dist/types/verify/typefloor.d.ts +50 -0
  78. package/package.json +84 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which voice says it.
3
+ *
4
+ * A voice id is not a preference the user should have to hold: `edge-tts
5
+ * --list-voices` prints several hundred rows, a wrong id fails at synthesis time
6
+ * with an unhelpful error, and the interesting choice — language and register —
7
+ * is already in `prefs`. So the table below maps (lang, tone) to an id that was
8
+ * read off this machine's own `--list-voices`, and an explicit
9
+ * `prefs.narration.voice` always wins over it.
10
+ *
11
+ * Multilingual neural voices are preferred wherever Microsoft ships one: they
12
+ * are the newer models, they read a foreign proper noun mid-sentence without
13
+ * switching accent, and they are the closest thing here to a presenter rather
14
+ * than a reader.
15
+ */
16
+ import type { z } from "zod";
17
+ import type { prefsSchema } from "../types.js";
18
+ type Prefs = z.infer<typeof prefsSchema>;
19
+ /**
20
+ * Pick the voice for these preferences.
21
+ *
22
+ * An unlisted language falls back to the English multilingual row, which speaks
23
+ * some forty languages: the accent will be wrong, but a deck that narrates in a
24
+ * borrowed accent is worth more than one that fails to narrate at all.
25
+ */
26
+ export declare function pickVoice(prefs: Prefs): string;
27
+ /** The languages with a voice of their own. Everything else borrows English's. */
28
+ export declare function narratableLangs(): string[];
29
+ export {};
@@ -0,0 +1,61 @@
1
+ import type { z } from "zod";
2
+ import type { mediaSchema } from "../types.js";
3
+ import type { PackFiles } from "./pack.js";
4
+ export type Media = z.infer<typeof mediaSchema>;
5
+ /** What the caller asks for, before the URL gets a say. */
6
+ export interface AssetRequest {
7
+ id: string;
8
+ /** An http(s) URL, a `data:` URL, or a local path. */
9
+ url: string;
10
+ /** The caller's choice. Ignored for player URLs, which are always embedded. */
11
+ prefer: "bake" | "link";
12
+ mime?: string;
13
+ }
14
+ /**
15
+ * Injected so tests never reach the network, and so a caller can supply its own
16
+ * cache. `mime` is whatever the transport knows — a sniffed type is better than
17
+ * a guessed extension.
18
+ */
19
+ export type Fetcher = (url: string) => Promise<{
20
+ bytes: Uint8Array;
21
+ mime?: string;
22
+ }>;
23
+ export interface MediaPlan {
24
+ media: Media[];
25
+ /** Paths under `media/`, ready to hand to `writePack`. */
26
+ files: PackFiles;
27
+ bakedCount: number;
28
+ /**
29
+ * Baking everything on a figure-heavy paper is the difference between a 2MB
30
+ * pack and a 200MB one, so the total is reported rather than discovered.
31
+ */
32
+ bakedBytes: number;
33
+ /** Ids the caller wanted baked that travel as link or embed instead. */
34
+ demoted: string[];
35
+ /** Ids the caller wanted linked that had to be baked — see `policyFor`. */
36
+ promoted: string[];
37
+ }
38
+ /** A player URL is one whose host is in the list, or a subdomain of one. */
39
+ export declare function isEmbed(url: string): boolean;
40
+ /**
41
+ * The final policy for one asset.
42
+ *
43
+ * Baking requires confidence that the URL points at a file. A local path or a
44
+ * `data:` URL is one by construction; a remote URL has to end in an extension we
45
+ * recognise. Without that we would happily bake an HTML error page under a
46
+ * `.jpg`-shaped id, so an unrecognised shape stays a `link` even when the caller
47
+ * asked to bake it.
48
+ *
49
+ * A local path is baked even under `--link`, and that is not the caller being
50
+ * overridden for its own good — it is that there is no link to keep. `ingest`
51
+ * downloads every figure and rewrites `src` to the local copy, so by pack time
52
+ * the original URL is gone and "link" could only record this machine's
53
+ * filesystem layout. That packs clean, unpacks clean, and rebuilds into a deck
54
+ * whose <img>s point at nothing; the lint gate calls it missing_local_asset on
55
+ * the recipient's machine, which is the worst possible place to find out.
56
+ */
57
+ export declare function policyFor(url: string, prefer: "bake" | "link"): Media["policy"];
58
+ /** Resolve every asset, fetching only the ones that end up baked. */
59
+ export declare function planMedia(assets: AssetRequest[], fetcher?: Fetcher): Promise<MediaPlan>;
60
+ /** One line a CLI can print, because a user should know which pack they got. */
61
+ export declare function mediaSummary(plan: MediaPlan): string;
@@ -0,0 +1,16 @@
1
+ import type { z } from "zod";
2
+ import { packSchema } from "../types.js";
3
+ export type Pack = z.infer<typeof packSchema>;
4
+ /** Everything in the archive except `deck.json`, keyed by its path inside it. */
5
+ export type PackFiles = Record<string, Uint8Array>;
6
+ /** Write `out`, returning the size of the pack so a caller can report it. */
7
+ export declare function writePack(pack: Pack, files: PackFiles, out: string): Promise<number>;
8
+ export declare function readPack(path: string): Promise<{
9
+ pack: Pack;
10
+ files: PackFiles;
11
+ }>;
12
+ /** The parse half of `readPack`, separated so bytes from anywhere can be checked. */
13
+ export declare function openPack(bytes: Uint8Array, label?: string): {
14
+ pack: Pack;
15
+ files: PackFiles;
16
+ };
@@ -0,0 +1,24 @@
1
+ import type { Prefs } from "../prefs.js";
2
+ import { type Source, type Storyboard } from "../types.js";
3
+ export declare const SCHEMA: unknown;
4
+ export interface CodexOptions {
5
+ /** Left unset by default: use whatever model the user's Codex is configured for. */
6
+ model?: string;
7
+ timeoutMs?: number;
8
+ /**
9
+ * What the person asking for the deck chose. Omitted means the schema's
10
+ * defaults, which is what a bare `codexPlanner(source)` has always sent.
11
+ */
12
+ prefs?: Prefs;
13
+ /** Swappable so a test can drive the real parse path without spawning anything. */
14
+ run?: Runner;
15
+ }
16
+ /** What the planner needs from the outside world: a prompt in, a final message out. */
17
+ export type Runner = (args: {
18
+ prompt: string;
19
+ schemaPath: string;
20
+ outPath: string;
21
+ model?: string;
22
+ timeoutMs: number;
23
+ }) => Promise<void>;
24
+ export declare function codexPlanner(source: Source, opts?: CodexOptions): Promise<Storyboard>;
@@ -0,0 +1,394 @@
1
+ /**
2
+ * A target duration, a slide count and a narration density, turned into the
3
+ * numbers the rest of the pipeline needs.
4
+ *
5
+ * WHY THIS IS NOT A SPEED SLIDER. `beatSeconds` (src/emit/composition.ts) is
6
+ * `max(authored, lastHold + spoken)`, and on the narrated demo the spoken term
7
+ * wins by a mile: 195.9s of speech across 37 stops. Multiplying every tween by
8
+ * `animationSpeed` scales the two terms it does not need to and leaves the one
9
+ * that decides the length untouched, so a duration control built on that knob
10
+ * alone would quietly do nothing. Measured, not reasoned: the demo is 246.5s at
11
+ * `animationSpeed: 1` and 208.6s at 0.25 — a 4× faster animation buys 15%.
12
+ *
13
+ * WHY THE BUDGET DIVIDES BY STOPS. `planSegments` cuts a beat's narration one
14
+ * sentence per stop, and the demo has 12 beats and 37 stops. A 60-second target
15
+ * spread over 37 stops is 1.6s each — 24 characters, four words. That is not
16
+ * narration, which is why low density means FEWER NARRATED STOPS rather than
17
+ * shorter sentences: one sentence per beat gives 12 stops and 5 seconds each.
18
+ *
19
+ * WHY BOTH KNOBS ARE STILL NEEDED. Once only one stop per beat speaks, the
20
+ * binding term flips: the demo drops to 116.9s, and what holds it there is
21
+ * `lastHold` — a six-stage pipeline reveals its last stage at 7.8s no matter how
22
+ * short its sentence is. `lastHold` scales with `animationSpeed` and nothing
23
+ * else, so hitting 60s over 12 slides needs a shorter sentence AND a faster
24
+ * animation. Neither alone gets there; that is the whole content of this file.
25
+ *
26
+ * Everything here is pure arithmetic over preferences, so it is testable with no
27
+ * planner, no TTS and no ffmpeg.
28
+ */
29
+ import type { Prefs } from "../prefs.js";
30
+ /**
31
+ * Characters of text per second of speech.
32
+ *
33
+ * `latin` is MEASURED on demo/audio/narration.json: 2829 characters over 195.9
34
+ * seconds is 14.44, and the per-cue mean is 15.03. `cjk` is a GUESS — a Korean
35
+ * or Japanese character carries several times the Latin one, and no narrated CJK
36
+ * deck exists in this repo to measure. It is here so a Korean deck's budget is
37
+ * wrong by a little rather than by 2×; replace it with a measurement the first
38
+ * time one is narrated.
39
+ */
40
+ export declare const SPEECH_CPS: {
41
+ readonly latin: 14.4;
42
+ readonly cjk: 6.5;
43
+ };
44
+ /**
45
+ * Seconds from a scene's start to its LAST hold, at `animationSpeed: 1`.
46
+ *
47
+ * MEASURED as the mean over the demo's twelve beats (50.5s / 12), from
48
+ * `emitScene(beat).holds` rather than from anything authored. It is the floor a
49
+ * beat cannot go under without cutting off its own final reveal, and it is why
50
+ * the speech budget below is `beat − lastHold`, not `beat`.
51
+ *
52
+ * ponytail: one mean over twelve beats, not a per-archetype table. The spread is
53
+ * real (1.9s for a title, 7.8s for a six-stage pipeline), so a deck of nothing
54
+ * but pipelines will overshoot — which the bounded playback retime then closes.
55
+ * Predict per-beat from `REVEALS` if that stops being good enough.
56
+ */
57
+ export declare const LAST_HOLD_SECONDS = 4.2;
58
+ /** Stops per beat, measured: 37 stops over the demo's 12 beats. */
59
+ export declare const STOPS_PER_BEAT = 3.1;
60
+ /**
61
+ * How much of a beat is reserved for its reveal schedule rather than its speech.
62
+ *
63
+ * ponytail: one tuned constant. Today's narrated demo sits at 0.20 (50.5s of
64
+ * holds in 246.5s of deck) because speech dominates; at one sentence per beat it
65
+ * sits at 0.43. 0.35 splits the difference and leaves speech the majority, which
66
+ * is the right way round for a narrated deck. Lower it if the animation reads as
67
+ * rushed, raise it if the voice sounds crammed.
68
+ */
69
+ export declare const MOTION_SHARE = 0.35;
70
+ /** Below this a "sentence" is a fragment. About five words of English. */
71
+ export declare const MIN_SENTENCE_CHARS = 30;
72
+ /**
73
+ * The scene chrome's settle time at `animationSpeed: 1`, in seconds.
74
+ *
75
+ * The eyebrow runs at 0.15s over 0.5s and the headline at 0.3s over 0.6s, so the
76
+ * headline lands at 0.9s and that is when the voice may start. `openSeconds` in
77
+ * src/emit/composition.ts reads the real number off the real scene; this is the
78
+ * same number in closed form, because the budget has to be struck before any
79
+ * scene exists to measure. They agree by construction on every archetype that
80
+ * uses the shared chrome, and the emitter's measured value is what the deck is
81
+ * actually built with.
82
+ */
83
+ export declare const OPEN_SECONDS_AT_SPEED = 0.9;
84
+ /** Stillness after the last word, before the cut. `HANDOFF_SECONDS`. */
85
+ export declare const SETTLE_SECONDS = 0.4;
86
+ /**
87
+ * The sentence length this deck aims for: what "explains something" measures.
88
+ *
89
+ * MEASURED on `demo/storyboard.json`, the deck every other number in this file
90
+ * is measured against: 39 narration sentences, mean 72.1 characters, median 74,
91
+ * first quartile 59.
92
+ *
93
+ * IT WAS 60 — the first quartile — and that was the wrong number twice over. It
94
+ * was picked as a FLOOR, "below this a slide captions rather than explains", and
95
+ * the owner then rejected two decks that cleared it, both times in the same
96
+ * words: too short to explain the paper. A floor is what a deck must not go
97
+ * under; a target is what it should hit. `fastEnough` reads this as a target, so
98
+ * a bar set at the worst quarter of the deck that works produced decks at the
99
+ * worst quarter of the deck that works.
100
+ *
101
+ * So it is the MEAN of the deck that works. At a 60-second target over twelve
102
+ * slides that is reached at a `+10%` speaking rate, against the `+30%` the
103
+ * first-quartile bar demanded — more words AND more readable captions, because
104
+ * the seconds came from overlapping speech with motion rather than from speed.
105
+ *
106
+ * Distinct from `MIN_SENTENCE_CHARS`, which is where a sentence stops being a
107
+ * sentence at all. Between the two the deck is perfectly buildable and thin,
108
+ * and that is a thing to be TOLD rather than prevented — the arithmetic is not
109
+ * wrong, it is just what the requested slide count costs, and only the author
110
+ * can decide whether to spend slides or seconds to buy it back.
111
+ *
112
+ * RAISE THIS to make every deck say more, at the cost of the subtitle: the
113
+ * physical ceiling is the speech time a target has (`duration - slides * quiet`)
114
+ * times the fastest rate `RATE_STEPS` allows, which at 60s over twelve slides is
115
+ * about 94 characters a slide.
116
+ */
117
+ export declare const EXPLAINING_CHARS = 72;
118
+ /**
119
+ * Sentences a beat needs before its narration can be a story rather than a label.
120
+ *
121
+ * TWO, and it is the demo that says so rather than taste. `demo/storyboard.json`
122
+ * writes 39 sentences over 12 beats — 3.25 each — and every bit of its
123
+ * storytelling lives in the second and third: "Then the field is cut into
124
+ * windows...", "Only at the very end does the decoder upsample...", "That loop is
125
+ * a single tick...", "So this is not the cheap option, and the paper never claims
126
+ * it is." The flow is INSIDE a beat, between its own sentences.
127
+ *
128
+ * Reduce that same deck to its FIRST sentence per beat — which is literally what
129
+ * a 60-second target over twelve slides buys — and the deck that works reads like
130
+ * this:
131
+ *
132
+ * The encoder does the heavy lifting first.
133
+ * The encoder turns the low-resolution image into a dense feature field.
134
+ * The first tick is worth almost a full decibel.
135
+ *
136
+ * Twelve disconnected statements. So one sentence per beat cannot flow no matter
137
+ * who writes it, and the owner's complaint — "they don't have a flow, just
138
+ * sentence by sentence" — is arithmetic, not prose quality.
139
+ *
140
+ * THAT CONCLUSION IS TRUE OF A LECTURE AND FALSE OF A TEASER, which is why this
141
+ * only governs beats longer than `FF_BEAT_SECONDS`. A conference fast-forward
142
+ * talk runs one sentence a slide across a dozen slides and flows perfectly well,
143
+ * because its flow comes from the SCRIPT being continuous — the prompt's "the
144
+ * deck is one script, not N captions" — rather than from paragraphs inside each
145
+ * beat. The demo is a four-minute deck, and reading its shape onto a
146
+ * sixty-second teaser is what produced five slides and 133 words a minute.
147
+ *
148
+ * WHY THIS IS A COUNT AND NOT A COHESION SCORE. The obvious check is to measure
149
+ * connectives and anaphora in the finished narration. It was built and then
150
+ * defeated: prefixing one discourse marker to each of the twelve rejected
151
+ * sentences — changing no content, no claim, no relation — moved the score from
152
+ * 42% to 92%, ABOVE the hand-written demo's 74%. That is §9's lesson exactly, so
153
+ * the thing measured here is the BUDGET, which is not a judgement and cannot be
154
+ * written around.
155
+ */
156
+ export declare const SENTENCES_PER_BEAT = 2;
157
+ /**
158
+ * Beat length below which a deck is a fast-forward talk rather than a lecture.
159
+ *
160
+ * The conference teaser: sixty seconds, a dozen slides, the voice never stopping.
161
+ * A beat this short cannot afford a leisurely delivery, so `fastEnough` stops
162
+ * asking for the SLOWEST rate that clears a sentence and takes the FASTEST the
163
+ * captions can carry — which is the difference between 133 words a minute and
164
+ * 180, on the same seconds.
165
+ */
166
+ export declare const FF_BEAT_SECONDS = 8;
167
+ /**
168
+ * The shortest a slide may be and still be seen.
169
+ *
170
+ * A fast-forward talk runs four to six seconds a slide; under four the deck is
171
+ * flicking rather than presenting, and the build has no room to read. This is the
172
+ * floor `slidesFor` clamps the beat length to, distinct from `FF_BEAT_SECONDS`,
173
+ * which is the threshold that decides how fast the voice goes — using one number
174
+ * for both put 60 seconds at eight slides when the reference is twelve.
175
+ */
176
+ export declare const MIN_BEAT_SECONDS = 4;
177
+ /**
178
+ * Subtitle rate a short-form viewer will take, as against broadcast television.
179
+ *
180
+ * `COMFORTABLE_CPS` is 17 and comes from broadcast practice, where the viewer is
181
+ * across a room and did not choose the subtitle. A one-minute research teaser is
182
+ * neither. The anchor is this project's own artifact: `demo/audio/narration.json`
183
+ * — the deck every other number here is measured against — runs a p95 of 18.41
184
+ * cps, already past the broadcast bar, and nobody has ever complained about it.
185
+ *
186
+ * 22 is that with the headroom a fast-forward talk needs, and it is the ceiling
187
+ * rather than a target: `fastEnough` only goes as fast as the beat requires.
188
+ */
189
+ export declare const SHORT_FORM_CPS = 22;
190
+ /**
191
+ * How much faster a CUE reads, at its p95, than the deck's mean spoken rate.
192
+ *
193
+ * TWO things at once, deliberately, because `fastEnough` has only one number to
194
+ * multiply: a cue spans the words and not the breath around them, and the CUE
195
+ * that decides whether captions are readable is the fastest one, not the average.
196
+ * So this converts a mean spoken cps into a p95 CUE cps — the quantity
197
+ * `SHORT_FORM_CPS` is a ceiling on.
198
+ *
199
+ * MEASURED on `demo/audio/narration.json`, this project's anchor deck, whose 37
200
+ * segments carry 39 cues. Its mean segment rate is 14.440 cps — `SPEECH_CPS.latin`
201
+ * to three figures, which is where that constant comes from — and its p95 cue
202
+ * rate is 18.409. That is a ratio of 1.278, and 1.28 rounds AWAY from admitting a
203
+ * step, which is the safe direction for a ceiling.
204
+ *
205
+ * IT SAID 1.17 UNTIL NOW, and 1.17 was never reconcilable with the file it lives
206
+ * in: `SHORT_FORM_CPS` two constants above already quotes the same deck's p95 at
207
+ * 18.41, and 18.41/14.4 is 1.28, not 1.17. The old table paired a `.srt` against
208
+ * an arithmetic speech rate and got 16.6 cps at `+0%` where the anchor artifact
209
+ * reads 18.41 — a measurement of something that is not this. What shipped from it
210
+ * was a fast-forward deck at `+20%` (speedup 1.252), whose captions land at
211
+ * 14.4 x 1.252 x 1.278 = 23.0 cps against a ceiling of 22. At 1.28 the same deck
212
+ * takes `+10%` and lands at 21.9. Wrong in the OUTPUT, not in a gate — no gate
213
+ * looks at this, which is why it survived.
214
+ *
215
+ * SCALE-INVARIANCE IS ASSUMED, and is the one thing here not measured: the ratio
216
+ * is taken at `+0%` and applied at every step, on the reasoning that speeding the
217
+ * voice up shrinks the cue windows and the breaths between them together.
218
+ * Measuring it at `+10%` and `+20%` means synthesising the demo again at those
219
+ * rates. `test/duration.test.ts` pins the `+0%` end against the artifact.
220
+ */
221
+ export declare const CUE_OVERHEAD = 1.28;
222
+ /** Broadcast subtitle practice. Past this the captions stop being readable. */
223
+ export declare const COMFORTABLE_CPS = 17;
224
+ /** How far playback may be sped up before the captions are the problem. */
225
+ export declare const MAX_PLAYBACK = 1.25;
226
+ /**
227
+ * How many of a beat's stops may speak, by narration density.
228
+ *
229
+ * `high` is today's behaviour — one sentence per reveal, every stop narrated.
230
+ * The lower two lean on `planSegments`'s existing rule: given fewer sentences
231
+ * than stops it fills the early ones and leaves the rest SILENT, deliberately,
232
+ * rather than inventing copy to fill a reveal.
233
+ */
234
+ export declare const SPEAKING_STOPS: Record<Prefs["narration"]["density"], number>;
235
+ /**
236
+ * What edge-tts's `--rate` actually buys, MEASURED — not what it says it buys.
237
+ *
238
+ * Synthesised on `en-US-AndrewMultilingualNeural` over a 72-character demo
239
+ * sentence, which speaks in 4.416s at `+0%` (16.30 cps, within 1% of the
240
+ * `SPEECH_CPS.latin` measured across the whole narrated demo):
241
+ *
242
+ * ```
243
+ * rate seconds chars/sec speedup p95 cue cps
244
+ * +0% 4.416 16.30 1.000 16.7
245
+ * +10% 3.720 19.35 1.187 19.9
246
+ * +20% 3.528 20.41 1.252 21.0
247
+ * +30% 3.168 22.73 1.394 23.6
248
+ * +40% 2.880 25.00 1.533 26.0
249
+ * +50% 2.904 24.79 1.521 25.7 <- SLOWER than +40%
250
+ * +60% 2.640 27.27 1.673 28.5
251
+ * ```
252
+ *
253
+ * TWO THINGS THIS TABLE EXISTS TO SAY. The nominal percentage overshoots — the
254
+ * first 10% buys 19% — so a linear model would under-speak every deck. And the
255
+ * curve is not monotonic: `+50%` came back slower than `+40%`, so the prosody
256
+ * rate is not a clean multiplier and interpolating between these points would be
257
+ * inventing data. A table of what was observed is the honest shape.
258
+ *
259
+ * WHY IT STOPS AT +40%. The subtitle, not the ear. At `+40%` the p95 cue rate is
260
+ * 26.0 cps, already half again over the `COMFORTABLE_CPS` broadcast practice; at
261
+ * `+60%` it is 28.5 and the caption is gone before it is read. `+50%` is
262
+ * excluded for measuring slower than the step below it.
263
+ *
264
+ * Latin-measured. A CJK deck gets the same steps, which is a guess of the same
265
+ * kind `SPEECH_CPS.cjk` already is — replace it the first time one is narrated.
266
+ */
267
+ export declare const RATE_STEPS: readonly (readonly [rate: string, speedup: number])[];
268
+ export interface DurationPlan {
269
+ /** Effective `animationSpeed`. Derived when `duration` is set, else the pref. */
270
+ speed: number;
271
+ /** Stops per beat allowed to speak. `Infinity` means every one. */
272
+ speakingStops: number;
273
+ /**
274
+ * edge-tts `--rate` for the narration. Derived when `duration` is set — a
275
+ * short target buys its words by speaking faster before it buys them by
276
+ * saying less. `"+0%"` whenever the budget already affords a real sentence.
277
+ */
278
+ rate: string;
279
+ /**
280
+ * Sentences this beat's budget affords. 1 means the deck can only caption; see
281
+ * `SENTENCES_PER_BEAT`. Absent when no duration was asked for.
282
+ */
283
+ sentences?: number;
284
+ /** Characters per narration sentence. Absent when no duration was asked for. */
285
+ chars?: number;
286
+ /** Seconds of speech one beat can afford. Absent without a target. */
287
+ speechSeconds?: number;
288
+ /** The per-beat length the target implies. Absent without a target. */
289
+ beatSeconds?: number;
290
+ /** Said, never fatal: what the target costs, or cannot buy. */
291
+ warnings: string[];
292
+ }
293
+ /**
294
+ * The two derived numbers, from the three the user chose.
295
+ *
296
+ * `duration` unset returns the preferences untouched — `speed` is whatever
297
+ * `animationSpeed` says and there is no character budget — so a deck built
298
+ * without a target is byte-for-byte the deck this file did not exist for.
299
+ *
300
+ * `duration` set OVERRIDES `animationSpeed`: the target owns the pace, because
301
+ * the two cannot both be honoured and the target is the one the user stated a
302
+ * number for. Said in a warning rather than silently.
303
+ */
304
+ export declare function durationPlan(prefs: Prefs): DurationPlan;
305
+ /**
306
+ * How many slides a target should have, by TEMPO.
307
+ *
308
+ * WHAT THIS REPLACED, and why it was wrong. It used to return the most slides a
309
+ * target could carry while every beat still afforded two sentences, which gave
310
+ * FIVE slides for a minute. The owner, watching it: "in auto mode the number of
311
+ * slides is too conservative when it's 1 minute... in their fast-forward
312
+ * presentation they never use stupidly small number of slides."
313
+ *
314
+ * He is describing the conference fast-forward talk — the one-minute teaser an
315
+ * author gives before the session — and that is the right reference for a short
316
+ * target. Those run ten to fifteen slides in sixty seconds, about five seconds
317
+ * each, with the voice carrying straight over the cuts. The two-sentence floor
318
+ * was derived from `demo/storyboard.json`, which is a FOUR-MINUTE deck at twenty
319
+ * seconds a beat, and applying its shape to a teaser is what produced five
320
+ * slides and 133 words per minute against a fast-forward talk's 160-190.
321
+ *
322
+ * So the anchor is the beat LENGTH, and the two references agree on twelve
323
+ * slides: a fast-forward talk is 12 beats in 60s, the demo is 12 beats in 246s.
324
+ * What changes with duration is how long each beat lasts, not how many there
325
+ * are — until the beat would run past twenty seconds, where a deck stops being
326
+ * one thought a slide and starts being a lecture.
327
+ *
328
+ * 30s -> 8 120s -> 12 600s -> 30
329
+ * 60s -> 12 300s -> 15
330
+ *
331
+ * This is the DEFAULT, never an override. `slides` is one of the three knobs the
332
+ * owner asked to hold — "user can give you the number of slides they want in the
333
+ * video with duration of their choice" — so an explicit count is obeyed.
334
+ */
335
+ export declare function slidesFor(prefs: Prefs): number;
336
+ /**
337
+ * How fast this deck speaks.
338
+ *
339
+ * TWO REGIMES, and which one applies is decided by the beat length.
340
+ *
341
+ * A LECTURE (`beatSeconds` over `FF_BEAT_SECONDS`) takes the SLOWEST step that
342
+ * lifts a sentence to something which explains — speech is sped up only as far as
343
+ * the shortfall needs, so a target with room to spare is spoken at `+0%` and its
344
+ * deck does not move a byte.
345
+ *
346
+ * A FAST-FORWARD TALK takes the FASTEST step the captions can carry. Below eight
347
+ * seconds a beat there is no such thing as spare room: every second not carrying
348
+ * a word is information the viewer does not get, and the owner's whole complaint
349
+ * — "you must utilize every single second" — is that this function was being
350
+ * thrifty with seconds nobody wanted saved. The ceiling is `SHORT_FORM_CPS`
351
+ * against the DECK'S OWN measured cue rate, not a constant, because how fast a
352
+ * caption can run depends on how densely this deck happens to be written.
353
+ *
354
+ * Measured on the sixty-second target: the slow rule gave `+10%` and 133 words a
355
+ * minute; the fast rule gives `+30%` and 180, which is conference teaser pace.
356
+ */
357
+ export declare function fastEnough(chars: number, cps?: number, beatSeconds?: number): readonly [rate: string, speedup: number];
358
+ /**
359
+ * What the finished video must be sped up by to hit its target.
360
+ *
361
+ * Returns 1 when it is already at or under the target — a video is never
362
+ * SLOWED to fill a duration, because padding a deck with dead air is worse than
363
+ * a deck that is 8 seconds short.
364
+ */
365
+ export declare function playbackFactor(actualSeconds: number, targetSeconds: number): number;
366
+ /**
367
+ * `atempo` is clamped to [0.5, 2.0] per instance, so anything past 2× is a
368
+ * chain of equal factors whose product is the whole. Two at 1.7 make 2.89.
369
+ */
370
+ export declare function tempoChain(factor: number): number[];
371
+ /**
372
+ * The 95th percentile of characters per second across a deck's subtitle cues.
373
+ *
374
+ * The p95 rather than the mean, and the deck's OWN cues rather than a constant:
375
+ * how much faster a video can safely play is a property of how densely this deck
376
+ * happens to be written, and the worst few cues are what a viewer notices. The
377
+ * demo measures 18.41 — already over broadcast practice, so it has no headroom
378
+ * at all, which is a fact worth telling someone before they ask for 1.5×.
379
+ */
380
+ export declare function p95CueRate(cues: readonly {
381
+ start: number;
382
+ end: number;
383
+ text: string;
384
+ }[]): number;
385
+ /**
386
+ * Whether this deck's own captions survive being sped up this much.
387
+ *
388
+ * Computed from the deck's MEASURED p95 cue rate against broadcast practice, not
389
+ * from a hardcoded ceiling: a deck whose captions already run at 18 cps has no
390
+ * headroom at all, and one written in short lines has plenty. Warns, never
391
+ * refuses — how readable a caption has to be is the user's call, and they can
392
+ * only make it if they are told the number.
393
+ */
394
+ export declare function playbackWarning(factor: number, p95: number): string | undefined;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What the model is told, and what it is told about.
3
+ *
4
+ * Planning quality is the product, and this file is where it is won or lost —
5
+ * the schema only guarantees the storyboard is well-formed, never that it is a
6
+ * good explanation of this particular document.
7
+ *
8
+ * `renderSource` deliberately puts every citable id next to its caption. The
9
+ * model cannot cite an id it was never shown, so the inventory is the cheapest
10
+ * defence against the failure mode that actually happens: a plausible-looking
11
+ * `figureId` that exists in no source.
12
+ */
13
+ import type { Prefs } from "../prefs.js";
14
+ import { type Source } from "../types.js";
15
+ /**
16
+ * How many sentences each archetype wants, because it is how many stops it has.
17
+ *
18
+ * MEASURED from `emitScene(beat).holds.length`, not reasoned about. The prose this
19
+ * replaced said "a pipeline of four stages wants five sentences" and the emitter
20
+ * produces FOUR — and a planning document that set out to correct it guessed six.
21
+ * Three different numbers for one archetype, none of them from the emitter.
22
+ *
23
+ * It matters because `narrate` cuts the audio sentence by sentence across the
24
+ * stops (`planSegments`): too few and the last reveals are silent, too many and
25
+ * everything after the first surplus sentence is heard over the wrong picture.
26
+ * The prompt was wrong for most archetypes, so the voice ran ahead of the
27
+ * animation in the shipped product by construction.
28
+ *
29
+ * `test/prompt.test.ts` recomputes every row from `emitScene` and fails if one
30
+ * drifts. That test is the reason this table is allowed to exist as text: the
31
+ * prompt is built before any beat exists, so it cannot call `emitScene` itself,
32
+ * and the only alternative to a checked table is prose nobody verifies.
33
+ */
34
+ export declare const REVEALS: Record<string, string>;
35
+ /**
36
+ * The rules, then the preferences the person asking for the deck chose. They go
37
+ * last because they are the part the model is most likely to drift from, and the
38
+ * end of a prompt is the part it holds hardest.
39
+ */
40
+ export declare function systemPrompt(prefs: Prefs): string;
41
+ /**
42
+ * The default prompt, for callers that have no preferences in hand. Kept so the
43
+ * planner's other entry points keep working unchanged.
44
+ */
45
+ export declare const SYSTEM: string;
46
+ /** The document plus every id the model is allowed to cite, and nothing else. */
47
+ export declare function renderSource(source: Source): string;
@@ -0,0 +1,22 @@
1
+ import type { Source, Storyboard } from "../types.js";
2
+ export declare function assertRefsResolve(storyboard: Storyboard, source: Source): void;
3
+ /**
4
+ * Every `inside` names a part the previous beat actually draws.
5
+ *
6
+ * THE SAME SHAPE AS THE CHECK ABOVE, one level in: a schema proves `inside` has
7
+ * a beat and an element, and cannot prove the element exists. RULE 11 says only a
8
+ * pipeline's `stageN`, a grid's `rgnN` and a stack's `layN` have interiors worth
9
+ * entering, and a real plan asked to fly into `stage1` of an ANNOTATED-FIGURE —
10
+ * which has notes and leader lines and no stages at all.
11
+ *
12
+ * WHY IT MOVED HERE. The emitter already refuses this, so nothing shipped
13
+ * broken. But it refuses at BUILD, which is after `narrate` has spent a minute
14
+ * and a dozen network round trips synthesising speech for a storyboard that was
15
+ * never going to build. `plan` is where the author is being told to read the file
16
+ * anyway, and emitting a scene is cheap — the emitters build strings.
17
+ *
18
+ * Cheap enough to be exact rather than a table: the previous beat is emitted and
19
+ * `enterableIds` is asked what it drew, so this cannot drift from the emitter the
20
+ * way a hardcoded list of archetype interiors would.
21
+ */
22
+ export declare function assertInsideResolves(storyboard: Storyboard, source: Source): void;