@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.
- package/README.md +794 -0
- package/dist/cli.js +8906 -0
- package/dist/deck-runtime.js +55 -0
- package/dist/index.js +8590 -0
- package/dist/mcp.js +7809 -0
- package/dist/server/errors.js +73 -0
- package/dist/server/http.js +504 -0
- package/dist/server/main.js +91 -0
- package/dist/server/options.js +198 -0
- package/dist/server/pipeline.js +356 -0
- package/dist/server/queue.js +195 -0
- package/dist/server/ui.js +1614 -0
- package/dist/server/upload.js +232 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/deck/runtime.d.ts +59 -0
- package/dist/types/deck/subtitles.d.ts +101 -0
- package/dist/types/emit/archetypes/annotated-figure.d.ts +103 -0
- package/dist/types/emit/archetypes/bar-compare.d.ts +30 -0
- package/dist/types/emit/archetypes/callout.d.ts +7 -0
- package/dist/types/emit/archetypes/claim-figure.d.ts +9 -0
- package/dist/types/emit/archetypes/data-table.d.ts +21 -0
- package/dist/types/emit/archetypes/equation-walk.d.ts +2 -0
- package/dist/types/emit/archetypes/grid.d.ts +16 -0
- package/dist/types/emit/archetypes/index.d.ts +19 -0
- package/dist/types/emit/archetypes/line-chart.d.ts +22 -0
- package/dist/types/emit/archetypes/pipeline.d.ts +81 -0
- package/dist/types/emit/archetypes/split-compare.d.ts +2 -0
- package/dist/types/emit/archetypes/stack.d.ts +93 -0
- package/dist/types/emit/archetypes/title.d.ts +188 -0
- package/dist/types/emit/camera.d.ts +397 -0
- package/dist/types/emit/composition.d.ts +191 -0
- package/dist/types/emit/island.d.ts +19 -0
- package/dist/types/emit/kit.d.ts +256 -0
- package/dist/types/emit/svg.d.ts +177 -0
- package/dist/types/emit/theme.d.ts +65 -0
- package/dist/types/emit/themes/index.d.ts +40 -0
- package/dist/types/emit/themes/ink.d.ts +12 -0
- package/dist/types/emit/themes/mono.d.ts +20 -0
- package/dist/types/emit/themes/paper.d.ts +18 -0
- package/dist/types/index.d.ts +200 -0
- package/dist/types/mcp/main.d.ts +2 -0
- package/dist/types/mcp/prereqs.d.ts +22 -0
- package/dist/types/mcp/tools.d.ts +212 -0
- package/dist/types/narrate/narrate.d.ts +87 -0
- package/dist/types/narrate/tts.d.ts +134 -0
- package/dist/types/narrate/voices.d.ts +29 -0
- package/dist/types/pack/media.d.ts +61 -0
- package/dist/types/pack/pack.d.ts +16 -0
- package/dist/types/plan/codex.d.ts +24 -0
- package/dist/types/plan/duration.d.ts +394 -0
- package/dist/types/plan/prompt.d.ts +47 -0
- package/dist/types/plan/refs.d.ts +22 -0
- package/dist/types/plan/select.d.ts +116 -0
- package/dist/types/prefs.d.ts +43 -0
- package/dist/types/render/captions.d.ts +108 -0
- package/dist/types/render/ffmpeg.d.ts +129 -0
- package/dist/types/render/render.d.ts +123 -0
- package/dist/types/render/timing.d.ts +290 -0
- package/dist/types/server/errors.d.ts +11 -0
- package/dist/types/server/http.d.ts +57 -0
- package/dist/types/server/main.d.ts +1 -0
- package/dist/types/server/options.d.ts +90 -0
- package/dist/types/server/pipeline.d.ts +21 -0
- package/dist/types/server/queue.d.ts +105 -0
- package/dist/types/server/ui.d.ts +9 -0
- package/dist/types/server/upload.d.ts +107 -0
- package/dist/types/source/assets.d.ts +11 -0
- package/dist/types/source/fonts.d.ts +15 -0
- package/dist/types/source/markdown.d.ts +8 -0
- package/dist/types/types.d.ts +1992 -0
- package/dist/types/verify/budget.d.ts +41 -0
- package/dist/types/verify/check.d.ts +78 -0
- package/dist/types/verify/drift.d.ts +158 -0
- package/dist/types/verify/fidelity.d.ts +247 -0
- package/dist/types/verify/index.d.ts +207 -0
- package/dist/types/verify/overprint.d.ts +133 -0
- package/dist/types/verify/typefloor.d.ts +50 -0
- 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;
|