@jokerized/decksmith 0.8.0 → 0.9.1
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 +333 -4
- package/dist/cli.js +14057 -4749
- package/dist/deck-player-element.js +1 -1
- package/dist/deck-player.js +1 -1
- package/dist/deck-runtime.js +70 -2
- package/dist/index.js +13309 -4178
- package/dist/mcp.js +5131 -765
- package/dist/types/bespoke/art.d.ts +173 -0
- package/dist/types/bespoke/cache.d.ts +69 -0
- package/dist/types/bespoke/callouts.d.ts +76 -0
- package/dist/types/bespoke/contract.d.ts +57 -0
- package/dist/types/bespoke/inspect.d.ts +69 -0
- package/dist/types/bespoke/pipeline.d.ts +207 -0
- package/dist/types/bespoke/probe.d.ts +34 -0
- package/dist/types/bespoke/prompt.d.ts +182 -0
- package/dist/types/bespoke/references.d.ts +73 -0
- package/dist/types/bespoke/repair.d.ts +121 -0
- package/dist/types/bespoke/scene.d.ts +97 -0
- package/dist/types/bespoke/select.d.ts +52 -0
- package/dist/types/bespoke/sheet.d.ts +26 -0
- package/dist/types/bespoke/shots.d.ts +128 -0
- package/dist/types/bespoke/untangle.d.ts +5 -0
- package/dist/types/build/files.d.ts +13 -1
- package/dist/types/deck/motion.d.ts +64 -0
- package/dist/types/deck/playback.d.ts +206 -0
- package/dist/types/deck/repack.d.ts +10 -0
- package/dist/types/deck/runtime.d.ts +96 -1
- package/dist/types/deck/subtitles.d.ts +29 -1
- package/dist/types/emit/archetypes/annotated-figure.d.ts +1 -1
- package/dist/types/emit/archetypes/bar-compare.d.ts +0 -28
- package/dist/types/emit/archetypes/callout.d.ts +0 -5
- package/dist/types/emit/archetypes/claim-figure.d.ts +15 -0
- package/dist/types/emit/archetypes/grid.d.ts +0 -14
- package/dist/types/emit/archetypes/index.d.ts +0 -7
- package/dist/types/emit/archetypes/line-chart.d.ts +0 -8
- package/dist/types/emit/archetypes/pipeline.d.ts +15 -1
- package/dist/types/emit/archetypes/stack.d.ts +1 -1
- package/dist/types/emit/composition.d.ts +83 -3
- package/dist/types/emit/faces.d.ts +22 -0
- package/dist/types/emit/fit.d.ts +127 -0
- package/dist/types/emit/kit.d.ts +121 -2
- package/dist/types/emit/look.d.ts +138 -0
- package/dist/types/emit/motion.d.ts +244 -0
- package/dist/types/emit/svg.d.ts +54 -16
- package/dist/types/emit/theme.d.ts +1 -1
- package/dist/types/emit/themes/index.d.ts +19 -1
- package/dist/types/emit/themes/packs.d.ts +15 -0
- package/dist/types/emit/themes/pick.d.ts +107 -0
- package/dist/types/emit/type.d.ts +111 -0
- package/dist/types/index.d.ts +52 -1
- package/dist/types/plan/codex.d.ts +36 -0
- package/dist/types/plan/direct.d.ts +66 -0
- package/dist/types/prefs.d.ts +25 -2
- package/dist/types/render/capture.d.ts +6 -0
- package/dist/types/render/timing.d.ts +3 -0
- package/dist/types/source/fonts.d.ts +11 -1
- package/dist/types/types.d.ts +109 -0
- package/dist/types/verify/check.d.ts +1 -1
- package/dist/types/verify/fidelity.d.ts +34 -0
- package/dist/types/verify/fill.d.ts +205 -0
- package/dist/types/verify/index.d.ts +14 -0
- package/dist/types/verify/motion.d.ts +33 -0
- package/dist/types/verify/scenes.d.ts +325 -0
- package/package.json +5 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DeckNarration } from "../emit/composition.js";
|
|
2
|
+
import type { Finding, Format, Source, Storyboard } from "../types.js";
|
|
3
|
+
import { type CamSample, type Layout, type SceneWindow } from "../verify/scenes.js";
|
|
4
|
+
import type { GateFn } from "./pipeline.js";
|
|
5
|
+
import type { Measured } from "./prompt.js";
|
|
6
|
+
import type { BespokeMap } from "./scene.js";
|
|
7
|
+
export interface ProbeDeck {
|
|
8
|
+
storyboard: Storyboard;
|
|
9
|
+
source: Source;
|
|
10
|
+
format: Format;
|
|
11
|
+
narration: DeckNarration;
|
|
12
|
+
theme: string;
|
|
13
|
+
/** Where the source's assets live (`build` copies them from here). */
|
|
14
|
+
assetsFrom: string;
|
|
15
|
+
/** Scratch: probe decks and contact sheets go here. */
|
|
16
|
+
work: string;
|
|
17
|
+
onStep?: (message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
/** Which scene a `verify` finding is about, if it says. */
|
|
20
|
+
export declare function attribute(f: Finding, windows: readonly SceneWindow[]): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A console error no scene id names fails every scene probed with it — unless
|
|
23
|
+
* the value it quotes appears in exactly one candidate, which is then the
|
|
24
|
+
* culprit: an SVG parse error quotes the bad attribute. MEASURED 2026-10-09:
|
|
25
|
+
* one draft's broken path sent all five drafts of a deck to critique.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pinPageErrors(findings: Finding[], candidates: BespokeMap, sidOf: ReadonlyMap<string, string>): Finding[];
|
|
28
|
+
/**
|
|
29
|
+
* What an illustrated scene's frames say about its staging: the distinct
|
|
30
|
+
* push-ins held at cue ends, whether it opens wide, and how many subjects are
|
|
31
|
+
* named by a label within reach at the end. Empty for a scene with no subjects.
|
|
32
|
+
*/
|
|
33
|
+
export declare function stagingMeasures(rows: readonly Layout[], cams: readonly CamSample[], open?: number): Partial<Measured>;
|
|
34
|
+
export declare function browserGate(deck: ProbeDeck): GateFn;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two prompts a bespoke beat may cost: generate, then — only when the
|
|
3
|
+
* gates or the rubric probe found something — critique-and-fix with the
|
|
4
|
+
* rendered frames attached.
|
|
5
|
+
*
|
|
6
|
+
* Production differs from the 2026-10-07 spike where it must:
|
|
7
|
+
* - the shell draws the eyebrow and headline and owns the exit, so the model
|
|
8
|
+
* draws only the body, into a box of known size;
|
|
9
|
+
* - GSAP callbacks are refused outright (invariant 11), and every tween's vars
|
|
10
|
+
* are a literal at an explicit second, so `checkScript` can read them;
|
|
11
|
+
* - the scene id is the token `SCENEID`, so the cached scene fits any slot;
|
|
12
|
+
* - the paper's text is fenced and labelled as untrusted data.
|
|
13
|
+
*
|
|
14
|
+
* ROUND 2 (2026-10-08) is about the LOOK, and grounds it in three places:
|
|
15
|
+
* - worked examples (src/bespoke/references.ts) instead of adjectives — the
|
|
16
|
+
* model is shown two complete scenes at the bar, in the deck's own palette;
|
|
17
|
+
* - motion-design rules with numbers, from what 3Blue1Brown, Kurzgesagt-style
|
|
18
|
+
* motion graphics, keynote diagram reveals and Distill figures have in
|
|
19
|
+
* common: one focal element per cue, everything else dimmed but kept;
|
|
20
|
+
* identity kept through change (transform, don't replace); a camera that
|
|
21
|
+
* moves to the part being named; numbers that count; big, flat, few;
|
|
22
|
+
* - semantic groups (Vector Prism, arXiv 2512.14336): parts that move
|
|
23
|
+
* together are one `<g data-cue>`, which is also what `early_reveal` reads.
|
|
24
|
+
* The critique round judges against an explicit rubric (TheoremExplainAgent,
|
|
25
|
+
* arXiv 2502.19400, found free-form VLM judgement unreliable on layout; a rubric
|
|
26
|
+
* with measured numbers attached is what it can act on) and returns the fix.
|
|
27
|
+
*
|
|
28
|
+
* The placement grid is Code2Video's visual anchor prompt (arXiv 2510.01174).
|
|
29
|
+
*/
|
|
30
|
+
import type { Theme } from "../emit/kit.js";
|
|
31
|
+
import { type Box, CAMERA_MAX_SCALE } from "./shots.js";
|
|
32
|
+
export { CAMERA_MAX_SCALE };
|
|
33
|
+
/** Bump with any change to either prompt or to a reference: it is part of every cache key. */
|
|
34
|
+
export declare const PROMPT_VERSION = "bespoke-5";
|
|
35
|
+
/** Bump with any change to what `checkFragment` accepts. Also part of every key. */
|
|
36
|
+
export declare const CONTRACT_VERSION = "contract-4";
|
|
37
|
+
/** What the model is told about one beat. */
|
|
38
|
+
export interface Brief {
|
|
39
|
+
lang: string;
|
|
40
|
+
eyebrow?: string;
|
|
41
|
+
headline: string;
|
|
42
|
+
intent: string;
|
|
43
|
+
claim?: string;
|
|
44
|
+
narration?: string;
|
|
45
|
+
archetype: string;
|
|
46
|
+
/** The old archetype's params, for wording only. */
|
|
47
|
+
params: unknown;
|
|
48
|
+
/** Paper excerpts and equations, quoted. Untrusted. */
|
|
49
|
+
context: string;
|
|
50
|
+
cues: ReadonlyArray<{
|
|
51
|
+
t0: number;
|
|
52
|
+
t1: number;
|
|
53
|
+
text: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** Scene length, seconds. */
|
|
56
|
+
duration: number;
|
|
57
|
+
/** The body box, reference px. */
|
|
58
|
+
region: {
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
};
|
|
62
|
+
theme: Theme;
|
|
63
|
+
/** Pack name, for the art direction line. */
|
|
64
|
+
pack: string;
|
|
65
|
+
/**
|
|
66
|
+
* The beat's illustration, attached to the call as an image (src/bespoke/art.ts),
|
|
67
|
+
* and its subjects in body-box px, left to right (src/bespoke/inspect.ts):
|
|
68
|
+
* what the labels are anchored to and the shots frame.
|
|
69
|
+
*/
|
|
70
|
+
art?: {
|
|
71
|
+
depicts: string;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
subjects?: Box[];
|
|
75
|
+
/** Where the shell draws each subject's label (src/bespoke/callouts.ts), and its longest fit. */
|
|
76
|
+
zones?: Array<Box & {
|
|
77
|
+
subject: number;
|
|
78
|
+
chars: number;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
/** A data beat (a chart or a table): built as a chart, never on a picture. */
|
|
82
|
+
data?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/** What the probe measured about a candidate, quoted to the critique round. */
|
|
85
|
+
export interface Measured {
|
|
86
|
+
/** Bounding box of the drawing over the body box, settled frame. */
|
|
87
|
+
fill?: number;
|
|
88
|
+
/** Share of a 6x4 grid over the box something is drawn in. */
|
|
89
|
+
cells?: number;
|
|
90
|
+
/** Largest label at the settled frame, px. */
|
|
91
|
+
maxType?: number;
|
|
92
|
+
/** Share of the box the settled drawing paints (filled shapes count, hairlines barely). */
|
|
93
|
+
mass?: number;
|
|
94
|
+
/** Share of the drawn parts left dimmed (under 0.6 opacity) at the end. */
|
|
95
|
+
dimmed?: number;
|
|
96
|
+
/** Motion kinds the script asks for (`motionKinds`). */
|
|
97
|
+
kinds?: readonly string[];
|
|
98
|
+
/** Per cue, the largest share of the frame that changed during it. */
|
|
99
|
+
cueChange?: readonly number[];
|
|
100
|
+
/** How many subjects the scene's illustration has. */
|
|
101
|
+
subjects?: number;
|
|
102
|
+
/** Distinct push-ins the held frames show (illustrated scenes, `shotsOf`). */
|
|
103
|
+
shots?: number;
|
|
104
|
+
/** Whether the first cue opens on the whole picture. */
|
|
105
|
+
establishing?: boolean;
|
|
106
|
+
/** Subjects named by a label within reach of them, at the end. */
|
|
107
|
+
anchored?: number;
|
|
108
|
+
/** The farthest a subject's label sits from it, box px, at the end. */
|
|
109
|
+
anchorMax?: number;
|
|
110
|
+
}
|
|
111
|
+
/** The structured reply. `--output-schema` holds the model to it. */
|
|
112
|
+
export declare const REPLY_SCHEMA: {
|
|
113
|
+
readonly type: "object";
|
|
114
|
+
readonly additionalProperties: false;
|
|
115
|
+
readonly required: readonly ["review", "plan", "markup", "css", "script", "shots", "labels"];
|
|
116
|
+
readonly properties: {
|
|
117
|
+
readonly review: {
|
|
118
|
+
readonly type: "string";
|
|
119
|
+
};
|
|
120
|
+
readonly plan: {
|
|
121
|
+
readonly type: "string";
|
|
122
|
+
};
|
|
123
|
+
readonly markup: {
|
|
124
|
+
readonly type: "string";
|
|
125
|
+
};
|
|
126
|
+
readonly css: {
|
|
127
|
+
readonly type: "string";
|
|
128
|
+
};
|
|
129
|
+
readonly script: {
|
|
130
|
+
readonly type: "string";
|
|
131
|
+
};
|
|
132
|
+
readonly labels: {
|
|
133
|
+
readonly type: "array";
|
|
134
|
+
readonly items: {
|
|
135
|
+
readonly type: "object";
|
|
136
|
+
readonly additionalProperties: false;
|
|
137
|
+
readonly required: readonly ["subject", "text"];
|
|
138
|
+
readonly properties: {
|
|
139
|
+
readonly subject: {
|
|
140
|
+
readonly type: "integer";
|
|
141
|
+
};
|
|
142
|
+
readonly text: {
|
|
143
|
+
readonly type: "string";
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
readonly shots: {
|
|
149
|
+
readonly type: "array";
|
|
150
|
+
readonly items: {
|
|
151
|
+
readonly type: "object";
|
|
152
|
+
readonly additionalProperties: false;
|
|
153
|
+
readonly required: readonly ["cue", "at", "subject"];
|
|
154
|
+
readonly properties: {
|
|
155
|
+
readonly cue: {
|
|
156
|
+
readonly type: "integer";
|
|
157
|
+
};
|
|
158
|
+
readonly at: {
|
|
159
|
+
readonly type: "number";
|
|
160
|
+
};
|
|
161
|
+
readonly subject: {
|
|
162
|
+
readonly type: "integer";
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
/** The subjects as the prompt lists them: box px, left to right. */
|
|
170
|
+
export declare function subjectLines(subjects: readonly Box[]): string;
|
|
171
|
+
/** The first call: draw this beat. */
|
|
172
|
+
export declare function generatePrompt(b: Brief): string;
|
|
173
|
+
/** The rubric the critique round scores, and the probe reads off the measures. */
|
|
174
|
+
export declare const RUBRIC: readonly ["FILLS THE STAGE: the drawing uses the whole box (>= 80% of its area, no empty third or band) with visual mass — filled shapes, not hairlines", "ONE FOCUS PER CUE: in every cue one element is clearly what to look at; the rest is dimmed", "MOTION EXPLAINS: the moves are the idea (flow, transform, morph, camera, counter), at least three kinds, nothing decorative, nothing still while the voice speaks; the camera goes where the voice is", "LEGIBLE HIERARCHY: focal label/number 88px+, labels 52px+, nothing under 44px, high contrast, no collisions or clipping", "CONSISTENT WITH THE PACK: only the pack's colours, big flat shapes, strokes 5-8px, the deck's font", "IN SYNC: each cue's change starts within 0.5s of its words and nothing appears before the cue that names it"];
|
|
175
|
+
/** The second call: score the frames against the rubric, then fix what is wrong. */
|
|
176
|
+
export declare function critiquePrompt(b: Brief, current: {
|
|
177
|
+
markup: string;
|
|
178
|
+
css: string;
|
|
179
|
+
script: string;
|
|
180
|
+
shots?: readonly unknown[];
|
|
181
|
+
labels?: readonly unknown[];
|
|
182
|
+
}, findings: readonly string[], legend: string | undefined, measured?: Measured): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-made reference scenes: the bar a generated scene is held to, shown to
|
|
3
|
+
* the model as worked examples (few-shot) and run through every gate by
|
|
4
|
+
* test/bespoke-references.test.ts, so a reference that stops passing a gate
|
|
5
|
+
* stops being a reference.
|
|
6
|
+
*
|
|
7
|
+
* WHY EXAMPLES AND NOT MORE PROSE. Round 1's prompt said "use the whole box",
|
|
8
|
+
* "keep it alive", "64px for the key quantity" and the model drew 40-56px boxes
|
|
9
|
+
* and arrows in two thirds of the frame. Code2Video (arXiv 2510.01174) and
|
|
10
|
+
* TheoremExplainAgent (arXiv 2502.19400) both found layout and visual quality
|
|
11
|
+
* respond to concrete anchors and examples far more than to adjectives. Each
|
|
12
|
+
* reference is a complete scene in the contract's own token form, written to the
|
|
13
|
+
* motion-design habits round 1 lacked:
|
|
14
|
+
*
|
|
15
|
+
* - THE STAGE IS FILLED: every reference spans >= 85% of its box at the end.
|
|
16
|
+
* - HIERARCHY: one 88-120px focal element per cue; labels 48-60px; nothing
|
|
17
|
+
* under 44px.
|
|
18
|
+
* - ONE FOCUS PER CUE: what the voice is naming is lit (accent, full opacity);
|
|
19
|
+
* what it is not is dimmed to ~0.3 — never removed, so the viewer keeps place.
|
|
20
|
+
* - IDENTITY IS KEPT: a thing that changes state moves or morphs into the new
|
|
21
|
+
* state (3Blue1Brown's transforms) instead of cutting to a new drawing.
|
|
22
|
+
* - SEMANTIC GROUPS (Vector Prism, arXiv 2512.14336): each part is a <g> with a
|
|
23
|
+
* descriptive id and `data-cue`, and motion is attached to groups.
|
|
24
|
+
* - A RICHER VERB SET, chosen to fit: flow (particles), camera (viewBox zoom),
|
|
25
|
+
* counters (textContent + snap), morph (morphSVG), staggered builds, draws.
|
|
26
|
+
* - ARROWHEADS ARE SHAPES, not markers, and appear when their line arrives.
|
|
27
|
+
*
|
|
28
|
+
* Colours are `{{token}}` placeholders filled from the deck's pack, so the
|
|
29
|
+
* examples are in the palette the model must use. All are written for a
|
|
30
|
+
* 1700x732 body box; the prompt says how to adapt to the real one.
|
|
31
|
+
*/
|
|
32
|
+
import type { Theme } from "../emit/kit.js";
|
|
33
|
+
import type { Fragment } from "./contract.js";
|
|
34
|
+
export interface Reference {
|
|
35
|
+
name: string;
|
|
36
|
+
/** What it demonstrates, one line, quoted in the prompt. */
|
|
37
|
+
shows: string;
|
|
38
|
+
/** Archetypes whose beats this reference suits best (selection order). */
|
|
39
|
+
fits: readonly string[];
|
|
40
|
+
/** The narration cues the script is timed to (scene seconds). */
|
|
41
|
+
cues: ReadonlyArray<{
|
|
42
|
+
t0: number;
|
|
43
|
+
t1: number;
|
|
44
|
+
text: string;
|
|
45
|
+
}>;
|
|
46
|
+
duration: number;
|
|
47
|
+
fragment: Fragment;
|
|
48
|
+
/** An illustrated reference's subjects, box px: what the prompt would list for its picture. */
|
|
49
|
+
subjects?: ReadonlyArray<{
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
w: number;
|
|
53
|
+
h: number;
|
|
54
|
+
}>;
|
|
55
|
+
}
|
|
56
|
+
/** The box every reference is drawn for. */
|
|
57
|
+
export declare const REFERENCE_BOX: {
|
|
58
|
+
readonly width: 1700;
|
|
59
|
+
readonly height: 732;
|
|
60
|
+
};
|
|
61
|
+
export declare const REFERENCES: readonly Reference[];
|
|
62
|
+
/** Whether a reference places the beat's illustration (and so needs a beat that has one). */
|
|
63
|
+
export declare function placesArt(r: Reference): boolean;
|
|
64
|
+
/** A reference with its `{{token}}` colours filled from `theme`. */
|
|
65
|
+
export declare function paint(f: Fragment, theme: Theme): Fragment;
|
|
66
|
+
/**
|
|
67
|
+
* The two references a beat is shown: the best fit for its archetype first,
|
|
68
|
+
* then the best fit that teaches a DIFFERENT motion, so every prompt carries
|
|
69
|
+
* at least two verbs beyond fade-and-draw. An illustrated beat leads with the
|
|
70
|
+
* staged reference and is never shown a scene that moves its own camera (the
|
|
71
|
+
* shell's shots do that); a data beat leads with the chart that builds.
|
|
72
|
+
*/
|
|
73
|
+
export declare function pickReferences(archetype: string, n?: number, art?: boolean, data?: boolean): Reference[];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic repair of a generated scene the gates refused for a reason a
|
|
3
|
+
* person would fix by hand in seconds — instead of throwing the scene away.
|
|
4
|
+
*
|
|
5
|
+
* ROUND 2 LOST 4 OF 21 BEATS to `text_overlap` / `graphic_crosses_text` after
|
|
6
|
+
* the critique round: big type collides more, and a model fixing one collision
|
|
7
|
+
* by rewriting coordinates makes the next one. Round 2 also kept three scenes
|
|
8
|
+
* that ended on a frame of ghosts. None of that needs another model call:
|
|
9
|
+
*
|
|
10
|
+
* - LABEL COLLISIONS are solved as constraint relaxation over the frames the
|
|
11
|
+
* probe measured. Each colliding label's unit — the `<text>`, or the
|
|
12
|
+
* smallest `<g>` holding it with its plate and no other label — gets ONE
|
|
13
|
+
* constant offset, the smallest (in rings of `STEP` px, sixteen directions
|
|
14
|
+
* in a fixed order) that clears every other label by `LABEL_GAP`, every
|
|
15
|
+
* stroke sample by `STROKE_GAP`, every shape painted over it, and stays in
|
|
16
|
+
* the box, at EVERY measured frame, with the camera's scale at each frame
|
|
17
|
+
* applied. The offset becomes a wrapping `<g transform="translate()">` in
|
|
18
|
+
* the markup, so the scene's own tweens on the unit are untouched.
|
|
19
|
+
* - A DIMMED END (`end_dimmed`): the elements dimming parts the scene had lit
|
|
20
|
+
* are tweened back to opacity 1 just before the scene ends.
|
|
21
|
+
* - A CAMERA LEFT ZOOMED (`camera_end`): the camera is tweened home.
|
|
22
|
+
*
|
|
23
|
+
* Every repair is appended as ordinary contract code (a `tl.to` with literal
|
|
24
|
+
* vars at a literal second, or a wrapper `<g>`), goes through `checkFragment`
|
|
25
|
+
* like a model's, and is gated again before it is used. Deterministic: the same
|
|
26
|
+
* geometry gives the same bytes, so a repaired scene caches like any other.
|
|
27
|
+
*/
|
|
28
|
+
import type { Geo, Layout } from "../verify/scenes.js";
|
|
29
|
+
import { type Fragment } from "./contract.js";
|
|
30
|
+
/** Error rules this module can fix. A scene failing anything else is not repaired. */
|
|
31
|
+
export declare const REPAIRABLE: Set<string>;
|
|
32
|
+
/** Clearance a moved label keeps from every other label, px. */
|
|
33
|
+
export declare const LABEL_GAP = 16;
|
|
34
|
+
/** Clearance from every visible stroke sample, px. */
|
|
35
|
+
export declare const STROKE_GAP = 10;
|
|
36
|
+
/** Ring step, px. */
|
|
37
|
+
export declare const STEP = 8;
|
|
38
|
+
/**
|
|
39
|
+
* The farthest a label may move: its own height times this, at least
|
|
40
|
+
* `MIN_REACH` px. A nudge, not a relocation — MEASURED 2026-10-09 with a flat
|
|
41
|
+
* 320px reach, the solver cleared a two-line label's overlap by carrying its
|
|
42
|
+
* second line onto the illustration, away from its plate: every gate passed and
|
|
43
|
+
* the label no longer read as one. A collision that needs more than this is the
|
|
44
|
+
* critique round's to redesign.
|
|
45
|
+
*/
|
|
46
|
+
export declare const REACH = 0.8;
|
|
47
|
+
export declare const MIN_REACH = 32;
|
|
48
|
+
/** The `error <rule>:` lines of a gate's findings, by rule. */
|
|
49
|
+
export declare function errorRules(findings: readonly string[]): string[];
|
|
50
|
+
/** Whether every error in `findings` is one this module can fix (and there is one). */
|
|
51
|
+
export declare function repairable(findings: readonly string[]): boolean;
|
|
52
|
+
/** One unit's offset, in the units of the unit's parent (screen px / its scale). */
|
|
53
|
+
export interface Move {
|
|
54
|
+
u: string;
|
|
55
|
+
dx: number;
|
|
56
|
+
dy: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Offsets that clear every collision in `frames`, or the units it could not
|
|
60
|
+
* clear. Pure: the probe measured `frames` (src/verify/scenes.ts, `geo`).
|
|
61
|
+
*/
|
|
62
|
+
export declare function solveNudges(frames: readonly Geo[]): {
|
|
63
|
+
moves: Move[];
|
|
64
|
+
unresolved: string[];
|
|
65
|
+
};
|
|
66
|
+
/** Each move as a wrapping `<g transform="translate(dx dy)">`, applied from the end so positions hold. */
|
|
67
|
+
export declare function applyMoves(markup: string, moves: readonly Move[]): string;
|
|
68
|
+
/**
|
|
69
|
+
* A PLATE TOO SMALL FOR ITS TEXT — the commonest crossing round 4 still lost
|
|
70
|
+
* scenes to (MEASURED 2026-10-09: zh run a, three of three fallbacks were a
|
|
71
|
+
* summary plate whose own border ran through its CJK text; en, a token disc
|
|
72
|
+
* round its own symbol). The model guesses a label's width. At a measured frame
|
|
73
|
+
* where the label is at scale 1 (camera home), a filled `<rect>` or `<circle>`
|
|
74
|
+
* painted before a label and covering at least half of it, but not all of it
|
|
75
|
+
* with a margin, is that label's plate, and it is grown — by the deltas the
|
|
76
|
+
* measured boxes call for, so a plate inside a translated group grows in its
|
|
77
|
+
* own units — to hold the label with `PLATE_PAD` of the label's height round
|
|
78
|
+
* it. Only a plate whose markup size IS its measured size (no scale between,
|
|
79
|
+
* not resized by a tween) is touched; anything else is left to the nudges.
|
|
80
|
+
*/
|
|
81
|
+
export declare const PLATE_PAD = 0.3;
|
|
82
|
+
export declare function fitPlates(markup: string, frames: readonly Geo[] | Geo | undefined): {
|
|
83
|
+
markup: string;
|
|
84
|
+
grown: number;
|
|
85
|
+
};
|
|
86
|
+
/** When the end-state repairs land: after the last cue starts, finished before the end frame. */
|
|
87
|
+
export declare function settleAt(duration: number, lastCueStart: number): number;
|
|
88
|
+
/**
|
|
89
|
+
* Bring the elements dimming lit parts back to full strength at `at`. Targets
|
|
90
|
+
* are the probe's `tag:index[#id]`, ids in the probe deck's form (`sid-…`);
|
|
91
|
+
* an element without an id gets one (`SCENEID-lit<k>`).
|
|
92
|
+
*/
|
|
93
|
+
export declare function relight(f: Fragment, targets: readonly string[], at: number, sid: string): Fragment;
|
|
94
|
+
/** Send the camera home at `at`: the shell's wrapper, or the svg's viewBox. */
|
|
95
|
+
export declare function homeCamera(f: Fragment, camOff: string, at: number): Fragment;
|
|
96
|
+
/** What a repair did, for the report. */
|
|
97
|
+
export interface RepairNote {
|
|
98
|
+
/** Overlapping tweens on one property were untangled (`seek_order`). */
|
|
99
|
+
untangled?: boolean;
|
|
100
|
+
moved: number;
|
|
101
|
+
/** Plates grown to hold their text (`fitPlates`). */
|
|
102
|
+
grown?: number;
|
|
103
|
+
relit: number;
|
|
104
|
+
camera: boolean;
|
|
105
|
+
rules: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The repaired scene, or undefined when the findings include something this
|
|
109
|
+
* cannot fix or the solver cannot clear every collision. Never a model call.
|
|
110
|
+
*/
|
|
111
|
+
export declare function repairScene(f: Fragment, g: {
|
|
112
|
+
findings: readonly string[];
|
|
113
|
+
layout?: readonly Layout[];
|
|
114
|
+
sid?: string;
|
|
115
|
+
}, scene: {
|
|
116
|
+
duration: number;
|
|
117
|
+
lastCueStart: number;
|
|
118
|
+
}): {
|
|
119
|
+
fragment: Fragment;
|
|
120
|
+
note: RepairNote;
|
|
121
|
+
} | undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { EmitContext, Scene } from "../emit/kit.js";
|
|
2
|
+
import type { Beat } from "../types.js";
|
|
3
|
+
import { type ArtRef } from "./art.js";
|
|
4
|
+
import { type Zone } from "./callouts.js";
|
|
5
|
+
import { type Fragment } from "./contract.js";
|
|
6
|
+
import { type Box, type CamMove } from "./shots.js";
|
|
7
|
+
/** One beat's bespoke scene, ready to emit. */
|
|
8
|
+
export interface BespokeEntry {
|
|
9
|
+
/** Token form, already through `checkFragment`. */
|
|
10
|
+
fragment: Fragment;
|
|
11
|
+
/** Scene-relative seconds, one per stop, from `bespokeHolds`. */
|
|
12
|
+
holds: number[];
|
|
13
|
+
/** The beat's illustration, when it has one: `<image data-art="1">` shows it (src/bespoke/art.ts). */
|
|
14
|
+
art?: ArtRef;
|
|
15
|
+
/**
|
|
16
|
+
* The narration cues on the scene's clock and its length, from the same
|
|
17
|
+
* timing the cue windows came from: what the shell's camera is timed to
|
|
18
|
+
* (src/bespoke/shots.ts). Present on an illustrated scene.
|
|
19
|
+
*/
|
|
20
|
+
stage?: {
|
|
21
|
+
cues: ReadonlyArray<{
|
|
22
|
+
t0: number;
|
|
23
|
+
t1: number;
|
|
24
|
+
}>;
|
|
25
|
+
duration: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The file `build` writes beside a deck with the bespoke pass's account of itself. */
|
|
29
|
+
export declare const BESPOKE_FILE = "bespoke.json";
|
|
30
|
+
/** Keyed by beat id: a scene id is a position, and a cut can move it. */
|
|
31
|
+
export type BespokeMap = Readonly<Record<string, BespokeEntry>>;
|
|
32
|
+
/** Space the body box leaves under the chrome — the same 34px `bodyBudget` charges by default. */
|
|
33
|
+
export declare const BODY_TOP = 34;
|
|
34
|
+
/** The box the generated drawing gets, in reference px. The prompt quotes it. */
|
|
35
|
+
export declare function bespokeRegion(beat: Beat, ctx: Pick<EmitContext, "format" | "theme">): {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
};
|
|
39
|
+
/** Whether a scene's script moves the shell's camera (`#SCENEID-cam`). */
|
|
40
|
+
export declare function usesCamera(script: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The illustration's href AND its placement, written by the shell into the
|
|
43
|
+
* scene's one `<image data-art="1">` — the contract refuses an href from the
|
|
44
|
+
* model, so the deck only ever references a file the build copied under its
|
|
45
|
+
* own name; and the picture always covers the body box (`slice`), so the
|
|
46
|
+
* subject boxes the camera and the labels are anchored to are where the
|
|
47
|
+
* picture's subjects are, whatever the scene wrote.
|
|
48
|
+
*/
|
|
49
|
+
export declare function withArt(markup: string, art: ArtRef | undefined, box?: {
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
}): string;
|
|
53
|
+
/**
|
|
54
|
+
* The subjects' boxes as the gates see them: invisible rects inside the camera
|
|
55
|
+
* (so they move with it), after the scene's markup (so the repair pass's
|
|
56
|
+
* `tag:index` addresses of the scene's own elements do not shift).
|
|
57
|
+
*/
|
|
58
|
+
export declare function subjectLayer(subjects: readonly Box[], width: number, height: number): string;
|
|
59
|
+
/**
|
|
60
|
+
* An illustrated entry's subjects in body px, its labels' zones, and the camera
|
|
61
|
+
* moves its shots compile to — each push-in framing the subject together with
|
|
62
|
+
* its label's zone, so the label the camera arrives for is in the shot.
|
|
63
|
+
*/
|
|
64
|
+
export declare function staging(entry: BespokeEntry, box: {
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
}): {
|
|
68
|
+
subjects: Box[];
|
|
69
|
+
zones: Zone[];
|
|
70
|
+
moves: CamMove[];
|
|
71
|
+
};
|
|
72
|
+
/** CSS feather for an illustration's edges: 6% at the sides, 10% top and bottom. */
|
|
73
|
+
export declare const ART_FEATHER = "-webkit-mask-image:linear-gradient(to right,transparent,#000 6%,#000 94%,transparent),linear-gradient(to bottom,transparent,#000 10%,#000 90%,transparent);-webkit-mask-composite:source-in;mask-composite:intersect";
|
|
74
|
+
/** Whether a scene's script tweens `morphSVG` (MorphSVGPlugin must be registered first). */
|
|
75
|
+
export declare function usesMorph(script: string): boolean;
|
|
76
|
+
export declare function bespokeScene(beat: Beat, ctx: EmitContext, entry: BespokeEntry): Scene;
|
|
77
|
+
/**
|
|
78
|
+
* Where a bespoke scene's stops go, given the stops its archetype had and the
|
|
79
|
+
* narration that will be spoken over it.
|
|
80
|
+
*
|
|
81
|
+
* THE STOP COUNT IS KEPT, because `narrate` recorded the narration against it
|
|
82
|
+
* and `assertNarrationStaging` refuses a deck whose beats moved. Only the
|
|
83
|
+
* POSITIONS move: an archetype's holds sit where its reveals land, all inside
|
|
84
|
+
* the first few seconds; a bespoke scene keeps explaining for the whole of the
|
|
85
|
+
* speech, so a stop belongs where its sentence starts.
|
|
86
|
+
*
|
|
87
|
+
* WHY THIS LEAVES THE VOICE WHERE IT WAS. `speechPlan` starts sentence i at
|
|
88
|
+
* `max(end of i-1, hold[stop_i])`. The new hold of every speaking stop past the
|
|
89
|
+
* first IS that start, computed with the old holds, so recomputing with the new
|
|
90
|
+
* ones returns the same starts; the first sentence starts at `open` regardless.
|
|
91
|
+
* The last hold never moves earlier than the archetype's own, so `beatSeconds`'
|
|
92
|
+
* `lastHold + SETTLE` term cannot shrink the scene either.
|
|
93
|
+
*
|
|
94
|
+
* Silent stops (narration density below `high`) are spread evenly between the
|
|
95
|
+
* speaking stops either side of them.
|
|
96
|
+
*/
|
|
97
|
+
export declare function bespokeHolds(archetypeHolds: readonly number[], starts: ReadonlyMap<number, number>, speechEnd: number): number[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which beats get a bespoke scene. A deterministic rule, so the same storyboard
|
|
3
|
+
* always asks the model about the same beats, and a cache hit is a cache hit.
|
|
4
|
+
*
|
|
5
|
+
* WHAT IS WORTH A MODEL CALL. A bespoke scene earns its cost where the fixed
|
|
6
|
+
* vocabulary is weakest: a beat that explains a MECHANISM — a process, an
|
|
7
|
+
* equation, a curve, a comparison — and that the narration talks over for many
|
|
8
|
+
* seconds while the v2 archetype has finished building after five (measured in
|
|
9
|
+
* the spike: v2's s3 and s7 froze for 15-20s of speech). Title cards, callouts
|
|
10
|
+
* and tables say a thing rather than show one, so they never qualify.
|
|
11
|
+
*
|
|
12
|
+
* WHAT NEVER QUALIFIES, whatever it scores:
|
|
13
|
+
* - a beat with no narration: its cues are the keyframes, and without them a
|
|
14
|
+
* generated scene has nothing to stay in step with;
|
|
15
|
+
* - a beat a camera enters or leaves (`inside`): the dive is aimed at a part
|
|
16
|
+
* the archetype drew, which a bespoke scene does not draw;
|
|
17
|
+
* - a beat the planner marked `bespoke: false`.
|
|
18
|
+
*
|
|
19
|
+
* The planner's `bespoke: true` is a strong hint, not an order: it adds a large
|
|
20
|
+
* bonus, so it wins a tie with anything, but an ineligible beat stays ineligible.
|
|
21
|
+
*/
|
|
22
|
+
import type { z } from "zod";
|
|
23
|
+
import type { Beat, segmentSchema } from "../types.js";
|
|
24
|
+
type Segment = z.infer<typeof segmentSchema>;
|
|
25
|
+
export interface Pick {
|
|
26
|
+
beatId: string;
|
|
27
|
+
score: number;
|
|
28
|
+
/** Why it scored what it did, for `bespoke.json`. */
|
|
29
|
+
why: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface Skip {
|
|
32
|
+
beatId: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
export interface Selection {
|
|
36
|
+
picked: Pick[];
|
|
37
|
+
skipped: Skip[];
|
|
38
|
+
}
|
|
39
|
+
export interface SelectOptions {
|
|
40
|
+
/** Hashed with each beat id to break ties, so two decks tie differently. */
|
|
41
|
+
seed: string;
|
|
42
|
+
/** Narration, by beat id. A beat without segments is ineligible. */
|
|
43
|
+
narration?: Readonly<Record<string, readonly Segment[]>>;
|
|
44
|
+
/** Fewest beats to aim for when that many are eligible. Default 4. */
|
|
45
|
+
min?: number;
|
|
46
|
+
/** Most beats. Default 6; the call cap may lower it further. */
|
|
47
|
+
max?: number;
|
|
48
|
+
}
|
|
49
|
+
/** The number of beats a deck of `n` kept beats aims for: a third, held to [min, max]. */
|
|
50
|
+
export declare function target(n: number, min?: number, max?: number): number;
|
|
51
|
+
export declare function selectBespoke(beats: readonly Beat[], opts: SelectOptions): Selection;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SheetFrame {
|
|
2
|
+
label: string;
|
|
3
|
+
png: Buffer;
|
|
4
|
+
}
|
|
5
|
+
/** Columns of 640x360 cells; the frames are 1920x1080. */
|
|
6
|
+
export declare function contactSheet(frames: readonly SheetFrame[], path: string, cols?: number): Promise<void>;
|
|
7
|
+
/** Several contact sheets in one Chrome (a probe round draws one per scene). */
|
|
8
|
+
export declare function contactSheets(sheets: ReadonlyArray<{
|
|
9
|
+
frames: readonly SheetFrame[];
|
|
10
|
+
path: string;
|
|
11
|
+
}>, cols?: number): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* The deck's copy of an illustration, and the draft call's copy with its
|
|
14
|
+
* subjects boxed and numbered, drawn on a canvas by the same Chrome. WebP,
|
|
15
|
+
* because a flat picture's PNG is 1-2 MB and its WebP at 0.84 is ~5-10% of
|
|
16
|
+
* that (round 3 shipped 7-10 MB of PNG per deck); the repository has no image
|
|
17
|
+
* encoder, and Chrome is already every gate's dependency.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pictureCopies(png: Buffer, boxes: ReadonlyArray<readonly [number, number, number, number]>, feather?: {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}, quality?: number): Promise<{
|
|
23
|
+
webp: Buffer;
|
|
24
|
+
boxed: Buffer;
|
|
25
|
+
feathered: boolean;
|
|
26
|
+
}>;
|