@jokerized/decksmith 0.3.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/dist/cli.js +232 -30
- package/dist/index.js +217 -28
- package/dist/mcp.js +119 -14
- package/dist/types/emit/archetypes/stack.d.ts +2 -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 +270 -0
- package/dist/types/verify/index.d.ts +64 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -293,10 +293,16 @@ var insideSchema = z.object({
|
|
|
293
293
|
*/
|
|
294
294
|
label: z.string().optional()
|
|
295
295
|
});
|
|
296
|
+
var beatRoleSchema = z.enum(["intro", "background", "limitations", "conclusion"]);
|
|
296
297
|
var beatCore = {
|
|
297
298
|
id: z.string(),
|
|
298
299
|
/** What the viewer should understand after this beat. */
|
|
299
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(),
|
|
300
306
|
/** Optional: this beat happens inside a named part of the beat before it. */
|
|
301
307
|
inside: insideSchema.optional(),
|
|
302
308
|
/** The source sentence or equation this beat is accountable to. */
|
|
@@ -443,6 +449,26 @@ var prefsSchema = z.object({
|
|
|
443
449
|
tone: z.enum(["plain", "academic", "conversational", "punchy"]).default("plain"),
|
|
444
450
|
/** How much text a slide may carry before it should have been a diagram. */
|
|
445
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"),
|
|
446
472
|
/**
|
|
447
473
|
* How long the finished thing should run, in seconds. Optional: absent means
|
|
448
474
|
* "as long as it takes", which is what every deck built before this did.
|
|
@@ -703,7 +729,10 @@ function selectBeats(storyboard, budget2, seconds = {}) {
|
|
|
703
729
|
const protectedIds = protect(live, len, cap);
|
|
704
730
|
const keep = knapsack(live, len, cap, protectedIds);
|
|
705
731
|
if (!keep) {
|
|
706
|
-
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());
|
|
707
736
|
const chosen = all ?? [live[0]];
|
|
708
737
|
return budgetDrops(live, chosen, dropped, storyboard, len, cap, budget2, false);
|
|
709
738
|
}
|
|
@@ -735,8 +764,17 @@ function protect(live, len, cap) {
|
|
|
735
764
|
}
|
|
736
765
|
const picture = cheapest && !ids.has(cheapest.id) ? [cheapest] : [];
|
|
737
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
|
+
}
|
|
738
776
|
const ends = /* @__PURE__ */ new Set([first?.id, last?.id]);
|
|
739
|
-
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));
|
|
740
778
|
const cost = () => live.filter((b) => ids.has(b.id)).reduce((s, b) => s + len(b), 0);
|
|
741
779
|
for (const b of releasable) {
|
|
742
780
|
if (cost() <= cap) break;
|
|
@@ -4717,6 +4755,9 @@ function depthCss(sid, pose, part) {
|
|
|
4717
4755
|
// src/emit/archetypes/stack.ts
|
|
4718
4756
|
var GAP3 = 44;
|
|
4719
4757
|
var NUM_X = 48;
|
|
4758
|
+
function numSpine(floor) {
|
|
4759
|
+
return Math.round(NUM_X * floor * 100 / MIN_FONT) / 100;
|
|
4760
|
+
}
|
|
4720
4761
|
var PROBE_X = 16;
|
|
4721
4762
|
var PROBE_H = 46;
|
|
4722
4763
|
var NUM_W = 70;
|
|
@@ -4899,7 +4940,7 @@ var stack = (beat, ctx) => {
|
|
|
4899
4940
|
);
|
|
4900
4941
|
const num = text(
|
|
4901
4942
|
String(i + 1),
|
|
4902
|
-
{ x:
|
|
4943
|
+
{ x: numSpine(L.floor), y: mid },
|
|
4903
4944
|
{ size: L.floor, weight: 600, fill: theme.dim, anchor: "end", vAlign: "middle" }
|
|
4904
4945
|
);
|
|
4905
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" });
|
|
@@ -5669,6 +5710,76 @@ import { tmpdir } from "node:os";
|
|
|
5669
5710
|
import { join as join3 } from "node:path";
|
|
5670
5711
|
import { z as z2 } from "zod";
|
|
5671
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
|
+
|
|
5672
5783
|
// src/plan/duration.ts
|
|
5673
5784
|
var SPEECH_CPS = { latin: 14.4, cjk: 6.5 };
|
|
5674
5785
|
var LAST_HOLD_SECONDS = 4.2;
|
|
@@ -6258,6 +6369,46 @@ ${REVEAL_COUNTS}` : ` - ${n3 === 1 ? "ONE SENTENCE" : `${n3} SENTENCES`} FOR TH
|
|
|
6258
6369
|
miss its duration.`;
|
|
6259
6370
|
return { sentences, length };
|
|
6260
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
|
+
}
|
|
6261
6412
|
function illustrations(images) {
|
|
6262
6413
|
return `
|
|
6263
6414
|
|
|
@@ -6286,7 +6437,7 @@ nothing, so it can never dangle.
|
|
|
6286
6437
|
}
|
|
6287
6438
|
function systemPrompt(prefs) {
|
|
6288
6439
|
const plan = durationPlan(prefs);
|
|
6289
|
-
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) : ""}
|
|
6290
6441
|
|
|
6291
6442
|
PREFERENCES \u2014 chosen by the person who asked for this deck.
|
|
6292
6443
|
${prefs.duration === void 0 ? "" : `
|
|
@@ -6597,8 +6748,11 @@ function stripNulls(node) {
|
|
|
6597
6748
|
return out;
|
|
6598
6749
|
}
|
|
6599
6750
|
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
6600
|
-
function
|
|
6601
|
-
|
|
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));
|
|
6602
6756
|
if (node === null || typeof node !== "object") return node;
|
|
6603
6757
|
const src = node;
|
|
6604
6758
|
const out = {};
|
|
@@ -6606,28 +6760,32 @@ function hideFromPlanner(node) {
|
|
|
6606
6760
|
if (key === "properties" && value && typeof value === "object") {
|
|
6607
6761
|
const kept = {};
|
|
6608
6762
|
for (const [prop, sub] of Object.entries(value))
|
|
6609
|
-
if (!
|
|
6763
|
+
if (!hidden.has(prop)) kept[prop] = hideFromPlanner(sub, hidden);
|
|
6610
6764
|
out.properties = kept;
|
|
6611
6765
|
continue;
|
|
6612
6766
|
}
|
|
6613
6767
|
if (key === "required" && Array.isArray(value)) {
|
|
6614
|
-
out.required = value.filter((r) => typeof r !== "string" || !
|
|
6768
|
+
out.required = value.filter((r) => typeof r !== "string" || !hidden.has(r));
|
|
6615
6769
|
continue;
|
|
6616
6770
|
}
|
|
6617
|
-
out[key] = hideFromPlanner(value);
|
|
6771
|
+
out[key] = hideFromPlanner(value, hidden);
|
|
6618
6772
|
}
|
|
6619
6773
|
return out;
|
|
6620
6774
|
}
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
)
|
|
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" });
|
|
6624
6782
|
async function codexPlanner(source, opts = {}) {
|
|
6625
6783
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
6626
6784
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
6627
6785
|
try {
|
|
6628
6786
|
const schemaPath = join3(dir, "storyboard.schema.json");
|
|
6629
6787
|
const outPath = join3(dir, "storyboard.json");
|
|
6630
|
-
await writeFile3(schemaPath, JSON.stringify(
|
|
6788
|
+
await writeFile3(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
6631
6789
|
await (opts.run ?? runCodex)({
|
|
6632
6790
|
prompt: buildPrompt(source, prefs),
|
|
6633
6791
|
schemaPath,
|
|
@@ -7692,6 +7850,7 @@ function prefsFromFlags(flags2) {
|
|
|
7692
7850
|
if (flags2.lang !== void 0) patch.lang = flags2.lang;
|
|
7693
7851
|
if (flags2.tone !== void 0) patch.tone = flags2.tone;
|
|
7694
7852
|
if (flags2.density !== void 0) patch.density = flags2.density;
|
|
7853
|
+
if (flags2.genre !== void 0) patch.genre = flags2.genre;
|
|
7695
7854
|
if (flags2.duration !== void 0) patch.duration = number("--duration", flags2.duration);
|
|
7696
7855
|
if (flags2.theme !== void 0) patch.theme = flags2.theme;
|
|
7697
7856
|
if (flags2.speed !== void 0) patch.animationSpeed = number("--speed", flags2.speed);
|
|
@@ -8443,13 +8602,14 @@ function pieceArgs(source, fromFrame, motion, freeze, fps, out) {
|
|
|
8443
8602
|
out
|
|
8444
8603
|
];
|
|
8445
8604
|
}
|
|
8605
|
+
var LOUDNESS = "loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000";
|
|
8446
8606
|
function audioGraph(inputs, seconds, first = 1) {
|
|
8447
8607
|
const lines = inputs.map(
|
|
8448
8608
|
(input, i) => `[${first + i}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${input.delayMs}:all=1[d${i}]`
|
|
8449
8609
|
);
|
|
8450
8610
|
const labels = inputs.map((_, i) => `[d${i}]`).join("");
|
|
8451
8611
|
lines.push(
|
|
8452
|
-
`${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]`
|
|
8453
8613
|
);
|
|
8454
8614
|
return lines.join(";\n");
|
|
8455
8615
|
}
|
|
@@ -9998,24 +10158,25 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
9998
10158
|
check2(dir, { ...opts, at: stops.map((s) => s.t) }),
|
|
9999
10159
|
opts.fidelity === false ? null : fidelity(dir, { stops })
|
|
10000
10160
|
]);
|
|
10001
|
-
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];
|
|
10002
10170
|
return {
|
|
10003
10171
|
passed: verdict.passed && seen.every((f) => f.severity !== "error"),
|
|
10004
10172
|
findings: [
|
|
10005
10173
|
...seen,
|
|
10006
|
-
// `scanBeatCount`
|
|
10007
|
-
// deck was asked for, and `verify <dir>` is handed a
|
|
10008
|
-
// nothing else — the same gating `scanNarrationLead`
|
|
10009
|
-
// the same reason: a check that cannot see its inputs
|
|
10010
|
-
// it found nothing.
|
|
10011
|
-
|
|
10012
|
-
...scanDiagrammatic(storyboard),
|
|
10013
|
-
...scanHeadlines(storyboard),
|
|
10014
|
-
...scanRepeatedObject(storyboard),
|
|
10015
|
-
// Needs the source as well, and says nothing without it — the same
|
|
10016
|
-
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
10017
|
-
...source ? scanUnusedFigures(storyboard, source) : []
|
|
10018
|
-
] : [],
|
|
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.
|
|
10019
10180
|
...verdict.findings
|
|
10020
10181
|
]
|
|
10021
10182
|
};
|
|
@@ -10200,6 +10361,32 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
10200
10361
|
}
|
|
10201
10362
|
];
|
|
10202
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
|
+
}
|
|
10203
10390
|
var LEAD_SECONDS = 1;
|
|
10204
10391
|
var EMPHASISED = /* @__PURE__ */ new Set(["equation-walk", "data-table"]);
|
|
10205
10392
|
function scanNarrationLead(beats, timing) {
|
|
@@ -10379,7 +10566,10 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
10379
10566
|
)}
|
|
10380
10567
|
`;
|
|
10381
10568
|
function planFlags(cmd) {
|
|
10382
|
-
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
|
+
);
|
|
10383
10573
|
}
|
|
10384
10574
|
function lengthFlags(cmd) {
|
|
10385
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(
|
|
@@ -10406,6 +10596,7 @@ function flags(o) {
|
|
|
10406
10596
|
"lang",
|
|
10407
10597
|
"tone",
|
|
10408
10598
|
"density",
|
|
10599
|
+
"genre",
|
|
10409
10600
|
"duration",
|
|
10410
10601
|
"narrationDensity",
|
|
10411
10602
|
"theme",
|
|
@@ -10475,6 +10666,11 @@ imageFlags(
|
|
|
10475
10666
|
}
|
|
10476
10667
|
for (const f of [
|
|
10477
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),
|
|
10478
10674
|
...scanHeadlines(storyboard),
|
|
10479
10675
|
...scanRepeatedObject(storyboard),
|
|
10480
10676
|
// A figure the plan ignored is cheapest to fix here, where the answer is one
|
|
@@ -10548,7 +10744,7 @@ voiceFlags(
|
|
|
10548
10744
|
});
|
|
10549
10745
|
lookFlags(
|
|
10550
10746
|
sizeFlags(
|
|
10551
|
-
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")
|
|
10552
10748
|
)
|
|
10553
10749
|
).action(
|
|
10554
10750
|
async (sbPath, o) => {
|
|
@@ -10562,6 +10758,10 @@ lookFlags(
|
|
|
10562
10758
|
await mkdir9(out, { recursive: true });
|
|
10563
10759
|
const found = await findNarration(sbPath, o.narration);
|
|
10564
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
|
+
}
|
|
10565
10765
|
if (narration && !format.navigable) {
|
|
10566
10766
|
step(`build: ${format.id} renders linearly, so its narration is timing only`);
|
|
10567
10767
|
}
|
|
@@ -10612,6 +10812,8 @@ lookFlags(
|
|
|
10612
10812
|
step(
|
|
10613
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")}`
|
|
10614
10814
|
);
|
|
10815
|
+
for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
|
|
10816
|
+
step(`build: ${f.message}`);
|
|
10615
10817
|
reportCut(cut);
|
|
10616
10818
|
if (deck.page) step(`build: navigable deck \u2192 ${join15(out, DECK_PAGE)}`);
|
|
10617
10819
|
await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
|