@jokerized/decksmith 0.1.4 → 0.3.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 +161 -26
- package/dist/cli.js +3864 -1816
- package/dist/ds-morph.js +1 -0
- package/dist/index.js +2701 -780
- package/dist/mcp.js +2509 -633
- package/dist/types/emit/archetypes/annotated-figure.d.ts +3 -2
- package/dist/types/emit/archetypes/data-table.d.ts +0 -19
- package/dist/types/emit/archetypes/equation-morph.d.ts +2 -0
- package/dist/types/emit/archetypes/equation-walk.d.ts +63 -1
- package/dist/types/emit/archetypes/index.d.ts +1 -1
- package/dist/types/emit/archetypes/pipeline.d.ts +2 -2
- package/dist/types/emit/archetypes/stack.d.ts +20 -1
- package/dist/types/emit/archetypes/title.d.ts +31 -14
- package/dist/types/emit/camera.d.ts +25 -0
- package/dist/types/emit/composition.d.ts +20 -2
- package/dist/types/emit/depth.d.ts +95 -0
- package/dist/types/emit/kit.d.ts +153 -0
- package/dist/types/emit/morph-runtime.d.ts +178 -0
- package/dist/types/emit/svg.d.ts +77 -13
- package/dist/types/emit/theme.d.ts +1 -0
- package/dist/types/images/illustrate.d.ts +34 -0
- package/dist/types/images/providers.d.ts +108 -0
- package/dist/types/index.d.ts +27 -8
- package/dist/types/mcp/prereqs.d.ts +18 -0
- package/dist/types/mcp/tools.d.ts +23 -0
- package/dist/types/plan/codex.d.ts +25 -3
- package/dist/types/plan/duration.d.ts +190 -15
- package/dist/types/plan/prompt.d.ts +7 -1
- package/dist/types/plan/refs.d.ts +41 -3
- package/dist/types/prefs.d.ts +21 -6
- package/dist/types/render/capture.d.ts +99 -0
- package/dist/types/render/render.d.ts +18 -0
- package/dist/types/server/options.d.ts +7 -0
- package/dist/types/server/pipeline.d.ts +14 -0
- package/dist/types/server/queue.d.ts +1 -1
- package/dist/types/types.d.ts +294 -4
- package/dist/types/verify/apparent.d.ts +123 -0
- package/dist/types/verify/index.d.ts +114 -6
- package/dist/types/verify/typefloor.d.ts +3 -1
- package/package.json +2 -2
|
@@ -23,10 +23,27 @@
|
|
|
23
23
|
* else, so hitting 60s over 12 slides needs a shorter sentence AND a faster
|
|
24
24
|
* animation. Neither alone gets there; that is the whole content of this file.
|
|
25
25
|
*
|
|
26
|
+
* WHICH SLIDE COUNT THE BUDGET IS STRUCK AT. `prefs.slides` is what was ASKED
|
|
27
|
+
* FOR and `beats` is what the planner came back with, and they are routinely not
|
|
28
|
+
* the same number — four of the last five real plans came back short. Every
|
|
29
|
+
* derived number here is `duration / count`, so striking the budget at the
|
|
30
|
+
* request while the deck has fewer beats paces each scene for a slide shorter
|
|
31
|
+
* than the one it is: eight beats against a sixty-second target were paced for
|
|
32
|
+
* five-second beats that are really seven and a half, and the video came in
|
|
33
|
+
* under its target with every gate green and nothing anywhere saying so. So
|
|
34
|
+
* every caller holding a storyboard passes its `beats.length`. The two that
|
|
35
|
+
* cannot are `systemPrompt`, which builds the prompt before a plan exists, and
|
|
36
|
+
* the MCP `estimate`, which is a pre-flight with no plan at all; they get the
|
|
37
|
+
* request, which is the only number they have. The SHORTFALL itself is not this
|
|
38
|
+
* file's to report — `scanBeatCount` in src/verify/index.ts reports it, because
|
|
39
|
+
* a beat count is arithmetic rather than a judgement and a prompt rule alone can
|
|
40
|
+
* always be met cosmetically.
|
|
41
|
+
*
|
|
26
42
|
* Everything here is pure arithmetic over preferences, so it is testable with no
|
|
27
43
|
* planner, no TTS and no ffmpeg.
|
|
28
44
|
*/
|
|
29
45
|
import type { Prefs } from "../prefs.js";
|
|
46
|
+
import type { Source } from "../types.js";
|
|
30
47
|
/**
|
|
31
48
|
* Characters of text per second of speech.
|
|
32
49
|
*
|
|
@@ -174,6 +191,17 @@ export declare const FF_BEAT_SECONDS = 8;
|
|
|
174
191
|
* for both put 60 seconds at eight slides when the reference is twelve.
|
|
175
192
|
*/
|
|
176
193
|
export declare const MIN_BEAT_SECONDS = 4;
|
|
194
|
+
/**
|
|
195
|
+
* The longest a slide may sit and still be one thought.
|
|
196
|
+
*
|
|
197
|
+
* Past twenty seconds a beat stops being a slide and becomes a section of a
|
|
198
|
+
* lecture: the demo sits at 20.5s and is already the slowest thing this project
|
|
199
|
+
* ships. It was the bare `20` inside `slidesFor`'s tempo clamp; it is named
|
|
200
|
+
* because that function now uses it twice — once to stop a long target from
|
|
201
|
+
* flattening into a handful of enormous beats, and once as the floor a thin
|
|
202
|
+
* DOCUMENT may shorten the deck to. Both are the same rule.
|
|
203
|
+
*/
|
|
204
|
+
export declare const MAX_BEAT_SECONDS = 20;
|
|
177
205
|
/**
|
|
178
206
|
* Subtitle rate a short-form viewer will take, as against broadcast television.
|
|
179
207
|
*
|
|
@@ -247,10 +275,27 @@ export declare const SHORT_FORM_CPS = 22;
|
|
|
247
275
|
* IT COSTS A STEP, KNOWINGLY. The demo's real p95 cue at `+20%` is 21.321,
|
|
248
276
|
* inside the ceiling of 22 — but this predicts 22.13 for that step and refuses
|
|
249
277
|
* it, so the deck takes `+10%` and speaks slower than the artifact proves it
|
|
250
|
-
* could.
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
278
|
+
* could. It also costs the 60-second case its full sentence: twelve slides at
|
|
279
|
+
* low density plan 66 characters against the 72 of `EXPLAINING_CHARS`, where
|
|
280
|
+
* ~1.26 would plan exactly 72.
|
|
281
|
+
*
|
|
282
|
+
* 1.26 IS NOT AVAILABLE, THOUGH, and it is worth writing down why because the
|
|
283
|
+
* arithmetic above makes it look free. **The floor is 1.2784**, which is this
|
|
284
|
+
* deck's own `DEMO_P95_CUE_CPS / SPEECH_CPS.latin` — 18.409 / 14.4 — and
|
|
285
|
+
* `test/duration.test.ts` fails anything under it, one-sided and on purpose.
|
|
286
|
+
* A constant below the anchor's measured ratio admits a step whose captions are
|
|
287
|
+
* over the ceiling, which is exactly the 1.17 bug this replaced.
|
|
288
|
+
*
|
|
289
|
+
* The five ratios in the table above centre on 1.2604 and tempt you under that
|
|
290
|
+
* floor. They are a DIFFERENT synthesis of the same text: edge-tts does not
|
|
291
|
+
* repeat itself, and the `+0%` re-run came back at 1.2467 where the artifact in
|
|
292
|
+
* `demo/audio` sits at 1.2784. The shipped artifact wins — it is the deck that
|
|
293
|
+
* actually exists. So the honest range is [1.2784, 1.2945], the second number
|
|
294
|
+
* being the highest ratio the re-run saw, and 1.30 clears both.
|
|
295
|
+
*
|
|
296
|
+
* The 60-second sentence is therefore not bought back by tuning this. It is
|
|
297
|
+
* bought with seconds or with slides, which is what the rest of this file has
|
|
298
|
+
* said all along, and 90s plans 102.
|
|
254
299
|
*
|
|
255
300
|
* `test/duration.test.ts` pins the `+0%` end against the artifact.
|
|
256
301
|
*/
|
|
@@ -344,7 +389,16 @@ export interface DurationPlan {
|
|
|
344
389
|
chars?: number;
|
|
345
390
|
/** Seconds of speech one beat can afford. Absent without a target. */
|
|
346
391
|
speechSeconds?: number;
|
|
347
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* The per-beat length the target implies, at the count the plan was struck at.
|
|
394
|
+
* Absent without a target.
|
|
395
|
+
*
|
|
396
|
+
* A BUDGET, NOT THE ANSWER, and the name collides with the thing that IS the
|
|
397
|
+
* answer. `beatSeconds` in src/emit/composition.ts — `max(authored·speed,
|
|
398
|
+
* lastHold + SETTLE, speechEnd + SETTLE)` — is what actually sizes a scene,
|
|
399
|
+
* and it never sees this number. `speed` is the only value this file sends
|
|
400
|
+
* into the emitted timeline; everything else here is advice to the planner.
|
|
401
|
+
*/
|
|
348
402
|
beatSeconds?: number;
|
|
349
403
|
/** Said, never fatal: what the target costs, or cannot buy. */
|
|
350
404
|
warnings: string[];
|
|
@@ -359,8 +413,14 @@ export interface DurationPlan {
|
|
|
359
413
|
* `duration` set OVERRIDES `animationSpeed`: the target owns the pace, because
|
|
360
414
|
* the two cannot both be honoured and the target is the one the user stated a
|
|
361
415
|
* number for. Said in a warning rather than silently.
|
|
416
|
+
*
|
|
417
|
+
* `beats` is the count the budget is struck at, and it defaults to the count
|
|
418
|
+
* that was asked for. Pass the storyboard's own `beats.length` wherever one is
|
|
419
|
+
* in hand — see the header for why, and for the two callers that structurally
|
|
420
|
+
* cannot. The default is not a convenience: it is the right answer for those
|
|
421
|
+
* two, and it is what keeps a run where the planner hit its number identical.
|
|
362
422
|
*/
|
|
363
|
-
export declare function durationPlan(prefs: Prefs): DurationPlan;
|
|
423
|
+
export declare function durationPlan(prefs: Prefs, beats?: number): DurationPlan;
|
|
364
424
|
/**
|
|
365
425
|
* How many slides a target should have, by TEMPO.
|
|
366
426
|
*
|
|
@@ -387,11 +447,90 @@ export declare function durationPlan(prefs: Prefs): DurationPlan;
|
|
|
387
447
|
* 30s -> 8 120s -> 12 600s -> 30
|
|
388
448
|
* 60s -> 12 300s -> 15
|
|
389
449
|
*
|
|
450
|
+
* AND HOW MUCH THE DOCUMENT HAS TO SAY, which is the half that was missing. The
|
|
451
|
+
* table above is flat at twelve from 48s to 240s no matter what it is pointed
|
|
452
|
+
* at: a four-page workshop note and a forty-page survey both got twelve beats,
|
|
453
|
+
* because the only input was a clock. Tempo says how long a beat may LAST; it
|
|
454
|
+
* cannot say how many points exist to spend beats on. So `source`, when the
|
|
455
|
+
* caller has one, scales the tempo count by what the document actually contains
|
|
456
|
+
* (`sourcePoints`), bounded by `SUPPLY_RANGE` because the measure is coarse and
|
|
457
|
+
* its authority should be too.
|
|
458
|
+
*
|
|
459
|
+
* 60s + a thin note -> 6 240s + a thin note -> 12
|
|
460
|
+
* 60s + a full paper -> 15 240s + a full paper -> 17
|
|
461
|
+
*
|
|
462
|
+
* The clock still wins at both ends. Sixty seconds cannot hold seventeen beats
|
|
463
|
+
* at `MIN_BEAT_SECONDS` apiece, so a rich document against a short target spends
|
|
464
|
+
* its extra points on nothing; and past four minutes `MAX_BEAT_SECONDS` holds
|
|
465
|
+
* the count up, so a thin note asked for a long video does not become six
|
|
466
|
+
* forty-second slides. Only where the clock leaves room does the document move
|
|
467
|
+
* the number — which is most of the range people actually ask for.
|
|
468
|
+
*
|
|
469
|
+
* WHO CAN PASS A SOURCE. The CLI's `plan` verb reads `source.json` before it
|
|
470
|
+
* resolves preferences, so it can and does. `parseOptions` (src/server/options.ts)
|
|
471
|
+
* derives the count while the upload is still a form, before anything is
|
|
472
|
+
* ingested, and the MCP `estimate` is a pre-flight with no document at all;
|
|
473
|
+
* both get the tempo number, which is what they got before. Omitting the
|
|
474
|
+
* argument is therefore not a degraded path, it is the old behaviour, byte for
|
|
475
|
+
* byte.
|
|
476
|
+
*
|
|
390
477
|
* This is the DEFAULT, never an override. `slides` is one of the three knobs the
|
|
391
478
|
* owner asked to hold — "user can give you the number of slides they want in the
|
|
392
|
-
* video with duration of their choice" — so an explicit count is obeyed
|
|
479
|
+
* video with duration of their choice" — so an explicit count is obeyed, which
|
|
480
|
+
* is `loadPrefs`'s business: it calls this only when nobody named a number.
|
|
481
|
+
*/
|
|
482
|
+
export declare function slidesFor(prefs: Prefs, source?: Source): number;
|
|
483
|
+
/**
|
|
484
|
+
* Characters of section prose one beat is worth.
|
|
485
|
+
*
|
|
486
|
+
* A GUESS, and marked one the way `SPEECH_CPS.cjk` is: no full-length document
|
|
487
|
+
* is stored in this repository to measure against — `demo/source.json` is a
|
|
488
|
+
* 231-character stub whose twelve beats were written by hand, and the fixture
|
|
489
|
+
* papers are 1.9 KB each. What matters is this number's RATIO to
|
|
490
|
+
* `REFERENCE_POINTS`, and the two are set together so an eight-section
|
|
491
|
+
* conference paper — around 30k characters, five figures, two tables, four
|
|
492
|
+
* equations — comes out above the reference rather than at it. Replace both with
|
|
493
|
+
* a measurement the first time a corpus of real sources exists.
|
|
494
|
+
*/
|
|
495
|
+
export declare const PROSE_CHARS_PER_BEAT = 1800;
|
|
496
|
+
/**
|
|
497
|
+
* The supply a document needs to earn the tempo's own beat count.
|
|
498
|
+
*
|
|
499
|
+
* Twenty points is roughly a solid conference paper minus its trimmings. Below
|
|
500
|
+
* it the deck shortens, above it the deck lengthens, and `SUPPLY_RANGE` decides
|
|
501
|
+
* how far either can go.
|
|
502
|
+
*/
|
|
503
|
+
export declare const REFERENCE_POINTS = 20;
|
|
504
|
+
/**
|
|
505
|
+
* How far the document may move the tempo's count, as a multiplier.
|
|
506
|
+
*
|
|
507
|
+
* Bounded rather than open because `sourcePoints` is a proxy and a proxy should
|
|
508
|
+
* not be trusted past the range where it is obviously right. Half is the
|
|
509
|
+
* shortest a deck of a real document should get before the answer is "this is
|
|
510
|
+
* not enough source"; one and a half keeps a rich paper under the twenty-second
|
|
511
|
+
* beat that makes a deck a lecture. A survey with a hundred points is not worth
|
|
512
|
+
* a hundred beats, and the clamp is what says so.
|
|
513
|
+
*/
|
|
514
|
+
export declare const SUPPLY_RANGE: {
|
|
515
|
+
readonly min: 0.5;
|
|
516
|
+
readonly max: 1.5;
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* How many distinct points a document offers a deck.
|
|
520
|
+
*
|
|
521
|
+
* Two supplies, added, because they are genuinely different things to make a
|
|
522
|
+
* beat out of: PROSE, which carries the argument, and EXHIBITS — the figures,
|
|
523
|
+
* tables and equations the authors made on purpose because a sentence was not
|
|
524
|
+
* enough. RULE 2 in the prompt tells the planner every figure should earn a
|
|
525
|
+
* beat, so a document with eight of them is asking for a longer deck than one
|
|
526
|
+
* with none, and this is where that is counted rather than hoped for.
|
|
527
|
+
*
|
|
528
|
+
* Headings are deliberately NOT counted. A section boundary is a formatting
|
|
529
|
+
* decision — one author writes six headings over 20k characters and another
|
|
530
|
+
* writes twenty-four over the same prose — so counting them would measure the
|
|
531
|
+
* template rather than the document.
|
|
393
532
|
*/
|
|
394
|
-
export declare function
|
|
533
|
+
export declare function sourcePoints(source: Source): number;
|
|
395
534
|
/**
|
|
396
535
|
* How fast this deck speaks.
|
|
397
536
|
*
|
|
@@ -442,12 +581,48 @@ export declare function p95CueRate(cues: readonly {
|
|
|
442
581
|
text: string;
|
|
443
582
|
}[]): number;
|
|
444
583
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* from
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
584
|
+
* What is wrong with speeding this deck up this much — everything that is.
|
|
585
|
+
*
|
|
586
|
+
* TWO CEILINGS, AND THEY ARE NOT THE SAME KIND OF THING. The caption one is
|
|
587
|
+
* computed from the deck's MEASURED p95 cue rate against broadcast practice
|
|
588
|
+
* rather than from a constant: a deck whose captions already run at 18 cps has
|
|
589
|
+
* no headroom at all, one written in short lines has plenty. That one stays
|
|
590
|
+
* pure advice — how readable a caption has to be is the user's call, and they
|
|
591
|
+
* can only make it if they are told the number. `MAX_PLAYBACK` is the other,
|
|
592
|
+
* and `playbackRefusal` below turns it into a refusal, because time-stretched
|
|
593
|
+
* audio is not a matter of taste.
|
|
594
|
+
*
|
|
595
|
+
* BOTH CLAUSES, NOT THE FIRST ONE. This used to `return` out of the caption
|
|
596
|
+
* branch, which meant a dense deck — every deck this tool plans is dense, by
|
|
597
|
+
* construction — could only ever be told about the ceiling that is advisory,
|
|
598
|
+
* and never about the one that is enforced. The measured shape: 2.14× on a
|
|
599
|
+
* 19.6 cps deck printed "readable captions cap out near 0.87×" and suppressed
|
|
600
|
+
* "past the 1.25× that reads comfortably", so the only line naming an
|
|
601
|
+
* enforceable limit was the one the user did not get.
|
|
452
602
|
*/
|
|
453
603
|
export declare function playbackWarning(factor: number, p95: number): string | undefined;
|
|
604
|
+
/**
|
|
605
|
+
* Why this target cannot be reached from this deck, when it cannot.
|
|
606
|
+
*
|
|
607
|
+
* `MAX_PLAYBACK` IS THE ONE THAT CAN BE ENFORCED. The caption ceiling cannot
|
|
608
|
+
* be, and the arithmetic is not close: `COMFORTABLE_CPS / DEMO_P95_CUE_CPS` is
|
|
609
|
+
* 17/18.409 = 0.92 on this project's own anchor deck, so a caption-derived
|
|
610
|
+
* limit is already breached at 1× and would refuse every speed-up there is —
|
|
611
|
+
* `--duration` would be dead on arrival. Every deck DeckSmith plans is denser
|
|
612
|
+
* than 17 cps by construction; see `CUE_OVERHEAD`. So the captions warn and
|
|
613
|
+
* the playback factor refuses, and `test/duration.test.ts` pins the reason.
|
|
614
|
+
*
|
|
615
|
+
* REFUSING IS NOT NEW POLICY, it is the policy the planner already keeps.
|
|
616
|
+
* `durationPlan` sizes a deck so the residual gap fits inside `MAX_PLAYBACK`,
|
|
617
|
+
* and `test/duration.test.ts` asserts exactly that at 60s, 120s, 180s and
|
|
618
|
+
* 240s. A 2.14× request is not a tight fit that slipped — it is 71% past a
|
|
619
|
+
* bound the rest of the file treats as arithmetic. What was shipping instead
|
|
620
|
+
* was a video nobody could follow, announced by a mid-render log line that 40
|
|
621
|
+
* lines of capture progress scrolled away.
|
|
622
|
+
*
|
|
623
|
+
* Both remedies are named because the honest one is not the flag: the length
|
|
624
|
+
* of a deck is decided at plan time by how much it says, so the fix is fewer
|
|
625
|
+
* words or more seconds, and `--allow-fast-playback` is for the person who has
|
|
626
|
+
* looked at the alternative and wants the fast video anyway.
|
|
627
|
+
*/
|
|
628
|
+
export declare function playbackRefusal(actualSeconds: number, targetSeconds: number, p95: number): string | undefined;
|
|
@@ -43,5 +43,11 @@ export declare function systemPrompt(prefs: Prefs): string;
|
|
|
43
43
|
* planner's other entry points keep working unchanged.
|
|
44
44
|
*/
|
|
45
45
|
export declare const SYSTEM: string;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The document plus every id the model is allowed to cite, and nothing else.
|
|
48
|
+
*
|
|
49
|
+
* The figure block carries two facts beyond the caption — the section the image
|
|
50
|
+
* sat under and the prose that refers to it — because a planner that cannot see
|
|
51
|
+
* the picture has nothing else to decide what the picture is FOR.
|
|
52
|
+
*/
|
|
47
53
|
export declare function renderSource(source: Source): string;
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import type { Source, Storyboard } from "../types.js";
|
|
2
|
-
|
|
2
|
+
/** A slot that asks for a picture nobody has drawn yet: a brief with no `figureId` beside it. */
|
|
3
|
+
export interface PendingIllustration {
|
|
4
|
+
beatId: string;
|
|
5
|
+
/** The field the figure id will land in, e.g. `params.left.figureId`. */
|
|
6
|
+
where: string;
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
|
-
* Every
|
|
9
|
+
* Every pending slot, in beat order. A claim-figure without a `figureId` is
|
|
10
|
+
* pending by construction — the schema insists on one of the two — and a
|
|
11
|
+
* split-compare side is pending only when it carries a brief, because a side
|
|
12
|
+
* with neither is a list.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pendingIllustrations(storyboard: Storyboard): PendingIllustration[];
|
|
15
|
+
/**
|
|
16
|
+
* Whether this deck was illustrated — the fact a pack records as
|
|
17
|
+
* `images.enabled`, the way `narration.enabled` records whether it was spoken.
|
|
18
|
+
* A brief with a figure the source really has is the trace `illustrate` leaves;
|
|
19
|
+
* a brief alone is a picture still owed, which `assertRefsResolve` refuses
|
|
20
|
+
* before anyone asks this; a figure alone is an ordinary figure.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasIllustrations(storyboard: Storyboard, source: Source): boolean;
|
|
23
|
+
export interface RefsOptions {
|
|
24
|
+
/**
|
|
25
|
+
* What a pending illustration means here. `refuse`, the default, is right for
|
|
26
|
+
* every reader that needs the figure — `build` would otherwise emit a slide
|
|
27
|
+
* around a picture that does not exist. Only the planner and the `plan` verb
|
|
28
|
+
* say `allow`, and only when images are on: a brief is what they were asked
|
|
29
|
+
* to produce, and `illustrate` is the step that makes it resolve.
|
|
30
|
+
*/
|
|
31
|
+
pending?: "allow" | "refuse";
|
|
32
|
+
}
|
|
33
|
+
export declare function assertRefsResolve(storyboard: Storyboard, source: Source, opts?: RefsOptions): void;
|
|
34
|
+
/**
|
|
35
|
+
* Every `inside` names a part the previous beat actually draws — and, where the
|
|
36
|
+
* plan said which part it meant, the RIGHT one.
|
|
5
37
|
*
|
|
6
38
|
* THE SAME SHAPE AS THE CHECK ABOVE, one level in: a schema proves `inside` has
|
|
7
39
|
* a beat and an element, and cannot prove the element exists. RULE 11 says only a
|
|
@@ -9,6 +41,11 @@ export declare function assertRefsResolve(storyboard: Storyboard, source: Source
|
|
|
9
41
|
* entering, and a real plan asked to fly into `stage1` of an ANNOTATED-FIGURE —
|
|
10
42
|
* which has notes and leader lines and no stages at all.
|
|
11
43
|
*
|
|
44
|
+
* AND ONE LEVEL IN AGAIN, because existing is not enough. `element` is an index,
|
|
45
|
+
* so a plan that means the second stage and writes the third names a part that
|
|
46
|
+
* does exist; `partLabelProblem` compares `inside.label` — what the plan says is
|
|
47
|
+
* there — against what the archetype reports drawing.
|
|
48
|
+
*
|
|
12
49
|
* WHY IT MOVED HERE. The emitter already refuses this, so nothing shipped
|
|
13
50
|
* broken. But it refuses at BUILD, which is after `narrate` has spent a minute
|
|
14
51
|
* and a dozen network round trips synthesising speech for a storyboard that was
|
|
@@ -17,6 +54,7 @@ export declare function assertRefsResolve(storyboard: Storyboard, source: Source
|
|
|
17
54
|
*
|
|
18
55
|
* Cheap enough to be exact rather than a table: the previous beat is emitted and
|
|
19
56
|
* `enterableIds` is asked what it drew, so this cannot drift from the emitter the
|
|
20
|
-
* way a hardcoded list of archetype interiors would.
|
|
57
|
+
* way a hardcoded list of archetype interiors would. The same `Scene` carries
|
|
58
|
+
* `parts`, so the label comparison rides along for nothing.
|
|
21
59
|
*/
|
|
22
60
|
export declare function assertInsideResolves(storyboard: Storyboard, source: Source): void;
|
package/dist/types/prefs.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
-
import { prefsSchema } from "./types.js";
|
|
2
|
+
import { prefsSchema, type Source } from "./types.js";
|
|
3
3
|
export type Prefs = z.infer<typeof prefsSchema>;
|
|
4
4
|
/**
|
|
5
|
-
* A partial Prefs. `narration`
|
|
6
|
-
* a patch merges rather than replaces: `--voice` must not
|
|
7
|
-
* config file set
|
|
5
|
+
* A partial Prefs. `narration` and `images` are the two nested objects, so they
|
|
6
|
+
* are the two places a patch merges rather than replaces: `--voice` must not
|
|
7
|
+
* wipe a `rate` the config file set, and `--image-style` must not wipe its
|
|
8
|
+
* `provider`.
|
|
8
9
|
*/
|
|
9
|
-
export type PrefsPatch = Partial<Omit<Prefs, "narration">> & {
|
|
10
|
+
export type PrefsPatch = Partial<Omit<Prefs, "narration" | "images">> & {
|
|
10
11
|
narration?: Partial<Prefs["narration"]>;
|
|
12
|
+
images?: Partial<Prefs["images"]>;
|
|
11
13
|
};
|
|
12
14
|
export declare const CONFIG_FILE = "decksmith.config.json";
|
|
13
15
|
/**
|
|
@@ -16,8 +18,15 @@ export declare const CONFIG_FILE = "decksmith.config.json";
|
|
|
16
18
|
* `cwd` is where the search for a config file starts; the search stops at the
|
|
17
19
|
* filesystem root. Returns a fully-populated Prefs — every consumer downstream
|
|
18
20
|
* reads fields, never optionals.
|
|
21
|
+
*
|
|
22
|
+
* `source` is the document the deck is being planned FROM, when the caller has
|
|
23
|
+
* already read one. It is optional because most callers have not: `build`,
|
|
24
|
+
* `narrate` and `pack` work from a storyboard that was planned long ago, and
|
|
25
|
+
* `illustrate` never needed a beat count. Only the planning path passes it, and
|
|
26
|
+
* only so `slidesFor` can size the deck to what the document contains rather
|
|
27
|
+
* than to a clock — see its header for what changes when it is absent.
|
|
19
28
|
*/
|
|
20
|
-
export declare function loadPrefs(overrides?: PrefsPatch, cwd?: string): Promise<Prefs>;
|
|
29
|
+
export declare function loadPrefs(overrides?: PrefsPatch, cwd?: string, source?: Source): Promise<Prefs>;
|
|
21
30
|
/**
|
|
22
31
|
* Turn CLI flags into a patch. Values arrive as strings from commander and are
|
|
23
32
|
* not validated here beyond the numbers: `loadPrefs` runs them through the
|
|
@@ -40,4 +49,10 @@ export interface PrefFlags {
|
|
|
40
49
|
subtitles?: boolean;
|
|
41
50
|
/** `--narration-density`. Spelled apart from `density`, which is a slide's. */
|
|
42
51
|
narrationDensity?: string;
|
|
52
|
+
/** `--images`. A boolean like `narrate`, so `flags()` in cli.ts reads it beside `--no-subtitles`. */
|
|
53
|
+
images?: boolean;
|
|
54
|
+
imageProvider?: string;
|
|
55
|
+
imageModel?: string;
|
|
56
|
+
imageStyle?: string;
|
|
57
|
+
imageMax?: string | number;
|
|
43
58
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture path, as a thing other code can drive.
|
|
3
|
+
*
|
|
4
|
+
* "Look at the artifact" is this project's strongest gate — six of its bugs
|
|
5
|
+
* shipped past green gates and every one was caught by a human looking at a
|
|
6
|
+
* frame. So the frame a human looks at has to be the frame the renderer would
|
|
7
|
+
* produce, and `hyperframes snapshot` is NOT that frame: it calls
|
|
8
|
+
* `player.renderSeek(t)` with no options, so `suppressEvents` is falsy and a
|
|
9
|
+
* GSAP `onUpdate` FIRES. Invariant 11 says the capture path suppresses events,
|
|
10
|
+
* so callback-driven motion plays under snapshot and renders frozen. Snapshot is
|
|
11
|
+
* permissive in exactly the direction that hides the project's most dangerous
|
|
12
|
+
* failure shape, and it was measured lying about CSS 3D as well — at t=3.9s it
|
|
13
|
+
* produced a flat, un-rotated frame where the render produced correct
|
|
14
|
+
* perspective (see `.planning/2026-09-04-css-3d-spike.md`).
|
|
15
|
+
*
|
|
16
|
+
* `fidelity` already drove the right path privately, and was measured doing so:
|
|
17
|
+
* against `experiments/015-decision/out/vocab-18.mp4`, a real render, at all
|
|
18
|
+
* twelve of its holds, it agreed to a worst case of 0.11 and a mean of 0.03
|
|
19
|
+
* percentage points — the residual being H.264 quantisation. This module is that
|
|
20
|
+
* code lifted out from under the ink arithmetic so the gate and a person asking
|
|
21
|
+
* for a PNG go through the same three calls: inject the pinned runtime,
|
|
22
|
+
* `renderSeek(t, { suppressEvents: true })`, then `Page.captureScreenshot` with
|
|
23
|
+
* the renderer's own clip.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The browser `render` already uses.
|
|
27
|
+
*
|
|
28
|
+
* `@puppeteer/browsers` and `puppeteer-core` are DIRECT dependencies of this
|
|
29
|
+
* package. They arrive with hyperframes too — a hard dependency of any verb that
|
|
30
|
+
* opens a browser — but relying on that made module imports resolve through
|
|
31
|
+
* somebody else's dependency tree, and the day hyperframes swaps its automation
|
|
32
|
+
* library those verbs break for a reason nothing in this repo mentions. They are
|
|
33
|
+
* still imported DYNAMICALLY, which is a different concern: a machine that
|
|
34
|
+
* cannot supply a browser must degrade rather than throw after the capture has
|
|
35
|
+
* already run.
|
|
36
|
+
*
|
|
37
|
+
* `need` completes the error message, because the three callers want different
|
|
38
|
+
* advice from it — captions can fall back to a sidecar, `frames` cannot. This
|
|
39
|
+
* was two copies until `frames` became the third caller the older of them said
|
|
40
|
+
* to promote it on.
|
|
41
|
+
*/
|
|
42
|
+
export declare function chromePath(need?: string): Promise<string>;
|
|
43
|
+
type Browser = Awaited<ReturnType<typeof import("puppeteer-core").launch>>;
|
|
44
|
+
type Page = Awaited<ReturnType<Browser["newPage"]>>;
|
|
45
|
+
/** An open deck, seekable and photographable. Close it. */
|
|
46
|
+
export interface DeckPage {
|
|
47
|
+
/** The canvas the deck declares, which is also the screenshot's clip. */
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
/** Commit the frame at an absolute time, events suppressed. */
|
|
51
|
+
seek(t: number): Promise<void>;
|
|
52
|
+
/** PNG bytes of the frame currently committed. */
|
|
53
|
+
shoot(): Promise<Buffer>;
|
|
54
|
+
/** For callers that need to read the DOM at a stop, as `fidelity` does. */
|
|
55
|
+
page: Page;
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export interface OpenOptions {
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Open `dir`'s deck in the renderer's own browser, ready to seek.
|
|
63
|
+
*
|
|
64
|
+
* Throws for every environmental reason — no Chrome, no `index.html`, no canvas
|
|
65
|
+
* size. Callers that must not fail a build over the environment (the `fidelity`
|
|
66
|
+
* gate) catch it; callers a person invoked directly (`frames`) let it surface,
|
|
67
|
+
* because a person who asked for a PNG and got nothing needs to hear why.
|
|
68
|
+
*/
|
|
69
|
+
export declare function openDeck(dir: string, opts?: OpenOptions): Promise<DeckPage>;
|
|
70
|
+
/** One written frame. */
|
|
71
|
+
export interface CapturedFrame {
|
|
72
|
+
t: number;
|
|
73
|
+
path: string;
|
|
74
|
+
bytes: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* What one frame is called.
|
|
78
|
+
*
|
|
79
|
+
* The index leads, zero-padded to the width of the largest, because a directory
|
|
80
|
+
* listing is how these actually get looked at and lexical order is what a file
|
|
81
|
+
* browser gives you: `t10.000s` sorts before `t3.900s`, so the reader opens the
|
|
82
|
+
* wrong frame and believes it. Padding the INDEX rather than the time keeps the
|
|
83
|
+
* two orders — asked-for and on-disk — identical without deciding how many
|
|
84
|
+
* digits a deck's duration might need.
|
|
85
|
+
*
|
|
86
|
+
* The time is still in the name, to 3 decimals, because that is the thing a
|
|
87
|
+
* reader is checking against and invariant 10 says 3 decimals is what the
|
|
88
|
+
* renderer rounds to. A frame whose filename disagreed with the timeline would
|
|
89
|
+
* be worse than one with no time in it at all.
|
|
90
|
+
*/
|
|
91
|
+
export declare function frameName(index: number, t: number, total: number): string;
|
|
92
|
+
/**
|
|
93
|
+
* Write a PNG per requested time into `outDir`.
|
|
94
|
+
*
|
|
95
|
+
* Times are rounded to 3 decimals to match invariant 10, so a filename names
|
|
96
|
+
* exactly the instant the renderer would.
|
|
97
|
+
*/
|
|
98
|
+
export declare function captureFrames(dir: string, times: readonly number[], outDir: string, opts?: OpenOptions): Promise<CapturedFrame[]>;
|
|
99
|
+
export {};
|
|
@@ -103,8 +103,20 @@ export interface RenderOptions {
|
|
|
103
103
|
* length is decided at plan time by how much it says, and this closes whatever
|
|
104
104
|
* gap survived that. A video already inside the target is left alone rather
|
|
105
105
|
* than padded — dead air is worse than eight seconds short.
|
|
106
|
+
*
|
|
107
|
+
* A gap wider than `MAX_PLAYBACK` is refused, not closed; see the check after
|
|
108
|
+
* `readTiming` and `playbackRefusal`.
|
|
106
109
|
*/
|
|
107
110
|
targetSeconds?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Speed past `MAX_PLAYBACK` anyway.
|
|
113
|
+
*
|
|
114
|
+
* For the person who has read the refusal, cannot re-plan, and wants the fast
|
|
115
|
+
* video regardless. It does not silence anything: both clauses of
|
|
116
|
+
* `playbackWarning` still print, and the summary still carries the factor and
|
|
117
|
+
* the caption rate it bought.
|
|
118
|
+
*/
|
|
119
|
+
allowFastPlayback?: boolean;
|
|
108
120
|
/** Leave the per-piece intermediates on disk. */
|
|
109
121
|
keep?: boolean;
|
|
110
122
|
/** Progress, one line at a time. */
|
|
@@ -119,5 +131,11 @@ export interface RenderResult {
|
|
|
119
131
|
burned: boolean;
|
|
120
132
|
/** What `targetSeconds` cost, if anything. 1 means the file was not respeeded. */
|
|
121
133
|
playback: number;
|
|
134
|
+
/**
|
|
135
|
+
* p95 characters per second of the subtitles AS SHIPPED — measured off the
|
|
136
|
+
* cues that were written, so at `playback > 1` it is already the sped-up rate
|
|
137
|
+
* a viewer actually reads at rather than the deck's rate at rest.
|
|
138
|
+
*/
|
|
139
|
+
captionCps: number;
|
|
122
140
|
}
|
|
123
141
|
export declare function render(opts: RenderOptions): Promise<RenderResult>;
|
|
@@ -61,6 +61,13 @@ export interface JobOptions {
|
|
|
61
61
|
prefs: Prefs;
|
|
62
62
|
narrate: boolean;
|
|
63
63
|
video: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Run the `illustrate` stage between `plan` and `build`. The same bit lands in
|
|
66
|
+
* `prefs.images.enabled`, which is what lets the planner write a brief; this
|
|
67
|
+
* copy is what `stagesFor` reads, the way `narrate` is read beside
|
|
68
|
+
* `prefs.narration.enabled`.
|
|
69
|
+
*/
|
|
70
|
+
images: boolean;
|
|
64
71
|
/**
|
|
65
72
|
* Whether the REQUEST said so, as opposed to the schema defaulting.
|
|
66
73
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ImageProvider, type Runner } from "../index.js";
|
|
1
2
|
import type { JobOptions } from "./options.js";
|
|
2
3
|
import type { JobHandle, JobResult, Stage } from "./queue.js";
|
|
3
4
|
import { type Upload } from "./upload.js";
|
|
@@ -15,6 +16,19 @@ export interface PipelineInput {
|
|
|
15
16
|
* owner's own box, own papers), it is fetched with a count and a timeout.
|
|
16
17
|
*/
|
|
17
18
|
fetchRemoteFigures: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The rungs `illustrate` draws through. A test injects the tool's own SVG and
|
|
21
|
+
* nothing else; absent, the stage resolves its providers from the environment
|
|
22
|
+
* at call time, like `narrate` does — so there is no server option to plumb
|
|
23
|
+
* and nothing to configure but the env vars.
|
|
24
|
+
*/
|
|
25
|
+
imageChain?: ImageProvider[];
|
|
26
|
+
/**
|
|
27
|
+
* The planner's `Runner`, handed straight to `codexPlanner`. The seam
|
|
28
|
+
* test/plan.test.ts already drives the parse path through; here it is what
|
|
29
|
+
* lets a test carry a job PAST `plan` — into `illustrate` — without a Codex.
|
|
30
|
+
*/
|
|
31
|
+
run?: Runner;
|
|
18
32
|
}
|
|
19
33
|
/** Which rows the step list should have, decided before anything runs. */
|
|
20
34
|
export declare function stagesFor(options: JobOptions): Stage[];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { type JobError } from "./errors.js";
|
|
17
17
|
export type JobState = "queued" | "running" | "done" | "error";
|
|
18
|
-
export type Stage = "ingest" | "plan" | "narrate" | "build" | "render";
|
|
18
|
+
export type Stage = "ingest" | "plan" | "illustrate" | "narrate" | "build" | "render";
|
|
19
19
|
export type StepState = "pending" | "running" | "done" | "skipped" | "error";
|
|
20
20
|
export interface StepView {
|
|
21
21
|
name: Stage;
|