@jokerized/decksmith 0.2.0 → 0.3.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 +2 -1
- package/dist/cli.js +679 -132
- package/dist/ds-morph.js +1 -0
- package/dist/index.js +661 -130
- package/dist/mcp.js +374 -39
- 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/stack.d.ts +9 -0
- package/dist/types/emit/depth.d.ts +95 -0
- package/dist/types/emit/kit.d.ts +7 -0
- package/dist/types/emit/morph-runtime.d.ts +178 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mcp/tools.d.ts +12 -0
- package/dist/types/plan/arc.d.ts +82 -0
- package/dist/types/plan/codex.d.ts +6 -0
- package/dist/types/prefs.d.ts +1 -0
- package/dist/types/render/ffmpeg.d.ts +0 -14
- package/dist/types/types.d.ts +412 -0
- package/dist/types/verify/apparent.d.ts +123 -0
- package/dist/types/verify/index.d.ts +64 -0
- package/dist/types/verify/typefloor.d.ts +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -110,6 +110,13 @@ var equationWalkParamsSchema = z.object({
|
|
|
110
110
|
/** Walked in order, one hold-point each. */
|
|
111
111
|
terms: z.array(termSchema).min(1).max(4)
|
|
112
112
|
});
|
|
113
|
+
var equationMorphParamsSchema = z.object({
|
|
114
|
+
eyebrow: z.string().optional(),
|
|
115
|
+
headline: z.string(),
|
|
116
|
+
fromId: z.string(),
|
|
117
|
+
toId: z.string(),
|
|
118
|
+
terms: z.array(termSchema).min(1).max(4)
|
|
119
|
+
});
|
|
113
120
|
var dataTableParamsSchema = z.object({
|
|
114
121
|
eyebrow: z.string().optional(),
|
|
115
122
|
headline: z.string(),
|
|
@@ -238,7 +245,22 @@ var stackParamsSchema = z.object({
|
|
|
238
245
|
headline: z.string(),
|
|
239
246
|
/** Drawn bottom-up as offset planes, revealed in order. */
|
|
240
247
|
layers: z.array(z.object({ label: z.string(), note: z.string().optional() })).min(2).max(7),
|
|
241
|
-
note: z.string().optional()
|
|
248
|
+
note: z.string().optional(),
|
|
249
|
+
/**
|
|
250
|
+
* Tilt the slab stack away from the viewer, so the planes read as stacked in
|
|
251
|
+
* depth rather than merely offset up the page.
|
|
252
|
+
*
|
|
253
|
+
* OPTIONAL, and absent means flat — every storyboard written before this
|
|
254
|
+
* existed still parses, and every deck that does not ask for it emits exactly
|
|
255
|
+
* the bytes it did before.
|
|
256
|
+
*
|
|
257
|
+
* Degrees, and bounded at 18 rather than by taste: the tilt is paid for in
|
|
258
|
+
* declared type, because perspective shrinks the far half of the plane and
|
|
259
|
+
* invariant 5 is about what the audience SEES. At 18 degrees a 40px floor
|
|
260
|
+
* already needs 53.8px declared (`src/emit/depth.ts`), and past that a
|
|
261
|
+
* headline cannot spend enough and still fit its own line.
|
|
262
|
+
*/
|
|
263
|
+
tilt: z.number().min(0).max(18).optional()
|
|
242
264
|
});
|
|
243
265
|
var splitSideSchema = z.object({
|
|
244
266
|
label: z.string(),
|
|
@@ -271,10 +293,16 @@ var insideSchema = z.object({
|
|
|
271
293
|
*/
|
|
272
294
|
label: z.string().optional()
|
|
273
295
|
});
|
|
296
|
+
var beatRoleSchema = z.enum(["intro", "background", "limitations", "conclusion"]);
|
|
274
297
|
var beatCore = {
|
|
275
298
|
id: z.string(),
|
|
276
299
|
/** What the viewer should understand after this beat. */
|
|
277
300
|
intent: z.string(),
|
|
301
|
+
/**
|
|
302
|
+
* OPTIONAL, and only ever present when `prefs.genre` is `paper`. The
|
|
303
|
+
* structural job this beat does; see `beatRoleSchema`.
|
|
304
|
+
*/
|
|
305
|
+
role: beatRoleSchema.optional(),
|
|
278
306
|
/** Optional: this beat happens inside a named part of the beat before it. */
|
|
279
307
|
inside: insideSchema.optional(),
|
|
280
308
|
/** The source sentence or equation this beat is accountable to. */
|
|
@@ -303,6 +331,12 @@ var beatSchema = z.discriminatedUnion("archetype", [
|
|
|
303
331
|
params: equationWalkParamsSchema,
|
|
304
332
|
...beatTail
|
|
305
333
|
}),
|
|
334
|
+
z.object({
|
|
335
|
+
...beatCore,
|
|
336
|
+
archetype: z.literal("equation-morph"),
|
|
337
|
+
params: equationMorphParamsSchema,
|
|
338
|
+
...beatTail
|
|
339
|
+
}),
|
|
306
340
|
z.object({
|
|
307
341
|
...beatCore,
|
|
308
342
|
archetype: z.literal("data-table"),
|
|
@@ -357,6 +391,7 @@ var DIAGRAMMATIC = /* @__PURE__ */ new Set([
|
|
|
357
391
|
"stack",
|
|
358
392
|
"split-compare",
|
|
359
393
|
"equation-walk",
|
|
394
|
+
"equation-morph",
|
|
360
395
|
"line-chart"
|
|
361
396
|
]);
|
|
362
397
|
var ARCHETYPE_FAMILY = {
|
|
@@ -371,7 +406,8 @@ var ARCHETYPE_FAMILY = {
|
|
|
371
406
|
"bar-compare": "quantity",
|
|
372
407
|
"line-chart": "quantity",
|
|
373
408
|
"data-table": "quantity",
|
|
374
|
-
"equation-walk": "formal"
|
|
409
|
+
"equation-walk": "formal",
|
|
410
|
+
"equation-morph": "formal"
|
|
375
411
|
};
|
|
376
412
|
var storyboardSchema = z.object({
|
|
377
413
|
sourceId: z.string(),
|
|
@@ -413,6 +449,26 @@ var prefsSchema = z.object({
|
|
|
413
449
|
tone: z.enum(["plain", "academic", "conversational", "punchy"]).default("plain"),
|
|
414
450
|
/** How much text a slide may carry before it should have been a diagram. */
|
|
415
451
|
density: z.enum(["sparse", "normal", "dense"]).default("normal"),
|
|
452
|
+
/**
|
|
453
|
+
* What kind of document is being explained, DECLARED and never sniffed.
|
|
454
|
+
*
|
|
455
|
+
* `paper` asks the planner for the shape a research talk has: open on the
|
|
456
|
+
* problem and the ground the work stands on, close on what it does not do and
|
|
457
|
+
* then what to take away. `general` is every deck built before this existed
|
|
458
|
+
* and changes nothing — no prompt block, no `role` in the planner's schema, no
|
|
459
|
+
* scan.
|
|
460
|
+
*
|
|
461
|
+
* WHY DECLARED. A ten-role heading lexicon (en/ko/ja/zh, numbered-prefix
|
|
462
|
+
* tolerant) run over all 351 markdown files in this repository scored 345 of
|
|
463
|
+
* them at zero role hits and none at three or more. `src/source/markdown.ts`
|
|
464
|
+
* says why in its first line: the input is a hypepaper-style ANALYSIS of a
|
|
465
|
+
* paper, a rewrite that has already discarded the headings a detector would
|
|
466
|
+
* key on. A classifier here would be a guess with a confidence score attached,
|
|
467
|
+
* and it would guess wrong on the Korean fixture. So the author says so once —
|
|
468
|
+
* `--genre paper`, or one line in a `decksmith.config.json` above a directory
|
|
469
|
+
* of papers — and every run under it costs no further typing.
|
|
470
|
+
*/
|
|
471
|
+
genre: z.enum(["general", "paper"]).default("general"),
|
|
416
472
|
/**
|
|
417
473
|
* How long the finished thing should run, in seconds. Optional: absent means
|
|
418
474
|
* "as long as it takes", which is what every deck built before this did.
|
|
@@ -673,7 +729,10 @@ function selectBeats(storyboard, budget2, seconds = {}) {
|
|
|
673
729
|
const protectedIds = protect(live, len, cap);
|
|
674
730
|
const keep = knapsack(live, len, cap, protectedIds);
|
|
675
731
|
if (!keep) {
|
|
676
|
-
const
|
|
732
|
+
const ends = new Set(
|
|
733
|
+
[live[0]?.id, live[live.length - 1]?.id].filter((id2) => !!id2)
|
|
734
|
+
);
|
|
735
|
+
const all = knapsack(live, len, cap, ends) ?? knapsack(live, len, cap, /* @__PURE__ */ new Set());
|
|
677
736
|
const chosen = all ?? [live[0]];
|
|
678
737
|
return budgetDrops(live, chosen, dropped, storyboard, len, cap, budget2, false);
|
|
679
738
|
}
|
|
@@ -705,8 +764,17 @@ function protect(live, len, cap) {
|
|
|
705
764
|
}
|
|
706
765
|
const picture = cheapest && !ids.has(cheapest.id) ? [cheapest] : [];
|
|
707
766
|
for (const b of picture) ids.add(b.id);
|
|
767
|
+
const byId = new Map(live.map((b) => [b.id, b]));
|
|
768
|
+
const roled = [];
|
|
769
|
+
for (const b of live) {
|
|
770
|
+
if (!b.role) continue;
|
|
771
|
+
for (let hop = b; hop && !ids.has(hop.id); hop = byId.get(hop.inside?.beat ?? "")) {
|
|
772
|
+
ids.add(hop.id);
|
|
773
|
+
roled.push(hop);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
708
776
|
const ends = /* @__PURE__ */ new Set([first?.id, last?.id]);
|
|
709
|
-
const releasable = [...tier, ...coverage, ...picture].filter((b) => !ends.has(b.id)).sort((a, b) => rate(a) - rate(b) || len(b) - len(a));
|
|
777
|
+
const releasable = [...tier, ...coverage, ...picture, ...roled].filter((b) => !ends.has(b.id)).sort((a, b) => rate(a) - rate(b) || len(b) - len(a));
|
|
710
778
|
const cost = () => live.filter((b) => ids.has(b.id)).reduce((s, b) => s + len(b), 0);
|
|
711
779
|
for (const b of releasable) {
|
|
712
780
|
if (cost() <= cap) break;
|
|
@@ -3079,7 +3147,7 @@ function locate(tex, term) {
|
|
|
3079
3147
|
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
3080
3148
|
return { start, end: Math.min(end, tex.length) };
|
|
3081
3149
|
}
|
|
3082
|
-
function wrapTerms(tex, terms, beatId) {
|
|
3150
|
+
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
3083
3151
|
let parts = [{ text: tex, raw: true }];
|
|
3084
3152
|
const used = [];
|
|
3085
3153
|
const missing = [];
|
|
@@ -3095,7 +3163,7 @@ function wrapTerms(tex, terms, beatId) {
|
|
|
3095
3163
|
1,
|
|
3096
3164
|
{ text: part.text.slice(0, at.start), raw: true },
|
|
3097
3165
|
{
|
|
3098
|
-
text: `\\htmlClass{
|
|
3166
|
+
text: `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`,
|
|
3099
3167
|
raw: false
|
|
3100
3168
|
},
|
|
3101
3169
|
{ text: part.text.slice(at.end), raw: true }
|
|
@@ -3128,6 +3196,24 @@ function statements(tex, stacked) {
|
|
|
3128
3196
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
3129
3197
|
return parts.length > 0 ? parts : [tex];
|
|
3130
3198
|
}
|
|
3199
|
+
function legendRows(sid, terms, theme) {
|
|
3200
|
+
return terms.map(
|
|
3201
|
+
(t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
|
|
3202
|
+
).join("\n ");
|
|
3203
|
+
}
|
|
3204
|
+
function legendCss(theme) {
|
|
3205
|
+
return [
|
|
3206
|
+
// `width:fit-content` + auto margins, not `align-items:center`: centring
|
|
3207
|
+
// each row individually gave the legend a ragged left edge, because a short
|
|
3208
|
+
// label indented its own chip further than a long one did. The column is
|
|
3209
|
+
// centred as one block and the rows start on a shared spine.
|
|
3210
|
+
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3211
|
+
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3212
|
+
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3213
|
+
// are one symbol each and their natural widths differ by a few pixels.
|
|
3214
|
+
`.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
|
|
3215
|
+
].join("\n");
|
|
3216
|
+
}
|
|
3131
3217
|
var equationWalk = (beat, ctx) => {
|
|
3132
3218
|
const { sid, theme } = ctx;
|
|
3133
3219
|
const p = beat.params;
|
|
@@ -3139,9 +3225,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3139
3225
|
}
|
|
3140
3226
|
const walk = wrapTerms(eq.tex, p.terms, beat.id);
|
|
3141
3227
|
const terms = walk.used;
|
|
3142
|
-
const legend = terms
|
|
3143
|
-
(t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
|
|
3144
|
-
).join("\n ");
|
|
3228
|
+
const legend = legendRows(sid, terms, theme);
|
|
3145
3229
|
const stacked = isPortrait(ctx.format);
|
|
3146
3230
|
const raw2 = statements(eq.tex, stacked);
|
|
3147
3231
|
const shown = statements(walk.tex, stacked);
|
|
@@ -3240,15 +3324,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3240
3324
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
3241
3325
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
3242
3326
|
".term{display:inline-block}",
|
|
3243
|
-
|
|
3244
|
-
// each row individually gave the legend a ragged left edge, because a short
|
|
3245
|
-
// label indented its own chip further than a long one did. The column is
|
|
3246
|
-
// centred as one block and the rows start on a shared spine.
|
|
3247
|
-
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3248
|
-
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3249
|
-
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3250
|
-
// are one symbol each and their natural widths differ by a few pixels.
|
|
3251
|
-
`.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`,
|
|
3327
|
+
legendCss(theme),
|
|
3252
3328
|
// The block, not the term under discussion: which term that is, is a fact
|
|
3253
3329
|
// about the paused timeline, and CSS cannot see it. The terms are also the
|
|
3254
3330
|
// one thing here GSAP tints and swells, so a rule on them would win the
|
|
@@ -3258,6 +3334,117 @@ var equationWalk = (beat, ctx) => {
|
|
|
3258
3334
|
};
|
|
3259
3335
|
};
|
|
3260
3336
|
|
|
3337
|
+
// src/emit/archetypes/equation-morph.ts
|
|
3338
|
+
var MORPH_SECONDS = 1.6;
|
|
3339
|
+
var equationMorph = (beat, ctx) => {
|
|
3340
|
+
const { sid, theme } = ctx;
|
|
3341
|
+
const p = beat.params;
|
|
3342
|
+
const find2 = (id2) => {
|
|
3343
|
+
const eq = ctx.source.equations.find((e) => e.id === id2);
|
|
3344
|
+
if (!eq)
|
|
3345
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${ctx.source.id}`);
|
|
3346
|
+
return eq;
|
|
3347
|
+
};
|
|
3348
|
+
const a = find2(p.fromId);
|
|
3349
|
+
const b = find2(p.toId);
|
|
3350
|
+
const both = p.terms.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
3351
|
+
if (both.length === 0) {
|
|
3352
|
+
throw new Error(
|
|
3353
|
+
`equation-morph ${beat.id}: none of its ${p.terms.length} term(s) occur in both equations. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. From: ${JSON.stringify(a.tex)}. To: ${JSON.stringify(b.tex)}`
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
3357
|
+
const wa = wrapTerms(a.tex, both, beat.id, cls).tex;
|
|
3358
|
+
const wb = wrapTerms(b.tex, both, beat.id, cls).tex;
|
|
3359
|
+
const size3 = Math.max(
|
|
3360
|
+
MIN_FONT,
|
|
3361
|
+
Math.min(
|
|
3362
|
+
equationSize(a.tex.length > b.tex.length ? a.tex : b.tex),
|
|
3363
|
+
Math.floor(contentW(ctx.format) / Math.max(texUnits(a.tex), texUnits(b.tex)))
|
|
3364
|
+
)
|
|
3365
|
+
);
|
|
3366
|
+
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
3367
|
+
<div class="eqslide">
|
|
3368
|
+
<div class="morph" id="${sid}-morph" style="font-size:${size3}px">
|
|
3369
|
+
<div class="side" data-morph="a" id="${sid}-eqa"></div>
|
|
3370
|
+
<div class="side" data-morph="b" id="${sid}-eqb"></div>
|
|
3371
|
+
</div>
|
|
3372
|
+
<div class="legend">
|
|
3373
|
+
${legendRows(sid, both, theme)}
|
|
3374
|
+
</div>
|
|
3375
|
+
</div>`;
|
|
3376
|
+
const setup = [
|
|
3377
|
+
`var OPTS = ${OPTS};`,
|
|
3378
|
+
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
3379
|
+
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
3380
|
+
...both.map(
|
|
3381
|
+
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
3382
|
+
)
|
|
3383
|
+
];
|
|
3384
|
+
const first = 1.8;
|
|
3385
|
+
const at = Math.round(
|
|
3386
|
+
Math.max(2.6, Math.min(beat.seconds - MORPH_SECONDS - 0.9, beat.seconds * 0.45)) * 100
|
|
3387
|
+
) / 100;
|
|
3388
|
+
const tl = [
|
|
3389
|
+
...chromeIn(sid, p.eyebrow !== void 0),
|
|
3390
|
+
tween(`#${sid}-morph`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8),
|
|
3391
|
+
...both.map(
|
|
3392
|
+
(t2, i) => tween(
|
|
3393
|
+
`#${sid}-leg-${t2.tone}`,
|
|
3394
|
+
{ opacity: 0, x: -18 },
|
|
3395
|
+
{ opacity: 1, x: 0, duration: 0.5 },
|
|
3396
|
+
1 + i * 0.15
|
|
3397
|
+
)
|
|
3398
|
+
),
|
|
3399
|
+
// ONE tween, on the host, driving the plugin. Its ease is "none" because the
|
|
3400
|
+
// plan carries its own eases per segment; `pace` scales this duration and
|
|
3401
|
+
// the plan, being in fractions of it, scales with it.
|
|
3402
|
+
tween(
|
|
3403
|
+
`#${sid}-morph`,
|
|
3404
|
+
{ dsMorph: 0 },
|
|
3405
|
+
{ dsMorph: 1, duration: MORPH_SECONDS, ease: "none" },
|
|
3406
|
+
at
|
|
3407
|
+
)
|
|
3408
|
+
];
|
|
3409
|
+
return {
|
|
3410
|
+
html,
|
|
3411
|
+
tl,
|
|
3412
|
+
setup,
|
|
3413
|
+
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
3414
|
+
// the ready gate, and the plugin tween above finds it on the host.
|
|
3415
|
+
measure: [`DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
3416
|
+
plugins: ["dsMorph"],
|
|
3417
|
+
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
3418
|
+
css: [
|
|
3419
|
+
chromeCss(theme),
|
|
3420
|
+
".eqslide{display:flex;flex-direction:column;justify-content:space-evenly;gap:64px;flex:1;min-height:0}",
|
|
3421
|
+
".katex-display{margin:0 !important}",
|
|
3422
|
+
// Both sides in one grid cell, so the host is as tall as the taller line
|
|
3423
|
+
// and neither needs a guessed height; the overlay is absolute over it.
|
|
3424
|
+
// Padded by the room an arc needs, so a bowing glyph stays inside its
|
|
3425
|
+
// offset parent and the layout gate's `escaped_container` stays quiet; the
|
|
3426
|
+
// bow is capped to the same 0.8em in `plan`.
|
|
3427
|
+
`.morph{position:relative;display:grid;place-items:center;text-align:center;padding:0.8em 0.5em;color:${theme.fg}}`,
|
|
3428
|
+
".side{grid-area:1/1}",
|
|
3429
|
+
// B is measured, never seen: the runtime lifts its glyphs into the overlay
|
|
3430
|
+
// and drives them from there. Hidden by the sheet so nothing is captured
|
|
3431
|
+
// before the gate has built the plan.
|
|
3432
|
+
'.side[data-morph="b"]{visibility:hidden}',
|
|
3433
|
+
".ds-morph-layer{position:absolute;inset:0}",
|
|
3434
|
+
".term{display:inline-block}",
|
|
3435
|
+
// Keys are tinted from the start, on BOTH lines — the colour is what lets
|
|
3436
|
+
// a viewer follow a body across the move. Scoped under `.morph` because
|
|
3437
|
+
// `equation-walk` tweens `.t-<tone>` from the foreground colour, and a
|
|
3438
|
+
// bare rule on the class would win that cascade and cancel its walk.
|
|
3439
|
+
...["a", "b", "c", "d"].map(
|
|
3440
|
+
(tone2) => `.morph .t-${tone2}{color:${theme.tones[tone2]}}`
|
|
3441
|
+
),
|
|
3442
|
+
legendCss(theme),
|
|
3443
|
+
ambient(sid, "-morph", BREATHE)
|
|
3444
|
+
].join("\n")
|
|
3445
|
+
};
|
|
3446
|
+
};
|
|
3447
|
+
|
|
3261
3448
|
// src/emit/archetypes/grid.ts
|
|
3262
3449
|
var LABEL = 42;
|
|
3263
3450
|
var LH = 1.25;
|
|
@@ -4541,9 +4728,36 @@ var splitCompare = (beat, ctx) => {
|
|
|
4541
4728
|
};
|
|
4542
4729
|
};
|
|
4543
4730
|
|
|
4731
|
+
// src/emit/depth.ts
|
|
4732
|
+
var DEFAULT_POSE = { rotateX: 12, perspective: 1400 };
|
|
4733
|
+
function scaleAt(pose, dy) {
|
|
4734
|
+
const t2 = pose.rotateX * Math.PI / 180;
|
|
4735
|
+
const denom = pose.perspective - dy * Math.sin(t2);
|
|
4736
|
+
if (denom <= 0) return 0;
|
|
4737
|
+
return Math.cos(t2) * (pose.perspective / denom) ** 2;
|
|
4738
|
+
}
|
|
4739
|
+
var MODEL_SLACK = 0.98;
|
|
4740
|
+
function worstScale(pose, height) {
|
|
4741
|
+
if (scaleAt(pose, height / 2) <= 0) return 0;
|
|
4742
|
+
return scaleAt(pose, -height / 2) * MODEL_SLACK;
|
|
4743
|
+
}
|
|
4744
|
+
function tiltedFloor(pose, height, floor) {
|
|
4745
|
+
const s = worstScale(pose, height);
|
|
4746
|
+
return s > 0 ? floor / s : Number.POSITIVE_INFINITY;
|
|
4747
|
+
}
|
|
4748
|
+
function depthCss(sid, pose, part) {
|
|
4749
|
+
return [
|
|
4750
|
+
`#${sid} { perspective: ${pose.perspective}px; }`,
|
|
4751
|
+
`#${sid} ${part} { transform: rotateX(${pose.rotateX}deg); transform-origin: 50% 50%; }`
|
|
4752
|
+
].join("\n");
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4544
4755
|
// src/emit/archetypes/stack.ts
|
|
4545
4756
|
var GAP3 = 44;
|
|
4546
4757
|
var NUM_X = 48;
|
|
4758
|
+
function numSpine(floor) {
|
|
4759
|
+
return Math.round(NUM_X * floor * 100 / MIN_FONT) / 100;
|
|
4760
|
+
}
|
|
4547
4761
|
var PROBE_X = 16;
|
|
4548
4762
|
var PROBE_H = 46;
|
|
4549
4763
|
var NUM_W = 70;
|
|
@@ -4564,15 +4778,20 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4564
4778
|
function labelWeight(i, count) {
|
|
4565
4779
|
return i === count - 1 ? 700 : 600;
|
|
4566
4780
|
}
|
|
4781
|
+
function floorFor(p, format) {
|
|
4782
|
+
if (!p.tilt) return MIN_FONT;
|
|
4783
|
+
return tiltedFloor({ ...DEFAULT_POSE, rotateX: p.tilt }, contentH(format), MIN_FONT);
|
|
4784
|
+
}
|
|
4567
4785
|
function solve2(p, format, inline, face) {
|
|
4568
4786
|
const width = contentW(format);
|
|
4569
4787
|
const boxH = contentH(format);
|
|
4788
|
+
const floor = floorFor(p, format);
|
|
4570
4789
|
const count = p.layers.length;
|
|
4571
4790
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4572
4791
|
const riseMax = RISE_MAX[k];
|
|
4573
4792
|
const syMax = SY_MAX[k];
|
|
4574
4793
|
const tMax = T_MAX[k];
|
|
4575
|
-
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note,
|
|
4794
|
+
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4576
4795
|
const want = Math.max(
|
|
4577
4796
|
...p.layers.map(
|
|
4578
4797
|
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count), 0, false, face) + noteW(l)
|
|
@@ -4587,7 +4806,7 @@ function solve2(p, format, inline, face) {
|
|
|
4587
4806
|
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count), 0, false, face))
|
|
4588
4807
|
)
|
|
4589
4808
|
);
|
|
4590
|
-
const labelSize = Math.max(
|
|
4809
|
+
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
4591
4810
|
const lines = p.layers.map((l, i) => {
|
|
4592
4811
|
const nw = noteW(l);
|
|
4593
4812
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
@@ -4595,14 +4814,14 @@ function solve2(p, format, inline, face) {
|
|
|
4595
4814
|
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count), 0, face),
|
|
4596
4815
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4597
4816
|
// short line" — and wrapping one would put its second line under the label.
|
|
4598
|
-
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note,
|
|
4817
|
+
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
4599
4818
|
noteW: nw,
|
|
4600
4819
|
labelMaxW
|
|
4601
4820
|
};
|
|
4602
4821
|
});
|
|
4603
4822
|
const blockH = Math.max(
|
|
4604
4823
|
...lines.map(
|
|
4605
|
-
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length *
|
|
4824
|
+
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length * floor) * 1.16 : l.label.length * labelSize * 1.16 + (l.note.length > 0 ? 6 + l.note.length * floor * 1.16 : 0)
|
|
4606
4825
|
)
|
|
4607
4826
|
);
|
|
4608
4827
|
const pad = Math.max(EDGE2, blockH / 2);
|
|
@@ -4622,6 +4841,7 @@ function solve2(p, format, inline, face) {
|
|
|
4622
4841
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
4623
4842
|
// layout that fits; it is one whose overflow is in the axis nothing measured.
|
|
4624
4843
|
fits: room >= blockH + 10 && height <= free && wide,
|
|
4844
|
+
floor,
|
|
4625
4845
|
wide,
|
|
4626
4846
|
inline,
|
|
4627
4847
|
width,
|
|
@@ -4661,7 +4881,7 @@ var stack = (beat, ctx) => {
|
|
|
4661
4881
|
const last = count - 1;
|
|
4662
4882
|
if (!L.fits) {
|
|
4663
4883
|
throw new Error(
|
|
4664
|
-
`stack ${beat.id}: ${count} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${
|
|
4884
|
+
`stack ${beat.id}: ${count} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${Math.round(L.floor)}px floor and with the notes already moved beside their labels. Nothing here may be set smaller, so the lever is upstream of this beat: drop a layer, or shorten the notes.`
|
|
4665
4885
|
);
|
|
4666
4886
|
}
|
|
4667
4887
|
const parts = {};
|
|
@@ -4683,7 +4903,7 @@ var stack = (beat, ctx) => {
|
|
|
4683
4903
|
const dot = circle({ x: L.x0 + L.w + L.sx / 2 + 8, y: mid }, 6, { fill: tint });
|
|
4684
4904
|
const block = L.lines[i] ?? { label: [], note: [], noteW: 0, labelMaxW: L.colW };
|
|
4685
4905
|
const labelH = block.label.length * L.labelSize * 1.16;
|
|
4686
|
-
const noteH = block.note.length > 0 ? 6 + block.note.length *
|
|
4906
|
+
const noteH = block.note.length > 0 ? 6 + block.note.length * L.floor * 1.16 : 0;
|
|
4687
4907
|
const label = text(
|
|
4688
4908
|
layer.label,
|
|
4689
4909
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
@@ -4709,7 +4929,7 @@ var stack = (beat, ctx) => {
|
|
|
4709
4929
|
{ x: L.labelX, y: mid + labelH / 2 + 3 }
|
|
4710
4930
|
),
|
|
4711
4931
|
{
|
|
4712
|
-
size:
|
|
4932
|
+
size: L.floor,
|
|
4713
4933
|
fill: theme.muted,
|
|
4714
4934
|
anchor: L.inline ? "end" : "start",
|
|
4715
4935
|
maxWidth: L.inline ? void 0 : L.colW,
|
|
@@ -4720,8 +4940,8 @@ var stack = (beat, ctx) => {
|
|
|
4720
4940
|
);
|
|
4721
4941
|
const num = text(
|
|
4722
4942
|
String(i + 1),
|
|
4723
|
-
{ x:
|
|
4724
|
-
{ size:
|
|
4943
|
+
{ x: numSpine(L.floor), y: mid },
|
|
4944
|
+
{ size: L.floor, weight: 600, fill: theme.dim, anchor: "end", vAlign: "middle" }
|
|
4725
4945
|
);
|
|
4726
4946
|
return group(slab(L.x0, y0, L, tint, lift2, stroke), { id: id(sid, "lay", i), class: "lay" }) + group(num + leader + dot + label + note2, { id: id(sid, "cap", i), class: "cap" });
|
|
4727
4947
|
}).join("");
|
|
@@ -4743,6 +4963,8 @@ var stack = (beat, ctx) => {
|
|
|
4743
4963
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4744
4964
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4745
4965
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
|
|
4966
|
+
const centre2 = (count - 1) / 2;
|
|
4967
|
+
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4746
4968
|
const first = 0.9;
|
|
4747
4969
|
const step2 = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count));
|
|
4748
4970
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
@@ -4753,7 +4975,7 @@ var stack = (beat, ctx) => {
|
|
|
4753
4975
|
tl.push(
|
|
4754
4976
|
tween(
|
|
4755
4977
|
`#${sid}-lay${i}`,
|
|
4756
|
-
{ opacity: 0, y:
|
|
4978
|
+
{ opacity: 0, y: enterFrom(i) },
|
|
4757
4979
|
{ opacity: 1, y: 0, duration: 0.55, ease: "power2.out" },
|
|
4758
4980
|
at
|
|
4759
4981
|
)
|
|
@@ -4796,8 +5018,12 @@ var stack = (beat, ctx) => {
|
|
|
4796
5018
|
// The top plane is the focal point — last built, differently toned, and the
|
|
4797
5019
|
// one the final hold sits on. Its entrance owns `opacity` and `transform`,
|
|
4798
5020
|
// so the breath takes `filter`, the property nothing else writes.
|
|
4799
|
-
ambient(sid, `-lay${last}`, BREATHE)
|
|
4800
|
-
|
|
5021
|
+
ambient(sid, `-lay${last}`, BREATHE),
|
|
5022
|
+
// Absent unless the beat asked for it, so a flat stack emits the bytes it
|
|
5023
|
+
// always did.
|
|
5024
|
+
// `.stackwrap` and not the scene: the slabs lean, the headline does not.
|
|
5025
|
+
p.tilt ? depthCss(sid, { ...DEFAULT_POSE, rotateX: p.tilt }, ".stackwrap") : ""
|
|
5026
|
+
].filter(Boolean).join("\n")
|
|
4801
5027
|
};
|
|
4802
5028
|
};
|
|
4803
5029
|
|
|
@@ -4812,6 +5038,7 @@ var emitters = {
|
|
|
4812
5038
|
stack,
|
|
4813
5039
|
"split-compare": splitCompare,
|
|
4814
5040
|
"equation-walk": equationWalk,
|
|
5041
|
+
"equation-morph": equationMorph,
|
|
4815
5042
|
"line-chart": lineChart,
|
|
4816
5043
|
// The ones that describe.
|
|
4817
5044
|
title,
|
|
@@ -5004,6 +5231,7 @@ function round4(n3) {
|
|
|
5004
5231
|
// src/emit/composition.ts
|
|
5005
5232
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5006
5233
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5234
|
+
var MORPH_SRC = "./vendor/ds-morph.js";
|
|
5007
5235
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5008
5236
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5009
5237
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5094,6 +5322,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5094
5322
|
});
|
|
5095
5323
|
let start = 0;
|
|
5096
5324
|
let builds = false;
|
|
5325
|
+
const plugins = /* @__PURE__ */ new Set();
|
|
5097
5326
|
cuts.forEach((cut2, i) => {
|
|
5098
5327
|
const { beat, sid, dive, inside, duration } = cut2;
|
|
5099
5328
|
if (cut2.segments?.length) spoken[sid] = cut2.segments;
|
|
@@ -5108,6 +5337,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5108
5337
|
}
|
|
5109
5338
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5110
5339
|
if (scene.measure?.length) builds = true;
|
|
5340
|
+
for (const p of scene.plugins ?? []) plugins.add(p);
|
|
5111
5341
|
scenes.push(
|
|
5112
5342
|
sceneHtml(
|
|
5113
5343
|
sid,
|
|
@@ -5139,7 +5369,8 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5139
5369
|
spoken,
|
|
5140
5370
|
total: start,
|
|
5141
5371
|
cut,
|
|
5142
|
-
builds
|
|
5372
|
+
builds,
|
|
5373
|
+
plugins
|
|
5143
5374
|
};
|
|
5144
5375
|
}
|
|
5145
5376
|
function enteredParts(beats) {
|
|
@@ -5220,6 +5451,9 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5220
5451
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5221
5452
|
const island = format.navigable ? `
|
|
5222
5453
|
${emitIsland(slides)}` : "";
|
|
5454
|
+
const morph = laid.plugins.has("dsMorph") ? `
|
|
5455
|
+
<script src="${MORPH_SRC}"></script>
|
|
5456
|
+
<script>gsap.registerPlugin(DSMorphPlugin);</script>` : "";
|
|
5223
5457
|
return `<!doctype html>
|
|
5224
5458
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5225
5459
|
<head>
|
|
@@ -5228,7 +5462,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5228
5462
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5229
5463
|
<script src="${GSAP_SRC}"></script>
|
|
5230
5464
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5231
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script
|
|
5465
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${morph}
|
|
5232
5466
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5233
5467
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5234
5468
|
<style>
|
|
@@ -5476,6 +5710,76 @@ import { tmpdir } from "node:os";
|
|
|
5476
5710
|
import { join as join3 } from "node:path";
|
|
5477
5711
|
import { z as z2 } from "zod";
|
|
5478
5712
|
|
|
5713
|
+
// src/plan/arc.ts
|
|
5714
|
+
var ARC_ROLES = ["intro", "background", "limitations", "conclusion"];
|
|
5715
|
+
function requiredRoles(beatCount) {
|
|
5716
|
+
if (beatCount >= 8) return ARC_ROLES;
|
|
5717
|
+
if (beatCount >= 5) return ["limitations", "conclusion"];
|
|
5718
|
+
return [];
|
|
5719
|
+
}
|
|
5720
|
+
function paperArcRequested(prefs) {
|
|
5721
|
+
return prefs.genre === "paper";
|
|
5722
|
+
}
|
|
5723
|
+
function arcBeats(storyboard) {
|
|
5724
|
+
const by = /* @__PURE__ */ new Map();
|
|
5725
|
+
for (const beat of storyboard.beats) {
|
|
5726
|
+
if (!beat.role) continue;
|
|
5727
|
+
by.set(beat.role, [...by.get(beat.role) ?? [], beat]);
|
|
5728
|
+
}
|
|
5729
|
+
return by;
|
|
5730
|
+
}
|
|
5731
|
+
function arcProblems(storyboard, prefs) {
|
|
5732
|
+
if (!paperArcRequested(prefs)) return [];
|
|
5733
|
+
const beats = storyboard.beats;
|
|
5734
|
+
const need = requiredRoles(Math.min(prefs.slides, beats.length));
|
|
5735
|
+
if (!need.length) return [];
|
|
5736
|
+
const out = [];
|
|
5737
|
+
const by = arcBeats(storyboard);
|
|
5738
|
+
for (const role of need) {
|
|
5739
|
+
const held = by.get(role) ?? [];
|
|
5740
|
+
if (held.length === 0) {
|
|
5741
|
+
out.push(
|
|
5742
|
+
`No beat carries role "${role}". A paper deck is meant to have one, and the source may not support it \u2014 write the beat, or drop \`--genre paper\` for this document.`
|
|
5743
|
+
);
|
|
5744
|
+
continue;
|
|
5745
|
+
}
|
|
5746
|
+
if (held.length > 1) {
|
|
5747
|
+
out.push(
|
|
5748
|
+
`${held.length} beats carry role "${role}" (${held.map((b) => b.id).join(", ")}). A structural job belongs to one slide.`
|
|
5749
|
+
);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
const last = beats[beats.length - 1];
|
|
5753
|
+
if (need.includes("conclusion") && by.has("conclusion") && last?.role !== "conclusion") {
|
|
5754
|
+
out.push(
|
|
5755
|
+
`The deck ends on "${last?.id}" (${last?.archetype}), not on its conclusion. The conclusion is the last thing the viewer sees or it is not a conclusion.`
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
const limIdx = beats.findIndex((b) => b.role === "limitations");
|
|
5759
|
+
const conIdx = beats.findIndex((b) => b.role === "conclusion");
|
|
5760
|
+
if (need.includes("limitations") && limIdx >= 0 && conIdx >= 0 && limIdx !== conIdx - 1) {
|
|
5761
|
+
out.push(
|
|
5762
|
+
`The limitations beat is not the slide immediately before the conclusion. The two are a pair and the caveat comes first.`
|
|
5763
|
+
);
|
|
5764
|
+
}
|
|
5765
|
+
const openingWindow = beats.slice(0, 3).map((b) => b.role);
|
|
5766
|
+
for (const role of need.filter((r) => r === "intro" || r === "background")) {
|
|
5767
|
+
if (by.has(role) && !openingWindow.includes(role)) {
|
|
5768
|
+
out.push(
|
|
5769
|
+
`The "${role}" beat is not in the first three slides. It is what the rest of the deck is understood against, so it has to arrive before the mechanism does.`
|
|
5770
|
+
);
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
const lim = by.get("limitations")?.[0];
|
|
5774
|
+
const con = by.get("conclusion")?.[0];
|
|
5775
|
+
if (lim && con && lim.archetype === con.archetype) {
|
|
5776
|
+
out.push(
|
|
5777
|
+
`The limitations and conclusion beats are both \`${lim.archetype}\`. Two of the same picture running is what RULE 1 forbids; draw the conclusion where the source states it.`
|
|
5778
|
+
);
|
|
5779
|
+
}
|
|
5780
|
+
return out;
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5479
5783
|
// src/plan/duration.ts
|
|
5480
5784
|
var SPEECH_CPS = { latin: 14.4, cjk: 6.5 };
|
|
5481
5785
|
var LAST_HOLD_SECONDS = 4.2;
|
|
@@ -5672,6 +5976,7 @@ var REVEALS = {
|
|
|
5672
5976
|
title: "1",
|
|
5673
5977
|
"claim-figure": "2",
|
|
5674
5978
|
"equation-walk": "one per term",
|
|
5979
|
+
"equation-morph": "2",
|
|
5675
5980
|
"data-table": "one per highlighted row, plus 1",
|
|
5676
5981
|
"line-chart": "1",
|
|
5677
5982
|
callout: "one per panel",
|
|
@@ -5703,9 +6008,9 @@ A beat is one idea, one visual, one hold. It carries:
|
|
|
5703
6008
|
beat immediately before it. See RULE 11. Leave it off unless the
|
|
5704
6009
|
source itself puts one inside the other.
|
|
5705
6010
|
|
|
5706
|
-
THE
|
|
6011
|
+
THE THIRTEEN ARCHETYPES
|
|
5707
6012
|
|
|
5708
|
-
|
|
6013
|
+
Nine of them DRAW: they build a vector graphic out of the source's own content
|
|
5709
6014
|
and reveal it stage by stage, so the viewer watches the idea assemble. Four only
|
|
5710
6015
|
describe. The drawing ones are the default. The describing ones are what you
|
|
5711
6016
|
fall back to when a point genuinely has no shape.
|
|
@@ -5783,6 +6088,14 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
5783
6088
|
An equation quoted to back a claim someone else is making is
|
|
5784
6089
|
evidence under another archetype, not a beat of its own.
|
|
5785
6090
|
|
|
6091
|
+
equation-morph One equation becoming the next, the shared terms carried
|
|
6092
|
+
across. The tell: THE SOURCE DERIVES ONE LINE FROM ANOTHER \u2014 a
|
|
6093
|
+
substitution, a rearrangement, a special case \u2014 and the point
|
|
6094
|
+
is what moved. \`fromId\` and \`toId\` name two equations from
|
|
6095
|
+
the inventory. Each terms[].tex must appear verbatim in BOTH,
|
|
6096
|
+
and travels as one piece; a term in only one of them is
|
|
6097
|
+
dropped. Four terms maximum.
|
|
6098
|
+
|
|
5786
6099
|
line-chart A trend the source states numerically but does not plot. The
|
|
5787
6100
|
tell: A QUANTITY MOVING ALONG AN ORDERED AXIS \u2014 over length, over
|
|
5788
6101
|
scale, over training. Points come from the source's numbers;
|
|
@@ -6056,6 +6369,46 @@ ${REVEAL_COUNTS}` : ` - ${n3 === 1 ? "ONE SENTENCE" : `${n3} SENTENCES`} FOR TH
|
|
|
6056
6369
|
miss its duration.`;
|
|
6057
6370
|
return { sentences, length };
|
|
6058
6371
|
}
|
|
6372
|
+
function paperArc(slides) {
|
|
6373
|
+
const asked = requiredRoles(slides);
|
|
6374
|
+
const full = asked.includes("intro");
|
|
6375
|
+
return `
|
|
6376
|
+
|
|
6377
|
+
PAPER ARC \u2014 this source was declared a research paper.
|
|
6378
|
+
|
|
6379
|
+
Four beats have a structural job, and each one NAMES its job in \`role\`. Every
|
|
6380
|
+
other beat leaves \`role\` off. A role is a job, not a heading: never write
|
|
6381
|
+
"Related work" or "Conclusion" as a headline, because RULE 8 still applies to
|
|
6382
|
+
all four.
|
|
6383
|
+
${full ? `
|
|
6384
|
+
role: "intro" Near the front. What problem exists and who has it, in
|
|
6385
|
+
the viewer's own terms. This is the opening RULE 6
|
|
6386
|
+
already asks for, named so the deck can be checked.
|
|
6387
|
+
role: "background" In the first three beats. What people did before this
|
|
6388
|
+
work, and where that ran out. Take it from what the
|
|
6389
|
+
source itself says about earlier approaches \u2014 if the
|
|
6390
|
+
source says nothing about them, leave the role off
|
|
6391
|
+
rather than inventing a literature (RULE 3).` : `
|
|
6392
|
+
This deck is short, so only the ENDING is required \u2014 an opening the deck
|
|
6393
|
+
already has is not worth a slide of its own here.`}
|
|
6394
|
+
role: "limitations" THE SECOND-TO-LAST beat. What the work does not do, in
|
|
6395
|
+
the source's own admission. Not a hedge inside another
|
|
6396
|
+
beat's sentence: its own slide.
|
|
6397
|
+
role: "conclusion" THE LAST beat, with nothing after it. What the viewer
|
|
6398
|
+
should carry away.
|
|
6399
|
+
|
|
6400
|
+
- The closing pair is TWO beats and they must not share an archetype (RULE 1).
|
|
6401
|
+
A limitation the source admits to is usually a panel; the conclusion is the
|
|
6402
|
+
claim the deck lands, so draw it where the source states it \u2014 bars, a
|
|
6403
|
+
contrast, the figure that settles it \u2014 and fall back to a panel only when it
|
|
6404
|
+
genuinely has no shape.
|
|
6405
|
+
- Give all four a weight of 0.8 or above. A short cut keeps the
|
|
6406
|
+
highest-weighted beats, and a structural beat below 0.8 is one the deck
|
|
6407
|
+
loses at the first budget.
|
|
6408
|
+
- If the source does not support one of these, LEAVE IT OUT. A slide that
|
|
6409
|
+
admits a limitation the paper never admits is worse than no slide.
|
|
6410
|
+
`;
|
|
6411
|
+
}
|
|
6059
6412
|
function illustrations(images) {
|
|
6060
6413
|
return `
|
|
6061
6414
|
|
|
@@ -6084,7 +6437,7 @@ nothing, so it can never dangle.
|
|
|
6084
6437
|
}
|
|
6085
6438
|
function systemPrompt(prefs) {
|
|
6086
6439
|
const plan = durationPlan(prefs);
|
|
6087
|
-
return `${rules(cadenceFor(prefs, plan))}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6440
|
+
return `${rules(cadenceFor(prefs, plan))}${paperArcRequested(prefs) ? paperArc(prefs.slides) : ""}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6088
6441
|
|
|
6089
6442
|
PREFERENCES \u2014 chosen by the person who asked for this deck.
|
|
6090
6443
|
${prefs.duration === void 0 ? "" : `
|
|
@@ -6181,7 +6534,8 @@ function renderSource(source) {
|
|
|
6181
6534
|
out.push("", "== EQUATIONS ==");
|
|
6182
6535
|
for (const e of source.equations)
|
|
6183
6536
|
out.push(`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${e.tex}`);
|
|
6184
|
-
if (!source.equations.length)
|
|
6537
|
+
if (!source.equations.length)
|
|
6538
|
+
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
6185
6539
|
out.push("", "== TABLES ==");
|
|
6186
6540
|
for (const t2 of source.tables) {
|
|
6187
6541
|
out.push(`[table ${t2.id}] ${t2.caption ?? "(no caption)"}`);
|
|
@@ -6253,6 +6607,10 @@ function assertRefsResolve(storyboard, source, opts = {}) {
|
|
|
6253
6607
|
case "equation-walk":
|
|
6254
6608
|
check3(beat, "equation", beat.params.equationId, "params.equationId");
|
|
6255
6609
|
break;
|
|
6610
|
+
case "equation-morph":
|
|
6611
|
+
check3(beat, "equation", beat.params.fromId, "params.fromId");
|
|
6612
|
+
check3(beat, "equation", beat.params.toId, "params.toId");
|
|
6613
|
+
break;
|
|
6256
6614
|
case "data-table": {
|
|
6257
6615
|
check3(beat, "table", beat.params.tableId, "params.tableId");
|
|
6258
6616
|
const table = source.tables.find((t2) => t2.id === beat.params.tableId);
|
|
@@ -6389,14 +6747,45 @@ function stripNulls(node) {
|
|
|
6389
6747
|
}
|
|
6390
6748
|
return out;
|
|
6391
6749
|
}
|
|
6392
|
-
var
|
|
6750
|
+
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
6751
|
+
function plannerInvisible(prefs) {
|
|
6752
|
+
return paperArcRequested(prefs) ? PLANNER_INVISIBLE : /* @__PURE__ */ new Set([...PLANNER_INVISIBLE, "role"]);
|
|
6753
|
+
}
|
|
6754
|
+
function hideFromPlanner(node, hidden) {
|
|
6755
|
+
if (Array.isArray(node)) return node.map((n3) => hideFromPlanner(n3, hidden));
|
|
6756
|
+
if (node === null || typeof node !== "object") return node;
|
|
6757
|
+
const src = node;
|
|
6758
|
+
const out = {};
|
|
6759
|
+
for (const [key, value] of Object.entries(src)) {
|
|
6760
|
+
if (key === "properties" && value && typeof value === "object") {
|
|
6761
|
+
const kept = {};
|
|
6762
|
+
for (const [prop, sub] of Object.entries(value))
|
|
6763
|
+
if (!hidden.has(prop)) kept[prop] = hideFromPlanner(sub, hidden);
|
|
6764
|
+
out.properties = kept;
|
|
6765
|
+
continue;
|
|
6766
|
+
}
|
|
6767
|
+
if (key === "required" && Array.isArray(value)) {
|
|
6768
|
+
out.required = value.filter((r) => typeof r !== "string" || !hidden.has(r));
|
|
6769
|
+
continue;
|
|
6770
|
+
}
|
|
6771
|
+
out[key] = hideFromPlanner(value, hidden);
|
|
6772
|
+
}
|
|
6773
|
+
return out;
|
|
6774
|
+
}
|
|
6775
|
+
function schemaFor(prefs) {
|
|
6776
|
+
return hideFromPlanner(
|
|
6777
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" })),
|
|
6778
|
+
plannerInvisible(prefs)
|
|
6779
|
+
);
|
|
6780
|
+
}
|
|
6781
|
+
var SCHEMA = schemaFor({ genre: "general" });
|
|
6393
6782
|
async function codexPlanner(source, opts = {}) {
|
|
6394
6783
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
6395
6784
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
6396
6785
|
try {
|
|
6397
6786
|
const schemaPath = join3(dir, "storyboard.schema.json");
|
|
6398
6787
|
const outPath = join3(dir, "storyboard.json");
|
|
6399
|
-
await writeFile3(schemaPath, JSON.stringify(
|
|
6788
|
+
await writeFile3(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
6400
6789
|
await (opts.run ?? runCodex)({
|
|
6401
6790
|
prompt: buildPrompt(source, prefs),
|
|
6402
6791
|
schemaPath,
|
|
@@ -7461,6 +7850,7 @@ function prefsFromFlags(flags2) {
|
|
|
7461
7850
|
if (flags2.lang !== void 0) patch.lang = flags2.lang;
|
|
7462
7851
|
if (flags2.tone !== void 0) patch.tone = flags2.tone;
|
|
7463
7852
|
if (flags2.density !== void 0) patch.density = flags2.density;
|
|
7853
|
+
if (flags2.genre !== void 0) patch.genre = flags2.genre;
|
|
7464
7854
|
if (flags2.duration !== void 0) patch.duration = number("--duration", flags2.duration);
|
|
7465
7855
|
if (flags2.theme !== void 0) patch.theme = flags2.theme;
|
|
7466
7856
|
if (flags2.speed !== void 0) patch.animationSpeed = number("--speed", flags2.speed);
|
|
@@ -8212,13 +8602,14 @@ function pieceArgs(source, fromFrame, motion, freeze, fps, out) {
|
|
|
8212
8602
|
out
|
|
8213
8603
|
];
|
|
8214
8604
|
}
|
|
8605
|
+
var LOUDNESS = "loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000";
|
|
8215
8606
|
function audioGraph(inputs, seconds, first = 1) {
|
|
8216
8607
|
const lines = inputs.map(
|
|
8217
8608
|
(input, i) => `[${first + i}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${input.delayMs}:all=1[d${i}]`
|
|
8218
8609
|
);
|
|
8219
8610
|
const labels = inputs.map((_, i) => `[d${i}]`).join("");
|
|
8220
8611
|
lines.push(
|
|
8221
|
-
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
8612
|
+
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,${LOUDNESS},apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
8222
8613
|
);
|
|
8223
8614
|
return lines.join(";\n");
|
|
8224
8615
|
}
|
|
@@ -8932,6 +9323,178 @@ function tail(s) {
|
|
|
8932
9323
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
8933
9324
|
import { join as join12 } from "node:path";
|
|
8934
9325
|
|
|
9326
|
+
// src/verify/typefloor.ts
|
|
9327
|
+
var TYPE_FLOOR_PX = 40;
|
|
9328
|
+
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
9329
|
+
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
9330
|
+
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
9331
|
+
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
9332
|
+
const findings = [];
|
|
9333
|
+
const zones = svgZones(html);
|
|
9334
|
+
const small = [];
|
|
9335
|
+
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
9336
|
+
pattern.lastIndex = 0;
|
|
9337
|
+
for (const m of html.matchAll(pattern)) {
|
|
9338
|
+
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
9339
|
+
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
9340
|
+
}
|
|
9341
|
+
}
|
|
9342
|
+
if (small.length > 0) {
|
|
9343
|
+
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
9344
|
+
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
9345
|
+
findings.push({
|
|
9346
|
+
severity: "error",
|
|
9347
|
+
gate: "typography",
|
|
9348
|
+
rule: "type_below_floor",
|
|
9349
|
+
message: `${file} declares ${small.length} text size(s) under the ${floorPx}px floor: ${named}${worst.length > 6 ? ", \u2026" : ""}. Sizes are in reference space, so this is the floor as authored and not an artefact of the format's zoom. Raise the size, or give the archetype less to say \u2014 a beat that only fits below the floor is a beat that has to be cut in two.`
|
|
9350
|
+
});
|
|
9351
|
+
}
|
|
9352
|
+
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
9353
|
+
if (units.length > 0) {
|
|
9354
|
+
findings.push({
|
|
9355
|
+
severity: "warning",
|
|
9356
|
+
gate: "typography",
|
|
9357
|
+
rule: "type_unmeasurable",
|
|
9358
|
+
message: `${file} sizes some text in ${units.join("/")}, which resolves against an inherited size this scan cannot follow, so the ${floorPx}px floor was not checked there. Declare it in px, or measure it in a browser.`
|
|
9359
|
+
});
|
|
9360
|
+
}
|
|
9361
|
+
return findings;
|
|
9362
|
+
}
|
|
9363
|
+
function svgZones(html) {
|
|
9364
|
+
const zones = [];
|
|
9365
|
+
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
9366
|
+
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
9367
|
+
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
9368
|
+
const close = html.indexOf("</svg>", m.index);
|
|
9369
|
+
zones.push({
|
|
9370
|
+
start: m.index,
|
|
9371
|
+
end: close < 0 ? html.length : close,
|
|
9372
|
+
unit: width > 0 && box > 0 ? width / box : 1
|
|
9373
|
+
});
|
|
9374
|
+
}
|
|
9375
|
+
return zones;
|
|
9376
|
+
}
|
|
9377
|
+
function userUnit(zones, at) {
|
|
9378
|
+
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
9379
|
+
}
|
|
9380
|
+
function where(html, at) {
|
|
9381
|
+
const open = html.lastIndexOf("<", at);
|
|
9382
|
+
const closed = html.lastIndexOf(">", at);
|
|
9383
|
+
if (open > closed) {
|
|
9384
|
+
const end = html.indexOf(">", at);
|
|
9385
|
+
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
9386
|
+
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
9387
|
+
if (id2) return `#${id2}`;
|
|
9388
|
+
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
9389
|
+
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
9390
|
+
return cls ? `${name}.${cls}` : name;
|
|
9391
|
+
}
|
|
9392
|
+
const brace = html.lastIndexOf("{", at);
|
|
9393
|
+
if (brace < 0) return "the stylesheet";
|
|
9394
|
+
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
9395
|
+
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
9396
|
+
return selector ? selector : "the stylesheet";
|
|
9397
|
+
}
|
|
9398
|
+
function round6(px) {
|
|
9399
|
+
return Math.round(px * 100) / 100;
|
|
9400
|
+
}
|
|
9401
|
+
|
|
9402
|
+
// src/verify/apparent.ts
|
|
9403
|
+
function collectApparent(sid) {
|
|
9404
|
+
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
9405
|
+
if (!scene) return { runs: [], stage: 1 };
|
|
9406
|
+
const declaredW = Number(scene.dataset.width ?? 0);
|
|
9407
|
+
const sceneRect = scene.getBoundingClientRect();
|
|
9408
|
+
const stage = declaredW > 0 && sceneRect.width > 0 ? sceneRect.width / declaredW : 1;
|
|
9409
|
+
const runs = [];
|
|
9410
|
+
const walk = (el, inSvg) => {
|
|
9411
|
+
const style = getComputedStyle(el);
|
|
9412
|
+
if (style.display === "none" || style.visibility === "hidden") return;
|
|
9413
|
+
if (Number(style.opacity) === 0) return;
|
|
9414
|
+
const within = inSvg || el.tagName.toLowerCase() === "svg";
|
|
9415
|
+
if (within && typeof el.getBBox === "function") {
|
|
9416
|
+
let hasText = false;
|
|
9417
|
+
for (const node of Array.from(el.childNodes))
|
|
9418
|
+
if (node.nodeType === 3 && node.textContent?.trim()) hasText = true;
|
|
9419
|
+
if (hasText) {
|
|
9420
|
+
let box = null;
|
|
9421
|
+
try {
|
|
9422
|
+
box = el.getBBox();
|
|
9423
|
+
} catch {
|
|
9424
|
+
box = null;
|
|
9425
|
+
}
|
|
9426
|
+
const painted = el.getBoundingClientRect().height;
|
|
9427
|
+
if (box && box.height > 0 && painted > 0) {
|
|
9428
|
+
let opacity = 1;
|
|
9429
|
+
for (let node = el; node; node = node.parentElement) {
|
|
9430
|
+
const own = Number(getComputedStyle(node).opacity);
|
|
9431
|
+
if (Number.isFinite(own)) opacity *= own;
|
|
9432
|
+
if (node === scene) break;
|
|
9433
|
+
}
|
|
9434
|
+
runs.push({
|
|
9435
|
+
text: (el.textContent ?? "").trim().slice(0, 40),
|
|
9436
|
+
declared: Number.parseFloat(getComputedStyle(el).fontSize) || 0,
|
|
9437
|
+
ratio: painted / box.height,
|
|
9438
|
+
opacity
|
|
9439
|
+
});
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9442
|
+
}
|
|
9443
|
+
for (const child of Array.from(el.children)) walk(child, within);
|
|
9444
|
+
};
|
|
9445
|
+
walk(scene, false);
|
|
9446
|
+
return { runs, stage };
|
|
9447
|
+
}
|
|
9448
|
+
function apparentPx(run4, stage) {
|
|
9449
|
+
if (stage <= 0) return run4.declared;
|
|
9450
|
+
return run4.declared * run4.ratio / stage;
|
|
9451
|
+
}
|
|
9452
|
+
var SETTLED_OPACITY = 0.95;
|
|
9453
|
+
function midpoints(stops) {
|
|
9454
|
+
const out = [];
|
|
9455
|
+
for (let i = 1; i < stops.length; i++) {
|
|
9456
|
+
const a = stops[i - 1];
|
|
9457
|
+
const b = stops[i];
|
|
9458
|
+
if (a.sid !== b.sid || b.t <= a.t) continue;
|
|
9459
|
+
out.push({ sid: b.sid, t: Math.round((a.t + b.t) / 2 * 1e3) / 1e3 });
|
|
9460
|
+
}
|
|
9461
|
+
return out;
|
|
9462
|
+
}
|
|
9463
|
+
function gradeApparent(stops, floor = TYPE_FLOOR_PX) {
|
|
9464
|
+
const worst = /* @__PURE__ */ new Map();
|
|
9465
|
+
for (const stop of stops) {
|
|
9466
|
+
for (const run4 of stop.runs) {
|
|
9467
|
+
if (!stop.settled && run4.opacity < SETTLED_OPACITY) continue;
|
|
9468
|
+
const px = apparentPx(run4, stop.stage);
|
|
9469
|
+
if (px >= floor - 0.1) continue;
|
|
9470
|
+
const seen = worst.get(run4.text);
|
|
9471
|
+
if (!seen || px < seen.px)
|
|
9472
|
+
worst.set(run4.text, { sid: stop.sid, t: stop.t, text: run4.text, px });
|
|
9473
|
+
}
|
|
9474
|
+
}
|
|
9475
|
+
if (worst.size === 0) return [];
|
|
9476
|
+
const byScene = /* @__PURE__ */ new Map();
|
|
9477
|
+
for (const row of worst.values()) {
|
|
9478
|
+
const list = byScene.get(row.sid) ?? [];
|
|
9479
|
+
list.push(row);
|
|
9480
|
+
byScene.set(row.sid, list);
|
|
9481
|
+
}
|
|
9482
|
+
return [...byScene].map(([sid, rows]) => {
|
|
9483
|
+
rows.sort((a, b) => a.px - b.px);
|
|
9484
|
+
const smallest = rows[0];
|
|
9485
|
+
const named = rows.slice(0, 3).map((r) => `"${r.text}" at ${Math.round(r.px * 10) / 10}px`).join(", ");
|
|
9486
|
+
return {
|
|
9487
|
+
severity: "error",
|
|
9488
|
+
gate: "apparent",
|
|
9489
|
+
rule: "apparent_type_floor",
|
|
9490
|
+
// `#${sid}` as a selector, like every other finding here: `scripts/sweep.mjs`
|
|
9491
|
+
// reads the selector back to decide which beat a finding belongs to, and a
|
|
9492
|
+
// bare id was once filed as a deck-level orphan while the beat reported clean.
|
|
9493
|
+
message: `#${sid} draws ${rows.length} text run(s) below the ${floor}px floor once the frame is rendered: ${named}, at t=${smallest.t}s. Their DECLARED sizes pass \u2014 something between the source and the glyph scales them down, a transform at a hold or a 3D projection. Invariant 5 is about what the audience sees, and the declared-size scan cannot see this.`
|
|
9494
|
+
};
|
|
9495
|
+
});
|
|
9496
|
+
}
|
|
9497
|
+
|
|
8935
9498
|
// src/verify/overprint.ts
|
|
8936
9499
|
var MIN_OVERLAP = 8;
|
|
8937
9500
|
function collectSvgTextRuns(sid) {
|
|
@@ -9215,6 +9778,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
9215
9778
|
const { page, height } = deck;
|
|
9216
9779
|
const measured = [];
|
|
9217
9780
|
const collided = [];
|
|
9781
|
+
const apparent = [];
|
|
9218
9782
|
for (const stop of stops) {
|
|
9219
9783
|
await deck.seek(stop.t);
|
|
9220
9784
|
const bandTopPx = await page.evaluate(
|
|
@@ -9227,6 +9791,11 @@ async function fidelity(dir, opts = {}) {
|
|
|
9227
9791
|
...stop,
|
|
9228
9792
|
pairs: overprints(await page.evaluate(collectSvgTextRuns, stop.sid))
|
|
9229
9793
|
});
|
|
9794
|
+
apparent.push({
|
|
9795
|
+
...stop,
|
|
9796
|
+
settled: true,
|
|
9797
|
+
...await page.evaluate(collectApparent, stop.sid)
|
|
9798
|
+
});
|
|
9230
9799
|
const frame = await decodePng(await deck.shoot());
|
|
9231
9800
|
measured.push({
|
|
9232
9801
|
...stop,
|
|
@@ -9234,9 +9803,17 @@ async function fidelity(dir, opts = {}) {
|
|
|
9234
9803
|
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
9235
9804
|
});
|
|
9236
9805
|
}
|
|
9806
|
+
for (const mid of midpoints(stops)) {
|
|
9807
|
+
await deck.seek(mid.t);
|
|
9808
|
+
apparent.push({ ...mid, settled: false, ...await page.evaluate(collectApparent, mid.sid) });
|
|
9809
|
+
}
|
|
9237
9810
|
return {
|
|
9238
9811
|
stops: measured,
|
|
9239
|
-
findings: [
|
|
9812
|
+
findings: [
|
|
9813
|
+
...gradeFidelity(measured, floor),
|
|
9814
|
+
...gradeOverprint(collided),
|
|
9815
|
+
...gradeApparent(apparent)
|
|
9816
|
+
],
|
|
9240
9817
|
elapsedMs: Date.now() - started
|
|
9241
9818
|
};
|
|
9242
9819
|
} catch (err) {
|
|
@@ -9247,82 +9824,6 @@ async function fidelity(dir, opts = {}) {
|
|
|
9247
9824
|
}
|
|
9248
9825
|
}
|
|
9249
9826
|
|
|
9250
|
-
// src/verify/typefloor.ts
|
|
9251
|
-
var TYPE_FLOOR_PX = 40;
|
|
9252
|
-
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
9253
|
-
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
9254
|
-
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
9255
|
-
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
9256
|
-
const findings = [];
|
|
9257
|
-
const zones = svgZones(html);
|
|
9258
|
-
const small = [];
|
|
9259
|
-
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
9260
|
-
pattern.lastIndex = 0;
|
|
9261
|
-
for (const m of html.matchAll(pattern)) {
|
|
9262
|
-
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
9263
|
-
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
9264
|
-
}
|
|
9265
|
-
}
|
|
9266
|
-
if (small.length > 0) {
|
|
9267
|
-
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
9268
|
-
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
9269
|
-
findings.push({
|
|
9270
|
-
severity: "error",
|
|
9271
|
-
gate: "typography",
|
|
9272
|
-
rule: "type_below_floor",
|
|
9273
|
-
message: `${file} declares ${small.length} text size(s) under the ${floorPx}px floor: ${named}${worst.length > 6 ? ", \u2026" : ""}. Sizes are in reference space, so this is the floor as authored and not an artefact of the format's zoom. Raise the size, or give the archetype less to say \u2014 a beat that only fits below the floor is a beat that has to be cut in two.`
|
|
9274
|
-
});
|
|
9275
|
-
}
|
|
9276
|
-
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
9277
|
-
if (units.length > 0) {
|
|
9278
|
-
findings.push({
|
|
9279
|
-
severity: "warning",
|
|
9280
|
-
gate: "typography",
|
|
9281
|
-
rule: "type_unmeasurable",
|
|
9282
|
-
message: `${file} sizes some text in ${units.join("/")}, which resolves against an inherited size this scan cannot follow, so the ${floorPx}px floor was not checked there. Declare it in px, or measure it in a browser.`
|
|
9283
|
-
});
|
|
9284
|
-
}
|
|
9285
|
-
return findings;
|
|
9286
|
-
}
|
|
9287
|
-
function svgZones(html) {
|
|
9288
|
-
const zones = [];
|
|
9289
|
-
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
9290
|
-
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
9291
|
-
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
9292
|
-
const close = html.indexOf("</svg>", m.index);
|
|
9293
|
-
zones.push({
|
|
9294
|
-
start: m.index,
|
|
9295
|
-
end: close < 0 ? html.length : close,
|
|
9296
|
-
unit: width > 0 && box > 0 ? width / box : 1
|
|
9297
|
-
});
|
|
9298
|
-
}
|
|
9299
|
-
return zones;
|
|
9300
|
-
}
|
|
9301
|
-
function userUnit(zones, at) {
|
|
9302
|
-
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
9303
|
-
}
|
|
9304
|
-
function where(html, at) {
|
|
9305
|
-
const open = html.lastIndexOf("<", at);
|
|
9306
|
-
const closed = html.lastIndexOf(">", at);
|
|
9307
|
-
if (open > closed) {
|
|
9308
|
-
const end = html.indexOf(">", at);
|
|
9309
|
-
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
9310
|
-
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
9311
|
-
if (id2) return `#${id2}`;
|
|
9312
|
-
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
9313
|
-
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
9314
|
-
return cls ? `${name}.${cls}` : name;
|
|
9315
|
-
}
|
|
9316
|
-
const brace = html.lastIndexOf("{", at);
|
|
9317
|
-
if (brace < 0) return "the stylesheet";
|
|
9318
|
-
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
9319
|
-
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
9320
|
-
return selector ? selector : "the stylesheet";
|
|
9321
|
-
}
|
|
9322
|
-
function round6(px) {
|
|
9323
|
-
return Math.round(px * 100) / 100;
|
|
9324
|
-
}
|
|
9325
|
-
|
|
9326
9827
|
// src/verify/drift.ts
|
|
9327
9828
|
import { execFile as execFile3 } from "node:child_process";
|
|
9328
9829
|
import { createHash as createHash8 } from "node:crypto";
|
|
@@ -9657,24 +10158,25 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
9657
10158
|
check2(dir, { ...opts, at: stops.map((s) => s.t) }),
|
|
9658
10159
|
opts.fidelity === false ? null : fidelity(dir, { stops })
|
|
9659
10160
|
]);
|
|
9660
|
-
const
|
|
10161
|
+
const storyboardFindings = storyboard ? [
|
|
10162
|
+
...scanDiagrammatic(storyboard),
|
|
10163
|
+
...scanHeadlines(storyboard),
|
|
10164
|
+
...scanRepeatedObject(storyboard),
|
|
10165
|
+
// Needs the source as well, and says nothing without it — the same
|
|
10166
|
+
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
10167
|
+
...source ? scanUnusedFigures(storyboard, source) : []
|
|
10168
|
+
] : [];
|
|
10169
|
+
const seen = [...ours, ...frames2?.findings ?? [], ...storyboardFindings];
|
|
9661
10170
|
return {
|
|
9662
10171
|
passed: verdict.passed && seen.every((f) => f.severity !== "error"),
|
|
9663
10172
|
findings: [
|
|
9664
10173
|
...seen,
|
|
9665
|
-
// `scanBeatCount`
|
|
9666
|
-
// deck was asked for, and `verify <dir>` is handed a
|
|
9667
|
-
// nothing else — the same gating `scanNarrationLead`
|
|
9668
|
-
// the same reason: a check that cannot see its inputs
|
|
9669
|
-
// it found nothing.
|
|
9670
|
-
|
|
9671
|
-
...scanDiagrammatic(storyboard),
|
|
9672
|
-
...scanHeadlines(storyboard),
|
|
9673
|
-
...scanRepeatedObject(storyboard),
|
|
9674
|
-
// Needs the source as well, and says nothing without it — the same
|
|
9675
|
-
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
9676
|
-
...source ? scanUnusedFigures(storyboard, source) : []
|
|
9677
|
-
] : [],
|
|
10174
|
+
// `scanBeatCount` and `scanPaperArc` are deliberately NOT here. Both need
|
|
10175
|
+
// the preferences the deck was asked for, and `verify <dir>` is handed a
|
|
10176
|
+
// built directory and nothing else — the same gating `scanNarrationLead`
|
|
10177
|
+
// gets above, and for the same reason: a check that cannot see its inputs
|
|
10178
|
+
// must not report that it found nothing. They run at `plan` and `build`,
|
|
10179
|
+
// where prefs exist.
|
|
9678
10180
|
...verdict.findings
|
|
9679
10181
|
]
|
|
9680
10182
|
};
|
|
@@ -9859,6 +10361,32 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
9859
10361
|
}
|
|
9860
10362
|
];
|
|
9861
10363
|
}
|
|
10364
|
+
function scanPaperArc(storyboard, prefs) {
|
|
10365
|
+
return arcProblems(storyboard, prefs).map((message) => ({
|
|
10366
|
+
severity: "warning",
|
|
10367
|
+
gate: "storyboard",
|
|
10368
|
+
rule: "paper_arc",
|
|
10369
|
+
message
|
|
10370
|
+
}));
|
|
10371
|
+
}
|
|
10372
|
+
function scanNarrationDrift(storyboard, narration) {
|
|
10373
|
+
const flat = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
10374
|
+
const stale = [];
|
|
10375
|
+
for (const beat of storyboard.beats) {
|
|
10376
|
+
const segments = narration.beats[beat.id];
|
|
10377
|
+
if (!segments?.length || !flat(beat.narration)) continue;
|
|
10378
|
+
if (flat(segments.map((s) => s.text).join(" ")) !== flat(beat.narration)) stale.push(beat.id);
|
|
10379
|
+
}
|
|
10380
|
+
if (!stale.length) return [];
|
|
10381
|
+
return [
|
|
10382
|
+
{
|
|
10383
|
+
severity: "error",
|
|
10384
|
+
gate: "storyboard",
|
|
10385
|
+
rule: "narration_drift",
|
|
10386
|
+
message: `The recorded narration does not say what ${stale.length} beat(s) say they say: ${stale.join(", ")}. narration.json is keyed by beat id and carries no link to the plan it was made for, so a storyboard whose beats were renumbered or rewritten keeps matching ids and speaks the wrong slide. Re-run \`decksmith narrate\` for this storyboard \u2014 the audio cache is keyed by TEXT, so lines that did not change are not re-synthesised.`
|
|
10387
|
+
}
|
|
10388
|
+
];
|
|
10389
|
+
}
|
|
9862
10390
|
var LEAD_SECONDS = 1;
|
|
9863
10391
|
var EMPHASISED = /* @__PURE__ */ new Set(["equation-walk", "data-table"]);
|
|
9864
10392
|
function scanNarrationLead(beats, timing) {
|
|
@@ -10023,6 +10551,10 @@ async function vendorScripts(out) {
|
|
|
10023
10551
|
const from = join15(dirname4(require2.resolve(pkg)), rel);
|
|
10024
10552
|
await cp(from, join15(out, "vendor", name));
|
|
10025
10553
|
}
|
|
10554
|
+
await cp(
|
|
10555
|
+
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
10556
|
+
join15(out, "vendor", "ds-morph.js")
|
|
10557
|
+
);
|
|
10026
10558
|
}
|
|
10027
10559
|
var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
10028
10560
|
{
|
|
@@ -10034,7 +10566,10 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
10034
10566
|
)}
|
|
10035
10567
|
`;
|
|
10036
10568
|
function planFlags(cmd) {
|
|
10037
|
-
return cmd.option("--lang <bcp47>", "language of the deck's copy").option("--tone <tone>", "plain | academic | conversational | punchy").option("--density <level>", "sparse | normal | dense")
|
|
10569
|
+
return cmd.option("--lang <bcp47>", "language of the deck's copy").option("--tone <tone>", "plain | academic | conversational | punchy").option("--density <level>", "sparse | normal | dense").option(
|
|
10570
|
+
"--genre <genre>",
|
|
10571
|
+
"general | paper \u2014 paper asks for an intro, background, limitations and conclusion"
|
|
10572
|
+
);
|
|
10038
10573
|
}
|
|
10039
10574
|
function lengthFlags(cmd) {
|
|
10040
10575
|
return cmd.option("--duration <s>", "target length of the finished video, in seconds (10\u20131800)").option("--slides <n>", "target beat count (3\u201340)").option(
|
|
@@ -10061,6 +10596,7 @@ function flags(o) {
|
|
|
10061
10596
|
"lang",
|
|
10062
10597
|
"tone",
|
|
10063
10598
|
"density",
|
|
10599
|
+
"genre",
|
|
10064
10600
|
"duration",
|
|
10065
10601
|
"narrationDensity",
|
|
10066
10602
|
"theme",
|
|
@@ -10130,6 +10666,11 @@ imageFlags(
|
|
|
10130
10666
|
}
|
|
10131
10667
|
for (const f of [
|
|
10132
10668
|
...scanBeatCount(storyboard, prefs),
|
|
10669
|
+
// Silent unless `--genre paper` was declared. Here for the same reason
|
|
10670
|
+
// `scanBeatCount` is: the fix is a beat in a file the author has open, and
|
|
10671
|
+
// this is the last moment before a minute of TTS is spent on a deck that
|
|
10672
|
+
// does not end where it was asked to.
|
|
10673
|
+
...scanPaperArc(storyboard, prefs),
|
|
10133
10674
|
...scanHeadlines(storyboard),
|
|
10134
10675
|
...scanRepeatedObject(storyboard),
|
|
10135
10676
|
// A figure the plan ignored is cheapest to fix here, where the answer is one
|
|
@@ -10203,7 +10744,7 @@ voiceFlags(
|
|
|
10203
10744
|
});
|
|
10204
10745
|
lookFlags(
|
|
10205
10746
|
sizeFlags(
|
|
10206
|
-
lengthFlags(program.command("build")).description("Emit the composition, write its assets, and run the gates.").argument("<storyboard>", "storyboard.json, edited to taste").requiredOption("--source <file>", "source.json the storyboard was planned from").requiredOption("-o, --out <dir>", "directory to write the deck into").option("--format <id>", `output profile: ${Object.keys(FORMATS).join(" | ")}`, "deck-16x9").option("--min-weight <n>", "keep only beats at or above this weight \u2014 see the budget gate").option("--narration <file>", `${NARRATION_FILE} from \`decksmith narrate\``).option("--no-narration", "ignore narration sitting beside the storyboard").option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser")
|
|
10747
|
+
lengthFlags(program.command("build")).description("Emit the composition, write its assets, and run the gates.").argument("<storyboard>", "storyboard.json, edited to taste").requiredOption("--source <file>", "source.json the storyboard was planned from").requiredOption("-o, --out <dir>", "directory to write the deck into").option("--format <id>", `output profile: ${Object.keys(FORMATS).join(" | ")}`, "deck-16x9").option("--min-weight <n>", "keep only beats at or above this weight \u2014 see the budget gate").option("--genre <genre>", "general | paper \u2014 report when a paper deck lacks its arc").option("--narration <file>", `${NARRATION_FILE} from \`decksmith narrate\``).option("--no-narration", "ignore narration sitting beside the storyboard").option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser")
|
|
10207
10748
|
)
|
|
10208
10749
|
).action(
|
|
10209
10750
|
async (sbPath, o) => {
|
|
@@ -10217,6 +10758,10 @@ lookFlags(
|
|
|
10217
10758
|
await mkdir9(out, { recursive: true });
|
|
10218
10759
|
const found = await findNarration(sbPath, o.narration);
|
|
10219
10760
|
const narration = found ? await loadNarration(found) : void 0;
|
|
10761
|
+
if (narration) {
|
|
10762
|
+
const drift2 = scanNarrationDrift(storyboard, narration);
|
|
10763
|
+
if (drift2.length > 0) throw new Error(drift2[0]?.message ?? "narration drift");
|
|
10764
|
+
}
|
|
10220
10765
|
if (narration && !format.navigable) {
|
|
10221
10766
|
step(`build: ${format.id} renders linearly, so its narration is timing only`);
|
|
10222
10767
|
}
|
|
@@ -10267,6 +10812,8 @@ lookFlags(
|
|
|
10267
10812
|
step(
|
|
10268
10813
|
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${join15(out, "index.html")}`
|
|
10269
10814
|
);
|
|
10815
|
+
for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
|
|
10816
|
+
step(`build: ${f.message}`);
|
|
10270
10817
|
reportCut(cut);
|
|
10271
10818
|
if (deck.page) step(`build: navigable deck \u2192 ${join15(out, DECK_PAGE)}`);
|
|
10272
10819
|
await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
|