@jokerized/decksmith 0.4.1 → 0.5.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 +13 -3
- package/dist/cli.js +483 -378
- package/dist/deck-runtime.js +5 -2
- package/dist/index.js +444 -306
- package/dist/mcp.js +391 -301
- package/dist/types/build/files.d.ts +102 -0
- package/dist/types/deck/runtime.d.ts +4 -2
- package/dist/types/emit/kit.d.ts +32 -0
- package/dist/types/index.d.ts +10 -6
- package/dist/types/render/render.d.ts +40 -12
- package/dist/types/render/timing.d.ts +11 -0
- package/dist/types/types.d.ts +64 -0
- package/dist/types/verify/fidelity.d.ts +55 -0
- package/dist/types/verify/index.d.ts +8 -3
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { DeckNarration } from "../emit/composition.js";
|
|
2
|
+
import type { Source, Storyboard } from "../types.js";
|
|
3
|
+
/** Progress, for a caller that has somewhere to put it. */
|
|
4
|
+
type Step = (message: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Where a narrated deck keeps its voice, in the built deck and inside a pack
|
|
7
|
+
* alike. One name in one place: the emitter writes it into the island, `build`
|
|
8
|
+
* copies into it, `pack` stores under it, and `unpack` restores it — and none of
|
|
9
|
+
* them has to agree with the others by memory.
|
|
10
|
+
*/
|
|
11
|
+
export declare const AUDIO_DIR = "audio";
|
|
12
|
+
/** The mp3s an island names, deduplicated and in a stable order. */
|
|
13
|
+
export declare function audioNames(narration: {
|
|
14
|
+
beats: Record<string, {
|
|
15
|
+
audio: string;
|
|
16
|
+
}[]>;
|
|
17
|
+
}): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Vendor KaTeX's stylesheet and woff2 fonts beside the deck.
|
|
20
|
+
*
|
|
21
|
+
* The HyperFrames compiler inlines `<script src>` but not `<link rel=stylesheet>`,
|
|
22
|
+
* so a CDN-linked stylesheet is fetched — with its fonts — during capture. That
|
|
23
|
+
* breaks "no network at render time" (invariant 4) and makes equation decks
|
|
24
|
+
* nondeterministic in a way that took four experiments to corner. The frame that
|
|
25
|
+
* finally showed it differed by exactly one glyph, the caligraphic W of
|
|
26
|
+
* `\mathcal{W}`, present in one render and not the other — a font that had
|
|
27
|
+
* arrived by that frame on one run and not on the next.
|
|
28
|
+
*
|
|
29
|
+
* Only woff2 is copied. KaTeX ships woff2, woff and ttf of every family; every
|
|
30
|
+
* browser this deck will ever open in reads woff2, and the other two are three
|
|
31
|
+
* quarters of the payload. The rewrite drops their `url(...)` entries so nothing
|
|
32
|
+
* requests a file that is not there.
|
|
33
|
+
*/
|
|
34
|
+
export declare function vendorKatex(out: string): Promise<string[]>;
|
|
35
|
+
/**
|
|
36
|
+
* The scripts a deck runs, copied beside it.
|
|
37
|
+
*
|
|
38
|
+
* Pinned by package.json rather than by a URL, so the version that renders is
|
|
39
|
+
* the version that was installed and tested. See the note on GSAP_SRC in
|
|
40
|
+
* src/emit/composition.ts for why this is not left to the compiler's inliner.
|
|
41
|
+
*
|
|
42
|
+
* WHAT THE HEAD LOADS, AND NOTHING ELSE. `composition` is read rather than
|
|
43
|
+
* `laid.plugins` threaded out of `emit`, because the question this is answering
|
|
44
|
+
* is literally "which of these does the page ask for" — and asking the page
|
|
45
|
+
* cannot drift from the page. It also makes the conditional plugins honest on
|
|
46
|
+
* disk as well as in the head: MorphSVG is 21,195 bytes that a deck without a
|
|
47
|
+
* reshape neither loads NOR carries, which is the rule `PLUGINS` states.
|
|
48
|
+
*/
|
|
49
|
+
export declare function vendorScripts(out: string, composition: string): Promise<string[]>;
|
|
50
|
+
/**
|
|
51
|
+
* `assets/` beside source.json, if there is one. Absent is the ordinary case.
|
|
52
|
+
*
|
|
53
|
+
* NAMED FILES ONLY, never the directory.
|
|
54
|
+
*
|
|
55
|
+
* This was `cp(from, ..., { recursive: true })`, which copied whatever happened
|
|
56
|
+
* to be beside the figures. Two costs, one of them a hole: the shipped demo
|
|
57
|
+
* carried 640 KB of JPEGs no beat referenced, and — since a deck is served over
|
|
58
|
+
* HTTP from a directory a stranger's upload contributed to — an `.svg` or
|
|
59
|
+
* `.html` that rode along was served from the deck's own path. The CSP sandbox
|
|
60
|
+
* now covers that, but a file that never arrives needs no containment.
|
|
61
|
+
*
|
|
62
|
+
* `fonts/` is the one directory that comes along, because `refreshFont` writes
|
|
63
|
+
* the subsetted bundle into it and the stylesheet names its own files.
|
|
64
|
+
*
|
|
65
|
+
* A CLIP CONTRIBUTES TWO FILES, and forgetting the second is a 404 at run time
|
|
66
|
+
* rather than a build error: `poster` is what the video shows before its first
|
|
67
|
+
* frame decodes, and what a player-page clip degrades to when there is no
|
|
68
|
+
* downloadable file at all. Named here because this set is the ONLY thing that
|
|
69
|
+
* reaches the built deck — a poster left out is simply absent, and the deck's
|
|
70
|
+
* own runtime reports it as `http_error 404`, a long way from this line.
|
|
71
|
+
*/
|
|
72
|
+
export declare function copyAssets(sourceDir: string, out: string, figures: readonly {
|
|
73
|
+
src: string;
|
|
74
|
+
poster?: string;
|
|
75
|
+
}[], step: Step): Promise<string[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Copy the spoken audio into the deck. Only the files the island references: an
|
|
78
|
+
* audio directory is content-addressed and accumulates every take ever made, and
|
|
79
|
+
* shipping the ones a re-edit orphaned would double a deck's size for nothing.
|
|
80
|
+
*
|
|
81
|
+
* `narration.dir`, NOT the `AUDIO_DIR` constant, and the difference is not a
|
|
82
|
+
* preference. `dir` is the string the emitter wrote into the island, so it is
|
|
83
|
+
* what the built page asks the network for; copying to a name the page does not
|
|
84
|
+
* name is a 404 per sentence and silence for the whole deck. `AUDIO_DIR` is
|
|
85
|
+
* where `dir` COMES FROM — `loadNarration` and the server both set it — which is
|
|
86
|
+
* why the two have never disagreed, and exactly why nothing would have caught
|
|
87
|
+
* them if they had.
|
|
88
|
+
*/
|
|
89
|
+
export declare function copyAudio(from: string, narration: DeckNarration, out: string, step: Step): Promise<string[]>;
|
|
90
|
+
/**
|
|
91
|
+
* The planner writes headlines the source never contained, so the subset `ingest`
|
|
92
|
+
* cut can be missing glyphs — and a missing glyph falls back silently, which is
|
|
93
|
+
* the failure invariant 9 exists to prevent. Re-cut over the text the deck really
|
|
94
|
+
* renders. The bundle is content-hashed, so this is a no-op when nothing new
|
|
95
|
+
* appeared, and a build that cannot reach the font service keeps what it copied.
|
|
96
|
+
*
|
|
97
|
+
* Every glyph a document or plan can put on screen: serializing the whole object
|
|
98
|
+
* over-collects ids and TeX, but those are ASCII and the bundle only exists for
|
|
99
|
+
* CJK — over-collecting costs a few bytes, under-collecting costs a tofu box.
|
|
100
|
+
*/
|
|
101
|
+
export declare function refreshFont(storyboard: Storyboard, source: Source, out: string, step: Step): Promise<string | undefined>;
|
|
102
|
+
export {};
|
|
@@ -158,8 +158,10 @@ export interface Voice {
|
|
|
158
158
|
* So this split decides what the strip says, and whether anything retries
|
|
159
159
|
* unasked. It does NOT decide whether a person may retry: a gesture re-arms
|
|
160
160
|
* either failure (see `unlock`), because someone asking is not the same as us
|
|
161
|
-
* guessing.
|
|
162
|
-
*
|
|
161
|
+
* guessing. `test/deck-page.test.ts` now opens deck.html and asserts the strip
|
|
162
|
+
* NAMES an unplayable segment instead of going quiet — it was one of the four
|
|
163
|
+
* regressions that gate was proved against. Nothing still plays a sound, so
|
|
164
|
+
* which of the two failures a real browser hits is untested.
|
|
163
165
|
*
|
|
164
166
|
* `NotSupportedError` is the unplayable one — observed rather than taken from
|
|
165
167
|
* the spec: a source that cannot be fetched at all rejects `play()` with
|
package/dist/types/emit/kit.d.ts
CHANGED
|
@@ -109,7 +109,39 @@ export declare function zoomOf(format: Format): number;
|
|
|
109
109
|
* afford it.
|
|
110
110
|
*/
|
|
111
111
|
export declare function contentW(format: Format): number;
|
|
112
|
+
/**
|
|
113
|
+
* REFERENCE PX, WHICH IS WHY THE RESERVE IS CONVERTED HERE. `format.captionReserve`
|
|
114
|
+
* is measured on the CANVAS — `bandReserve` derives it from `burnStyle`'s
|
|
115
|
+
* ratios, which are canvas ratios — and everything on this side of the codebase
|
|
116
|
+
* does its arithmetic in reference px, which `refHeight` has already scaled. At
|
|
117
|
+
* 1080x1920 the canvas-to-reference factor is 2560/1920, so a 301px reserve is
|
|
118
|
+
* 402 reference px; subtracting the canvas number raw would under-reserve by a
|
|
119
|
+
* quarter and put the ink back under the band.
|
|
120
|
+
*
|
|
121
|
+
* `Math.ceil`, not `round`: a fractional px of reserve that gets rounded away is
|
|
122
|
+
* a fractional px of ink inside the band.
|
|
123
|
+
*
|
|
124
|
+
* SUBTRACTING HERE IS NOT ENOUGH ON ITS OWN, and the design note this implements
|
|
125
|
+
* was wrong about that. It claimed every archetype lays out into `contentH`, so
|
|
126
|
+
* that one subtraction would move all of them. Ten of them — `bar-compare`,
|
|
127
|
+
* `callout`, `claim-figure`, `data-table`, `equation-morph`, `equation-walk`,
|
|
128
|
+
* `grid`, `index`, `line-chart`, `pipeline` — never mention `contentH` at all.
|
|
129
|
+
* What actually bounds them is `.scene`'s CSS padding in `theme.ts`, which
|
|
130
|
+
* centres their content in the box the stylesheet leaves. MEASURED: subtracting
|
|
131
|
+
* only here moved the 9x16 demo's worst stop by nothing at all — 0.529% of the
|
|
132
|
+
* frame was ink inside the band both before and after, byte for byte.
|
|
133
|
+
*
|
|
134
|
+
* So the reserve comes off BOTH, and the pair is exactly the hazard the header
|
|
135
|
+
* of this file warns about: a stylesheet that disagrees with the arithmetic
|
|
136
|
+
* clips at the canvas edge, and no gate reads the stylesheet.
|
|
137
|
+
*
|
|
138
|
+
* NOT IN `PAD_Y` ITSELF. The padding is symmetric and the reserve is not — a
|
|
139
|
+
* symmetric version would throw away as much off the TOP of every slide for
|
|
140
|
+
* nothing. `theme.ts` adds it to the bottom only.
|
|
141
|
+
*/
|
|
112
142
|
export declare function contentH(format: Format): number;
|
|
143
|
+
/** `format.captionReserve`, canvas px, expressed in this file's reference px. */
|
|
144
|
+
export declare function reserveRef(format: Format): number;
|
|
113
145
|
export interface Theme {
|
|
114
146
|
bg: string;
|
|
115
147
|
fg: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -226,7 +226,10 @@ export interface BuildDeckOptions {
|
|
|
226
226
|
narration?: DeckNarration;
|
|
227
227
|
/** Directory holding those mp3s now; they are copied into the deck. */
|
|
228
228
|
audioFrom?: string;
|
|
229
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* Directory whose `assets/` is copied in. Absent, nothing is. Only the files
|
|
231
|
+
* `source.figures` names travel, plus `fonts/` — see `copyAssets`.
|
|
232
|
+
*/
|
|
230
233
|
assetsFrom?: string;
|
|
231
234
|
/** Progress lines. Silent by default: a library that prints is a library you
|
|
232
235
|
* cannot run inside a request handler. */
|
|
@@ -242,10 +245,11 @@ export interface BuildDeckOptions {
|
|
|
242
245
|
* which a caller may not have and may not want on the request path; call it
|
|
243
246
|
* yourself when you do.
|
|
244
247
|
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
248
|
+
* The file work is `src/build/files.ts`, which `build` in src/cli.ts calls too —
|
|
249
|
+
* it used to be written out twice, and the three ways the copies had already
|
|
250
|
+
* drifted are recorded in that module's header. Anything a deck directory
|
|
251
|
+
* contains beyond `index.html` and its two manifests goes there, not here, so
|
|
252
|
+
* the two callers cannot answer "what does a deck directory contain" differently
|
|
253
|
+
* again.
|
|
250
254
|
*/
|
|
251
255
|
export declare function buildDeck(storyboard: Storyboard, source: Source, outDir: string, opts?: BuildDeckOptions): Promise<BuildResult>;
|
|
@@ -27,10 +27,22 @@ export interface SubtitlePlan {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function subtitlePlan(mode: SubtitleMode): SubtitlePlan;
|
|
29
29
|
/**
|
|
30
|
-
* WHAT A REAL FIX
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* the
|
|
30
|
+
* WHAT A REAL FIX NEEDED — BUILT, on 2026-09-12, as `build --reserve-captions`.
|
|
31
|
+
* The five steps below are the plan it was built from and they are kept because
|
|
32
|
+
* four of them are still the reason the code is shaped this way. Step 3 is kept
|
|
33
|
+
* for the opposite reason: it was WRONG, and the correction is the useful part.
|
|
34
|
+
*
|
|
35
|
+
* WHERE IT IS NOW. `bandReserve` (src/types.ts) is the one source for the band's
|
|
36
|
+
* box; `Format.captionReserve` carries it; `contentH` AND `.scene`'s padding
|
|
37
|
+
* both give it up; `fidelity`'s `ink_in_caption_reserve` fails a deck that draws
|
|
38
|
+
* into it; and `render --subtitles burn` refuses a deck whose `timing.json`
|
|
39
|
+
* reserved nothing. Measured on the twelve-beat demo at 1080x1920 over 31 stops:
|
|
40
|
+
* reserved, 0 of 31 stops put ink in the band; the same deck unreserved and
|
|
41
|
+
* judged against the same band, 1 of 31 at 0.529% of the frame.
|
|
42
|
+
*
|
|
43
|
+
* Making the band optional did not stop it covering the slide; it only meant you
|
|
44
|
+
* could choose not to be covered. The shape of the real fix was not obvious, so
|
|
45
|
+
* it was written down here first.
|
|
34
46
|
*
|
|
35
47
|
* THE MEASUREMENT. At 1080x1920 the band is 862x120px and `burnStyle`'s
|
|
36
48
|
* `marginV` puts its baseline 173px off the bottom, so it occupies the bottom
|
|
@@ -58,14 +70,30 @@ export declare function subtitlePlan(mode: SubtitleMode): SubtitlePlan;
|
|
|
58
70
|
* `--reserve-captions` flag. It belongs on `Format` and not on `DeckOptions`
|
|
59
71
|
* because it changes the drawable box, which is what a format IS.
|
|
60
72
|
*
|
|
61
|
-
* 3. THE DRAWABLE BOX SHRINKS, IN ONE PLACE
|
|
62
|
-
* becomes `height - 2·padY - captionReserve
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
73
|
+
* 3. THE DRAWABLE BOX SHRINKS, IN ONE PLACE — AND THIS STEP WAS WRONG. It said
|
|
74
|
+
* `contentH` in src/emit/kit.ts becomes `height - 2·padY - captionReserve`,
|
|
75
|
+
* and that "they all lay out into `contentH`, so subtracting there moves all
|
|
76
|
+
* of them at once."
|
|
77
|
+
*
|
|
78
|
+
* They do not. TEN of the archetypes never mention `contentH` —
|
|
79
|
+
* `bar-compare`, `callout`, `claim-figure`, `data-table`, `equation-morph`,
|
|
80
|
+
* `equation-walk`, `grid`, `index`, `line-chart`, `pipeline`. What bounds
|
|
81
|
+
* them is `.scene`'s CSS padding in src/emit/theme.ts, which centres their
|
|
82
|
+
* content in whatever box the stylesheet leaves. The audit behind the
|
|
83
|
+
* original claim asked which archetypes read `format.height`, which is a
|
|
84
|
+
* different question and has a comforting answer.
|
|
85
|
+
*
|
|
86
|
+
* MEASURED, because the plan read convincingly enough to ship on: with the
|
|
87
|
+
* subtraction in `contentH` alone, the 9x16 demo's worst stop was 0.529% of
|
|
88
|
+
* the frame in ink inside the band — identical, to the digit, to the same
|
|
89
|
+
* deck with no reserve at all. So the reserve is taken in BOTH places, and
|
|
90
|
+
* they are the exact pair src/emit/kit.ts's own header warns about: a
|
|
91
|
+
* stylesheet that disagrees with the arithmetic, which no gate reads.
|
|
92
|
+
*
|
|
93
|
+
* Still true, and still the reason it is not simply `padY`: the padding is
|
|
94
|
+
* symmetric and the reserve is not. `theme.ts` adds it to the bottom only,
|
|
95
|
+
* and emits the two-value padding unchanged when the reserve is 0, so a deck
|
|
96
|
+
* that reserves nothing is byte-identical to one built before this existed.
|
|
69
97
|
*
|
|
70
98
|
* 4. A GATE, OR THIS REGRESSES. `src/verify/check.ts` must fail a deck whose
|
|
71
99
|
* audience text falls inside the reserve — invariant 5 with a new floor. A
|
|
@@ -100,6 +100,17 @@ export interface Timing {
|
|
|
100
100
|
/** Directory holding the mp3s, relative to the deck. `""` when silent. */
|
|
101
101
|
audioDir: string;
|
|
102
102
|
voice: string;
|
|
103
|
+
/**
|
|
104
|
+
* Canvas px the deck's layout gave up at the bottom for a burned caption, 0
|
|
105
|
+
* if it gave up none. Recorded because the burn decision is taken at RENDER
|
|
106
|
+
* time and the layout was fixed at BUILD time: without this, `--subtitles
|
|
107
|
+
* burn` over a deck built before the reserve existed silently puts the band
|
|
108
|
+
* back on the slide's own text, which is the defect that started all of this.
|
|
109
|
+
*
|
|
110
|
+
* Optional, and absent reads as 0: every `timing.json` already on disk was
|
|
111
|
+
* written without it, and those decks did reserve nothing.
|
|
112
|
+
*/
|
|
113
|
+
captionReserve?: number;
|
|
103
114
|
scenes: TimedScene[];
|
|
104
115
|
segments: TimedSegment[];
|
|
105
116
|
}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -2439,7 +2439,71 @@ export interface Format {
|
|
|
2439
2439
|
* failure. Omitted when every destination for the format agrees on `maxSeconds`.
|
|
2440
2440
|
*/
|
|
2441
2441
|
warnSeconds?: number;
|
|
2442
|
+
/**
|
|
2443
|
+
* Canvas px of the bottom of the frame reserved for a burned-in caption, which
|
|
2444
|
+
* the drawable box gives up. 0 — the default — means nothing is reserved.
|
|
2445
|
+
*
|
|
2446
|
+
* ON `Format` AND NOT ON `DeckOptions`, because it changes the drawable box,
|
|
2447
|
+
* and the drawable box is what a format IS. Two decks built from one
|
|
2448
|
+
* storyboard, one reserving and one not, are different formats in every sense
|
|
2449
|
+
* that matters downstream: different `contentH`, different archetype layouts,
|
|
2450
|
+
* different type sizes. `id` is left alone — the profile's pacing, budget and
|
|
2451
|
+
* navigability are unchanged — but `writeTiming` records the number so
|
|
2452
|
+
* `render` can refuse to burn over a deck that never made room. See the
|
|
2453
|
+
* design note above `RenderOptions` in src/render/render.ts.
|
|
2454
|
+
*
|
|
2455
|
+
* Optional in the TYPE for the same reason as `maxSeconds`: every test fixture
|
|
2456
|
+
* that needs a canvas would otherwise have to invent a reserve it does not
|
|
2457
|
+
* care about, and absent must read as "reserved nothing", which is the state
|
|
2458
|
+
* every deck built before this existed is actually in.
|
|
2459
|
+
*/
|
|
2460
|
+
captionReserve?: number;
|
|
2442
2461
|
}
|
|
2462
|
+
/**
|
|
2463
|
+
* THE ONE SOURCE FOR THE BAND'S BOX, because a second copy of these ratios is
|
|
2464
|
+
* exactly how the collision came back.
|
|
2465
|
+
*
|
|
2466
|
+
* These live in `types.ts` rather than beside `burnStyle` (src/render/ffmpeg.ts)
|
|
2467
|
+
* for a layering reason that is not negotiable: `src/emit` may not import
|
|
2468
|
+
* `src/render` — the dependency runs the other way, `src/render/timing.ts`
|
|
2469
|
+
* imports four things out of `src/emit`. The emitter is the side that has to
|
|
2470
|
+
* shrink its drawable box, so the numbers have to sit where BOTH can reach
|
|
2471
|
+
* them, and `Format` is already here.
|
|
2472
|
+
*
|
|
2473
|
+
* `burnStyle` reads these; it does not restate them.
|
|
2474
|
+
*/
|
|
2475
|
+
export declare const CAPTION_FONT_RATIO = 0.037;
|
|
2476
|
+
export declare const CAPTION_MARGIN_V_RATIO = 0.09;
|
|
2477
|
+
export declare const CAPTION_MARGIN_X_RATIO = 0.04;
|
|
2478
|
+
/** The `--line` default `captionPage` writes, as a multiple of the font size. */
|
|
2479
|
+
export declare const CAPTION_LINE_RATIO = 1.6;
|
|
2480
|
+
/**
|
|
2481
|
+
* Two, and it is bounded rather than hoped for: `splitCue` caps a cue at 84
|
|
2482
|
+
* characters and `wrap` breaks it near the middle, and `burnStyle`'s font size
|
|
2483
|
+
* is chosen so the longer half still fits one line with 9% to spare. A third
|
|
2484
|
+
* line is the failure that sizing exists to prevent, so reserving for two is
|
|
2485
|
+
* reserving for the worst case that can actually occur.
|
|
2486
|
+
*/
|
|
2487
|
+
export declare const CAPTION_MAX_LINES = 2;
|
|
2488
|
+
/** The burned band's font size at this canvas width. */
|
|
2489
|
+
export declare function captionFontSize(width: number): number;
|
|
2490
|
+
/**
|
|
2491
|
+
* The bottom strip the burned caption may occupy, `marginV` included, in CANVAS
|
|
2492
|
+
* px — everything `src/emit` must therefore keep its ink out of.
|
|
2493
|
+
*
|
|
2494
|
+
* AN UPPER BOUND, DELIBERATELY, and derived from the CSS rather than from a
|
|
2495
|
+
* browser. `captionPage` declares `line-height: round(F * 1.6)` and the real
|
|
2496
|
+
* painted line box is whatever the font actually produces, which is smaller: at
|
|
2497
|
+
* 1080x1920 this returns **301px** against the **293px** measured off two
|
|
2498
|
+
* rendered frames in `.planning/EXPERIMENT-010-reconcile.md`. Eight px of slack,
|
|
2499
|
+
* on the safe side. A bound that needed a browser to compute could not be read
|
|
2500
|
+
* by the emitter at all, which is the whole reason the collision survived this
|
|
2501
|
+
* long — the band's height was only known after layout, an hour too late.
|
|
2502
|
+
*
|
|
2503
|
+
* Under-reserving reintroduces the defect silently, so if these two ever have to
|
|
2504
|
+
* disagree, this one is the one that must be larger.
|
|
2505
|
+
*/
|
|
2506
|
+
export declare function bandReserve(width: number, height: number): number;
|
|
2443
2507
|
/** A real place a built file gets uploaded, and the length it stops accepting at. */
|
|
2444
2508
|
export interface Destination {
|
|
2445
2509
|
/** What a person would call it. Printed in budget findings. */
|
|
@@ -162,7 +162,37 @@ export interface Measured extends Stop {
|
|
|
162
162
|
ink: number;
|
|
163
163
|
/** Where the band began, as a fraction of frame height. Reported for triage. */
|
|
164
164
|
bandTop: number;
|
|
165
|
+
/**
|
|
166
|
+
* Non-background pixels inside the caption RESERVE, over the whole frame's
|
|
167
|
+
* pixels. `undefined` on a deck that reserved nothing — there is no band to
|
|
168
|
+
* be inside, and 0 would read as "measured and clear".
|
|
169
|
+
*/
|
|
170
|
+
reserveInk?: number;
|
|
165
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Ink is allowed in the reserve up to this fraction of the whole frame, and the
|
|
174
|
+
* number is measured rather than chosen.
|
|
175
|
+
*
|
|
176
|
+
* MEASURED on the twelve-beat demo at `short-9x16`, 1080x1920, 31 stops, the two
|
|
177
|
+
* builds differing only in `--reserve-captions`:
|
|
178
|
+
*
|
|
179
|
+
* | build | stops with ink in the band | worst |
|
|
180
|
+
* | --- | --- | --- |
|
|
181
|
+
* | `--reserve-captions` | 0 of 31 | 0 |
|
|
182
|
+
* | the same deck, unreserved, judged against the same band | 1 of 31 | 0.529% |
|
|
183
|
+
*
|
|
184
|
+
* So on a correct build the strip is background and anything at all is a real
|
|
185
|
+
* intrusion. The tolerance exists only so a stray antialiased edge cannot fail a
|
|
186
|
+
* build, and it is an ORDER OF MAGNITUDE below `INK_FLOOR`: one short headline
|
|
187
|
+
* is 0.15% of the frame, so 0.01% cannot be a glyph and can only be a fringe.
|
|
188
|
+
*
|
|
189
|
+
* NOT fitted to the failing case: the intrusion it has to catch is 0.529%, fifty
|
|
190
|
+
* times this, so the gate is not balanced on a margin. And note what the control
|
|
191
|
+
* is worth — the first version of this fix subtracted the reserve from
|
|
192
|
+
* `contentH` alone, and the reserved build measured 0.529% too, identically.
|
|
193
|
+
* The gate is what found that; no other gate in the stack moved.
|
|
194
|
+
*/
|
|
195
|
+
export declare const RESERVE_INK_TOLERANCE = 0.0001;
|
|
166
196
|
export interface FidelityOptions {
|
|
167
197
|
floor?: number;
|
|
168
198
|
/**
|
|
@@ -172,7 +202,15 @@ export interface FidelityOptions {
|
|
|
172
202
|
*/
|
|
173
203
|
stops?: readonly Stop[];
|
|
174
204
|
timeoutMs?: number;
|
|
205
|
+
/** Override the reserve read off `timing.json`. For tests and the 015 harness. */
|
|
206
|
+
captionReserve?: number;
|
|
175
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* The caption reserve this deck was BUILT with, in canvas px, out of its own
|
|
210
|
+
* `timing.json`. 0 when the file is missing, unparseable, or predates the field
|
|
211
|
+
* — all three mean the same thing: nothing was reserved.
|
|
212
|
+
*/
|
|
213
|
+
export declare function readReserve(timingText: string | null): number;
|
|
176
214
|
export interface FidelityReport {
|
|
177
215
|
stops: Measured[];
|
|
178
216
|
/**
|
|
@@ -236,6 +274,23 @@ export declare function readStops(timing: string | null, deckPage: string | null
|
|
|
236
274
|
* is named because that is the frame to open.
|
|
237
275
|
*/
|
|
238
276
|
export declare function gradeFidelity(rows: readonly Measured[], floor?: number): Finding[];
|
|
277
|
+
/**
|
|
278
|
+
* Ink that got into the strip the burned caption is going to occupy.
|
|
279
|
+
*
|
|
280
|
+
* WHY THIS RULE EXISTS AT ALL. `contentH` already subtracts the reserve, so on
|
|
281
|
+
* a correct build this can never fire — which is exactly the argument for
|
|
282
|
+
* having it. The geometry is one refactor, one new archetype, or one absolute
|
|
283
|
+
* position away from being undone, and the failure is invisible to every other
|
|
284
|
+
* gate in the stack: the deck passes, the video renders, the band lands on the
|
|
285
|
+
* slide's own text, and the only way anyone finds out is by opening a frame.
|
|
286
|
+
* That is the failure mode this project keeps producing, and a purely geometric
|
|
287
|
+
* fix with no gate is how it comes back.
|
|
288
|
+
*
|
|
289
|
+
* An ERROR, not a warning. The deck asked for the reserve — `--reserve-captions`
|
|
290
|
+
* is typed on purpose — so ink inside it is the build failing to deliver the
|
|
291
|
+
* thing it was told to deliver, not a note about taste.
|
|
292
|
+
*/
|
|
293
|
+
export declare function gradeReserve(rows: readonly Measured[], tolerance?: number): Finding[];
|
|
239
294
|
/**
|
|
240
295
|
* Seek every declared stop and count the ink below its caption.
|
|
241
296
|
*
|
|
@@ -60,11 +60,16 @@ export declare function verify(dir: string, opts?: VerifyOptions, storyboard?: S
|
|
|
60
60
|
/**
|
|
61
61
|
* Every mp3 the narration island promises is actually in the deck.
|
|
62
62
|
*
|
|
63
|
-
* Nothing else notices this. `hyperframes check` never opens
|
|
64
|
-
* the runtime treats a missing file exactly like a browser that
|
|
65
|
-
* autoplay — it clears the subtitles and moves on. So a deck that lost its audio
|
|
63
|
+
* Nothing else notices this in a BUILD. `hyperframes check` never opens
|
|
64
|
+
* `deck.html`, and the runtime treats a missing file exactly like a browser that
|
|
65
|
+
* refused to autoplay — it clears the subtitles and moves on. So a deck that lost its audio
|
|
66
66
|
* on the way to a web host presents in silence and says nothing about why, which
|
|
67
67
|
* is the failure mode a gate exists for.
|
|
68
|
+
*
|
|
69
|
+
* `test/deck-page.test.ts` does now open `deck.html` in a browser, and asserts
|
|
70
|
+
* that an unplayable segment is NAMED in the flag strip rather than going quiet.
|
|
71
|
+
* That is a test, not a build gate: it runs on a deck the suite builds, and says
|
|
72
|
+
* nothing about the deck a user is shipping. This still has to exist.
|
|
68
73
|
*/
|
|
69
74
|
export declare function scanNarration(page: string, files: ReadonlySet<string>): Finding[];
|
|
70
75
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jokerized/decksmith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Turn a source document into an animated explanation deck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"commander": "^14.0.1",
|
|
64
64
|
"fflate": "^0.8.3",
|
|
65
65
|
"gsap": "^3.14.2",
|
|
66
|
-
"hyperframes": "0.8.
|
|
66
|
+
"hyperframes": "0.8.35",
|
|
67
67
|
"katex": "^0.16.11",
|
|
68
68
|
"puppeteer-core": "^25.3.0",
|
|
69
69
|
"remark-gfm": "^4.0.1",
|