@jokerized/decksmith 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -14
- package/dist/cli.js +498 -163
- package/dist/deck-runtime.js +2 -2
- package/dist/index.js +407 -80
- package/dist/mcp.js +496 -186
- 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/render/capture.d.ts +27 -1
- 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,29 +6991,37 @@ function message(err) {
|
|
|
6728
6991
|
|
|
6729
6992
|
// src/render/capture.ts
|
|
6730
6993
|
import { homedir } from "node:os";
|
|
6731
|
-
import { join as
|
|
6732
|
-
async function
|
|
6733
|
-
const
|
|
6734
|
-
if (
|
|
6994
|
+
import { join as join3 } from "node:path";
|
|
6995
|
+
async function resolveChrome(need = "open the deck with") {
|
|
6996
|
+
const env2 = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
6997
|
+
if (env2) return { path: env2, source: "env" };
|
|
6735
6998
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6736
|
-
const
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6999
|
+
const pick = async (cacheDir) => {
|
|
7000
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
7001
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
7002
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
7003
|
+
};
|
|
7004
|
+
const hf = await pick(join3(homedir(), ".cache", "hyperframes", "chrome"));
|
|
7005
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
7006
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer"));
|
|
7007
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
6740
7008
|
throw new Error(
|
|
6741
7009
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
6742
7010
|
);
|
|
6743
7011
|
}
|
|
7012
|
+
async function chromePath(need = "open the deck with") {
|
|
7013
|
+
return (await resolveChrome(need)).path;
|
|
7014
|
+
}
|
|
6744
7015
|
|
|
6745
7016
|
// src/source/assets.ts
|
|
6746
7017
|
import { createHash as createHash4 } from "node:crypto";
|
|
6747
7018
|
import { mkdir as mkdir2, readdir, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
6748
|
-
import { extname as extname2, join as
|
|
7019
|
+
import { extname as extname2, join as join4 } from "node:path";
|
|
6749
7020
|
import { z as z2 } from "zod";
|
|
6750
7021
|
async function fetchFigures(source, dir, warnings) {
|
|
6751
7022
|
const drops = warnings ?? [];
|
|
6752
7023
|
await mkdir2(dir, { recursive: true });
|
|
6753
|
-
const
|
|
7024
|
+
const cached2 = await readdir(dir).catch(() => []);
|
|
6754
7025
|
const figures = [];
|
|
6755
7026
|
for (const figure of source.figures) {
|
|
6756
7027
|
if (figure.kind === "clip") {
|
|
@@ -6758,7 +7029,7 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6758
7029
|
continue;
|
|
6759
7030
|
}
|
|
6760
7031
|
try {
|
|
6761
|
-
figures.push(figureSchema.parse(await localize(figure, dir,
|
|
7032
|
+
figures.push(figureSchema.parse(await localize(figure, dir, cached2)));
|
|
6762
7033
|
} catch (err) {
|
|
6763
7034
|
drops.push(`figure ${figure.id} was left out: ${reason(err)}`);
|
|
6764
7035
|
}
|
|
@@ -6772,15 +7043,15 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6772
7043
|
}
|
|
6773
7044
|
return parsed.data;
|
|
6774
7045
|
}
|
|
6775
|
-
async function localize(figure, dir,
|
|
7046
|
+
async function localize(figure, dir, cached2) {
|
|
6776
7047
|
const stem = assetStem(figure.id, figure.src);
|
|
6777
|
-
const hit =
|
|
6778
|
-
const bytes = hit ? await readFile3(
|
|
7048
|
+
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7049
|
+
const bytes = hit ? await readFile3(join4(dir, hit)) : await load(figure.src);
|
|
6779
7050
|
const size = imageSize(bytes);
|
|
6780
7051
|
if (hit) return { ...figure, src: hit, ...size };
|
|
6781
7052
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6782
|
-
await writeFile2(
|
|
6783
|
-
|
|
7053
|
+
await writeFile2(join4(dir, src), bytes);
|
|
7054
|
+
cached2.push(src);
|
|
6784
7055
|
return { ...figure, src, ...size };
|
|
6785
7056
|
}
|
|
6786
7057
|
function assetStem(id2, src) {
|
|
@@ -7341,7 +7612,7 @@ function readContentRegion() {
|
|
|
7341
7612
|
|
|
7342
7613
|
// src/source/transcode.ts
|
|
7343
7614
|
import { readFile as readFile4, rm } from "node:fs/promises";
|
|
7344
|
-
import { basename } from "node:path";
|
|
7615
|
+
import { basename as basename2 } from "node:path";
|
|
7345
7616
|
|
|
7346
7617
|
// src/render/ffmpeg.ts
|
|
7347
7618
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -7366,7 +7637,7 @@ ${tail}`);
|
|
|
7366
7637
|
}
|
|
7367
7638
|
}
|
|
7368
7639
|
function runLive(file, args) {
|
|
7369
|
-
return new Promise((
|
|
7640
|
+
return new Promise((resolve8, reject) => {
|
|
7370
7641
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7371
7642
|
child.on("error", (err) => {
|
|
7372
7643
|
reject(
|
|
@@ -7374,7 +7645,7 @@ function runLive(file, args) {
|
|
|
7374
7645
|
);
|
|
7375
7646
|
});
|
|
7376
7647
|
child.on("close", (code, signal) => {
|
|
7377
|
-
if (code === 0)
|
|
7648
|
+
if (code === 0) resolve8();
|
|
7378
7649
|
else if (signal) {
|
|
7379
7650
|
reject(
|
|
7380
7651
|
new Error(
|
|
@@ -7570,7 +7841,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7570
7841
|
const file = opts.ffmpeg ?? "ffmpeg";
|
|
7571
7842
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7572
7843
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7573
|
-
const name =
|
|
7844
|
+
const name = basename2(input);
|
|
7574
7845
|
const source = videoSize(await readFile4(input));
|
|
7575
7846
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7576
7847
|
const kept = (why2) => ({
|
|
@@ -7639,7 +7910,7 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7639
7910
|
async function harvest(url, dir, opts = {}) {
|
|
7640
7911
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7641
7912
|
const warnings = [];
|
|
7642
|
-
const assets =
|
|
7913
|
+
const assets = resolve2(dir);
|
|
7643
7914
|
await mkdir3(assets, { recursive: true });
|
|
7644
7915
|
const page = await fetchGuarded(url, {
|
|
7645
7916
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
@@ -7701,7 +7972,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
7701
7972
|
"--disable-background-networking",
|
|
7702
7973
|
"--disable-extensions",
|
|
7703
7974
|
"--no-default-browser-check",
|
|
7704
|
-
"--no-first-run"
|
|
7975
|
+
"--no-first-run",
|
|
7976
|
+
// The floor under interception — see the note above. Not a hardening
|
|
7977
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
7978
|
+
// happens to be that run.
|
|
7979
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
7705
7980
|
]
|
|
7706
7981
|
});
|
|
7707
7982
|
try {
|
|
@@ -7967,7 +8242,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
7967
8242
|
const clips = [];
|
|
7968
8243
|
let spent = 0;
|
|
7969
8244
|
const done = /* @__PURE__ */ new Map();
|
|
7970
|
-
const ref = (got) => opts.refs === "relative" ?
|
|
8245
|
+
const ref = (got) => opts.refs === "relative" ? basename3(got.path) : got.path;
|
|
7971
8246
|
const overdrawn = () => {
|
|
7972
8247
|
if (Date.now() >= deadline) {
|
|
7973
8248
|
return `the harvest has used its ${Math.round(wallMs / 1e3)}s budget \u2014 raise maxWallMs`;
|
|
@@ -8018,7 +8293,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8018
8293
|
`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
8294
|
);
|
|
8020
8295
|
}
|
|
8021
|
-
const path2 =
|
|
8296
|
+
const path2 = join5(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8022
8297
|
await writeFile3(path2, bytes);
|
|
8023
8298
|
assets.push(path2);
|
|
8024
8299
|
got = { path: path2, width: size.width, height: size.height };
|
|
@@ -8031,7 +8306,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8031
8306
|
};
|
|
8032
8307
|
const shrink = async (url, path2, measured, what) => {
|
|
8033
8308
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8034
|
-
const out2 =
|
|
8309
|
+
const out2 = join5(dir, assetName(url, ".vp9.webm"));
|
|
8035
8310
|
let small;
|
|
8036
8311
|
try {
|
|
8037
8312
|
small = await transcode(path2, out2, {
|
|
@@ -8046,7 +8321,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8046
8321
|
const [before, after] = await Promise.all([sizeOf(path2), sizeOf(small.path)]);
|
|
8047
8322
|
if (before > 0 && after > before) {
|
|
8048
8323
|
warnings.push(
|
|
8049
|
-
`${what} \u2014 ${
|
|
8324
|
+
`${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
8325
|
);
|
|
8051
8326
|
}
|
|
8052
8327
|
await rm2(path2, { force: true });
|
|
@@ -8073,7 +8348,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8073
8348
|
try {
|
|
8074
8349
|
const bytes = await bytesOf(url);
|
|
8075
8350
|
const measured = videoSize(bytes);
|
|
8076
|
-
const path2 =
|
|
8351
|
+
const path2 = join5(dir, assetName(url, `.${measured.container}`));
|
|
8077
8352
|
await writeFile3(path2, bytes);
|
|
8078
8353
|
return await shrink(url, path2, measured, what);
|
|
8079
8354
|
} catch (err) {
|
|
@@ -8438,8 +8713,8 @@ ${fence}`;
|
|
|
8438
8713
|
// src/plan/codex.ts
|
|
8439
8714
|
import { spawn as spawn2 } from "node:child_process";
|
|
8440
8715
|
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
|
|
8716
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
8717
|
+
import { join as join6 } from "node:path";
|
|
8443
8718
|
import { z as z3 } from "zod";
|
|
8444
8719
|
|
|
8445
8720
|
// src/plan/arc.ts
|
|
@@ -8651,7 +8926,10 @@ var REVEALS = {
|
|
|
8651
8926
|
"equation-walk": "one per term",
|
|
8652
8927
|
"equation-morph": "2",
|
|
8653
8928
|
"data-table": "one per highlighted row, plus 1",
|
|
8654
|
-
"
|
|
8929
|
+
// "long enough" is not a hedge: below the emitter's own floor the comparison
|
|
8930
|
+
// is dropped and the chart stops twice becoming a chart that stops once. See
|
|
8931
|
+
// the line-chart entry under THE THIRTEEN ARCHETYPES for what that costs.
|
|
8932
|
+
"line-chart": "1, or 2 when compare is given and the beat is long enough for it",
|
|
8655
8933
|
callout: "one per panel",
|
|
8656
8934
|
pipeline: "one per stage",
|
|
8657
8935
|
"annotated-figure": "one per note, plus 1",
|
|
@@ -8774,6 +9052,29 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
8774
9052
|
scale, over training. Points come from the source's numbers;
|
|
8775
9053
|
deltas, if given, are the steps between consecutive points, so
|
|
8776
9054
|
there is always one fewer.
|
|
9055
|
+
\`compare\` adds a SECOND CONDITION over the same axis \u2014 the tell
|
|
9056
|
+
there is THE SAME QUANTITY MEASURED TWO WAYS, a baseline and the
|
|
9057
|
+
result, where what the source is claiming is the change in the
|
|
9058
|
+
SHAPE of the curve and not two numbers. Its \`label\` names the
|
|
9059
|
+
baseline ("without pretraining", "FP32") and is drawn on the
|
|
9060
|
+
slide where the chart has room for it. Keep it SHORT \u2014 two or
|
|
9061
|
+
three words. A label wider than the plot is refused and the
|
|
9062
|
+
whole slide goes with it, and one that cannot be placed clear of
|
|
9063
|
+
the axis names, the values and both curves is dropped without a
|
|
9064
|
+
word, leaving the baseline unnamed. A \`readout\` takes the plot
|
|
9065
|
+
down to about two thirds of its width, so a beat that has one
|
|
9066
|
+
has room for a shorter label still.
|
|
9067
|
+
\`compare.points\` must carry the SAME x values, in the
|
|
9068
|
+
same order, as \`points\`; the baseline is drawn first, then the
|
|
9069
|
+
curve reshapes into \`points\` and leaves the baseline behind. Do
|
|
9070
|
+
not use it for two unrelated series that happen to share an
|
|
9071
|
+
axis \u2014 nothing reshapes into something it is not a version of.
|
|
9072
|
+
A compare beat also costs TIME \u2014 the baseline draws, is held,
|
|
9073
|
+
then reshapes \u2014 so give it \`seconds\` of 7, or 8 with a
|
|
9074
|
+
\`readout\`. Give it too few and the emitter draws the chart
|
|
9075
|
+
without the comparison rather than stopping on a half-drawn one:
|
|
9076
|
+
the slide is still there, but the point about the baseline is
|
|
9077
|
+
gone.
|
|
8777
9078
|
|
|
8778
9079
|
DESCRIBING ARCHETYPES \u2014 the fallbacks
|
|
8779
9080
|
|
|
@@ -9431,10 +9732,10 @@ function schemaFor(prefs) {
|
|
|
9431
9732
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9432
9733
|
async function codexPlanner(source, opts = {}) {
|
|
9433
9734
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9434
|
-
const dir = await mkdtemp(
|
|
9735
|
+
const dir = await mkdtemp(join6(tmpdir2(), "decksmith-plan-"));
|
|
9435
9736
|
try {
|
|
9436
|
-
const schemaPath =
|
|
9437
|
-
const outPath =
|
|
9737
|
+
const schemaPath = join6(dir, "storyboard.schema.json");
|
|
9738
|
+
const outPath = join6(dir, "storyboard.json");
|
|
9438
9739
|
await writeFile4(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9439
9740
|
await (opts.run ?? runCodex)({
|
|
9440
9741
|
prompt: buildPrompt(source, prefs),
|
|
@@ -9516,7 +9817,7 @@ function codexCommand(args) {
|
|
|
9516
9817
|
}
|
|
9517
9818
|
function runCodex(args) {
|
|
9518
9819
|
const { argv, env: env2 } = codexCommand(args);
|
|
9519
|
-
return new Promise((
|
|
9820
|
+
return new Promise((resolve8, reject) => {
|
|
9520
9821
|
const child = spawn2("codex", argv, {
|
|
9521
9822
|
stdio: ["pipe", "ignore", "pipe"],
|
|
9522
9823
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -9539,7 +9840,7 @@ function runCodex(args) {
|
|
|
9539
9840
|
});
|
|
9540
9841
|
child.on("close", (code) => {
|
|
9541
9842
|
clearTimeout(timer);
|
|
9542
|
-
if (code === 0) return
|
|
9843
|
+
if (code === 0) return resolve8();
|
|
9543
9844
|
reject(
|
|
9544
9845
|
new Error(`codex exec exited ${code}.
|
|
9545
9846
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -9552,13 +9853,13 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
9552
9853
|
// src/images/illustrate.ts
|
|
9553
9854
|
import { createHash as createHash7 } from "node:crypto";
|
|
9554
9855
|
import { mkdir as mkdir4, readFile as readFile7, rename, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
|
|
9555
|
-
import { join as
|
|
9856
|
+
import { join as join8 } from "node:path";
|
|
9556
9857
|
|
|
9557
9858
|
// src/images/providers.ts
|
|
9558
9859
|
import { createHash as createHash6 } from "node:crypto";
|
|
9559
9860
|
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
|
|
9861
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
9862
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
9562
9863
|
import { z as z4 } from "zod";
|
|
9563
9864
|
var SIZE = {
|
|
9564
9865
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -9690,10 +9991,10 @@ function codexImages(opts = {}) {
|
|
|
9690
9991
|
async check() {
|
|
9691
9992
|
},
|
|
9692
9993
|
async generate(req) {
|
|
9693
|
-
const dir = await mkdtemp2(
|
|
9994
|
+
const dir = await mkdtemp2(join7(tmpdir3(), "decksmith-image-"));
|
|
9694
9995
|
try {
|
|
9695
|
-
const schemaPath =
|
|
9696
|
-
const outPath =
|
|
9996
|
+
const schemaPath = join7(dir, "answer.schema.json");
|
|
9997
|
+
const outPath = join7(dir, "answer.json");
|
|
9697
9998
|
await writeFile5(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
9698
9999
|
await run5({
|
|
9699
10000
|
prompt: codexPrompt(req),
|
|
@@ -9711,8 +10012,8 @@ function codexImages(opts = {}) {
|
|
|
9711
10012
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
9712
10013
|
);
|
|
9713
10014
|
}
|
|
9714
|
-
const candidates2 = [
|
|
9715
|
-
if (answer.data.file) candidates2.push(
|
|
10015
|
+
const candidates2 = [join7(dir, "picture.png")];
|
|
10016
|
+
if (answer.data.file) candidates2.push(resolve3(dir, answer.data.file));
|
|
9716
10017
|
for (const path2 of candidates2) {
|
|
9717
10018
|
const bytes = await readFile6(path2).catch(() => null);
|
|
9718
10019
|
if (bytes) return raster(bytes, "codex");
|
|
@@ -9943,16 +10244,16 @@ function cacheKey(providerId, req) {
|
|
|
9943
10244
|
async function draw(provider, req, name, dir) {
|
|
9944
10245
|
for (const ext of Object.values(EXT2)) {
|
|
9945
10246
|
const src2 = `${name}${ext}`;
|
|
9946
|
-
const bytes = await readFile7(
|
|
10247
|
+
const bytes = await readFile7(join8(dir, src2)).catch(() => null);
|
|
9947
10248
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
9948
10249
|
}
|
|
9949
10250
|
await provider.check();
|
|
9950
10251
|
const img = await provider.generate(req);
|
|
9951
10252
|
const src = `${name}${EXT2[img.mime]}`;
|
|
9952
|
-
const tmp =
|
|
10253
|
+
const tmp = join8(dir, `.${name}.tmp`);
|
|
9953
10254
|
try {
|
|
9954
10255
|
await writeFile6(tmp, img.bytes);
|
|
9955
|
-
await rename(tmp,
|
|
10256
|
+
await rename(tmp, join8(dir, src));
|
|
9956
10257
|
} finally {
|
|
9957
10258
|
await rm5(tmp, { force: true });
|
|
9958
10259
|
}
|
|
@@ -9967,7 +10268,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
9967
10268
|
import { createHash as createHash8 } from "node:crypto";
|
|
9968
10269
|
import { mkdir as mkdir5, readFile as readFile8, rm as rm6, writeFile as writeFile7 } from "node:fs/promises";
|
|
9969
10270
|
import { homedir as homedir2 } from "node:os";
|
|
9970
|
-
import { join as
|
|
10271
|
+
import { join as join9 } from "node:path";
|
|
9971
10272
|
var MISSING = [
|
|
9972
10273
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
9973
10274
|
"",
|
|
@@ -9984,8 +10285,8 @@ function candidates() {
|
|
|
9984
10285
|
["edge-tts"],
|
|
9985
10286
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
9986
10287
|
// default, and both are where this actually lands in practice.
|
|
9987
|
-
[
|
|
9988
|
-
[
|
|
10288
|
+
[join9(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10289
|
+
[join9(home, ".local", "bin", "edge-tts")],
|
|
9989
10290
|
// Last resort: the module is installed even though its console script is
|
|
9990
10291
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
9991
10292
|
["python3", "-m", "edge_tts"]
|
|
@@ -10010,7 +10311,7 @@ async function find2(can) {
|
|
|
10010
10311
|
throw new Error(MISSING);
|
|
10011
10312
|
}
|
|
10012
10313
|
function runArgv(cmd, args) {
|
|
10013
|
-
return new Promise((
|
|
10314
|
+
return new Promise((resolve8) => {
|
|
10014
10315
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10015
10316
|
let stdout = "";
|
|
10016
10317
|
let stderr = "";
|
|
@@ -10020,8 +10321,8 @@ function runArgv(cmd, args) {
|
|
|
10020
10321
|
child.stderr.on("data", (b) => {
|
|
10021
10322
|
stderr += b.toString();
|
|
10022
10323
|
});
|
|
10023
|
-
child.on("error", (e) =>
|
|
10024
|
-
child.on("close", (code) =>
|
|
10324
|
+
child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
|
|
10325
|
+
child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
|
|
10025
10326
|
});
|
|
10026
10327
|
}
|
|
10027
10328
|
var edgeTts = {
|
|
@@ -10106,10 +10407,10 @@ async function synthesize(text2, opts) {
|
|
|
10106
10407
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10107
10408
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10108
10409
|
const file = `${key}.mp3`;
|
|
10109
|
-
const audio =
|
|
10110
|
-
const sidecar =
|
|
10111
|
-
const
|
|
10112
|
-
if (
|
|
10410
|
+
const audio = join9(opts.dir, file);
|
|
10411
|
+
const sidecar = join9(opts.dir, `${key}.json`);
|
|
10412
|
+
const cached2 = await readSidecar(sidecar);
|
|
10413
|
+
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10113
10414
|
await mkdir5(opts.dir, { recursive: true });
|
|
10114
10415
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10115
10416
|
const { cues, seconds } = spoken;
|
|
@@ -10273,11 +10574,11 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
10273
10574
|
|
|
10274
10575
|
// src/render/render.ts
|
|
10275
10576
|
import { mkdir as mkdir7, readFile as readFile9, rename as rename2, rm as rm7, writeFile as writeFile9 } from "node:fs/promises";
|
|
10276
|
-
import { basename as
|
|
10577
|
+
import { basename as basename4, dirname as dirname2, join as join11, resolve as resolve4 } from "node:path";
|
|
10277
10578
|
|
|
10278
10579
|
// src/render/captions.ts
|
|
10279
10580
|
import { mkdir as mkdir6, writeFile as writeFile8 } from "node:fs/promises";
|
|
10280
|
-
import { join as
|
|
10581
|
+
import { join as join10 } from "node:path";
|
|
10281
10582
|
import { pathToFileURL } from "node:url";
|
|
10282
10583
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
10283
10584
|
function cssString(value) {
|
|
@@ -10368,9 +10669,9 @@ async function captionBlocker() {
|
|
|
10368
10669
|
}
|
|
10369
10670
|
async function renderCaptions(cues, style, deck, work2) {
|
|
10370
10671
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
10371
|
-
const fontCss =
|
|
10672
|
+
const fontCss = join10(deck, DECK_FONT_CSS);
|
|
10372
10673
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL(fontCss).href : null;
|
|
10373
|
-
const page =
|
|
10674
|
+
const page = join10(work2, "captions.html");
|
|
10374
10675
|
await mkdir6(work2, { recursive: true });
|
|
10375
10676
|
await writeFile8(page, captionPage(cues, style, href));
|
|
10376
10677
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
@@ -10392,7 +10693,7 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
10392
10693
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
10393
10694
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
10394
10695
|
await tab.screenshot({
|
|
10395
|
-
path:
|
|
10696
|
+
path: join10(work2, name),
|
|
10396
10697
|
type: "png",
|
|
10397
10698
|
omitBackground: true,
|
|
10398
10699
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -10426,16 +10727,16 @@ function overlayInputs(band) {
|
|
|
10426
10727
|
}
|
|
10427
10728
|
|
|
10428
10729
|
// src/render/render.ts
|
|
10429
|
-
var SRT_NAME = (out) => `${
|
|
10730
|
+
var SRT_NAME = (out) => `${basename4(out).replace(/\.[^.]+$/, "")}.srt`;
|
|
10430
10731
|
function subtitlePlan(mode) {
|
|
10431
10732
|
return { sidecar: mode !== "none", burn: mode === "burn" };
|
|
10432
10733
|
}
|
|
10433
10734
|
async function render(opts) {
|
|
10434
10735
|
const log = opts.log ?? (() => {
|
|
10435
10736
|
});
|
|
10436
|
-
const deck =
|
|
10437
|
-
const out =
|
|
10438
|
-
const work2 =
|
|
10737
|
+
const deck = resolve4(opts.deck);
|
|
10738
|
+
const out = resolve4(opts.out);
|
|
10739
|
+
const work2 = join11(dirname2(out), `.${basename4(out)}.parts`);
|
|
10439
10740
|
const timing = await readTiming(deck);
|
|
10440
10741
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
10441
10742
|
const refusal = playbackRefusal(
|
|
@@ -10453,7 +10754,7 @@ async function render(opts) {
|
|
|
10453
10754
|
const burnable = plan0.burn;
|
|
10454
10755
|
await mkdir7(work2, { recursive: true });
|
|
10455
10756
|
try {
|
|
10456
|
-
const raw2 = opts.video ?
|
|
10757
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join11(work2, "raw.mp4"), opts, log);
|
|
10457
10758
|
const shot = await probe(raw2);
|
|
10458
10759
|
log(
|
|
10459
10760
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -10466,7 +10767,7 @@ async function render(opts) {
|
|
|
10466
10767
|
);
|
|
10467
10768
|
}
|
|
10468
10769
|
const retimed = await retime(raw2, plan, work2, log);
|
|
10469
|
-
const srtPath =
|
|
10770
|
+
const srtPath = join11(dirname2(out), SRT_NAME(out));
|
|
10470
10771
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
10471
10772
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
10472
10773
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
@@ -10477,7 +10778,7 @@ async function render(opts) {
|
|
|
10477
10778
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
10478
10779
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
10479
10780
|
if (warning) log(`render: ${warning}`);
|
|
10480
|
-
const fast =
|
|
10781
|
+
const fast = join11(work2, `fast.${basename4(out)}`);
|
|
10481
10782
|
const muxed = await probe(out);
|
|
10482
10783
|
await runTool(
|
|
10483
10784
|
"ffmpeg",
|
|
@@ -10507,7 +10808,7 @@ async function render(opts) {
|
|
|
10507
10808
|
}
|
|
10508
10809
|
}
|
|
10509
10810
|
async function readTiming(deck) {
|
|
10510
|
-
const path2 =
|
|
10811
|
+
const path2 = join11(deck, TIMING_FILE);
|
|
10511
10812
|
const text2 = await readFile9(path2, "utf8").catch(() => {
|
|
10512
10813
|
throw new Error(
|
|
10513
10814
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
@@ -10547,20 +10848,20 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10547
10848
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
10548
10849
|
const list = [];
|
|
10549
10850
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
10550
|
-
const file =
|
|
10851
|
+
const file = join11(work2, `p${String(i).padStart(4, "0")}.ts`);
|
|
10551
10852
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
10552
10853
|
list.push(file);
|
|
10553
10854
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
10554
10855
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
10555
10856
|
}
|
|
10556
10857
|
}
|
|
10557
|
-
const listFile =
|
|
10858
|
+
const listFile = join11(work2, "pieces.txt");
|
|
10558
10859
|
await writeFile9(
|
|
10559
10860
|
listFile,
|
|
10560
10861
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
10561
10862
|
`
|
|
10562
10863
|
);
|
|
10563
|
-
const joined =
|
|
10864
|
+
const joined = join11(work2, "retimed.mp4");
|
|
10564
10865
|
await runTool("ffmpeg", [
|
|
10565
10866
|
"-y",
|
|
10566
10867
|
"-hide_banner",
|
|
@@ -10582,7 +10883,7 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10582
10883
|
}
|
|
10583
10884
|
async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
10584
10885
|
const inputs = plan.audio.map((a) => ({
|
|
10585
|
-
file:
|
|
10886
|
+
file: join11(deck, timing.audioDir, a.audio),
|
|
10586
10887
|
delayMs: a.delayMs
|
|
10587
10888
|
}));
|
|
10588
10889
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -10603,7 +10904,7 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10603
10904
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
10604
10905
|
}
|
|
10605
10906
|
if (graph.length > 0) {
|
|
10606
|
-
const script =
|
|
10907
|
+
const script = join11(work2, "mux.filter");
|
|
10607
10908
|
await writeFile9(script, `${graph.join(";\n")}
|
|
10608
10909
|
`);
|
|
10609
10910
|
args.push("-filter_complex_script", script);
|
|
@@ -10619,13 +10920,13 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10619
10920
|
log(
|
|
10620
10921
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
10621
10922
|
);
|
|
10622
|
-
await mkdir7(
|
|
10923
|
+
await mkdir7(dirname2(out), { recursive: true });
|
|
10623
10924
|
await runTool("ffmpeg", args, { cwd: work2 });
|
|
10624
10925
|
}
|
|
10625
10926
|
|
|
10626
10927
|
// src/pack/pack.ts
|
|
10627
10928
|
import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile10 } from "node:fs/promises";
|
|
10628
|
-
import { dirname as
|
|
10929
|
+
import { dirname as dirname3, extname as extname3 } from "node:path";
|
|
10629
10930
|
import { unzipSync, zipSync } from "fflate";
|
|
10630
10931
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
10631
10932
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -10664,7 +10965,7 @@ async function writePack(pack3, files, out) {
|
|
|
10664
10965
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
10665
10966
|
}
|
|
10666
10967
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
10667
|
-
await mkdir8(
|
|
10968
|
+
await mkdir8(dirname3(out), { recursive: true });
|
|
10668
10969
|
await writeFile10(out, zip);
|
|
10669
10970
|
return zip.length;
|
|
10670
10971
|
}
|
|
@@ -10684,7 +10985,7 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10684
10985
|
if (!format) throw new Error("no deck-16x9 format");
|
|
10685
10986
|
const step = opts.onStep ?? (() => {
|
|
10686
10987
|
});
|
|
10687
|
-
const out =
|
|
10988
|
+
const out = resolve5(outDir);
|
|
10688
10989
|
await mkdir9(out, { recursive: true });
|
|
10689
10990
|
const speed = opts.speed ?? 1;
|
|
10690
10991
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
@@ -10693,12 +10994,13 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10693
10994
|
...opts.theme ? { theme: opts.theme } : {},
|
|
10694
10995
|
...opts.narration ? { narration: opts.narration } : {},
|
|
10695
10996
|
...opts.onBeatError ? { onBeatError: opts.onBeatError } : {},
|
|
10997
|
+
...opts.onBeatWarning ? { onBeatWarning: opts.onBeatWarning } : {},
|
|
10696
10998
|
...fontCss ? { fontCss } : {}
|
|
10697
10999
|
});
|
|
10698
11000
|
const files = [];
|
|
10699
11001
|
const write = async (name, text2) => {
|
|
10700
|
-
await writeFile11(
|
|
10701
|
-
files.push(
|
|
11002
|
+
await writeFile11(join12(out, name), text2);
|
|
11003
|
+
files.push(join12(out, name));
|
|
10702
11004
|
};
|
|
10703
11005
|
await write("index.html", deck.composition);
|
|
10704
11006
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
@@ -10727,8 +11029,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10727
11029
|
}
|
|
10728
11030
|
if (deck.page) {
|
|
10729
11031
|
await write(DECK_PAGE, deck.page);
|
|
10730
|
-
await cp(playerBundle(),
|
|
10731
|
-
files.push(
|
|
11032
|
+
await cp(playerBundle(), join12(out, PLAYER_FILE));
|
|
11033
|
+
files.push(join12(out, PLAYER_FILE));
|
|
10732
11034
|
}
|
|
10733
11035
|
files.push(...await vendorKatex(out));
|
|
10734
11036
|
if (opts.assetsFrom) files.push(...await copyAssets(opts.assetsFrom, out));
|
|
@@ -10737,7 +11039,7 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10737
11039
|
}
|
|
10738
11040
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
10739
11041
|
step(
|
|
10740
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
11042
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join12(out, "index.html")}`
|
|
10741
11043
|
);
|
|
10742
11044
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
10743
11045
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -10761,40 +11063,40 @@ async function deckRuntime() {
|
|
|
10761
11063
|
}
|
|
10762
11064
|
}
|
|
10763
11065
|
function playerBundle() {
|
|
10764
|
-
const require2 =
|
|
11066
|
+
const require2 = createRequire3(import.meta.url);
|
|
10765
11067
|
try {
|
|
10766
|
-
return
|
|
11068
|
+
return join12(dirname4(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
10767
11069
|
} catch {
|
|
10768
11070
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
10769
11071
|
}
|
|
10770
11072
|
}
|
|
10771
11073
|
async function vendorKatex(out) {
|
|
10772
|
-
const require2 =
|
|
10773
|
-
const dist =
|
|
10774
|
-
const css = await readFile11(
|
|
11074
|
+
const require2 = createRequire3(import.meta.url);
|
|
11075
|
+
const dist = join12(dirname4(require2.resolve("katex/package.json")), "dist");
|
|
11076
|
+
const css = await readFile11(join12(dist, "katex.min.css"), "utf8");
|
|
10775
11077
|
const written = [];
|
|
10776
|
-
await mkdir9(
|
|
10777
|
-
for (const file of await readdir2(
|
|
11078
|
+
await mkdir9(join12(out, "katex/fonts"), { recursive: true });
|
|
11079
|
+
for (const file of await readdir2(join12(dist, "fonts"))) {
|
|
10778
11080
|
if (!file.endsWith(".woff2")) continue;
|
|
10779
|
-
await cp(
|
|
10780
|
-
written.push(
|
|
11081
|
+
await cp(join12(dist, "fonts", file), join12(out, "katex/fonts", file));
|
|
11082
|
+
written.push(join12(out, "katex/fonts", file));
|
|
10781
11083
|
}
|
|
10782
11084
|
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
10783
11085
|
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
10784
11086
|
return kept ? `src:${kept}` : whole;
|
|
10785
11087
|
});
|
|
10786
|
-
await writeFile11(
|
|
10787
|
-
written.push(
|
|
11088
|
+
await writeFile11(join12(out, "katex/katex.min.css"), woff2Only);
|
|
11089
|
+
written.push(join12(out, "katex/katex.min.css"));
|
|
10788
11090
|
return written;
|
|
10789
11091
|
}
|
|
10790
11092
|
async function copyAssets(sourceDir, out) {
|
|
10791
|
-
const from =
|
|
11093
|
+
const from = join12(resolve5(sourceDir), "assets");
|
|
10792
11094
|
if (!await stat2(from).catch(() => null)) return [];
|
|
10793
|
-
await cp(from,
|
|
10794
|
-
return [
|
|
11095
|
+
await cp(from, join12(out, "assets"), { recursive: true });
|
|
11096
|
+
return [join12(out, "assets")];
|
|
10795
11097
|
}
|
|
10796
11098
|
async function copyAudio(from, narration, out) {
|
|
10797
|
-
const dir =
|
|
11099
|
+
const dir = join12(out, narration.dir);
|
|
10798
11100
|
await mkdir9(dir, { recursive: true });
|
|
10799
11101
|
const names = [
|
|
10800
11102
|
...new Set(
|
|
@@ -10802,18 +11104,18 @@ async function copyAudio(from, narration, out) {
|
|
|
10802
11104
|
)
|
|
10803
11105
|
].sort();
|
|
10804
11106
|
for (const name of names) {
|
|
10805
|
-
await cp(
|
|
11107
|
+
await cp(join12(resolve5(from), name), join12(dir, name)).catch(() => {
|
|
10806
11108
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
10807
11109
|
});
|
|
10808
11110
|
}
|
|
10809
|
-
return names.map((n3) =>
|
|
11111
|
+
return names.map((n3) => join12(dir, n3));
|
|
10810
11112
|
}
|
|
10811
11113
|
async function refreshFont(storyboard, source, out, step) {
|
|
10812
11114
|
try {
|
|
10813
11115
|
const bundle = await bundleFont(
|
|
10814
11116
|
storyboard.lang,
|
|
10815
11117
|
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
10816
|
-
|
|
11118
|
+
join12(out, "assets", "fonts")
|
|
10817
11119
|
);
|
|
10818
11120
|
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
10819
11121
|
return bundle?.css;
|
|
@@ -10826,7 +11128,7 @@ async function refreshFont(storyboard, source, out, step) {
|
|
|
10826
11128
|
}
|
|
10827
11129
|
|
|
10828
11130
|
// src/server/upload.ts
|
|
10829
|
-
import { posix, sep } from "node:path";
|
|
11131
|
+
import { posix, sep as sep2 } from "node:path";
|
|
10830
11132
|
import { unzipSync as unzipSync2 } from "fflate";
|
|
10831
11133
|
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
10832
11134
|
var ZIP_LIMITS = {
|
|
@@ -10863,8 +11165,8 @@ function safeEntryPath(name) {
|
|
|
10863
11165
|
if (path2.length > 512 || kept.some((s) => s.length > 200)) return null;
|
|
10864
11166
|
return path2;
|
|
10865
11167
|
}
|
|
10866
|
-
function
|
|
10867
|
-
return joined === root2 || joined.startsWith(root2.endsWith(
|
|
11168
|
+
function insideRoot2(root2, joined) {
|
|
11169
|
+
return joined === root2 || joined.startsWith(root2.endsWith(sep2) ? root2 : root2 + sep2);
|
|
10868
11170
|
}
|
|
10869
11171
|
function readZip(bytes, limits = ZIP_LIMITS) {
|
|
10870
11172
|
const seen = [];
|
|
@@ -11169,7 +11471,7 @@ function int(name, raw2) {
|
|
|
11169
11471
|
|
|
11170
11472
|
// src/server/pipeline.ts
|
|
11171
11473
|
import { mkdir as mkdir10, readFile as readFile12, stat as stat3, writeFile as writeFile12 } from "node:fs/promises";
|
|
11172
|
-
import { dirname as
|
|
11474
|
+
import { dirname as dirname5, isAbsolute, join as join13, relative, resolve as resolve6 } from "node:path";
|
|
11173
11475
|
var AUDIO_DIR = "audio";
|
|
11174
11476
|
var NARRATION_FILE = "narration.json";
|
|
11175
11477
|
var MAX_REMOTE_FIGURES = 40;
|
|
@@ -11192,10 +11494,10 @@ function stagesFor(options) {
|
|
|
11192
11494
|
async function runPipeline(job, input) {
|
|
11193
11495
|
const { options } = input;
|
|
11194
11496
|
const dirs = {
|
|
11195
|
-
upload:
|
|
11196
|
-
src:
|
|
11197
|
-
audio:
|
|
11198
|
-
deck:
|
|
11497
|
+
upload: join13(job.dir, "upload"),
|
|
11498
|
+
src: join13(job.dir, "src"),
|
|
11499
|
+
audio: join13(job.dir, AUDIO_DIR),
|
|
11500
|
+
deck: join13(job.dir, "deck")
|
|
11199
11501
|
};
|
|
11200
11502
|
const warnings = [...options.warnings];
|
|
11201
11503
|
const url = (rel) => `/d/${job.id}/${rel}`;
|
|
@@ -11219,21 +11521,21 @@ async function runPipeline(job, input) {
|
|
|
11219
11521
|
theme: options.stated.theme ? prefs.theme : planned.theme
|
|
11220
11522
|
};
|
|
11221
11523
|
assertRefsResolve(storyboard, source, { pending: options.images ? "allow" : "refuse" });
|
|
11222
|
-
await writeJson(
|
|
11524
|
+
await writeJson(join13(job.dir, "storyboard.json"), storyboard);
|
|
11223
11525
|
job.done("plan", `${storyboard.beats.length} beats`);
|
|
11224
11526
|
for (const f of scanBeatCount(storyboard, prefs)) warnings.push(f.message);
|
|
11225
11527
|
if (options.images) {
|
|
11226
11528
|
job.begin("illustrate");
|
|
11227
11529
|
const drawn = await illustrate(storyboard, source, {
|
|
11228
11530
|
prefs,
|
|
11229
|
-
assetsDir:
|
|
11531
|
+
assetsDir: join13(dirs.src, "assets"),
|
|
11230
11532
|
...input.imageChain ? { chain: input.imageChain } : {},
|
|
11231
11533
|
onStep: (line2) => job.log(line2)
|
|
11232
11534
|
});
|
|
11233
11535
|
storyboard = drawn.storyboard;
|
|
11234
11536
|
source = drawn.source;
|
|
11235
|
-
await writeJson(
|
|
11236
|
-
await writeJson(
|
|
11537
|
+
await writeJson(join13(dirs.src, "source.json"), source);
|
|
11538
|
+
await writeJson(join13(job.dir, "storyboard.json"), storyboard);
|
|
11237
11539
|
for (const p of drawn.illustrated) {
|
|
11238
11540
|
job.log(
|
|
11239
11541
|
`illustrate: ${p.beatId} \u2192 assets/${p.src} via ${p.provider}${p.cached ? " (cached)" : ""}`
|
|
@@ -11260,7 +11562,7 @@ async function runPipeline(job, input) {
|
|
|
11260
11562
|
dir: dirs.audio,
|
|
11261
11563
|
format: options.format
|
|
11262
11564
|
});
|
|
11263
|
-
await writeJson(
|
|
11565
|
+
await writeJson(join13(dirs.audio, NARRATION_FILE), spoken);
|
|
11264
11566
|
narration = { voice: spoken.voice, dir: AUDIO_DIR, beats: spoken.beats };
|
|
11265
11567
|
const segments2 = Object.values(spoken.beats).flat();
|
|
11266
11568
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
@@ -11286,6 +11588,13 @@ async function runPipeline(job, input) {
|
|
|
11286
11588
|
onBeatError: (id2, err) => {
|
|
11287
11589
|
warnings.push(`slide ${id2} was left out: ${err.message}`);
|
|
11288
11590
|
job.log(`build: dropped ${id2} \u2014 ${err.message}`);
|
|
11591
|
+
},
|
|
11592
|
+
// A beat that IS in the deck, drawn with one of its parts dropped so it
|
|
11593
|
+
// would fit. The slide looks finished, so the job's warnings are the only
|
|
11594
|
+
// place the person who asked for it finds out otherwise.
|
|
11595
|
+
onBeatWarning: (id2, warning) => {
|
|
11596
|
+
warnings.push(`slide ${id2} was drawn short: ${warning}`);
|
|
11597
|
+
job.log(`build: kept ${id2} \u2014 ${warning}`);
|
|
11289
11598
|
}
|
|
11290
11599
|
});
|
|
11291
11600
|
for (const d of built.cut.dropped) warnings.push(`cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
@@ -11307,7 +11616,7 @@ async function runPipeline(job, input) {
|
|
|
11307
11616
|
job.log("render: capturing frames \u2014 two minutes for a four-minute video");
|
|
11308
11617
|
const out = await render({
|
|
11309
11618
|
deck: dirs.deck,
|
|
11310
|
-
out:
|
|
11619
|
+
out: join13(dirs.deck, "video.mp4"),
|
|
11311
11620
|
subtitles: "sidecar",
|
|
11312
11621
|
log: (line2) => job.log(line2),
|
|
11313
11622
|
// The last of the three length levers, and the only one that acts on a
|
|
@@ -11357,7 +11666,7 @@ async function runPipeline(job, input) {
|
|
|
11357
11666
|
}
|
|
11358
11667
|
async function ingest(job, input, dirs, warnings) {
|
|
11359
11668
|
await mkdir10(dirs.upload, { recursive: true });
|
|
11360
|
-
const root2 =
|
|
11669
|
+
const root2 = resolve6(dirs.upload);
|
|
11361
11670
|
let docPath;
|
|
11362
11671
|
const upload = input.upload;
|
|
11363
11672
|
const bothOrNeither = new UploadError(
|
|
@@ -11367,12 +11676,12 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11367
11676
|
);
|
|
11368
11677
|
if (input.url !== void 0) {
|
|
11369
11678
|
if (upload !== void 0) throw bothOrNeither;
|
|
11370
|
-
const harvested = await harvestBounded(input.url,
|
|
11679
|
+
const harvested = await harvestBounded(input.url, join13(root2, "assets"), {
|
|
11371
11680
|
...HARVEST_LIMITS,
|
|
11372
11681
|
...input.harvest
|
|
11373
11682
|
});
|
|
11374
11683
|
warnings.push(...harvested.warnings);
|
|
11375
|
-
docPath =
|
|
11684
|
+
docPath = join13(root2, "document.md");
|
|
11376
11685
|
await writeFile12(docPath, harvested.markdown);
|
|
11377
11686
|
job.log(
|
|
11378
11687
|
`ingest: harvested ${input.url} \u2014 "${harvested.title}", ${harvested.assets.length} asset(s)`
|
|
@@ -11389,13 +11698,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11389
11698
|
const { files, warnings: zipWarnings } = readZip(upload.bytes);
|
|
11390
11699
|
warnings.push(...zipWarnings);
|
|
11391
11700
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
11392
|
-
const to =
|
|
11393
|
-
if (!
|
|
11701
|
+
const to = resolve6(join13(root2, rel));
|
|
11702
|
+
if (!insideRoot2(root2, to))
|
|
11394
11703
|
throw new UploadError(`Refusing to write ${rel}.`, "Re-zip from inside the folder.");
|
|
11395
|
-
await mkdir10(
|
|
11704
|
+
await mkdir10(dirname5(to), { recursive: true });
|
|
11396
11705
|
await writeFile12(to, bytes);
|
|
11397
11706
|
}
|
|
11398
|
-
docPath =
|
|
11707
|
+
docPath = join13(root2, pickMarkdown(files));
|
|
11399
11708
|
job.log(
|
|
11400
11709
|
`ingest: unpacked ${Object.keys(files).length} file(s), reading ${pickMarkdown(files)}`
|
|
11401
11710
|
);
|
|
@@ -11407,7 +11716,7 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11407
11716
|
`DeckSmith reads ${MARKDOWN_EXTS.join(", ")} or a .zip containing one. Export the document to markdown first.`
|
|
11408
11717
|
);
|
|
11409
11718
|
}
|
|
11410
|
-
docPath =
|
|
11719
|
+
docPath = join13(root2, "document.md");
|
|
11411
11720
|
await writeFile12(docPath, upload.bytes);
|
|
11412
11721
|
}
|
|
11413
11722
|
const text2 = await readFile12(docPath, "utf8");
|
|
@@ -11430,8 +11739,8 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11430
11739
|
`ingest: ${parsed.sections.length} sections, ${parsed.figures.length} figures, ${parsed.equations.length} equations, ${parsed.tables.length} tables`
|
|
11431
11740
|
);
|
|
11432
11741
|
const guarded = await guardFigures(parsed, root2, input.fetchRemoteFigures, warnings);
|
|
11433
|
-
const source = await fetchFigures(guarded,
|
|
11434
|
-
await writeJson(
|
|
11742
|
+
const source = await fetchFigures(guarded, join13(resolve6(dirs.src), "assets"), warnings);
|
|
11743
|
+
await writeJson(join13(dirs.src, "source.json"), source);
|
|
11435
11744
|
return source;
|
|
11436
11745
|
}
|
|
11437
11746
|
async function harvestBounded(url, dir, opts) {
|
|
@@ -11507,8 +11816,8 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11507
11816
|
warnings.push(`figure ${figure.id} was left out: "${scheme}:" figures are not read`);
|
|
11508
11817
|
continue;
|
|
11509
11818
|
}
|
|
11510
|
-
const abs = isAbsolute(src) ?
|
|
11511
|
-
if (!
|
|
11819
|
+
const abs = isAbsolute(src) ? resolve6(src) : resolve6(join13(root2, src.replace(/^\.?\//, "")));
|
|
11820
|
+
if (!insideRoot2(root2, abs)) {
|
|
11512
11821
|
warnings.push(`figure ${figure.id} was left out: "${src}" points outside the upload`);
|
|
11513
11822
|
continue;
|
|
11514
11823
|
}
|
|
@@ -11522,10 +11831,10 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11522
11831
|
}
|
|
11523
11832
|
async function pack2(job, ctx) {
|
|
11524
11833
|
try {
|
|
11525
|
-
const assets =
|
|
11834
|
+
const assets = join13(resolve6(ctx.dirs.src), "assets");
|
|
11526
11835
|
const requests = ctx.source.figures.map((f) => ({
|
|
11527
11836
|
id: f.id,
|
|
11528
|
-
url:
|
|
11837
|
+
url: join13(assets, f.src),
|
|
11529
11838
|
prefer: "bake"
|
|
11530
11839
|
}));
|
|
11531
11840
|
const plan = await planMedia(requests, async (url) => ({
|
|
@@ -11534,7 +11843,7 @@ async function pack2(job, ctx) {
|
|
|
11534
11843
|
const files = { ...plan.files };
|
|
11535
11844
|
if (ctx.narration) {
|
|
11536
11845
|
for (const name of audioNames(ctx.narration)) {
|
|
11537
|
-
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile12(
|
|
11846
|
+
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile12(join13(ctx.dirs.audio, name)));
|
|
11538
11847
|
}
|
|
11539
11848
|
}
|
|
11540
11849
|
const container = {
|
|
@@ -11551,10 +11860,10 @@ async function pack2(job, ctx) {
|
|
|
11551
11860
|
},
|
|
11552
11861
|
source: ctx.source,
|
|
11553
11862
|
storyboard: ctx.storyboard,
|
|
11554
|
-
...ctx.narration ? { narration: await readJson(
|
|
11863
|
+
...ctx.narration ? { narration: await readJson(join13(ctx.dirs.audio, NARRATION_FILE)) } : {},
|
|
11555
11864
|
media: plan.media
|
|
11556
11865
|
};
|
|
11557
|
-
const bytes = await writePack(container, files,
|
|
11866
|
+
const bytes = await writePack(container, files, join13(ctx.dirs.deck, "deck.deck"));
|
|
11558
11867
|
job.log(`pack: ${Math.round(bytes / 1024)} KB \u2192 deck.deck`);
|
|
11559
11868
|
return ctx.url("deck.deck");
|
|
11560
11869
|
} catch (err) {
|
|
@@ -11577,7 +11886,7 @@ function host(src) {
|
|
|
11577
11886
|
}
|
|
11578
11887
|
}
|
|
11579
11888
|
async function writeJson(path2, value) {
|
|
11580
|
-
await mkdir10(
|
|
11889
|
+
await mkdir10(dirname5(path2), { recursive: true });
|
|
11581
11890
|
await writeFile12(path2, `${JSON.stringify(value, null, 2)}
|
|
11582
11891
|
`);
|
|
11583
11892
|
}
|
|
@@ -11989,13 +12298,13 @@ var PAGE_BUDGET = {
|
|
|
11989
12298
|
};
|
|
11990
12299
|
async function readPage(url, work2) {
|
|
11991
12300
|
await mkdir11(work2, { recursive: true });
|
|
11992
|
-
const dir = await mkdtemp3(
|
|
12301
|
+
const dir = await mkdtemp3(join14(work2, "harvest-"));
|
|
11993
12302
|
try {
|
|
11994
12303
|
const page = await harvest(url, dir, PAGE_BUDGET);
|
|
11995
12304
|
const files = {
|
|
11996
12305
|
"document.md": new TextEncoder().encode(page.markdown)
|
|
11997
12306
|
};
|
|
11998
|
-
for (const asset of page.assets) files[
|
|
12307
|
+
for (const asset of page.assets) files[basename5(asset)] = await readFile13(asset);
|
|
11999
12308
|
const warnings = [...page.warnings];
|
|
12000
12309
|
if (page.clips.length > 0) {
|
|
12001
12310
|
warnings.push(
|
|
@@ -12029,16 +12338,16 @@ var statusSchema = z5.object({
|
|
|
12029
12338
|
var DEFAULT_WAIT = 45;
|
|
12030
12339
|
function deckTools(opts) {
|
|
12031
12340
|
const queue = new Queue({ maxQueued: 8 });
|
|
12032
|
-
const root2 =
|
|
12033
|
-
let
|
|
12341
|
+
const root2 = resolve7(opts.root);
|
|
12342
|
+
let cached2;
|
|
12034
12343
|
const found = async () => {
|
|
12035
|
-
|
|
12036
|
-
return
|
|
12344
|
+
cached2 ??= await (opts.probe ?? prereqs)();
|
|
12345
|
+
return cached2;
|
|
12037
12346
|
};
|
|
12038
|
-
const
|
|
12347
|
+
const insideRoot3 = (p) => {
|
|
12039
12348
|
if (!isAbsolute2(p)) throw new Error(`document_path must be absolute; got "${p}".`);
|
|
12040
|
-
const full =
|
|
12041
|
-
if (full !== root2 && !full.startsWith(root2 +
|
|
12349
|
+
const full = resolve7(p);
|
|
12350
|
+
if (full !== root2 && !full.startsWith(root2 + sep3)) {
|
|
12042
12351
|
throw new Error(
|
|
12043
12352
|
`document_path is outside the server's root. It may only read files under ${root2}.`
|
|
12044
12353
|
);
|
|
@@ -12099,7 +12408,7 @@ function deckTools(opts) {
|
|
|
12099
12408
|
}
|
|
12100
12409
|
const settings = input.settings ?? {};
|
|
12101
12410
|
const options = parseOptions(fieldsFor(settings));
|
|
12102
|
-
const file = input.document_path ?
|
|
12411
|
+
const file = input.document_path ? insideRoot3(input.document_path) : void 0;
|
|
12103
12412
|
const missing = missingFor(await found(), options);
|
|
12104
12413
|
if (missing.length) {
|
|
12105
12414
|
throw new Error(
|
|
@@ -12115,7 +12424,7 @@ function deckTools(opts) {
|
|
|
12115
12424
|
}
|
|
12116
12425
|
}
|
|
12117
12426
|
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(
|
|
12427
|
+
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(sep3).pop() : "document.md";
|
|
12119
12428
|
const bytes = page ? page.zip : file ? await readFile13(file) : new TextEncoder().encode(input.document_text);
|
|
12120
12429
|
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
12121
12430
|
throw new Error(
|
|
@@ -12123,7 +12432,7 @@ function deckTools(opts) {
|
|
|
12123
12432
|
);
|
|
12124
12433
|
}
|
|
12125
12434
|
const id2 = randomBytes(16).toString("base64url");
|
|
12126
|
-
const dir =
|
|
12435
|
+
const dir = join14(opts.work, id2);
|
|
12127
12436
|
await mkdir11(dir, { recursive: true });
|
|
12128
12437
|
queue.submit({
|
|
12129
12438
|
id: id2,
|
|
@@ -12151,7 +12460,7 @@ function deckTools(opts) {
|
|
|
12151
12460
|
};
|
|
12152
12461
|
}
|
|
12153
12462
|
function dirOf(work2, id2) {
|
|
12154
|
-
return
|
|
12463
|
+
return join14(work2, id2);
|
|
12155
12464
|
}
|
|
12156
12465
|
function waitFor(queue, id2, seconds) {
|
|
12157
12466
|
const now = queue.view(id2);
|
|
@@ -12183,9 +12492,9 @@ function report(view, dir) {
|
|
|
12183
12492
|
// (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
|
|
12184
12493
|
// is the shape of the CLI's output directory and not the server's, and an
|
|
12185
12494
|
// agent told to read it would have found nothing there.
|
|
12186
|
-
storyboard_path:
|
|
12495
|
+
storyboard_path: join14(dir, "storyboard.json"),
|
|
12187
12496
|
...done && view.result ? {
|
|
12188
|
-
deck_path:
|
|
12497
|
+
deck_path: join14(dir, "deck"),
|
|
12189
12498
|
slides: view.result.slides,
|
|
12190
12499
|
duration_seconds: view.result.duration,
|
|
12191
12500
|
warnings: view.result.warnings
|
|
@@ -12194,7 +12503,7 @@ function report(view, dir) {
|
|
|
12194
12503
|
};
|
|
12195
12504
|
}
|
|
12196
12505
|
function defaultWork() {
|
|
12197
|
-
return
|
|
12506
|
+
return join14(tmpdir4(), "decksmith-mcp");
|
|
12198
12507
|
}
|
|
12199
12508
|
|
|
12200
12509
|
// src/mcp/main.ts
|
|
@@ -12204,6 +12513,7 @@ for (const level of ["log", "info", "debug", "warn", "trace"]) {
|
|
|
12204
12513
|
`);
|
|
12205
12514
|
};
|
|
12206
12515
|
}
|
|
12516
|
+
guardTmpdir();
|
|
12207
12517
|
var env = process.env;
|
|
12208
12518
|
var root = env.DECKSMITH_MCP_ROOT ?? homedir3();
|
|
12209
12519
|
var work = env.DECKSMITH_MCP_WORK ?? defaultWork();
|