@jokerized/decksmith 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -14
- package/dist/cli.js +450 -151
- package/dist/deck-runtime.js +2 -2
- package/dist/index.js +359 -68
- package/dist/mcp.js +477 -179
- package/dist/types/deck/runtime.d.ts +127 -0
- package/dist/types/emit/camera.d.ts +14 -6
- package/dist/types/emit/composition.d.ts +13 -0
- package/dist/types/emit/kit.d.ts +33 -8
- package/dist/types/emit/svg.d.ts +27 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/tmpdir.d.ts +33 -0
- package/dist/types/types.d.ts +28 -0
- package/package.json +2 -2
package/dist/mcp.js
CHANGED
|
@@ -7,22 +7,70 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { z as z6 } from "zod";
|
|
9
9
|
|
|
10
|
-
// src/
|
|
10
|
+
// src/tmpdir.ts
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
11
12
|
import { createRequire } from "node:module";
|
|
12
|
-
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
15
|
+
var cached;
|
|
16
|
+
function real(dir) {
|
|
17
|
+
let at = resolve(dir);
|
|
18
|
+
const tail = [];
|
|
19
|
+
for (; ; ) {
|
|
20
|
+
try {
|
|
21
|
+
return join(realpathSync(at), ...tail);
|
|
22
|
+
} catch {
|
|
23
|
+
const up = dirname(at);
|
|
24
|
+
if (up === at) return resolve(dir);
|
|
25
|
+
tail.unshift(basename(at));
|
|
26
|
+
at = up;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function packageRoot() {
|
|
31
|
+
cached ??= real(dirname(createRequire(import.meta.url).resolve("../package.json")));
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
function insideRoot(dir) {
|
|
35
|
+
const root2 = packageRoot();
|
|
36
|
+
const at = real(dir);
|
|
37
|
+
return at === root2 || at.startsWith(root2 + sep);
|
|
38
|
+
}
|
|
39
|
+
function guardTmpdir() {
|
|
40
|
+
const before = tmpdir();
|
|
41
|
+
if (!insideRoot(before)) return;
|
|
42
|
+
delete process.env.TMPDIR;
|
|
43
|
+
const after = tmpdir();
|
|
44
|
+
if (insideRoot(after)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`tmpdir: ${after} is still inside ${packageRoot()}. Set TMPDIR to a real temp directory.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const worker = process.env.VITEST_WORKER_ID;
|
|
50
|
+
if (worker === void 0 || worker === "1") {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`tmpdir: TMPDIR pointed at ${before}, inside the source tree, so every mkdtemp would land in the repo. Using ${after}. Fix the environment \u2014 this only stops the mess.
|
|
53
|
+
`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/version.ts
|
|
59
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
60
|
+
var VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
13
61
|
|
|
14
62
|
// src/mcp/tools.ts
|
|
15
63
|
import { randomBytes } from "node:crypto";
|
|
16
64
|
import { mkdir as mkdir11, mkdtemp as mkdtemp3, readFile as readFile13, rm as rm8 } from "node:fs/promises";
|
|
17
|
-
import { tmpdir as
|
|
18
|
-
import { basename as
|
|
65
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
66
|
+
import { basename as basename5, isAbsolute as isAbsolute2, join as join14, resolve as resolve7, sep as sep3 } from "node:path";
|
|
19
67
|
import { zipSync as zipSync2 } from "fflate";
|
|
20
68
|
import { z as z5 } from "zod";
|
|
21
69
|
|
|
22
70
|
// src/index.ts
|
|
23
71
|
import { cp, mkdir as mkdir9, readdir as readdir2, readFile as readFile11, stat as stat2, writeFile as writeFile11 } from "node:fs/promises";
|
|
24
|
-
import { createRequire as
|
|
25
|
-
import { dirname as
|
|
72
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
73
|
+
import { dirname as dirname4, join as join12, resolve as resolve5 } from "node:path";
|
|
26
74
|
import { fileURLToPath } from "node:url";
|
|
27
75
|
|
|
28
76
|
// src/pack/media.ts
|
|
@@ -415,15 +463,53 @@ var dataTableParamsSchema = z.object({
|
|
|
415
463
|
path: ["highlight"],
|
|
416
464
|
message: "every highlight must name a row that params.rows draws"
|
|
417
465
|
});
|
|
466
|
+
var chartPointSchema = z.object({ x: z.string(), y: z.number() });
|
|
418
467
|
var lineChartParamsSchema = z.object({
|
|
419
468
|
eyebrow: z.string().optional(),
|
|
420
469
|
headline: z.string(),
|
|
421
470
|
xLabel: z.string(),
|
|
422
471
|
yLabel: z.string(),
|
|
423
|
-
points: z.array(
|
|
472
|
+
points: z.array(chartPointSchema).min(2),
|
|
424
473
|
/** Inter-point annotations, e.g. per-step deltas. One fewer than `points`. */
|
|
425
474
|
deltas: z.array(z.string()).optional(),
|
|
426
|
-
readout: z.string().optional()
|
|
475
|
+
readout: z.string().optional(),
|
|
476
|
+
/**
|
|
477
|
+
* THE SAME MEASUREMENT UNDER A SECOND CONDITION — a baseline the main series
|
|
478
|
+
* is to be read against, where the point is the change in the SHAPE of the
|
|
479
|
+
* curve rather than two numbers.
|
|
480
|
+
*
|
|
481
|
+
* Drawn first and alone; then the curve lifts off it and reshapes into
|
|
482
|
+
* `points`, leaving this one behind as a ghost. `label` names the ghost and
|
|
483
|
+
* is drawn wherever the chart has room for it.
|
|
484
|
+
*
|
|
485
|
+
* KEEP IT SHORT — two or three words. A label wider than the plot is refused
|
|
486
|
+
* outright, and that refusal reaches `onBeatError` and costs the whole beat.
|
|
487
|
+
* One that fits but cannot be placed clear of the axis names, the tick and
|
|
488
|
+
* category labels, the values, the deltas and both curves is DROPPED
|
|
489
|
+
* instead, silently: an unnamed ghost is still legibly the fainter, earlier
|
|
490
|
+
* curve, where a name printed through a number is a defect in both of them.
|
|
491
|
+
* Nothing warns about that one — see the placement note in
|
|
492
|
+
* `src/emit/archetypes/line-chart.ts`.
|
|
493
|
+
*/
|
|
494
|
+
compare: z.object({ label: z.string(), points: z.array(chartPointSchema).min(2) }).optional()
|
|
495
|
+
}).superRefine((p, ctx) => {
|
|
496
|
+
if (!p.compare) return;
|
|
497
|
+
if (p.compare.points.length !== p.points.length) {
|
|
498
|
+
ctx.addIssue({
|
|
499
|
+
code: z.ZodIssueCode.custom,
|
|
500
|
+
path: ["compare", "points"],
|
|
501
|
+
message: `compare has ${p.compare.points.length} points against ${p.points.length}; a comparison is over the same x values`
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const off = p.compare.points.findIndex((c, i) => c.x !== p.points[i]?.x);
|
|
506
|
+
if (off >= 0) {
|
|
507
|
+
ctx.addIssue({
|
|
508
|
+
code: z.ZodIssueCode.custom,
|
|
509
|
+
path: ["compare", "points", off, "x"],
|
|
510
|
+
message: `compare point ${off} is "${p.compare.points[off]?.x}" where points has "${p.points[off]?.x}"`
|
|
511
|
+
});
|
|
512
|
+
}
|
|
427
513
|
});
|
|
428
514
|
var calloutParamsSchema = z.object({
|
|
429
515
|
eyebrow: z.string().optional(),
|
|
@@ -1171,7 +1257,7 @@ function clock(seconds) {
|
|
|
1171
1257
|
// src/source/fonts.ts
|
|
1172
1258
|
import { createHash as createHash2 } from "node:crypto";
|
|
1173
1259
|
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
1174
|
-
import { join } from "node:path";
|
|
1260
|
+
import { join as join2 } from "node:path";
|
|
1175
1261
|
function familyFor(lang) {
|
|
1176
1262
|
const tag = lang.toLowerCase();
|
|
1177
1263
|
if (tag.startsWith("ko")) return "Noto Sans KR";
|
|
@@ -1187,9 +1273,9 @@ async function bundleFont(lang, glyphs, dir) {
|
|
|
1187
1273
|
const stamp = `/* decksmith ${createHash2("sha256").update(`${family}
|
|
1188
1274
|
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1189
1275
|
await mkdir(dir, { recursive: true });
|
|
1190
|
-
const cssPath =
|
|
1191
|
-
const
|
|
1192
|
-
if (
|
|
1276
|
+
const cssPath = join2(dir, "fonts.css");
|
|
1277
|
+
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1278
|
+
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1193
1279
|
const res = await fetch(
|
|
1194
1280
|
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
1195
1281
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1203,7 +1289,7 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1203
1289
|
const font = await fetch(url);
|
|
1204
1290
|
if (!font.ok) throw new Error(`${url}: HTTP ${font.status}`);
|
|
1205
1291
|
const name = `${slug}-${i}.woff2`;
|
|
1206
|
-
await writeFile(
|
|
1292
|
+
await writeFile(join2(dir, name), Buffer.from(await font.arrayBuffer()));
|
|
1207
1293
|
css = css.replaceAll(url, name);
|
|
1208
1294
|
files.push(name);
|
|
1209
1295
|
}
|
|
@@ -1402,6 +1488,21 @@ function nv(v) {
|
|
|
1402
1488
|
}
|
|
1403
1489
|
var DRAW_FROM = { drawSVG: "0%" };
|
|
1404
1490
|
var DRAW_TO = { drawSVG: "100%" };
|
|
1491
|
+
var SHAPE_INDEX = 0;
|
|
1492
|
+
function reshape(target, to, at, seconds, first) {
|
|
1493
|
+
if (!(seconds > 0)) throw new Error(`reshape ${target}: ${seconds}s is no time to reshape in`);
|
|
1494
|
+
return fromTo(
|
|
1495
|
+
target,
|
|
1496
|
+
{ morphSVG: { shape: target, shapeIndex: SHAPE_INDEX } },
|
|
1497
|
+
{
|
|
1498
|
+
morphSVG: { shape: to, shapeIndex: SHAPE_INDEX },
|
|
1499
|
+
duration: sec(seconds),
|
|
1500
|
+
ease: "power2.inOut",
|
|
1501
|
+
...first ? {} : { immediateRender: false }
|
|
1502
|
+
},
|
|
1503
|
+
sec(at)
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1405
1506
|
function travel(target, route, at, seconds) {
|
|
1406
1507
|
if (!(seconds > 0)) throw new Error(`travel ${target}: ${seconds}s is no time to travel in`);
|
|
1407
1508
|
const legs = [];
|
|
@@ -4083,6 +4184,12 @@ var CHART_GAP = 60;
|
|
|
4083
4184
|
var READOUT_W = 460;
|
|
4084
4185
|
var READOUT_LH = 1.35;
|
|
4085
4186
|
var TALL_ASPECT = 1.15;
|
|
4187
|
+
var RESHAPE_SECONDS = 1.2;
|
|
4188
|
+
var GHOST_OPACITY = 0.28;
|
|
4189
|
+
var DRAW_SECONDS = 1.8;
|
|
4190
|
+
var DRAW_FLOOR = 1;
|
|
4191
|
+
var SEPARATE = 0.5;
|
|
4192
|
+
var STEP_FLOOR = 0.15;
|
|
4086
4193
|
function chartScale(values) {
|
|
4087
4194
|
const lo = Math.min(...values);
|
|
4088
4195
|
const hi = Math.max(...values);
|
|
@@ -4113,7 +4220,8 @@ var lineChart = (beat, ctx) => {
|
|
|
4113
4220
|
const { sid, theme } = ctx;
|
|
4114
4221
|
const p = beat.params;
|
|
4115
4222
|
const face = faceOf(theme.fontStack);
|
|
4116
|
-
const
|
|
4223
|
+
const cmp = p.compare;
|
|
4224
|
+
const scale = chartScale([...p.points, ...cmp?.points ?? []].map((pt) => pt.y));
|
|
4117
4225
|
const box = contentW(ctx.format);
|
|
4118
4226
|
const tall = isPortrait(ctx.format);
|
|
4119
4227
|
const width = tall ? box : box - (p.readout ? CHART_GAP + READOUT_W : 0);
|
|
@@ -4156,11 +4264,15 @@ var lineChart = (beat, ctx) => {
|
|
|
4156
4264
|
const LABEL_SIZE3 = 40;
|
|
4157
4265
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4158
4266
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4267
|
+
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
4268
|
+
const ghostW = (s) => textWidth(s, LABEL_SIZE3, 600, 0, false, face);
|
|
4159
4269
|
const shownX = fitIndices((i) => catW(p.points[i]?.x ?? ""), false);
|
|
4160
4270
|
const xLabels = p.points.map(
|
|
4161
4271
|
(pt, i) => shownX.has(i) ? `<text x="${n2(x(i))}" y="${H - PAD2.b + 56}">${esc(pt.x)}</text>` : ""
|
|
4162
4272
|
).join("");
|
|
4163
|
-
const
|
|
4273
|
+
const pathOf = (pts) => pts.map((pt, i) => `${i === 0 ? "M" : "L"}${n2(x(i))},${n2(y(pt.y))}`).join(" ");
|
|
4274
|
+
const path2 = pathOf(p.points);
|
|
4275
|
+
const basePath = cmp ? pathOf(cmp.points) : "";
|
|
4164
4276
|
const dots = p.points.map(
|
|
4165
4277
|
(pt, i) => `<circle class="dot" cx="${n2(x(i))}" cy="${n2(y(pt.y))}" r="${i === p.points.length - 1 ? 11 : 9}" fill="${i === p.points.length - 1 ? theme.tones.b : theme.accent}" />`
|
|
4166
4278
|
).join("");
|
|
@@ -4196,6 +4308,75 @@ var lineChart = (beat, ctx) => {
|
|
|
4196
4308
|
}).join("");
|
|
4197
4309
|
const first = { x: x(0), y: y(p.points[0]?.y ?? 0) };
|
|
4198
4310
|
const ring = p.points.length > 1 ? `<circle id="${sid}-ring" cx="${n2(first.x)}" cy="${n2(first.y)}" r="20" fill="none" stroke="${theme.tones.b}" stroke-width="5" opacity="0" />` : "";
|
|
4311
|
+
const lineTag = (part, d, extra = "") => `<path class="chartline" id="${sid}-${part}" d="${d}" fill="none" stroke="${theme.accent}"${extra} />`;
|
|
4312
|
+
const chartlines = cmp ? [
|
|
4313
|
+
lineTag("base", basePath),
|
|
4314
|
+
lineTag("line", basePath, ' opacity="0"'),
|
|
4315
|
+
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
4316
|
+
].join("\n ") : lineTag("line", path2);
|
|
4317
|
+
const curveBand = (pts, x0, x1) => {
|
|
4318
|
+
const ys = [];
|
|
4319
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
4320
|
+
for (let i = 0; i < pts.length; i++) {
|
|
4321
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
4322
|
+
}
|
|
4323
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
4324
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
4325
|
+
for (const edge of [x0, x1]) {
|
|
4326
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
4327
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
4332
|
+
};
|
|
4333
|
+
const fixedBoxes = [
|
|
4334
|
+
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
4335
|
+
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
4336
|
+
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
4337
|
+
{ x: 0, y: 42 - LABEL_SIZE3, w: nameW(p.yLabel), h: LABEL_SIZE3 },
|
|
4338
|
+
{
|
|
4339
|
+
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
4340
|
+
y: H - 16 - LABEL_SIZE3,
|
|
4341
|
+
w: nameW(p.xLabel),
|
|
4342
|
+
h: LABEL_SIZE3
|
|
4343
|
+
},
|
|
4344
|
+
...ticks.map((v) => {
|
|
4345
|
+
const w = runW(v.toFixed(scale.decimals));
|
|
4346
|
+
return { x: PAD2.l - 22 - w, y: y(v) + 13 - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4347
|
+
}),
|
|
4348
|
+
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
4349
|
+
];
|
|
4350
|
+
const lastI = p.points.length - 1;
|
|
4351
|
+
const ghost = (() => {
|
|
4352
|
+
if (!cmp) return void 0;
|
|
4353
|
+
const w = ghostW(cmp.label);
|
|
4354
|
+
if (w > plotW) {
|
|
4355
|
+
throw new Error(
|
|
4356
|
+
`line-chart ${beat.id}: the compare label "${cmp.label}" is ${Math.ceil(w)}px against the ${Math.floor(plotW)}px of plot it is set in. Shorten it.`
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
const atEnd = (i, start) => {
|
|
4360
|
+
const cy = y(cmp.points[i]?.y ?? 0);
|
|
4361
|
+
const away = Math.max(44, cy - 26);
|
|
4362
|
+
const toward = Math.min(cy + 52, H - PAD2.b + 4);
|
|
4363
|
+
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
4364
|
+
return sides.map((by) => ({ i, start, by }));
|
|
4365
|
+
};
|
|
4366
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
4367
|
+
return [...atEnd(lastI, false), ...atEnd(0, true)].find((c) => {
|
|
4368
|
+
const box2 = { x: c.start ? x(c.i) : x(c.i) - w, y: c.by - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4369
|
+
if (fixedBoxes.some((f) => overlaps(box2, f))) return false;
|
|
4370
|
+
if (valueBoxes.some((v) => overlaps(box2, v))) return false;
|
|
4371
|
+
if (drawnDeltas.some((d) => d !== null && overlaps(box2, d))) return false;
|
|
4372
|
+
return ![p.points, cmp.points].some((series) => {
|
|
4373
|
+
const [top, bottom] = curveBand(series, box2.x, box2.x + box2.w);
|
|
4374
|
+
return Math.min(box2.y + box2.h, bottom) - Math.max(box2.y, top) > 8;
|
|
4375
|
+
});
|
|
4376
|
+
});
|
|
4377
|
+
})();
|
|
4378
|
+
const ghostLabel = cmp && ghost ? `
|
|
4379
|
+
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.start ? "start" : "end"}" x="${n2(x(ghost.i))}" y="${n2(ghost.by)}">${esc(cmp.label)}</text>` : "";
|
|
4199
4380
|
const readout = p.readout ? `
|
|
4200
4381
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
4201
4382
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -4208,7 +4389,7 @@ var lineChart = (beat, ctx) => {
|
|
|
4208
4389
|
the svg and the layout gate reports container_overflow. -->
|
|
4209
4390
|
<text class="axname" x="0" y="42">${esc(p.yLabel)}</text>
|
|
4210
4391
|
<text class="axname" x="${n2(PAD2.l + plotW / 2)}" y="${H - 16}" text-anchor="middle">${esc(p.xLabel)}</text>
|
|
4211
|
-
|
|
4392
|
+
${chartlines}${ghostLabel}
|
|
4212
4393
|
<g>${dots}</g>
|
|
4213
4394
|
${ring}
|
|
4214
4395
|
<g class="ptlab" text-anchor="middle">${values}</g>
|
|
@@ -4216,10 +4397,52 @@ var lineChart = (beat, ctx) => {
|
|
|
4216
4397
|
</svg>${readout}
|
|
4217
4398
|
</div>`;
|
|
4218
4399
|
const draw2 = 0.8;
|
|
4219
|
-
const
|
|
4400
|
+
const count = p.points.length;
|
|
4401
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count);
|
|
4402
|
+
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
4403
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count - 1) : 0;
|
|
4404
|
+
const tailAfter = (s) => Math.max(
|
|
4405
|
+
s * count + 0.4,
|
|
4406
|
+
// the ring: its walk, then the 0.4s it takes to leave
|
|
4407
|
+
0.3 + s * (count - 1),
|
|
4408
|
+
// the dots
|
|
4409
|
+
0.5 + s * (count - 1),
|
|
4410
|
+
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
4411
|
+
);
|
|
4412
|
+
const room = beat.seconds - spine;
|
|
4413
|
+
let drawCents = Math.round(DRAW_SECONDS * 100);
|
|
4414
|
+
let stepCents = Math.floor(idealStep * 100);
|
|
4415
|
+
if (cmp) {
|
|
4416
|
+
while (drawCents / 100 + tailAfter(stepCents / 100) > room) {
|
|
4417
|
+
if (drawCents > Math.round(DRAW_FLOOR * 100)) drawCents--;
|
|
4418
|
+
else if (stepCents > Math.round(STEP_FLOOR * 100)) stepCents--;
|
|
4419
|
+
else break;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
const drawFor = cmp ? drawCents / 100 : DRAW_SECONDS;
|
|
4423
|
+
const step = cmp ? stepCents / 100 : idealStep;
|
|
4424
|
+
const lift2 = sec(draw2 + drawFor);
|
|
4425
|
+
const settled = cmp ? sec(lift2 + SEPARATE + RESHAPE_SECONDS) : draw2;
|
|
4220
4426
|
const tl = [
|
|
4221
4427
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
4222
|
-
tween(
|
|
4428
|
+
tween(
|
|
4429
|
+
`#${sid}-${cmp ? "base" : "line"}`,
|
|
4430
|
+
DRAW_FROM,
|
|
4431
|
+
{ ...DRAW_TO, duration: drawFor, ease: "none" },
|
|
4432
|
+
draw2
|
|
4433
|
+
)
|
|
4434
|
+
];
|
|
4435
|
+
if (cmp) {
|
|
4436
|
+
tl.push(
|
|
4437
|
+
...ghost ? [tween(`#${sid}-ghostlab`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, draw2 + 0.4)] : [],
|
|
4438
|
+
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
4439
|
+
// separating from itself rather than as a second line arriving.
|
|
4440
|
+
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
4441
|
+
tween(`#${sid}-base`, { opacity: 1 }, { opacity: GHOST_OPACITY, duration: SEPARATE }, lift2),
|
|
4442
|
+
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
4443
|
+
);
|
|
4444
|
+
}
|
|
4445
|
+
tl.push(
|
|
4223
4446
|
tween(
|
|
4224
4447
|
`#${sid} .dot`,
|
|
4225
4448
|
// Origin in both halves, or GSAP's smoothOrigin compensates the change with
|
|
@@ -4227,37 +4450,43 @@ var lineChart = (beat, ctx) => {
|
|
|
4227
4450
|
// polyline they mark, inside the frame and so invisible to every gate.
|
|
4228
4451
|
{ opacity: 0, scale: 0, transformOrigin: "center" },
|
|
4229
4452
|
{ opacity: 1, scale: 1, transformOrigin: "center", duration: 0.3, stagger: step },
|
|
4230
|
-
|
|
4453
|
+
settled
|
|
4231
4454
|
),
|
|
4232
|
-
tween(
|
|
4233
|
-
|
|
4455
|
+
tween(
|
|
4456
|
+
`#${sid} .pv`,
|
|
4457
|
+
{ opacity: 0 },
|
|
4458
|
+
{ opacity: 1, duration: 0.3, stagger: step },
|
|
4459
|
+
settled + 0.2
|
|
4460
|
+
)
|
|
4461
|
+
);
|
|
4234
4462
|
if (deltas) {
|
|
4235
4463
|
tl.push(
|
|
4236
4464
|
tween(
|
|
4237
4465
|
`#${sid} .dv`,
|
|
4238
4466
|
{ opacity: 0, y: -10 },
|
|
4239
4467
|
{ opacity: 1, y: 0, duration: 0.35, stagger: step },
|
|
4240
|
-
|
|
4468
|
+
settled + 0.6
|
|
4241
4469
|
)
|
|
4242
4470
|
);
|
|
4243
4471
|
}
|
|
4244
|
-
|
|
4472
|
+
const walk = cmp ? sec(step * count) : DRAW_SECONDS;
|
|
4473
|
+
if (count > 1) {
|
|
4245
4474
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
4246
4475
|
tl.push(
|
|
4247
|
-
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 },
|
|
4248
|
-
...travel(`#${sid}-ring`, route,
|
|
4476
|
+
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
4477
|
+
...travel(`#${sid}-ring`, route, settled, walk),
|
|
4249
4478
|
// And it leaves once the curve is whole: a marker parked on the last
|
|
4250
4479
|
// point for the rest of the beat reads as a defect, not as emphasis.
|
|
4251
4480
|
tween(
|
|
4252
4481
|
`#${sid}-ring`,
|
|
4253
4482
|
{ opacity: 1 },
|
|
4254
4483
|
{ opacity: 0, duration: 0.4, immediateRender: false },
|
|
4255
|
-
|
|
4484
|
+
settled + walk
|
|
4256
4485
|
)
|
|
4257
4486
|
);
|
|
4258
4487
|
}
|
|
4259
|
-
const drawn =
|
|
4260
|
-
const holds = [drawn];
|
|
4488
|
+
const drawn = settled + (cmp ? tailAfter(step) : step * count + 0.4);
|
|
4489
|
+
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
4261
4490
|
if (p.readout) {
|
|
4262
4491
|
tl.push(
|
|
4263
4492
|
// It enters from wherever it sits: from the right when it is beside the
|
|
@@ -4266,9 +4495,22 @@ var lineChart = (beat, ctx) => {
|
|
|
4266
4495
|
);
|
|
4267
4496
|
holds.push(drawn + 0.8);
|
|
4268
4497
|
}
|
|
4498
|
+
const end = holds[holds.length - 1] ?? 0;
|
|
4499
|
+
const need = sec(end + 0.15);
|
|
4500
|
+
if (cmp && need > beat.seconds + 1e-9) {
|
|
4501
|
+
return {
|
|
4502
|
+
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
4503
|
+
warnings: [
|
|
4504
|
+
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${count} points needs ${need}s and the beat is ${beat.seconds}s, so the chart was drawn without it. Lengthen the beat to keep the comparison.`
|
|
4505
|
+
]
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4269
4508
|
return {
|
|
4270
4509
|
html,
|
|
4271
4510
|
tl,
|
|
4511
|
+
// Named only when a beat actually reshapes, which is what keeps MorphSVG's
|
|
4512
|
+
// 21,195 bytes off every deck that does not. See `PLUGINS` in composition.ts.
|
|
4513
|
+
...cmp ? { plugins: ["morphSVG"] } : {},
|
|
4272
4514
|
holds: holdsWithin(holds, beat.seconds),
|
|
4273
4515
|
css: [
|
|
4274
4516
|
chromeCss(theme),
|
|
@@ -4293,6 +4535,14 @@ var lineChart = (beat, ctx) => {
|
|
|
4293
4535
|
`.axname{font-size:40px;fill:${theme.muted};font-weight:500}`,
|
|
4294
4536
|
`.ptlab{font-size:40px;fill:${theme.fg};font-weight:600}`,
|
|
4295
4537
|
`.delta{font-size:40px;fill:${theme.tones.b};font-weight:600}`,
|
|
4538
|
+
// Only when a ghost was actually named — not merely when there is a ghost,
|
|
4539
|
+
// since the label is dropped where neither side of the baseline is clear.
|
|
4540
|
+
// An unconditional rule would move the stylesheet bytes of every line chart
|
|
4541
|
+
// ever built for a part they do not draw, which is the whole thing
|
|
4542
|
+
// `Scene.plugins` is careful about one level up. `muted`, not `dim`: it
|
|
4543
|
+
// names a series, so it is read, and the curve it names is the thing that
|
|
4544
|
+
// has been faded, not its label.
|
|
4545
|
+
...ghost ? [`.ghostlab{font-size:40px;fill:${theme.muted};font-weight:600}`] : [],
|
|
4296
4546
|
// 1.7 set the two lines of a wrapped readout 68px apart, which reads as two
|
|
4297
4547
|
// unrelated fragments rather than one sentence. 1.35 keeps it a paragraph.
|
|
4298
4548
|
`.readout{font-size:${BODY_SIZE}px;line-height:${READOUT_LH};color:${theme.muted};max-width:${READOUT_W}px}`,
|
|
@@ -5538,7 +5788,10 @@ function round4(n3) {
|
|
|
5538
5788
|
// src/emit/composition.ts
|
|
5539
5789
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5540
5790
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5541
|
-
var
|
|
5791
|
+
var PLUGINS = {
|
|
5792
|
+
dsMorph: { src: "./vendor/ds-morph.js", global: "DSMorphPlugin" },
|
|
5793
|
+
morphSVG: { src: "./vendor/MorphSVGPlugin.min.js", global: "MorphSVGPlugin" }
|
|
5794
|
+
};
|
|
5542
5795
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5543
5796
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5544
5797
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5646,7 +5899,15 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5646
5899
|
}
|
|
5647
5900
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5648
5901
|
if (scene.measure?.length) builds = true;
|
|
5649
|
-
for (const
|
|
5902
|
+
for (const w of cut2.scene.warnings ?? []) opts.onBeatWarning?.(beat.id, w);
|
|
5903
|
+
for (const p of scene.plugins ?? []) {
|
|
5904
|
+
if (!Object.hasOwn(PLUGINS, p)) {
|
|
5905
|
+
throw new Error(
|
|
5906
|
+
`${beat.archetype} ${beat.id}: no vendored plugin named "${p}" \u2014 Scene.plugins takes ${Object.keys(PLUGINS).join(" or ")}`
|
|
5907
|
+
);
|
|
5908
|
+
}
|
|
5909
|
+
plugins.add(p);
|
|
5910
|
+
}
|
|
5650
5911
|
scenes.push(
|
|
5651
5912
|
sceneHtml(
|
|
5652
5913
|
sid,
|
|
@@ -5763,9 +6024,11 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5763
6024
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5764
6025
|
const island = format.navigable ? `
|
|
5765
6026
|
${emitIsland(slides)}` : "";
|
|
5766
|
-
const
|
|
5767
|
-
|
|
5768
|
-
<script
|
|
6027
|
+
const plugins = Object.entries(PLUGINS).filter(([name]) => laid.plugins.has(name)).map(
|
|
6028
|
+
([, p]) => `
|
|
6029
|
+
<script src="${p.src}"></script>
|
|
6030
|
+
<script>gsap.registerPlugin(${p.global});</script>`
|
|
6031
|
+
).join("");
|
|
5769
6032
|
return `<!doctype html>
|
|
5770
6033
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5771
6034
|
<head>
|
|
@@ -5774,7 +6037,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5774
6037
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5775
6038
|
<script src="${GSAP_SRC}"></script>
|
|
5776
6039
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5777
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${
|
|
6040
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${plugins}
|
|
5778
6041
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5779
6042
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5780
6043
|
<style>
|
|
@@ -6443,7 +6706,7 @@ function textOf(node) {
|
|
|
6443
6706
|
// src/source/harvest.ts
|
|
6444
6707
|
import { createHash as createHash5 } from "node:crypto";
|
|
6445
6708
|
import { copyFile, mkdir as mkdir3, rm as rm2, stat, writeFile as writeFile3 } from "node:fs/promises";
|
|
6446
|
-
import { basename as
|
|
6709
|
+
import { basename as basename3, join as join5, resolve as resolve2 } from "node:path";
|
|
6447
6710
|
|
|
6448
6711
|
// src/net/fetch.ts
|
|
6449
6712
|
import { lookup as resolveHost } from "node:dns/promises";
|
|
@@ -6643,8 +6906,8 @@ async function send(url, target, headers, signal, opts) {
|
|
|
6643
6906
|
...secure && isIP(host2) === 0 ? { servername: host2 } : {}
|
|
6644
6907
|
};
|
|
6645
6908
|
try {
|
|
6646
|
-
return await new Promise((
|
|
6647
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
6909
|
+
return await new Promise((resolve8, reject) => {
|
|
6910
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve8);
|
|
6648
6911
|
req.on("error", reject);
|
|
6649
6912
|
req.end();
|
|
6650
6913
|
});
|
|
@@ -6728,12 +6991,12 @@ function message(err) {
|
|
|
6728
6991
|
|
|
6729
6992
|
// src/render/capture.ts
|
|
6730
6993
|
import { homedir } from "node:os";
|
|
6731
|
-
import { join as
|
|
6994
|
+
import { join as join3 } from "node:path";
|
|
6732
6995
|
async function chromePath(need = "open the deck with") {
|
|
6733
6996
|
const explicit = process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
6734
6997
|
if (explicit) return explicit;
|
|
6735
6998
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6736
|
-
const cacheDir = process.env.PUPPETEER_CACHE_DIR ||
|
|
6999
|
+
const cacheDir = process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer");
|
|
6737
7000
|
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
6738
7001
|
const found = installed2.find((b) => b.browser === "chrome-headless-shell") ?? installed2.find((b) => b.browser === "chrome");
|
|
6739
7002
|
if (found) return found.executablePath;
|
|
@@ -6745,12 +7008,12 @@ async function chromePath(need = "open the deck with") {
|
|
|
6745
7008
|
// src/source/assets.ts
|
|
6746
7009
|
import { createHash as createHash4 } from "node:crypto";
|
|
6747
7010
|
import { mkdir as mkdir2, readdir, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
6748
|
-
import { extname as extname2, join as
|
|
7011
|
+
import { extname as extname2, join as join4 } from "node:path";
|
|
6749
7012
|
import { z as z2 } from "zod";
|
|
6750
7013
|
async function fetchFigures(source, dir, warnings) {
|
|
6751
7014
|
const drops = warnings ?? [];
|
|
6752
7015
|
await mkdir2(dir, { recursive: true });
|
|
6753
|
-
const
|
|
7016
|
+
const cached2 = await readdir(dir).catch(() => []);
|
|
6754
7017
|
const figures = [];
|
|
6755
7018
|
for (const figure of source.figures) {
|
|
6756
7019
|
if (figure.kind === "clip") {
|
|
@@ -6758,7 +7021,7 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6758
7021
|
continue;
|
|
6759
7022
|
}
|
|
6760
7023
|
try {
|
|
6761
|
-
figures.push(figureSchema.parse(await localize(figure, dir,
|
|
7024
|
+
figures.push(figureSchema.parse(await localize(figure, dir, cached2)));
|
|
6762
7025
|
} catch (err) {
|
|
6763
7026
|
drops.push(`figure ${figure.id} was left out: ${reason(err)}`);
|
|
6764
7027
|
}
|
|
@@ -6772,15 +7035,15 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6772
7035
|
}
|
|
6773
7036
|
return parsed.data;
|
|
6774
7037
|
}
|
|
6775
|
-
async function localize(figure, dir,
|
|
7038
|
+
async function localize(figure, dir, cached2) {
|
|
6776
7039
|
const stem = assetStem(figure.id, figure.src);
|
|
6777
|
-
const hit =
|
|
6778
|
-
const bytes = hit ? await readFile3(
|
|
7040
|
+
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7041
|
+
const bytes = hit ? await readFile3(join4(dir, hit)) : await load(figure.src);
|
|
6779
7042
|
const size = imageSize(bytes);
|
|
6780
7043
|
if (hit) return { ...figure, src: hit, ...size };
|
|
6781
7044
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6782
|
-
await writeFile2(
|
|
6783
|
-
|
|
7045
|
+
await writeFile2(join4(dir, src), bytes);
|
|
7046
|
+
cached2.push(src);
|
|
6784
7047
|
return { ...figure, src, ...size };
|
|
6785
7048
|
}
|
|
6786
7049
|
function assetStem(id2, src) {
|
|
@@ -7341,7 +7604,7 @@ function readContentRegion() {
|
|
|
7341
7604
|
|
|
7342
7605
|
// src/source/transcode.ts
|
|
7343
7606
|
import { readFile as readFile4, rm } from "node:fs/promises";
|
|
7344
|
-
import { basename } from "node:path";
|
|
7607
|
+
import { basename as basename2 } from "node:path";
|
|
7345
7608
|
|
|
7346
7609
|
// src/render/ffmpeg.ts
|
|
7347
7610
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -7366,7 +7629,7 @@ ${tail}`);
|
|
|
7366
7629
|
}
|
|
7367
7630
|
}
|
|
7368
7631
|
function runLive(file, args) {
|
|
7369
|
-
return new Promise((
|
|
7632
|
+
return new Promise((resolve8, reject) => {
|
|
7370
7633
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7371
7634
|
child.on("error", (err) => {
|
|
7372
7635
|
reject(
|
|
@@ -7374,7 +7637,7 @@ function runLive(file, args) {
|
|
|
7374
7637
|
);
|
|
7375
7638
|
});
|
|
7376
7639
|
child.on("close", (code, signal) => {
|
|
7377
|
-
if (code === 0)
|
|
7640
|
+
if (code === 0) resolve8();
|
|
7378
7641
|
else if (signal) {
|
|
7379
7642
|
reject(
|
|
7380
7643
|
new Error(
|
|
@@ -7570,7 +7833,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7570
7833
|
const file = opts.ffmpeg ?? "ffmpeg";
|
|
7571
7834
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7572
7835
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7573
|
-
const name =
|
|
7836
|
+
const name = basename2(input);
|
|
7574
7837
|
const source = videoSize(await readFile4(input));
|
|
7575
7838
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7576
7839
|
const kept = (why2) => ({
|
|
@@ -7639,7 +7902,7 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7639
7902
|
async function harvest(url, dir, opts = {}) {
|
|
7640
7903
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7641
7904
|
const warnings = [];
|
|
7642
|
-
const assets =
|
|
7905
|
+
const assets = resolve2(dir);
|
|
7643
7906
|
await mkdir3(assets, { recursive: true });
|
|
7644
7907
|
const page = await fetchGuarded(url, {
|
|
7645
7908
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
@@ -7967,7 +8230,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
7967
8230
|
const clips = [];
|
|
7968
8231
|
let spent = 0;
|
|
7969
8232
|
const done = /* @__PURE__ */ new Map();
|
|
7970
|
-
const ref = (got) => opts.refs === "relative" ?
|
|
8233
|
+
const ref = (got) => opts.refs === "relative" ? basename3(got.path) : got.path;
|
|
7971
8234
|
const overdrawn = () => {
|
|
7972
8235
|
if (Date.now() >= deadline) {
|
|
7973
8236
|
return `the harvest has used its ${Math.round(wallMs / 1e3)}s budget \u2014 raise maxWallMs`;
|
|
@@ -8018,7 +8281,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8018
8281
|
`it is ${size.width}x${size.height}, under ${MIN_FIGURE_PX}px \u2014 spacers, icons and tracking pixels look like this, and a slide cannot use one`
|
|
8019
8282
|
);
|
|
8020
8283
|
}
|
|
8021
|
-
const path2 =
|
|
8284
|
+
const path2 = join5(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8022
8285
|
await writeFile3(path2, bytes);
|
|
8023
8286
|
assets.push(path2);
|
|
8024
8287
|
got = { path: path2, width: size.width, height: size.height };
|
|
@@ -8031,7 +8294,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8031
8294
|
};
|
|
8032
8295
|
const shrink = async (url, path2, measured, what) => {
|
|
8033
8296
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8034
|
-
const out2 =
|
|
8297
|
+
const out2 = join5(dir, assetName(url, ".vp9.webm"));
|
|
8035
8298
|
let small;
|
|
8036
8299
|
try {
|
|
8037
8300
|
small = await transcode(path2, out2, {
|
|
@@ -8046,7 +8309,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8046
8309
|
const [before, after] = await Promise.all([sizeOf(path2), sizeOf(small.path)]);
|
|
8047
8310
|
if (before > 0 && after > before) {
|
|
8048
8311
|
warnings.push(
|
|
8049
|
-
`${what} \u2014 ${
|
|
8312
|
+
`${what} \u2014 ${basename3(small.path)} came back LARGER than the page's own file (${mb(before)} \u2192 ${mb(after)}) at ${small.width}x${small.height}. The encode is sized for the render, which pre-decodes every clip to one still per output frame, so it still costs less to render; pass --no-transcode if the deck's size is what matters here.`
|
|
8050
8313
|
);
|
|
8051
8314
|
}
|
|
8052
8315
|
await rm2(path2, { force: true });
|
|
@@ -8073,7 +8336,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8073
8336
|
try {
|
|
8074
8337
|
const bytes = await bytesOf(url);
|
|
8075
8338
|
const measured = videoSize(bytes);
|
|
8076
|
-
const path2 =
|
|
8339
|
+
const path2 = join5(dir, assetName(url, `.${measured.container}`));
|
|
8077
8340
|
await writeFile3(path2, bytes);
|
|
8078
8341
|
return await shrink(url, path2, measured, what);
|
|
8079
8342
|
} catch (err) {
|
|
@@ -8438,8 +8701,8 @@ ${fence}`;
|
|
|
8438
8701
|
// src/plan/codex.ts
|
|
8439
8702
|
import { spawn as spawn2 } from "node:child_process";
|
|
8440
8703
|
import { mkdtemp, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
8441
|
-
import { tmpdir } from "node:os";
|
|
8442
|
-
import { join as
|
|
8704
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
8705
|
+
import { join as join6 } from "node:path";
|
|
8443
8706
|
import { z as z3 } from "zod";
|
|
8444
8707
|
|
|
8445
8708
|
// src/plan/arc.ts
|
|
@@ -8651,7 +8914,10 @@ var REVEALS = {
|
|
|
8651
8914
|
"equation-walk": "one per term",
|
|
8652
8915
|
"equation-morph": "2",
|
|
8653
8916
|
"data-table": "one per highlighted row, plus 1",
|
|
8654
|
-
"
|
|
8917
|
+
// "long enough" is not a hedge: below the emitter's own floor the comparison
|
|
8918
|
+
// is dropped and the chart stops twice becoming a chart that stops once. See
|
|
8919
|
+
// the line-chart entry under THE THIRTEEN ARCHETYPES for what that costs.
|
|
8920
|
+
"line-chart": "1, or 2 when compare is given and the beat is long enough for it",
|
|
8655
8921
|
callout: "one per panel",
|
|
8656
8922
|
pipeline: "one per stage",
|
|
8657
8923
|
"annotated-figure": "one per note, plus 1",
|
|
@@ -8774,6 +9040,29 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
8774
9040
|
scale, over training. Points come from the source's numbers;
|
|
8775
9041
|
deltas, if given, are the steps between consecutive points, so
|
|
8776
9042
|
there is always one fewer.
|
|
9043
|
+
\`compare\` adds a SECOND CONDITION over the same axis \u2014 the tell
|
|
9044
|
+
there is THE SAME QUANTITY MEASURED TWO WAYS, a baseline and the
|
|
9045
|
+
result, where what the source is claiming is the change in the
|
|
9046
|
+
SHAPE of the curve and not two numbers. Its \`label\` names the
|
|
9047
|
+
baseline ("without pretraining", "FP32") and is drawn on the
|
|
9048
|
+
slide where the chart has room for it. Keep it SHORT \u2014 two or
|
|
9049
|
+
three words. A label wider than the plot is refused and the
|
|
9050
|
+
whole slide goes with it, and one that cannot be placed clear of
|
|
9051
|
+
the axis names, the values and both curves is dropped without a
|
|
9052
|
+
word, leaving the baseline unnamed. A \`readout\` takes the plot
|
|
9053
|
+
down to about two thirds of its width, so a beat that has one
|
|
9054
|
+
has room for a shorter label still.
|
|
9055
|
+
\`compare.points\` must carry the SAME x values, in the
|
|
9056
|
+
same order, as \`points\`; the baseline is drawn first, then the
|
|
9057
|
+
curve reshapes into \`points\` and leaves the baseline behind. Do
|
|
9058
|
+
not use it for two unrelated series that happen to share an
|
|
9059
|
+
axis \u2014 nothing reshapes into something it is not a version of.
|
|
9060
|
+
A compare beat also costs TIME \u2014 the baseline draws, is held,
|
|
9061
|
+
then reshapes \u2014 so give it \`seconds\` of 7, or 8 with a
|
|
9062
|
+
\`readout\`. Give it too few and the emitter draws the chart
|
|
9063
|
+
without the comparison rather than stopping on a half-drawn one:
|
|
9064
|
+
the slide is still there, but the point about the baseline is
|
|
9065
|
+
gone.
|
|
8777
9066
|
|
|
8778
9067
|
DESCRIBING ARCHETYPES \u2014 the fallbacks
|
|
8779
9068
|
|
|
@@ -9431,10 +9720,10 @@ function schemaFor(prefs) {
|
|
|
9431
9720
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9432
9721
|
async function codexPlanner(source, opts = {}) {
|
|
9433
9722
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9434
|
-
const dir = await mkdtemp(
|
|
9723
|
+
const dir = await mkdtemp(join6(tmpdir2(), "decksmith-plan-"));
|
|
9435
9724
|
try {
|
|
9436
|
-
const schemaPath =
|
|
9437
|
-
const outPath =
|
|
9725
|
+
const schemaPath = join6(dir, "storyboard.schema.json");
|
|
9726
|
+
const outPath = join6(dir, "storyboard.json");
|
|
9438
9727
|
await writeFile4(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9439
9728
|
await (opts.run ?? runCodex)({
|
|
9440
9729
|
prompt: buildPrompt(source, prefs),
|
|
@@ -9516,7 +9805,7 @@ function codexCommand(args) {
|
|
|
9516
9805
|
}
|
|
9517
9806
|
function runCodex(args) {
|
|
9518
9807
|
const { argv, env: env2 } = codexCommand(args);
|
|
9519
|
-
return new Promise((
|
|
9808
|
+
return new Promise((resolve8, reject) => {
|
|
9520
9809
|
const child = spawn2("codex", argv, {
|
|
9521
9810
|
stdio: ["pipe", "ignore", "pipe"],
|
|
9522
9811
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -9539,7 +9828,7 @@ function runCodex(args) {
|
|
|
9539
9828
|
});
|
|
9540
9829
|
child.on("close", (code) => {
|
|
9541
9830
|
clearTimeout(timer);
|
|
9542
|
-
if (code === 0) return
|
|
9831
|
+
if (code === 0) return resolve8();
|
|
9543
9832
|
reject(
|
|
9544
9833
|
new Error(`codex exec exited ${code}.
|
|
9545
9834
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -9552,13 +9841,13 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
9552
9841
|
// src/images/illustrate.ts
|
|
9553
9842
|
import { createHash as createHash7 } from "node:crypto";
|
|
9554
9843
|
import { mkdir as mkdir4, readFile as readFile7, rename, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
|
|
9555
|
-
import { join as
|
|
9844
|
+
import { join as join8 } from "node:path";
|
|
9556
9845
|
|
|
9557
9846
|
// src/images/providers.ts
|
|
9558
9847
|
import { createHash as createHash6 } from "node:crypto";
|
|
9559
9848
|
import { mkdtemp as mkdtemp2, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
|
|
9560
|
-
import { tmpdir as
|
|
9561
|
-
import { join as
|
|
9849
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
9850
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
9562
9851
|
import { z as z4 } from "zod";
|
|
9563
9852
|
var SIZE = {
|
|
9564
9853
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -9690,10 +9979,10 @@ function codexImages(opts = {}) {
|
|
|
9690
9979
|
async check() {
|
|
9691
9980
|
},
|
|
9692
9981
|
async generate(req) {
|
|
9693
|
-
const dir = await mkdtemp2(
|
|
9982
|
+
const dir = await mkdtemp2(join7(tmpdir3(), "decksmith-image-"));
|
|
9694
9983
|
try {
|
|
9695
|
-
const schemaPath =
|
|
9696
|
-
const outPath =
|
|
9984
|
+
const schemaPath = join7(dir, "answer.schema.json");
|
|
9985
|
+
const outPath = join7(dir, "answer.json");
|
|
9697
9986
|
await writeFile5(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
9698
9987
|
await run5({
|
|
9699
9988
|
prompt: codexPrompt(req),
|
|
@@ -9711,8 +10000,8 @@ function codexImages(opts = {}) {
|
|
|
9711
10000
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
9712
10001
|
);
|
|
9713
10002
|
}
|
|
9714
|
-
const candidates2 = [
|
|
9715
|
-
if (answer.data.file) candidates2.push(
|
|
10003
|
+
const candidates2 = [join7(dir, "picture.png")];
|
|
10004
|
+
if (answer.data.file) candidates2.push(resolve3(dir, answer.data.file));
|
|
9716
10005
|
for (const path2 of candidates2) {
|
|
9717
10006
|
const bytes = await readFile6(path2).catch(() => null);
|
|
9718
10007
|
if (bytes) return raster(bytes, "codex");
|
|
@@ -9943,16 +10232,16 @@ function cacheKey(providerId, req) {
|
|
|
9943
10232
|
async function draw(provider, req, name, dir) {
|
|
9944
10233
|
for (const ext of Object.values(EXT2)) {
|
|
9945
10234
|
const src2 = `${name}${ext}`;
|
|
9946
|
-
const bytes = await readFile7(
|
|
10235
|
+
const bytes = await readFile7(join8(dir, src2)).catch(() => null);
|
|
9947
10236
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
9948
10237
|
}
|
|
9949
10238
|
await provider.check();
|
|
9950
10239
|
const img = await provider.generate(req);
|
|
9951
10240
|
const src = `${name}${EXT2[img.mime]}`;
|
|
9952
|
-
const tmp =
|
|
10241
|
+
const tmp = join8(dir, `.${name}.tmp`);
|
|
9953
10242
|
try {
|
|
9954
10243
|
await writeFile6(tmp, img.bytes);
|
|
9955
|
-
await rename(tmp,
|
|
10244
|
+
await rename(tmp, join8(dir, src));
|
|
9956
10245
|
} finally {
|
|
9957
10246
|
await rm5(tmp, { force: true });
|
|
9958
10247
|
}
|
|
@@ -9967,7 +10256,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
9967
10256
|
import { createHash as createHash8 } from "node:crypto";
|
|
9968
10257
|
import { mkdir as mkdir5, readFile as readFile8, rm as rm6, writeFile as writeFile7 } from "node:fs/promises";
|
|
9969
10258
|
import { homedir as homedir2 } from "node:os";
|
|
9970
|
-
import { join as
|
|
10259
|
+
import { join as join9 } from "node:path";
|
|
9971
10260
|
var MISSING = [
|
|
9972
10261
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
9973
10262
|
"",
|
|
@@ -9984,8 +10273,8 @@ function candidates() {
|
|
|
9984
10273
|
["edge-tts"],
|
|
9985
10274
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
9986
10275
|
// default, and both are where this actually lands in practice.
|
|
9987
|
-
[
|
|
9988
|
-
[
|
|
10276
|
+
[join9(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10277
|
+
[join9(home, ".local", "bin", "edge-tts")],
|
|
9989
10278
|
// Last resort: the module is installed even though its console script is
|
|
9990
10279
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
9991
10280
|
["python3", "-m", "edge_tts"]
|
|
@@ -10010,7 +10299,7 @@ async function find2(can) {
|
|
|
10010
10299
|
throw new Error(MISSING);
|
|
10011
10300
|
}
|
|
10012
10301
|
function runArgv(cmd, args) {
|
|
10013
|
-
return new Promise((
|
|
10302
|
+
return new Promise((resolve8) => {
|
|
10014
10303
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10015
10304
|
let stdout = "";
|
|
10016
10305
|
let stderr = "";
|
|
@@ -10020,8 +10309,8 @@ function runArgv(cmd, args) {
|
|
|
10020
10309
|
child.stderr.on("data", (b) => {
|
|
10021
10310
|
stderr += b.toString();
|
|
10022
10311
|
});
|
|
10023
|
-
child.on("error", (e) =>
|
|
10024
|
-
child.on("close", (code) =>
|
|
10312
|
+
child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
|
|
10313
|
+
child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
|
|
10025
10314
|
});
|
|
10026
10315
|
}
|
|
10027
10316
|
var edgeTts = {
|
|
@@ -10106,10 +10395,10 @@ async function synthesize(text2, opts) {
|
|
|
10106
10395
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10107
10396
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10108
10397
|
const file = `${key}.mp3`;
|
|
10109
|
-
const audio =
|
|
10110
|
-
const sidecar =
|
|
10111
|
-
const
|
|
10112
|
-
if (
|
|
10398
|
+
const audio = join9(opts.dir, file);
|
|
10399
|
+
const sidecar = join9(opts.dir, `${key}.json`);
|
|
10400
|
+
const cached2 = await readSidecar(sidecar);
|
|
10401
|
+
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10113
10402
|
await mkdir5(opts.dir, { recursive: true });
|
|
10114
10403
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10115
10404
|
const { cues, seconds } = spoken;
|
|
@@ -10273,11 +10562,11 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
10273
10562
|
|
|
10274
10563
|
// src/render/render.ts
|
|
10275
10564
|
import { mkdir as mkdir7, readFile as readFile9, rename as rename2, rm as rm7, writeFile as writeFile9 } from "node:fs/promises";
|
|
10276
|
-
import { basename as
|
|
10565
|
+
import { basename as basename4, dirname as dirname2, join as join11, resolve as resolve4 } from "node:path";
|
|
10277
10566
|
|
|
10278
10567
|
// src/render/captions.ts
|
|
10279
10568
|
import { mkdir as mkdir6, writeFile as writeFile8 } from "node:fs/promises";
|
|
10280
|
-
import { join as
|
|
10569
|
+
import { join as join10 } from "node:path";
|
|
10281
10570
|
import { pathToFileURL } from "node:url";
|
|
10282
10571
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
10283
10572
|
function cssString(value) {
|
|
@@ -10368,9 +10657,9 @@ async function captionBlocker() {
|
|
|
10368
10657
|
}
|
|
10369
10658
|
async function renderCaptions(cues, style, deck, work2) {
|
|
10370
10659
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
10371
|
-
const fontCss =
|
|
10660
|
+
const fontCss = join10(deck, DECK_FONT_CSS);
|
|
10372
10661
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL(fontCss).href : null;
|
|
10373
|
-
const page =
|
|
10662
|
+
const page = join10(work2, "captions.html");
|
|
10374
10663
|
await mkdir6(work2, { recursive: true });
|
|
10375
10664
|
await writeFile8(page, captionPage(cues, style, href));
|
|
10376
10665
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
@@ -10392,7 +10681,7 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
10392
10681
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
10393
10682
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
10394
10683
|
await tab.screenshot({
|
|
10395
|
-
path:
|
|
10684
|
+
path: join10(work2, name),
|
|
10396
10685
|
type: "png",
|
|
10397
10686
|
omitBackground: true,
|
|
10398
10687
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -10426,16 +10715,16 @@ function overlayInputs(band) {
|
|
|
10426
10715
|
}
|
|
10427
10716
|
|
|
10428
10717
|
// src/render/render.ts
|
|
10429
|
-
var SRT_NAME = (out) => `${
|
|
10718
|
+
var SRT_NAME = (out) => `${basename4(out).replace(/\.[^.]+$/, "")}.srt`;
|
|
10430
10719
|
function subtitlePlan(mode) {
|
|
10431
10720
|
return { sidecar: mode !== "none", burn: mode === "burn" };
|
|
10432
10721
|
}
|
|
10433
10722
|
async function render(opts) {
|
|
10434
10723
|
const log = opts.log ?? (() => {
|
|
10435
10724
|
});
|
|
10436
|
-
const deck =
|
|
10437
|
-
const out =
|
|
10438
|
-
const work2 =
|
|
10725
|
+
const deck = resolve4(opts.deck);
|
|
10726
|
+
const out = resolve4(opts.out);
|
|
10727
|
+
const work2 = join11(dirname2(out), `.${basename4(out)}.parts`);
|
|
10439
10728
|
const timing = await readTiming(deck);
|
|
10440
10729
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
10441
10730
|
const refusal = playbackRefusal(
|
|
@@ -10453,7 +10742,7 @@ async function render(opts) {
|
|
|
10453
10742
|
const burnable = plan0.burn;
|
|
10454
10743
|
await mkdir7(work2, { recursive: true });
|
|
10455
10744
|
try {
|
|
10456
|
-
const raw2 = opts.video ?
|
|
10745
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join11(work2, "raw.mp4"), opts, log);
|
|
10457
10746
|
const shot = await probe(raw2);
|
|
10458
10747
|
log(
|
|
10459
10748
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -10466,7 +10755,7 @@ async function render(opts) {
|
|
|
10466
10755
|
);
|
|
10467
10756
|
}
|
|
10468
10757
|
const retimed = await retime(raw2, plan, work2, log);
|
|
10469
|
-
const srtPath =
|
|
10758
|
+
const srtPath = join11(dirname2(out), SRT_NAME(out));
|
|
10470
10759
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
10471
10760
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
10472
10761
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
@@ -10477,7 +10766,7 @@ async function render(opts) {
|
|
|
10477
10766
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
10478
10767
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
10479
10768
|
if (warning) log(`render: ${warning}`);
|
|
10480
|
-
const fast =
|
|
10769
|
+
const fast = join11(work2, `fast.${basename4(out)}`);
|
|
10481
10770
|
const muxed = await probe(out);
|
|
10482
10771
|
await runTool(
|
|
10483
10772
|
"ffmpeg",
|
|
@@ -10507,7 +10796,7 @@ async function render(opts) {
|
|
|
10507
10796
|
}
|
|
10508
10797
|
}
|
|
10509
10798
|
async function readTiming(deck) {
|
|
10510
|
-
const path2 =
|
|
10799
|
+
const path2 = join11(deck, TIMING_FILE);
|
|
10511
10800
|
const text2 = await readFile9(path2, "utf8").catch(() => {
|
|
10512
10801
|
throw new Error(
|
|
10513
10802
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
@@ -10547,20 +10836,20 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10547
10836
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
10548
10837
|
const list = [];
|
|
10549
10838
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
10550
|
-
const file =
|
|
10839
|
+
const file = join11(work2, `p${String(i).padStart(4, "0")}.ts`);
|
|
10551
10840
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
10552
10841
|
list.push(file);
|
|
10553
10842
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
10554
10843
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
10555
10844
|
}
|
|
10556
10845
|
}
|
|
10557
|
-
const listFile =
|
|
10846
|
+
const listFile = join11(work2, "pieces.txt");
|
|
10558
10847
|
await writeFile9(
|
|
10559
10848
|
listFile,
|
|
10560
10849
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
10561
10850
|
`
|
|
10562
10851
|
);
|
|
10563
|
-
const joined =
|
|
10852
|
+
const joined = join11(work2, "retimed.mp4");
|
|
10564
10853
|
await runTool("ffmpeg", [
|
|
10565
10854
|
"-y",
|
|
10566
10855
|
"-hide_banner",
|
|
@@ -10582,7 +10871,7 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10582
10871
|
}
|
|
10583
10872
|
async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
10584
10873
|
const inputs = plan.audio.map((a) => ({
|
|
10585
|
-
file:
|
|
10874
|
+
file: join11(deck, timing.audioDir, a.audio),
|
|
10586
10875
|
delayMs: a.delayMs
|
|
10587
10876
|
}));
|
|
10588
10877
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -10603,7 +10892,7 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10603
10892
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
10604
10893
|
}
|
|
10605
10894
|
if (graph.length > 0) {
|
|
10606
|
-
const script =
|
|
10895
|
+
const script = join11(work2, "mux.filter");
|
|
10607
10896
|
await writeFile9(script, `${graph.join(";\n")}
|
|
10608
10897
|
`);
|
|
10609
10898
|
args.push("-filter_complex_script", script);
|
|
@@ -10619,13 +10908,13 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10619
10908
|
log(
|
|
10620
10909
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
10621
10910
|
);
|
|
10622
|
-
await mkdir7(
|
|
10911
|
+
await mkdir7(dirname2(out), { recursive: true });
|
|
10623
10912
|
await runTool("ffmpeg", args, { cwd: work2 });
|
|
10624
10913
|
}
|
|
10625
10914
|
|
|
10626
10915
|
// src/pack/pack.ts
|
|
10627
10916
|
import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile10 } from "node:fs/promises";
|
|
10628
|
-
import { dirname as
|
|
10917
|
+
import { dirname as dirname3, extname as extname3 } from "node:path";
|
|
10629
10918
|
import { unzipSync, zipSync } from "fflate";
|
|
10630
10919
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
10631
10920
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -10664,7 +10953,7 @@ async function writePack(pack3, files, out) {
|
|
|
10664
10953
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
10665
10954
|
}
|
|
10666
10955
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
10667
|
-
await mkdir8(
|
|
10956
|
+
await mkdir8(dirname3(out), { recursive: true });
|
|
10668
10957
|
await writeFile10(out, zip);
|
|
10669
10958
|
return zip.length;
|
|
10670
10959
|
}
|
|
@@ -10684,7 +10973,7 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10684
10973
|
if (!format) throw new Error("no deck-16x9 format");
|
|
10685
10974
|
const step = opts.onStep ?? (() => {
|
|
10686
10975
|
});
|
|
10687
|
-
const out =
|
|
10976
|
+
const out = resolve5(outDir);
|
|
10688
10977
|
await mkdir9(out, { recursive: true });
|
|
10689
10978
|
const speed = opts.speed ?? 1;
|
|
10690
10979
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
@@ -10693,12 +10982,13 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10693
10982
|
...opts.theme ? { theme: opts.theme } : {},
|
|
10694
10983
|
...opts.narration ? { narration: opts.narration } : {},
|
|
10695
10984
|
...opts.onBeatError ? { onBeatError: opts.onBeatError } : {},
|
|
10985
|
+
...opts.onBeatWarning ? { onBeatWarning: opts.onBeatWarning } : {},
|
|
10696
10986
|
...fontCss ? { fontCss } : {}
|
|
10697
10987
|
});
|
|
10698
10988
|
const files = [];
|
|
10699
10989
|
const write = async (name, text2) => {
|
|
10700
|
-
await writeFile11(
|
|
10701
|
-
files.push(
|
|
10990
|
+
await writeFile11(join12(out, name), text2);
|
|
10991
|
+
files.push(join12(out, name));
|
|
10702
10992
|
};
|
|
10703
10993
|
await write("index.html", deck.composition);
|
|
10704
10994
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
@@ -10727,8 +11017,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10727
11017
|
}
|
|
10728
11018
|
if (deck.page) {
|
|
10729
11019
|
await write(DECK_PAGE, deck.page);
|
|
10730
|
-
await cp(playerBundle(),
|
|
10731
|
-
files.push(
|
|
11020
|
+
await cp(playerBundle(), join12(out, PLAYER_FILE));
|
|
11021
|
+
files.push(join12(out, PLAYER_FILE));
|
|
10732
11022
|
}
|
|
10733
11023
|
files.push(...await vendorKatex(out));
|
|
10734
11024
|
if (opts.assetsFrom) files.push(...await copyAssets(opts.assetsFrom, out));
|
|
@@ -10737,7 +11027,7 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10737
11027
|
}
|
|
10738
11028
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
10739
11029
|
step(
|
|
10740
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
11030
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join12(out, "index.html")}`
|
|
10741
11031
|
);
|
|
10742
11032
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
10743
11033
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -10761,40 +11051,40 @@ async function deckRuntime() {
|
|
|
10761
11051
|
}
|
|
10762
11052
|
}
|
|
10763
11053
|
function playerBundle() {
|
|
10764
|
-
const require2 =
|
|
11054
|
+
const require2 = createRequire3(import.meta.url);
|
|
10765
11055
|
try {
|
|
10766
|
-
return
|
|
11056
|
+
return join12(dirname4(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
10767
11057
|
} catch {
|
|
10768
11058
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
10769
11059
|
}
|
|
10770
11060
|
}
|
|
10771
11061
|
async function vendorKatex(out) {
|
|
10772
|
-
const require2 =
|
|
10773
|
-
const dist =
|
|
10774
|
-
const css = await readFile11(
|
|
11062
|
+
const require2 = createRequire3(import.meta.url);
|
|
11063
|
+
const dist = join12(dirname4(require2.resolve("katex/package.json")), "dist");
|
|
11064
|
+
const css = await readFile11(join12(dist, "katex.min.css"), "utf8");
|
|
10775
11065
|
const written = [];
|
|
10776
|
-
await mkdir9(
|
|
10777
|
-
for (const file of await readdir2(
|
|
11066
|
+
await mkdir9(join12(out, "katex/fonts"), { recursive: true });
|
|
11067
|
+
for (const file of await readdir2(join12(dist, "fonts"))) {
|
|
10778
11068
|
if (!file.endsWith(".woff2")) continue;
|
|
10779
|
-
await cp(
|
|
10780
|
-
written.push(
|
|
11069
|
+
await cp(join12(dist, "fonts", file), join12(out, "katex/fonts", file));
|
|
11070
|
+
written.push(join12(out, "katex/fonts", file));
|
|
10781
11071
|
}
|
|
10782
11072
|
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
10783
11073
|
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
10784
11074
|
return kept ? `src:${kept}` : whole;
|
|
10785
11075
|
});
|
|
10786
|
-
await writeFile11(
|
|
10787
|
-
written.push(
|
|
11076
|
+
await writeFile11(join12(out, "katex/katex.min.css"), woff2Only);
|
|
11077
|
+
written.push(join12(out, "katex/katex.min.css"));
|
|
10788
11078
|
return written;
|
|
10789
11079
|
}
|
|
10790
11080
|
async function copyAssets(sourceDir, out) {
|
|
10791
|
-
const from =
|
|
11081
|
+
const from = join12(resolve5(sourceDir), "assets");
|
|
10792
11082
|
if (!await stat2(from).catch(() => null)) return [];
|
|
10793
|
-
await cp(from,
|
|
10794
|
-
return [
|
|
11083
|
+
await cp(from, join12(out, "assets"), { recursive: true });
|
|
11084
|
+
return [join12(out, "assets")];
|
|
10795
11085
|
}
|
|
10796
11086
|
async function copyAudio(from, narration, out) {
|
|
10797
|
-
const dir =
|
|
11087
|
+
const dir = join12(out, narration.dir);
|
|
10798
11088
|
await mkdir9(dir, { recursive: true });
|
|
10799
11089
|
const names = [
|
|
10800
11090
|
...new Set(
|
|
@@ -10802,18 +11092,18 @@ async function copyAudio(from, narration, out) {
|
|
|
10802
11092
|
)
|
|
10803
11093
|
].sort();
|
|
10804
11094
|
for (const name of names) {
|
|
10805
|
-
await cp(
|
|
11095
|
+
await cp(join12(resolve5(from), name), join12(dir, name)).catch(() => {
|
|
10806
11096
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
10807
11097
|
});
|
|
10808
11098
|
}
|
|
10809
|
-
return names.map((n3) =>
|
|
11099
|
+
return names.map((n3) => join12(dir, n3));
|
|
10810
11100
|
}
|
|
10811
11101
|
async function refreshFont(storyboard, source, out, step) {
|
|
10812
11102
|
try {
|
|
10813
11103
|
const bundle = await bundleFont(
|
|
10814
11104
|
storyboard.lang,
|
|
10815
11105
|
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
10816
|
-
|
|
11106
|
+
join12(out, "assets", "fonts")
|
|
10817
11107
|
);
|
|
10818
11108
|
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
10819
11109
|
return bundle?.css;
|
|
@@ -10826,7 +11116,7 @@ async function refreshFont(storyboard, source, out, step) {
|
|
|
10826
11116
|
}
|
|
10827
11117
|
|
|
10828
11118
|
// src/server/upload.ts
|
|
10829
|
-
import { posix, sep } from "node:path";
|
|
11119
|
+
import { posix, sep as sep2 } from "node:path";
|
|
10830
11120
|
import { unzipSync as unzipSync2 } from "fflate";
|
|
10831
11121
|
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
10832
11122
|
var ZIP_LIMITS = {
|
|
@@ -10863,8 +11153,8 @@ function safeEntryPath(name) {
|
|
|
10863
11153
|
if (path2.length > 512 || kept.some((s) => s.length > 200)) return null;
|
|
10864
11154
|
return path2;
|
|
10865
11155
|
}
|
|
10866
|
-
function
|
|
10867
|
-
return joined === root2 || joined.startsWith(root2.endsWith(
|
|
11156
|
+
function insideRoot2(root2, joined) {
|
|
11157
|
+
return joined === root2 || joined.startsWith(root2.endsWith(sep2) ? root2 : root2 + sep2);
|
|
10868
11158
|
}
|
|
10869
11159
|
function readZip(bytes, limits = ZIP_LIMITS) {
|
|
10870
11160
|
const seen = [];
|
|
@@ -11169,7 +11459,7 @@ function int(name, raw2) {
|
|
|
11169
11459
|
|
|
11170
11460
|
// src/server/pipeline.ts
|
|
11171
11461
|
import { mkdir as mkdir10, readFile as readFile12, stat as stat3, writeFile as writeFile12 } from "node:fs/promises";
|
|
11172
|
-
import { dirname as
|
|
11462
|
+
import { dirname as dirname5, isAbsolute, join as join13, relative, resolve as resolve6 } from "node:path";
|
|
11173
11463
|
var AUDIO_DIR = "audio";
|
|
11174
11464
|
var NARRATION_FILE = "narration.json";
|
|
11175
11465
|
var MAX_REMOTE_FIGURES = 40;
|
|
@@ -11192,10 +11482,10 @@ function stagesFor(options) {
|
|
|
11192
11482
|
async function runPipeline(job, input) {
|
|
11193
11483
|
const { options } = input;
|
|
11194
11484
|
const dirs = {
|
|
11195
|
-
upload:
|
|
11196
|
-
src:
|
|
11197
|
-
audio:
|
|
11198
|
-
deck:
|
|
11485
|
+
upload: join13(job.dir, "upload"),
|
|
11486
|
+
src: join13(job.dir, "src"),
|
|
11487
|
+
audio: join13(job.dir, AUDIO_DIR),
|
|
11488
|
+
deck: join13(job.dir, "deck")
|
|
11199
11489
|
};
|
|
11200
11490
|
const warnings = [...options.warnings];
|
|
11201
11491
|
const url = (rel) => `/d/${job.id}/${rel}`;
|
|
@@ -11219,21 +11509,21 @@ async function runPipeline(job, input) {
|
|
|
11219
11509
|
theme: options.stated.theme ? prefs.theme : planned.theme
|
|
11220
11510
|
};
|
|
11221
11511
|
assertRefsResolve(storyboard, source, { pending: options.images ? "allow" : "refuse" });
|
|
11222
|
-
await writeJson(
|
|
11512
|
+
await writeJson(join13(job.dir, "storyboard.json"), storyboard);
|
|
11223
11513
|
job.done("plan", `${storyboard.beats.length} beats`);
|
|
11224
11514
|
for (const f of scanBeatCount(storyboard, prefs)) warnings.push(f.message);
|
|
11225
11515
|
if (options.images) {
|
|
11226
11516
|
job.begin("illustrate");
|
|
11227
11517
|
const drawn = await illustrate(storyboard, source, {
|
|
11228
11518
|
prefs,
|
|
11229
|
-
assetsDir:
|
|
11519
|
+
assetsDir: join13(dirs.src, "assets"),
|
|
11230
11520
|
...input.imageChain ? { chain: input.imageChain } : {},
|
|
11231
11521
|
onStep: (line2) => job.log(line2)
|
|
11232
11522
|
});
|
|
11233
11523
|
storyboard = drawn.storyboard;
|
|
11234
11524
|
source = drawn.source;
|
|
11235
|
-
await writeJson(
|
|
11236
|
-
await writeJson(
|
|
11525
|
+
await writeJson(join13(dirs.src, "source.json"), source);
|
|
11526
|
+
await writeJson(join13(job.dir, "storyboard.json"), storyboard);
|
|
11237
11527
|
for (const p of drawn.illustrated) {
|
|
11238
11528
|
job.log(
|
|
11239
11529
|
`illustrate: ${p.beatId} \u2192 assets/${p.src} via ${p.provider}${p.cached ? " (cached)" : ""}`
|
|
@@ -11260,7 +11550,7 @@ async function runPipeline(job, input) {
|
|
|
11260
11550
|
dir: dirs.audio,
|
|
11261
11551
|
format: options.format
|
|
11262
11552
|
});
|
|
11263
|
-
await writeJson(
|
|
11553
|
+
await writeJson(join13(dirs.audio, NARRATION_FILE), spoken);
|
|
11264
11554
|
narration = { voice: spoken.voice, dir: AUDIO_DIR, beats: spoken.beats };
|
|
11265
11555
|
const segments2 = Object.values(spoken.beats).flat();
|
|
11266
11556
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
@@ -11286,6 +11576,13 @@ async function runPipeline(job, input) {
|
|
|
11286
11576
|
onBeatError: (id2, err) => {
|
|
11287
11577
|
warnings.push(`slide ${id2} was left out: ${err.message}`);
|
|
11288
11578
|
job.log(`build: dropped ${id2} \u2014 ${err.message}`);
|
|
11579
|
+
},
|
|
11580
|
+
// A beat that IS in the deck, drawn with one of its parts dropped so it
|
|
11581
|
+
// would fit. The slide looks finished, so the job's warnings are the only
|
|
11582
|
+
// place the person who asked for it finds out otherwise.
|
|
11583
|
+
onBeatWarning: (id2, warning) => {
|
|
11584
|
+
warnings.push(`slide ${id2} was drawn short: ${warning}`);
|
|
11585
|
+
job.log(`build: kept ${id2} \u2014 ${warning}`);
|
|
11289
11586
|
}
|
|
11290
11587
|
});
|
|
11291
11588
|
for (const d of built.cut.dropped) warnings.push(`cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
@@ -11307,7 +11604,7 @@ async function runPipeline(job, input) {
|
|
|
11307
11604
|
job.log("render: capturing frames \u2014 two minutes for a four-minute video");
|
|
11308
11605
|
const out = await render({
|
|
11309
11606
|
deck: dirs.deck,
|
|
11310
|
-
out:
|
|
11607
|
+
out: join13(dirs.deck, "video.mp4"),
|
|
11311
11608
|
subtitles: "sidecar",
|
|
11312
11609
|
log: (line2) => job.log(line2),
|
|
11313
11610
|
// The last of the three length levers, and the only one that acts on a
|
|
@@ -11357,7 +11654,7 @@ async function runPipeline(job, input) {
|
|
|
11357
11654
|
}
|
|
11358
11655
|
async function ingest(job, input, dirs, warnings) {
|
|
11359
11656
|
await mkdir10(dirs.upload, { recursive: true });
|
|
11360
|
-
const root2 =
|
|
11657
|
+
const root2 = resolve6(dirs.upload);
|
|
11361
11658
|
let docPath;
|
|
11362
11659
|
const upload = input.upload;
|
|
11363
11660
|
const bothOrNeither = new UploadError(
|
|
@@ -11367,12 +11664,12 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11367
11664
|
);
|
|
11368
11665
|
if (input.url !== void 0) {
|
|
11369
11666
|
if (upload !== void 0) throw bothOrNeither;
|
|
11370
|
-
const harvested = await harvestBounded(input.url,
|
|
11667
|
+
const harvested = await harvestBounded(input.url, join13(root2, "assets"), {
|
|
11371
11668
|
...HARVEST_LIMITS,
|
|
11372
11669
|
...input.harvest
|
|
11373
11670
|
});
|
|
11374
11671
|
warnings.push(...harvested.warnings);
|
|
11375
|
-
docPath =
|
|
11672
|
+
docPath = join13(root2, "document.md");
|
|
11376
11673
|
await writeFile12(docPath, harvested.markdown);
|
|
11377
11674
|
job.log(
|
|
11378
11675
|
`ingest: harvested ${input.url} \u2014 "${harvested.title}", ${harvested.assets.length} asset(s)`
|
|
@@ -11389,13 +11686,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11389
11686
|
const { files, warnings: zipWarnings } = readZip(upload.bytes);
|
|
11390
11687
|
warnings.push(...zipWarnings);
|
|
11391
11688
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
11392
|
-
const to =
|
|
11393
|
-
if (!
|
|
11689
|
+
const to = resolve6(join13(root2, rel));
|
|
11690
|
+
if (!insideRoot2(root2, to))
|
|
11394
11691
|
throw new UploadError(`Refusing to write ${rel}.`, "Re-zip from inside the folder.");
|
|
11395
|
-
await mkdir10(
|
|
11692
|
+
await mkdir10(dirname5(to), { recursive: true });
|
|
11396
11693
|
await writeFile12(to, bytes);
|
|
11397
11694
|
}
|
|
11398
|
-
docPath =
|
|
11695
|
+
docPath = join13(root2, pickMarkdown(files));
|
|
11399
11696
|
job.log(
|
|
11400
11697
|
`ingest: unpacked ${Object.keys(files).length} file(s), reading ${pickMarkdown(files)}`
|
|
11401
11698
|
);
|
|
@@ -11407,7 +11704,7 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11407
11704
|
`DeckSmith reads ${MARKDOWN_EXTS.join(", ")} or a .zip containing one. Export the document to markdown first.`
|
|
11408
11705
|
);
|
|
11409
11706
|
}
|
|
11410
|
-
docPath =
|
|
11707
|
+
docPath = join13(root2, "document.md");
|
|
11411
11708
|
await writeFile12(docPath, upload.bytes);
|
|
11412
11709
|
}
|
|
11413
11710
|
const text2 = await readFile12(docPath, "utf8");
|
|
@@ -11430,8 +11727,8 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11430
11727
|
`ingest: ${parsed.sections.length} sections, ${parsed.figures.length} figures, ${parsed.equations.length} equations, ${parsed.tables.length} tables`
|
|
11431
11728
|
);
|
|
11432
11729
|
const guarded = await guardFigures(parsed, root2, input.fetchRemoteFigures, warnings);
|
|
11433
|
-
const source = await fetchFigures(guarded,
|
|
11434
|
-
await writeJson(
|
|
11730
|
+
const source = await fetchFigures(guarded, join13(resolve6(dirs.src), "assets"), warnings);
|
|
11731
|
+
await writeJson(join13(dirs.src, "source.json"), source);
|
|
11435
11732
|
return source;
|
|
11436
11733
|
}
|
|
11437
11734
|
async function harvestBounded(url, dir, opts) {
|
|
@@ -11507,8 +11804,8 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11507
11804
|
warnings.push(`figure ${figure.id} was left out: "${scheme}:" figures are not read`);
|
|
11508
11805
|
continue;
|
|
11509
11806
|
}
|
|
11510
|
-
const abs = isAbsolute(src) ?
|
|
11511
|
-
if (!
|
|
11807
|
+
const abs = isAbsolute(src) ? resolve6(src) : resolve6(join13(root2, src.replace(/^\.?\//, "")));
|
|
11808
|
+
if (!insideRoot2(root2, abs)) {
|
|
11512
11809
|
warnings.push(`figure ${figure.id} was left out: "${src}" points outside the upload`);
|
|
11513
11810
|
continue;
|
|
11514
11811
|
}
|
|
@@ -11522,10 +11819,10 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11522
11819
|
}
|
|
11523
11820
|
async function pack2(job, ctx) {
|
|
11524
11821
|
try {
|
|
11525
|
-
const assets =
|
|
11822
|
+
const assets = join13(resolve6(ctx.dirs.src), "assets");
|
|
11526
11823
|
const requests = ctx.source.figures.map((f) => ({
|
|
11527
11824
|
id: f.id,
|
|
11528
|
-
url:
|
|
11825
|
+
url: join13(assets, f.src),
|
|
11529
11826
|
prefer: "bake"
|
|
11530
11827
|
}));
|
|
11531
11828
|
const plan = await planMedia(requests, async (url) => ({
|
|
@@ -11534,7 +11831,7 @@ async function pack2(job, ctx) {
|
|
|
11534
11831
|
const files = { ...plan.files };
|
|
11535
11832
|
if (ctx.narration) {
|
|
11536
11833
|
for (const name of audioNames(ctx.narration)) {
|
|
11537
|
-
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile12(
|
|
11834
|
+
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile12(join13(ctx.dirs.audio, name)));
|
|
11538
11835
|
}
|
|
11539
11836
|
}
|
|
11540
11837
|
const container = {
|
|
@@ -11551,10 +11848,10 @@ async function pack2(job, ctx) {
|
|
|
11551
11848
|
},
|
|
11552
11849
|
source: ctx.source,
|
|
11553
11850
|
storyboard: ctx.storyboard,
|
|
11554
|
-
...ctx.narration ? { narration: await readJson(
|
|
11851
|
+
...ctx.narration ? { narration: await readJson(join13(ctx.dirs.audio, NARRATION_FILE)) } : {},
|
|
11555
11852
|
media: plan.media
|
|
11556
11853
|
};
|
|
11557
|
-
const bytes = await writePack(container, files,
|
|
11854
|
+
const bytes = await writePack(container, files, join13(ctx.dirs.deck, "deck.deck"));
|
|
11558
11855
|
job.log(`pack: ${Math.round(bytes / 1024)} KB \u2192 deck.deck`);
|
|
11559
11856
|
return ctx.url("deck.deck");
|
|
11560
11857
|
} catch (err) {
|
|
@@ -11577,7 +11874,7 @@ function host(src) {
|
|
|
11577
11874
|
}
|
|
11578
11875
|
}
|
|
11579
11876
|
async function writeJson(path2, value) {
|
|
11580
|
-
await mkdir10(
|
|
11877
|
+
await mkdir10(dirname5(path2), { recursive: true });
|
|
11581
11878
|
await writeFile12(path2, `${JSON.stringify(value, null, 2)}
|
|
11582
11879
|
`);
|
|
11583
11880
|
}
|
|
@@ -11989,13 +12286,13 @@ var PAGE_BUDGET = {
|
|
|
11989
12286
|
};
|
|
11990
12287
|
async function readPage(url, work2) {
|
|
11991
12288
|
await mkdir11(work2, { recursive: true });
|
|
11992
|
-
const dir = await mkdtemp3(
|
|
12289
|
+
const dir = await mkdtemp3(join14(work2, "harvest-"));
|
|
11993
12290
|
try {
|
|
11994
12291
|
const page = await harvest(url, dir, PAGE_BUDGET);
|
|
11995
12292
|
const files = {
|
|
11996
12293
|
"document.md": new TextEncoder().encode(page.markdown)
|
|
11997
12294
|
};
|
|
11998
|
-
for (const asset of page.assets) files[
|
|
12295
|
+
for (const asset of page.assets) files[basename5(asset)] = await readFile13(asset);
|
|
11999
12296
|
const warnings = [...page.warnings];
|
|
12000
12297
|
if (page.clips.length > 0) {
|
|
12001
12298
|
warnings.push(
|
|
@@ -12029,16 +12326,16 @@ var statusSchema = z5.object({
|
|
|
12029
12326
|
var DEFAULT_WAIT = 45;
|
|
12030
12327
|
function deckTools(opts) {
|
|
12031
12328
|
const queue = new Queue({ maxQueued: 8 });
|
|
12032
|
-
const root2 =
|
|
12033
|
-
let
|
|
12329
|
+
const root2 = resolve7(opts.root);
|
|
12330
|
+
let cached2;
|
|
12034
12331
|
const found = async () => {
|
|
12035
|
-
|
|
12036
|
-
return
|
|
12332
|
+
cached2 ??= await (opts.probe ?? prereqs)();
|
|
12333
|
+
return cached2;
|
|
12037
12334
|
};
|
|
12038
|
-
const
|
|
12335
|
+
const insideRoot3 = (p) => {
|
|
12039
12336
|
if (!isAbsolute2(p)) throw new Error(`document_path must be absolute; got "${p}".`);
|
|
12040
|
-
const full =
|
|
12041
|
-
if (full !== root2 && !full.startsWith(root2 +
|
|
12337
|
+
const full = resolve7(p);
|
|
12338
|
+
if (full !== root2 && !full.startsWith(root2 + sep3)) {
|
|
12042
12339
|
throw new Error(
|
|
12043
12340
|
`document_path is outside the server's root. It may only read files under ${root2}.`
|
|
12044
12341
|
);
|
|
@@ -12099,7 +12396,7 @@ function deckTools(opts) {
|
|
|
12099
12396
|
}
|
|
12100
12397
|
const settings = input.settings ?? {};
|
|
12101
12398
|
const options = parseOptions(fieldsFor(settings));
|
|
12102
|
-
const file = input.document_path ?
|
|
12399
|
+
const file = input.document_path ? insideRoot3(input.document_path) : void 0;
|
|
12103
12400
|
const missing = missingFor(await found(), options);
|
|
12104
12401
|
if (missing.length) {
|
|
12105
12402
|
throw new Error(
|
|
@@ -12115,7 +12412,7 @@ function deckTools(opts) {
|
|
|
12115
12412
|
}
|
|
12116
12413
|
}
|
|
12117
12414
|
const page = input.document_url === void 0 ? void 0 : await readPage(input.document_url, opts.work);
|
|
12118
|
-
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(
|
|
12415
|
+
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(sep3).pop() : "document.md";
|
|
12119
12416
|
const bytes = page ? page.zip : file ? await readFile13(file) : new TextEncoder().encode(input.document_text);
|
|
12120
12417
|
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
12121
12418
|
throw new Error(
|
|
@@ -12123,7 +12420,7 @@ function deckTools(opts) {
|
|
|
12123
12420
|
);
|
|
12124
12421
|
}
|
|
12125
12422
|
const id2 = randomBytes(16).toString("base64url");
|
|
12126
|
-
const dir =
|
|
12423
|
+
const dir = join14(opts.work, id2);
|
|
12127
12424
|
await mkdir11(dir, { recursive: true });
|
|
12128
12425
|
queue.submit({
|
|
12129
12426
|
id: id2,
|
|
@@ -12151,7 +12448,7 @@ function deckTools(opts) {
|
|
|
12151
12448
|
};
|
|
12152
12449
|
}
|
|
12153
12450
|
function dirOf(work2, id2) {
|
|
12154
|
-
return
|
|
12451
|
+
return join14(work2, id2);
|
|
12155
12452
|
}
|
|
12156
12453
|
function waitFor(queue, id2, seconds) {
|
|
12157
12454
|
const now = queue.view(id2);
|
|
@@ -12183,9 +12480,9 @@ function report(view, dir) {
|
|
|
12183
12480
|
// (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
|
|
12184
12481
|
// is the shape of the CLI's output directory and not the server's, and an
|
|
12185
12482
|
// agent told to read it would have found nothing there.
|
|
12186
|
-
storyboard_path:
|
|
12483
|
+
storyboard_path: join14(dir, "storyboard.json"),
|
|
12187
12484
|
...done && view.result ? {
|
|
12188
|
-
deck_path:
|
|
12485
|
+
deck_path: join14(dir, "deck"),
|
|
12189
12486
|
slides: view.result.slides,
|
|
12190
12487
|
duration_seconds: view.result.duration,
|
|
12191
12488
|
warnings: view.result.warnings
|
|
@@ -12194,7 +12491,7 @@ function report(view, dir) {
|
|
|
12194
12491
|
};
|
|
12195
12492
|
}
|
|
12196
12493
|
function defaultWork() {
|
|
12197
|
-
return
|
|
12494
|
+
return join14(tmpdir4(), "decksmith-mcp");
|
|
12198
12495
|
}
|
|
12199
12496
|
|
|
12200
12497
|
// src/mcp/main.ts
|
|
@@ -12204,6 +12501,7 @@ for (const level of ["log", "info", "debug", "warn", "trace"]) {
|
|
|
12204
12501
|
`);
|
|
12205
12502
|
};
|
|
12206
12503
|
}
|
|
12504
|
+
guardTmpdir();
|
|
12207
12505
|
var env = process.env;
|
|
12208
12506
|
var root = env.DECKSMITH_MCP_ROOT ?? homedir3();
|
|
12209
12507
|
var work = env.DECKSMITH_MCP_WORK ?? defaultWork();
|