@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/index.js
CHANGED
|
@@ -290,10 +290,16 @@ var insideSchema = z.object({
|
|
|
290
290
|
*/
|
|
291
291
|
label: z.string().optional()
|
|
292
292
|
});
|
|
293
|
+
var beatRoleSchema = z.enum(["intro", "background", "limitations", "conclusion"]);
|
|
293
294
|
var beatCore = {
|
|
294
295
|
id: z.string(),
|
|
295
296
|
/** What the viewer should understand after this beat. */
|
|
296
297
|
intent: z.string(),
|
|
298
|
+
/**
|
|
299
|
+
* OPTIONAL, and only ever present when `prefs.genre` is `paper`. The
|
|
300
|
+
* structural job this beat does; see `beatRoleSchema`.
|
|
301
|
+
*/
|
|
302
|
+
role: beatRoleSchema.optional(),
|
|
297
303
|
/** Optional: this beat happens inside a named part of the beat before it. */
|
|
298
304
|
inside: insideSchema.optional(),
|
|
299
305
|
/** The source sentence or equation this beat is accountable to. */
|
|
@@ -440,6 +446,26 @@ var prefsSchema = z.object({
|
|
|
440
446
|
tone: z.enum(["plain", "academic", "conversational", "punchy"]).default("plain"),
|
|
441
447
|
/** How much text a slide may carry before it should have been a diagram. */
|
|
442
448
|
density: z.enum(["sparse", "normal", "dense"]).default("normal"),
|
|
449
|
+
/**
|
|
450
|
+
* What kind of document is being explained, DECLARED and never sniffed.
|
|
451
|
+
*
|
|
452
|
+
* `paper` asks the planner for the shape a research talk has: open on the
|
|
453
|
+
* problem and the ground the work stands on, close on what it does not do and
|
|
454
|
+
* then what to take away. `general` is every deck built before this existed
|
|
455
|
+
* and changes nothing — no prompt block, no `role` in the planner's schema, no
|
|
456
|
+
* scan.
|
|
457
|
+
*
|
|
458
|
+
* WHY DECLARED. A ten-role heading lexicon (en/ko/ja/zh, numbered-prefix
|
|
459
|
+
* tolerant) run over all 351 markdown files in this repository scored 345 of
|
|
460
|
+
* them at zero role hits and none at three or more. `src/source/markdown.ts`
|
|
461
|
+
* says why in its first line: the input is a hypepaper-style ANALYSIS of a
|
|
462
|
+
* paper, a rewrite that has already discarded the headings a detector would
|
|
463
|
+
* key on. A classifier here would be a guess with a confidence score attached,
|
|
464
|
+
* and it would guess wrong on the Korean fixture. So the author says so once —
|
|
465
|
+
* `--genre paper`, or one line in a `decksmith.config.json` above a directory
|
|
466
|
+
* of papers — and every run under it costs no further typing.
|
|
467
|
+
*/
|
|
468
|
+
genre: z.enum(["general", "paper"]).default("general"),
|
|
443
469
|
/**
|
|
444
470
|
* How long the finished thing should run, in seconds. Optional: absent means
|
|
445
471
|
* "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" });
|
|
@@ -6135,6 +6176,76 @@ import { tmpdir } from "node:os";
|
|
|
6135
6176
|
import { join as join3 } from "node:path";
|
|
6136
6177
|
import { z as z2 } from "zod";
|
|
6137
6178
|
|
|
6179
|
+
// src/plan/arc.ts
|
|
6180
|
+
var ARC_ROLES = ["intro", "background", "limitations", "conclusion"];
|
|
6181
|
+
function requiredRoles(beatCount) {
|
|
6182
|
+
if (beatCount >= 8) return ARC_ROLES;
|
|
6183
|
+
if (beatCount >= 5) return ["limitations", "conclusion"];
|
|
6184
|
+
return [];
|
|
6185
|
+
}
|
|
6186
|
+
function paperArcRequested(prefs) {
|
|
6187
|
+
return prefs.genre === "paper";
|
|
6188
|
+
}
|
|
6189
|
+
function arcBeats(storyboard) {
|
|
6190
|
+
const by = /* @__PURE__ */ new Map();
|
|
6191
|
+
for (const beat of storyboard.beats) {
|
|
6192
|
+
if (!beat.role) continue;
|
|
6193
|
+
by.set(beat.role, [...by.get(beat.role) ?? [], beat]);
|
|
6194
|
+
}
|
|
6195
|
+
return by;
|
|
6196
|
+
}
|
|
6197
|
+
function arcProblems(storyboard, prefs) {
|
|
6198
|
+
if (!paperArcRequested(prefs)) return [];
|
|
6199
|
+
const beats = storyboard.beats;
|
|
6200
|
+
const need = requiredRoles(Math.min(prefs.slides, beats.length));
|
|
6201
|
+
if (!need.length) return [];
|
|
6202
|
+
const out = [];
|
|
6203
|
+
const by = arcBeats(storyboard);
|
|
6204
|
+
for (const role of need) {
|
|
6205
|
+
const held = by.get(role) ?? [];
|
|
6206
|
+
if (held.length === 0) {
|
|
6207
|
+
out.push(
|
|
6208
|
+
`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.`
|
|
6209
|
+
);
|
|
6210
|
+
continue;
|
|
6211
|
+
}
|
|
6212
|
+
if (held.length > 1) {
|
|
6213
|
+
out.push(
|
|
6214
|
+
`${held.length} beats carry role "${role}" (${held.map((b) => b.id).join(", ")}). A structural job belongs to one slide.`
|
|
6215
|
+
);
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
const last = beats[beats.length - 1];
|
|
6219
|
+
if (need.includes("conclusion") && by.has("conclusion") && last?.role !== "conclusion") {
|
|
6220
|
+
out.push(
|
|
6221
|
+
`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.`
|
|
6222
|
+
);
|
|
6223
|
+
}
|
|
6224
|
+
const limIdx = beats.findIndex((b) => b.role === "limitations");
|
|
6225
|
+
const conIdx = beats.findIndex((b) => b.role === "conclusion");
|
|
6226
|
+
if (need.includes("limitations") && limIdx >= 0 && conIdx >= 0 && limIdx !== conIdx - 1) {
|
|
6227
|
+
out.push(
|
|
6228
|
+
`The limitations beat is not the slide immediately before the conclusion. The two are a pair and the caveat comes first.`
|
|
6229
|
+
);
|
|
6230
|
+
}
|
|
6231
|
+
const openingWindow = beats.slice(0, 3).map((b) => b.role);
|
|
6232
|
+
for (const role of need.filter((r) => r === "intro" || r === "background")) {
|
|
6233
|
+
if (by.has(role) && !openingWindow.includes(role)) {
|
|
6234
|
+
out.push(
|
|
6235
|
+
`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.`
|
|
6236
|
+
);
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
const lim = by.get("limitations")?.[0];
|
|
6240
|
+
const con = by.get("conclusion")?.[0];
|
|
6241
|
+
if (lim && con && lim.archetype === con.archetype) {
|
|
6242
|
+
out.push(
|
|
6243
|
+
`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.`
|
|
6244
|
+
);
|
|
6245
|
+
}
|
|
6246
|
+
return out;
|
|
6247
|
+
}
|
|
6248
|
+
|
|
6138
6249
|
// src/plan/duration.ts
|
|
6139
6250
|
var SPEECH_CPS = { latin: 14.4, cjk: 6.5 };
|
|
6140
6251
|
var LAST_HOLD_SECONDS = 4.2;
|
|
@@ -6724,6 +6835,46 @@ ${REVEAL_COUNTS}` : ` - ${n3 === 1 ? "ONE SENTENCE" : `${n3} SENTENCES`} FOR TH
|
|
|
6724
6835
|
miss its duration.`;
|
|
6725
6836
|
return { sentences, length };
|
|
6726
6837
|
}
|
|
6838
|
+
function paperArc(slides) {
|
|
6839
|
+
const asked = requiredRoles(slides);
|
|
6840
|
+
const full = asked.includes("intro");
|
|
6841
|
+
return `
|
|
6842
|
+
|
|
6843
|
+
PAPER ARC \u2014 this source was declared a research paper.
|
|
6844
|
+
|
|
6845
|
+
Four beats have a structural job, and each one NAMES its job in \`role\`. Every
|
|
6846
|
+
other beat leaves \`role\` off. A role is a job, not a heading: never write
|
|
6847
|
+
"Related work" or "Conclusion" as a headline, because RULE 8 still applies to
|
|
6848
|
+
all four.
|
|
6849
|
+
${full ? `
|
|
6850
|
+
role: "intro" Near the front. What problem exists and who has it, in
|
|
6851
|
+
the viewer's own terms. This is the opening RULE 6
|
|
6852
|
+
already asks for, named so the deck can be checked.
|
|
6853
|
+
role: "background" In the first three beats. What people did before this
|
|
6854
|
+
work, and where that ran out. Take it from what the
|
|
6855
|
+
source itself says about earlier approaches \u2014 if the
|
|
6856
|
+
source says nothing about them, leave the role off
|
|
6857
|
+
rather than inventing a literature (RULE 3).` : `
|
|
6858
|
+
This deck is short, so only the ENDING is required \u2014 an opening the deck
|
|
6859
|
+
already has is not worth a slide of its own here.`}
|
|
6860
|
+
role: "limitations" THE SECOND-TO-LAST beat. What the work does not do, in
|
|
6861
|
+
the source's own admission. Not a hedge inside another
|
|
6862
|
+
beat's sentence: its own slide.
|
|
6863
|
+
role: "conclusion" THE LAST beat, with nothing after it. What the viewer
|
|
6864
|
+
should carry away.
|
|
6865
|
+
|
|
6866
|
+
- The closing pair is TWO beats and they must not share an archetype (RULE 1).
|
|
6867
|
+
A limitation the source admits to is usually a panel; the conclusion is the
|
|
6868
|
+
claim the deck lands, so draw it where the source states it \u2014 bars, a
|
|
6869
|
+
contrast, the figure that settles it \u2014 and fall back to a panel only when it
|
|
6870
|
+
genuinely has no shape.
|
|
6871
|
+
- Give all four a weight of 0.8 or above. A short cut keeps the
|
|
6872
|
+
highest-weighted beats, and a structural beat below 0.8 is one the deck
|
|
6873
|
+
loses at the first budget.
|
|
6874
|
+
- If the source does not support one of these, LEAVE IT OUT. A slide that
|
|
6875
|
+
admits a limitation the paper never admits is worse than no slide.
|
|
6876
|
+
`;
|
|
6877
|
+
}
|
|
6727
6878
|
function illustrations(images) {
|
|
6728
6879
|
return `
|
|
6729
6880
|
|
|
@@ -6752,7 +6903,7 @@ nothing, so it can never dangle.
|
|
|
6752
6903
|
}
|
|
6753
6904
|
function systemPrompt(prefs) {
|
|
6754
6905
|
const plan = durationPlan(prefs);
|
|
6755
|
-
return `${rules(cadenceFor(prefs, plan))}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6906
|
+
return `${rules(cadenceFor(prefs, plan))}${paperArcRequested(prefs) ? paperArc(prefs.slides) : ""}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6756
6907
|
|
|
6757
6908
|
PREFERENCES \u2014 chosen by the person who asked for this deck.
|
|
6758
6909
|
${prefs.duration === void 0 ? "" : `
|
|
@@ -7063,8 +7214,11 @@ function stripNulls(node) {
|
|
|
7063
7214
|
return out;
|
|
7064
7215
|
}
|
|
7065
7216
|
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
7066
|
-
function
|
|
7067
|
-
|
|
7217
|
+
function plannerInvisible(prefs) {
|
|
7218
|
+
return paperArcRequested(prefs) ? PLANNER_INVISIBLE : /* @__PURE__ */ new Set([...PLANNER_INVISIBLE, "role"]);
|
|
7219
|
+
}
|
|
7220
|
+
function hideFromPlanner(node, hidden) {
|
|
7221
|
+
if (Array.isArray(node)) return node.map((n3) => hideFromPlanner(n3, hidden));
|
|
7068
7222
|
if (node === null || typeof node !== "object") return node;
|
|
7069
7223
|
const src = node;
|
|
7070
7224
|
const out = {};
|
|
@@ -7072,28 +7226,32 @@ function hideFromPlanner(node) {
|
|
|
7072
7226
|
if (key === "properties" && value && typeof value === "object") {
|
|
7073
7227
|
const kept = {};
|
|
7074
7228
|
for (const [prop, sub] of Object.entries(value))
|
|
7075
|
-
if (!
|
|
7229
|
+
if (!hidden.has(prop)) kept[prop] = hideFromPlanner(sub, hidden);
|
|
7076
7230
|
out.properties = kept;
|
|
7077
7231
|
continue;
|
|
7078
7232
|
}
|
|
7079
7233
|
if (key === "required" && Array.isArray(value)) {
|
|
7080
|
-
out.required = value.filter((r) => typeof r !== "string" || !
|
|
7234
|
+
out.required = value.filter((r) => typeof r !== "string" || !hidden.has(r));
|
|
7081
7235
|
continue;
|
|
7082
7236
|
}
|
|
7083
|
-
out[key] = hideFromPlanner(value);
|
|
7237
|
+
out[key] = hideFromPlanner(value, hidden);
|
|
7084
7238
|
}
|
|
7085
7239
|
return out;
|
|
7086
7240
|
}
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
)
|
|
7241
|
+
function schemaFor(prefs) {
|
|
7242
|
+
return hideFromPlanner(
|
|
7243
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" })),
|
|
7244
|
+
plannerInvisible(prefs)
|
|
7245
|
+
);
|
|
7246
|
+
}
|
|
7247
|
+
var SCHEMA = schemaFor({ genre: "general" });
|
|
7090
7248
|
async function codexPlanner(source, opts = {}) {
|
|
7091
7249
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
7092
7250
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
7093
7251
|
try {
|
|
7094
7252
|
const schemaPath = join3(dir, "storyboard.schema.json");
|
|
7095
7253
|
const outPath = join3(dir, "storyboard.json");
|
|
7096
|
-
await writeFile3(schemaPath, JSON.stringify(
|
|
7254
|
+
await writeFile3(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
7097
7255
|
await (opts.run ?? runCodex)({
|
|
7098
7256
|
prompt: buildPrompt(source, prefs),
|
|
7099
7257
|
schemaPath,
|
|
@@ -9127,24 +9285,25 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
9127
9285
|
check(dir, { ...opts, at: stops.map((s) => s.t) }),
|
|
9128
9286
|
opts.fidelity === false ? null : fidelity(dir, { stops })
|
|
9129
9287
|
]);
|
|
9130
|
-
const
|
|
9288
|
+
const storyboardFindings = storyboard ? [
|
|
9289
|
+
...scanDiagrammatic(storyboard),
|
|
9290
|
+
...scanHeadlines(storyboard),
|
|
9291
|
+
...scanRepeatedObject(storyboard),
|
|
9292
|
+
// Needs the source as well, and says nothing without it — the same
|
|
9293
|
+
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
9294
|
+
...source ? scanUnusedFigures(storyboard, source) : []
|
|
9295
|
+
] : [];
|
|
9296
|
+
const seen = [...ours, ...frames2?.findings ?? [], ...storyboardFindings];
|
|
9131
9297
|
return {
|
|
9132
9298
|
passed: verdict.passed && seen.every((f) => f.severity !== "error"),
|
|
9133
9299
|
findings: [
|
|
9134
9300
|
...seen,
|
|
9135
|
-
// `scanBeatCount`
|
|
9136
|
-
// deck was asked for, and `verify <dir>` is handed a
|
|
9137
|
-
// nothing else — the same gating `scanNarrationLead`
|
|
9138
|
-
// the same reason: a check that cannot see its inputs
|
|
9139
|
-
// it found nothing.
|
|
9140
|
-
|
|
9141
|
-
...scanDiagrammatic(storyboard),
|
|
9142
|
-
...scanHeadlines(storyboard),
|
|
9143
|
-
...scanRepeatedObject(storyboard),
|
|
9144
|
-
// Needs the source as well, and says nothing without it — the same
|
|
9145
|
-
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
9146
|
-
...source ? scanUnusedFigures(storyboard, source) : []
|
|
9147
|
-
] : [],
|
|
9301
|
+
// `scanBeatCount` and `scanPaperArc` are deliberately NOT here. Both need
|
|
9302
|
+
// the preferences the deck was asked for, and `verify <dir>` is handed a
|
|
9303
|
+
// built directory and nothing else — the same gating `scanNarrationLead`
|
|
9304
|
+
// gets above, and for the same reason: a check that cannot see its inputs
|
|
9305
|
+
// must not report that it found nothing. They run at `plan` and `build`,
|
|
9306
|
+
// where prefs exist.
|
|
9148
9307
|
...verdict.findings
|
|
9149
9308
|
]
|
|
9150
9309
|
};
|
|
@@ -9329,6 +9488,32 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
9329
9488
|
}
|
|
9330
9489
|
];
|
|
9331
9490
|
}
|
|
9491
|
+
function scanPaperArc(storyboard, prefs) {
|
|
9492
|
+
return arcProblems(storyboard, prefs).map((message) => ({
|
|
9493
|
+
severity: "warning",
|
|
9494
|
+
gate: "storyboard",
|
|
9495
|
+
rule: "paper_arc",
|
|
9496
|
+
message
|
|
9497
|
+
}));
|
|
9498
|
+
}
|
|
9499
|
+
function scanNarrationDrift(storyboard, narration) {
|
|
9500
|
+
const flat = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
9501
|
+
const stale = [];
|
|
9502
|
+
for (const beat of storyboard.beats) {
|
|
9503
|
+
const segments = narration.beats[beat.id];
|
|
9504
|
+
if (!segments?.length || !flat(beat.narration)) continue;
|
|
9505
|
+
if (flat(segments.map((s) => s.text).join(" ")) !== flat(beat.narration)) stale.push(beat.id);
|
|
9506
|
+
}
|
|
9507
|
+
if (!stale.length) return [];
|
|
9508
|
+
return [
|
|
9509
|
+
{
|
|
9510
|
+
severity: "error",
|
|
9511
|
+
gate: "storyboard",
|
|
9512
|
+
rule: "narration_drift",
|
|
9513
|
+
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.`
|
|
9514
|
+
}
|
|
9515
|
+
];
|
|
9516
|
+
}
|
|
9332
9517
|
var LEAD_SECONDS = 1;
|
|
9333
9518
|
var EMPHASISED = /* @__PURE__ */ new Set(["equation-walk", "data-table"]);
|
|
9334
9519
|
function scanNarrationLead(beats, timing) {
|
|
@@ -9712,13 +9897,14 @@ function pieceArgs(source, fromFrame, motion, freeze, fps, out) {
|
|
|
9712
9897
|
out
|
|
9713
9898
|
];
|
|
9714
9899
|
}
|
|
9900
|
+
var LOUDNESS = "loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000";
|
|
9715
9901
|
function audioGraph(inputs, seconds, first = 1) {
|
|
9716
9902
|
const lines = inputs.map(
|
|
9717
9903
|
(input, i) => `[${first + i}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${input.delayMs}:all=1[d${i}]`
|
|
9718
9904
|
);
|
|
9719
9905
|
const labels = inputs.map((_, i) => `[d${i}]`).join("");
|
|
9720
9906
|
lines.push(
|
|
9721
|
-
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
9907
|
+
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,${LOUDNESS},apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
9722
9908
|
);
|
|
9723
9909
|
return lines.join(";\n");
|
|
9724
9910
|
}
|
|
@@ -10451,6 +10637,7 @@ export {
|
|
|
10451
10637
|
assertInsideResolves,
|
|
10452
10638
|
assertRefsResolve,
|
|
10453
10639
|
barCompareParamsSchema,
|
|
10640
|
+
beatRoleSchema,
|
|
10454
10641
|
beatSchema,
|
|
10455
10642
|
buildDeck,
|
|
10456
10643
|
bundleFont,
|
|
@@ -10517,7 +10704,9 @@ export {
|
|
|
10517
10704
|
sampleTimes,
|
|
10518
10705
|
scanBeatCount,
|
|
10519
10706
|
scanHeadlines,
|
|
10707
|
+
scanNarrationDrift,
|
|
10520
10708
|
scanNarrationLead,
|
|
10709
|
+
scanPaperArc,
|
|
10521
10710
|
scanRepeatedObject,
|
|
10522
10711
|
scanUnusedFigures,
|
|
10523
10712
|
sectionSchema,
|
package/dist/mcp.js
CHANGED
|
@@ -310,10 +310,16 @@ var insideSchema = z.object({
|
|
|
310
310
|
*/
|
|
311
311
|
label: z.string().optional()
|
|
312
312
|
});
|
|
313
|
+
var beatRoleSchema = z.enum(["intro", "background", "limitations", "conclusion"]);
|
|
313
314
|
var beatCore = {
|
|
314
315
|
id: z.string(),
|
|
315
316
|
/** What the viewer should understand after this beat. */
|
|
316
317
|
intent: z.string(),
|
|
318
|
+
/**
|
|
319
|
+
* OPTIONAL, and only ever present when `prefs.genre` is `paper`. The
|
|
320
|
+
* structural job this beat does; see `beatRoleSchema`.
|
|
321
|
+
*/
|
|
322
|
+
role: beatRoleSchema.optional(),
|
|
317
323
|
/** Optional: this beat happens inside a named part of the beat before it. */
|
|
318
324
|
inside: insideSchema.optional(),
|
|
319
325
|
/** The source sentence or equation this beat is accountable to. */
|
|
@@ -448,6 +454,26 @@ var prefsSchema = z.object({
|
|
|
448
454
|
tone: z.enum(["plain", "academic", "conversational", "punchy"]).default("plain"),
|
|
449
455
|
/** How much text a slide may carry before it should have been a diagram. */
|
|
450
456
|
density: z.enum(["sparse", "normal", "dense"]).default("normal"),
|
|
457
|
+
/**
|
|
458
|
+
* What kind of document is being explained, DECLARED and never sniffed.
|
|
459
|
+
*
|
|
460
|
+
* `paper` asks the planner for the shape a research talk has: open on the
|
|
461
|
+
* problem and the ground the work stands on, close on what it does not do and
|
|
462
|
+
* then what to take away. `general` is every deck built before this existed
|
|
463
|
+
* and changes nothing — no prompt block, no `role` in the planner's schema, no
|
|
464
|
+
* scan.
|
|
465
|
+
*
|
|
466
|
+
* WHY DECLARED. A ten-role heading lexicon (en/ko/ja/zh, numbered-prefix
|
|
467
|
+
* tolerant) run over all 351 markdown files in this repository scored 345 of
|
|
468
|
+
* them at zero role hits and none at three or more. `src/source/markdown.ts`
|
|
469
|
+
* says why in its first line: the input is a hypepaper-style ANALYSIS of a
|
|
470
|
+
* paper, a rewrite that has already discarded the headings a detector would
|
|
471
|
+
* key on. A classifier here would be a guess with a confidence score attached,
|
|
472
|
+
* and it would guess wrong on the Korean fixture. So the author says so once —
|
|
473
|
+
* `--genre paper`, or one line in a `decksmith.config.json` above a directory
|
|
474
|
+
* of papers — and every run under it costs no further typing.
|
|
475
|
+
*/
|
|
476
|
+
genre: z.enum(["general", "paper"]).default("general"),
|
|
451
477
|
/**
|
|
452
478
|
* How long the finished thing should run, in seconds. Optional: absent means
|
|
453
479
|
* "as long as it takes", which is what every deck built before this did.
|
|
@@ -705,7 +731,10 @@ function selectBeats(storyboard, budget2, seconds = {}) {
|
|
|
705
731
|
const protectedIds = protect(live, len, cap);
|
|
706
732
|
const keep = knapsack(live, len, cap, protectedIds);
|
|
707
733
|
if (!keep) {
|
|
708
|
-
const
|
|
734
|
+
const ends = new Set(
|
|
735
|
+
[live[0]?.id, live[live.length - 1]?.id].filter((id2) => !!id2)
|
|
736
|
+
);
|
|
737
|
+
const all = knapsack(live, len, cap, ends) ?? knapsack(live, len, cap, /* @__PURE__ */ new Set());
|
|
709
738
|
const chosen = all ?? [live[0]];
|
|
710
739
|
return budgetDrops(live, chosen, dropped, storyboard, len, cap, budget2, false);
|
|
711
740
|
}
|
|
@@ -737,8 +766,17 @@ function protect(live, len, cap) {
|
|
|
737
766
|
}
|
|
738
767
|
const picture = cheapest && !ids.has(cheapest.id) ? [cheapest] : [];
|
|
739
768
|
for (const b of picture) ids.add(b.id);
|
|
769
|
+
const byId = new Map(live.map((b) => [b.id, b]));
|
|
770
|
+
const roled = [];
|
|
771
|
+
for (const b of live) {
|
|
772
|
+
if (!b.role) continue;
|
|
773
|
+
for (let hop = b; hop && !ids.has(hop.id); hop = byId.get(hop.inside?.beat ?? "")) {
|
|
774
|
+
ids.add(hop.id);
|
|
775
|
+
roled.push(hop);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
740
778
|
const ends = /* @__PURE__ */ new Set([first?.id, last?.id]);
|
|
741
|
-
const releasable = [...tier, ...coverage, ...picture].filter((b) => !ends.has(b.id)).sort((a, b) => rate(a) - rate(b) || len(b) - len(a));
|
|
779
|
+
const releasable = [...tier, ...coverage, ...picture, ...roled].filter((b) => !ends.has(b.id)).sort((a, b) => rate(a) - rate(b) || len(b) - len(a));
|
|
742
780
|
const cost = () => live.filter((b) => ids.has(b.id)).reduce((s, b) => s + len(b), 0);
|
|
743
781
|
for (const b of releasable) {
|
|
744
782
|
if (cost() <= cap) break;
|
|
@@ -4719,6 +4757,9 @@ function depthCss(sid, pose, part) {
|
|
|
4719
4757
|
// src/emit/archetypes/stack.ts
|
|
4720
4758
|
var GAP3 = 44;
|
|
4721
4759
|
var NUM_X = 48;
|
|
4760
|
+
function numSpine(floor) {
|
|
4761
|
+
return Math.round(NUM_X * floor * 100 / MIN_FONT) / 100;
|
|
4762
|
+
}
|
|
4722
4763
|
var PROBE_X = 16;
|
|
4723
4764
|
var PROBE_H = 46;
|
|
4724
4765
|
var NUM_W = 70;
|
|
@@ -4901,7 +4942,7 @@ var stack = (beat, ctx) => {
|
|
|
4901
4942
|
);
|
|
4902
4943
|
const num2 = text(
|
|
4903
4944
|
String(i + 1),
|
|
4904
|
-
{ x:
|
|
4945
|
+
{ x: numSpine(L.floor), y: mid },
|
|
4905
4946
|
{ size: L.floor, weight: 600, fill: theme.dim, anchor: "end", vAlign: "middle" }
|
|
4906
4947
|
);
|
|
4907
4948
|
return group(slab(L.x0, y0, L, tint, lift2, stroke), { id: id(sid, "lay", i), class: "lay" }) + group(num2 + leader + dot + label + note, { id: id(sid, "cap", i), class: "cap" });
|
|
@@ -6134,6 +6175,17 @@ import { tmpdir } from "node:os";
|
|
|
6134
6175
|
import { join as join3 } from "node:path";
|
|
6135
6176
|
import { z as z2 } from "zod";
|
|
6136
6177
|
|
|
6178
|
+
// src/plan/arc.ts
|
|
6179
|
+
var ARC_ROLES = ["intro", "background", "limitations", "conclusion"];
|
|
6180
|
+
function requiredRoles(beatCount) {
|
|
6181
|
+
if (beatCount >= 8) return ARC_ROLES;
|
|
6182
|
+
if (beatCount >= 5) return ["limitations", "conclusion"];
|
|
6183
|
+
return [];
|
|
6184
|
+
}
|
|
6185
|
+
function paperArcRequested(prefs) {
|
|
6186
|
+
return prefs.genre === "paper";
|
|
6187
|
+
}
|
|
6188
|
+
|
|
6137
6189
|
// src/plan/duration.ts
|
|
6138
6190
|
var SPEECH_CPS = { latin: 14.4, cjk: 6.5 };
|
|
6139
6191
|
var LAST_HOLD_SECONDS = 4.2;
|
|
@@ -6723,6 +6775,46 @@ ${REVEAL_COUNTS}` : ` - ${n3 === 1 ? "ONE SENTENCE" : `${n3} SENTENCES`} FOR TH
|
|
|
6723
6775
|
miss its duration.`;
|
|
6724
6776
|
return { sentences, length };
|
|
6725
6777
|
}
|
|
6778
|
+
function paperArc(slides) {
|
|
6779
|
+
const asked = requiredRoles(slides);
|
|
6780
|
+
const full = asked.includes("intro");
|
|
6781
|
+
return `
|
|
6782
|
+
|
|
6783
|
+
PAPER ARC \u2014 this source was declared a research paper.
|
|
6784
|
+
|
|
6785
|
+
Four beats have a structural job, and each one NAMES its job in \`role\`. Every
|
|
6786
|
+
other beat leaves \`role\` off. A role is a job, not a heading: never write
|
|
6787
|
+
"Related work" or "Conclusion" as a headline, because RULE 8 still applies to
|
|
6788
|
+
all four.
|
|
6789
|
+
${full ? `
|
|
6790
|
+
role: "intro" Near the front. What problem exists and who has it, in
|
|
6791
|
+
the viewer's own terms. This is the opening RULE 6
|
|
6792
|
+
already asks for, named so the deck can be checked.
|
|
6793
|
+
role: "background" In the first three beats. What people did before this
|
|
6794
|
+
work, and where that ran out. Take it from what the
|
|
6795
|
+
source itself says about earlier approaches \u2014 if the
|
|
6796
|
+
source says nothing about them, leave the role off
|
|
6797
|
+
rather than inventing a literature (RULE 3).` : `
|
|
6798
|
+
This deck is short, so only the ENDING is required \u2014 an opening the deck
|
|
6799
|
+
already has is not worth a slide of its own here.`}
|
|
6800
|
+
role: "limitations" THE SECOND-TO-LAST beat. What the work does not do, in
|
|
6801
|
+
the source's own admission. Not a hedge inside another
|
|
6802
|
+
beat's sentence: its own slide.
|
|
6803
|
+
role: "conclusion" THE LAST beat, with nothing after it. What the viewer
|
|
6804
|
+
should carry away.
|
|
6805
|
+
|
|
6806
|
+
- The closing pair is TWO beats and they must not share an archetype (RULE 1).
|
|
6807
|
+
A limitation the source admits to is usually a panel; the conclusion is the
|
|
6808
|
+
claim the deck lands, so draw it where the source states it \u2014 bars, a
|
|
6809
|
+
contrast, the figure that settles it \u2014 and fall back to a panel only when it
|
|
6810
|
+
genuinely has no shape.
|
|
6811
|
+
- Give all four a weight of 0.8 or above. A short cut keeps the
|
|
6812
|
+
highest-weighted beats, and a structural beat below 0.8 is one the deck
|
|
6813
|
+
loses at the first budget.
|
|
6814
|
+
- If the source does not support one of these, LEAVE IT OUT. A slide that
|
|
6815
|
+
admits a limitation the paper never admits is worse than no slide.
|
|
6816
|
+
`;
|
|
6817
|
+
}
|
|
6726
6818
|
function illustrations(images) {
|
|
6727
6819
|
return `
|
|
6728
6820
|
|
|
@@ -6751,7 +6843,7 @@ nothing, so it can never dangle.
|
|
|
6751
6843
|
}
|
|
6752
6844
|
function systemPrompt(prefs) {
|
|
6753
6845
|
const plan = durationPlan(prefs);
|
|
6754
|
-
return `${rules(cadenceFor(prefs, plan))}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6846
|
+
return `${rules(cadenceFor(prefs, plan))}${paperArcRequested(prefs) ? paperArc(prefs.slides) : ""}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6755
6847
|
|
|
6756
6848
|
PREFERENCES \u2014 chosen by the person who asked for this deck.
|
|
6757
6849
|
${prefs.duration === void 0 ? "" : `
|
|
@@ -7020,8 +7112,11 @@ function stripNulls(node) {
|
|
|
7020
7112
|
return out;
|
|
7021
7113
|
}
|
|
7022
7114
|
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
7023
|
-
function
|
|
7024
|
-
|
|
7115
|
+
function plannerInvisible(prefs) {
|
|
7116
|
+
return paperArcRequested(prefs) ? PLANNER_INVISIBLE : /* @__PURE__ */ new Set([...PLANNER_INVISIBLE, "role"]);
|
|
7117
|
+
}
|
|
7118
|
+
function hideFromPlanner(node, hidden) {
|
|
7119
|
+
if (Array.isArray(node)) return node.map((n3) => hideFromPlanner(n3, hidden));
|
|
7025
7120
|
if (node === null || typeof node !== "object") return node;
|
|
7026
7121
|
const src = node;
|
|
7027
7122
|
const out = {};
|
|
@@ -7029,28 +7124,32 @@ function hideFromPlanner(node) {
|
|
|
7029
7124
|
if (key === "properties" && value && typeof value === "object") {
|
|
7030
7125
|
const kept = {};
|
|
7031
7126
|
for (const [prop, sub] of Object.entries(value))
|
|
7032
|
-
if (!
|
|
7127
|
+
if (!hidden.has(prop)) kept[prop] = hideFromPlanner(sub, hidden);
|
|
7033
7128
|
out.properties = kept;
|
|
7034
7129
|
continue;
|
|
7035
7130
|
}
|
|
7036
7131
|
if (key === "required" && Array.isArray(value)) {
|
|
7037
|
-
out.required = value.filter((r) => typeof r !== "string" || !
|
|
7132
|
+
out.required = value.filter((r) => typeof r !== "string" || !hidden.has(r));
|
|
7038
7133
|
continue;
|
|
7039
7134
|
}
|
|
7040
|
-
out[key] = hideFromPlanner(value);
|
|
7135
|
+
out[key] = hideFromPlanner(value, hidden);
|
|
7041
7136
|
}
|
|
7042
7137
|
return out;
|
|
7043
7138
|
}
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
)
|
|
7139
|
+
function schemaFor(prefs) {
|
|
7140
|
+
return hideFromPlanner(
|
|
7141
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" })),
|
|
7142
|
+
plannerInvisible(prefs)
|
|
7143
|
+
);
|
|
7144
|
+
}
|
|
7145
|
+
var SCHEMA = schemaFor({ genre: "general" });
|
|
7047
7146
|
async function codexPlanner(source, opts = {}) {
|
|
7048
7147
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
7049
7148
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
7050
7149
|
try {
|
|
7051
7150
|
const schemaPath = join3(dir, "storyboard.schema.json");
|
|
7052
7151
|
const outPath = join3(dir, "storyboard.json");
|
|
7053
|
-
await writeFile3(schemaPath, JSON.stringify(
|
|
7152
|
+
await writeFile3(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
7054
7153
|
await (opts.run ?? runCodex)({
|
|
7055
7154
|
prompt: buildPrompt(source, prefs),
|
|
7056
7155
|
schemaPath,
|
|
@@ -8173,13 +8272,14 @@ function pieceArgs(source, fromFrame, motion, freeze, fps, out) {
|
|
|
8173
8272
|
out
|
|
8174
8273
|
];
|
|
8175
8274
|
}
|
|
8275
|
+
var LOUDNESS = "loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000";
|
|
8176
8276
|
function audioGraph(inputs, seconds, first = 1) {
|
|
8177
8277
|
const lines = inputs.map(
|
|
8178
8278
|
(input, i) => `[${first + i}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${input.delayMs}:all=1[d${i}]`
|
|
8179
8279
|
);
|
|
8180
8280
|
const labels = inputs.map((_, i) => `[d${i}]`).join("");
|
|
8181
8281
|
lines.push(
|
|
8182
|
-
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
8282
|
+
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,${LOUDNESS},apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
8183
8283
|
);
|
|
8184
8284
|
return lines.join(";\n");
|
|
8185
8285
|
}
|
|
@@ -8968,6 +9068,7 @@ function parseOptions(fields) {
|
|
|
8968
9068
|
if (str(fields.lang) !== void 0) patch.lang = requireLang(str(fields.lang));
|
|
8969
9069
|
if (str(fields.tone) !== void 0) patch.tone = str(fields.tone);
|
|
8970
9070
|
if (str(fields.density) !== void 0) patch.density = str(fields.density);
|
|
9071
|
+
if (str(fields.genre) !== void 0) patch.genre = str(fields.genre);
|
|
8971
9072
|
if (str(fields.duration) !== void 0)
|
|
8972
9073
|
patch.duration = num("duration", fields.duration);
|
|
8973
9074
|
if (str(fields.speed) !== void 0) patch.animationSpeed = num("speed", fields.speed);
|
|
@@ -9818,6 +9919,9 @@ var settingsSchema = z4.object({
|
|
|
9818
9919
|
),
|
|
9819
9920
|
tone: z4.enum(["plain", "academic", "conversational", "punchy"]).optional(),
|
|
9820
9921
|
density: z4.enum(["sparse", "normal", "dense"]).optional().describe("How much text a SLIDE carries. A different axis from narration_density."),
|
|
9922
|
+
genre: z4.enum(["general", "paper"]).optional().describe(
|
|
9923
|
+
"DECLARED, never detected. `paper` asks for a research-talk shape \u2014 an introduction and background at the front, a limitations slide and then a conclusion at the end \u2014 and reports where the deck missed it. There is no detector: the documents this tool ingests are analyses OF papers, rewritten in a way that drops the headings a detector would need, so nothing infers this and absent means `general`."
|
|
9924
|
+
),
|
|
9821
9925
|
duration: z4.number().min(10).max(1800).optional().describe(
|
|
9822
9926
|
"Target seconds. Sets the pace, derives the slide count when you give none, and overrides animation_speed. Call decksmith_estimate_length first \u2014 a short target buys its words by saying less, not by playing faster."
|
|
9823
9927
|
),
|
|
@@ -9849,6 +9953,7 @@ function fieldsFor(s) {
|
|
|
9849
9953
|
put("lang", s.lang);
|
|
9850
9954
|
put("tone", s.tone);
|
|
9851
9955
|
put("density", s.density);
|
|
9956
|
+
put("genre", s.genre);
|
|
9852
9957
|
put("duration", s.duration);
|
|
9853
9958
|
put("slides", s.slides);
|
|
9854
9959
|
put("speed", s.animation_speed);
|
|
@@ -25,6 +25,8 @@ import type { BeatOf, Format } from "../../types.js";
|
|
|
25
25
|
import type { Emitter } from "../kit.js";
|
|
26
26
|
import { type Face } from "../svg.js";
|
|
27
27
|
type Params = BeatOf<"stack">["params"];
|
|
28
|
+
/** Where the numerals' right edge lands once the type floor has moved. */
|
|
29
|
+
export declare function numSpine(floor: number): number;
|
|
28
30
|
export interface StackLayout {
|
|
29
31
|
/**
|
|
30
32
|
* The type floor this layout solved against, which is `MIN_FONT` for a flat
|
package/dist/types/index.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export { verify } from "./verify/index.js";
|
|
|
111
111
|
* deck was asked for, which a built directory does not carry — and
|
|
112
112
|
* `scanUnusedFigures` folds in only where the source was passed alongside.
|
|
113
113
|
*/
|
|
114
|
-
export { scanBeatCount, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
|
|
114
|
+
export { scanBeatCount, scanNarrationDrift, scanPaperArc, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
|
|
115
115
|
export { check, parseCheckReport, sampleTimes } from "./verify/check.js";
|
|
116
116
|
export type { CheckOptions } from "./verify/check.js";
|
|
117
117
|
/**
|