@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/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { cp, mkdir as mkdir10, mkdtemp as mkdtemp4, readdir as readdir4, readFile as readFile17, rm as rm9, stat as stat2, writeFile as writeFile12 } from "node:fs/promises";
|
|
5
|
-
import { createRequire as
|
|
6
|
-
import { tmpdir as
|
|
7
|
-
import { dirname as
|
|
5
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
6
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
7
|
+
import { dirname as dirname5, join as join17, relative as relative2, resolve as resolve6 } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
@@ -418,15 +418,53 @@ var dataTableParamsSchema = z.object({
|
|
|
418
418
|
path: ["highlight"],
|
|
419
419
|
message: "every highlight must name a row that params.rows draws"
|
|
420
420
|
});
|
|
421
|
+
var chartPointSchema = z.object({ x: z.string(), y: z.number() });
|
|
421
422
|
var lineChartParamsSchema = z.object({
|
|
422
423
|
eyebrow: z.string().optional(),
|
|
423
424
|
headline: z.string(),
|
|
424
425
|
xLabel: z.string(),
|
|
425
426
|
yLabel: z.string(),
|
|
426
|
-
points: z.array(
|
|
427
|
+
points: z.array(chartPointSchema).min(2),
|
|
427
428
|
/** Inter-point annotations, e.g. per-step deltas. One fewer than `points`. */
|
|
428
429
|
deltas: z.array(z.string()).optional(),
|
|
429
|
-
readout: z.string().optional()
|
|
430
|
+
readout: z.string().optional(),
|
|
431
|
+
/**
|
|
432
|
+
* THE SAME MEASUREMENT UNDER A SECOND CONDITION — a baseline the main series
|
|
433
|
+
* is to be read against, where the point is the change in the SHAPE of the
|
|
434
|
+
* curve rather than two numbers.
|
|
435
|
+
*
|
|
436
|
+
* Drawn first and alone; then the curve lifts off it and reshapes into
|
|
437
|
+
* `points`, leaving this one behind as a ghost. `label` names the ghost and
|
|
438
|
+
* is drawn wherever the chart has room for it.
|
|
439
|
+
*
|
|
440
|
+
* KEEP IT SHORT — two or three words. A label wider than the plot is refused
|
|
441
|
+
* outright, and that refusal reaches `onBeatError` and costs the whole beat.
|
|
442
|
+
* One that fits but cannot be placed clear of the axis names, the tick and
|
|
443
|
+
* category labels, the values, the deltas and both curves is DROPPED
|
|
444
|
+
* instead, silently: an unnamed ghost is still legibly the fainter, earlier
|
|
445
|
+
* curve, where a name printed through a number is a defect in both of them.
|
|
446
|
+
* Nothing warns about that one — see the placement note in
|
|
447
|
+
* `src/emit/archetypes/line-chart.ts`.
|
|
448
|
+
*/
|
|
449
|
+
compare: z.object({ label: z.string(), points: z.array(chartPointSchema).min(2) }).optional()
|
|
450
|
+
}).superRefine((p, ctx) => {
|
|
451
|
+
if (!p.compare) return;
|
|
452
|
+
if (p.compare.points.length !== p.points.length) {
|
|
453
|
+
ctx.addIssue({
|
|
454
|
+
code: z.ZodIssueCode.custom,
|
|
455
|
+
path: ["compare", "points"],
|
|
456
|
+
message: `compare has ${p.compare.points.length} points against ${p.points.length}; a comparison is over the same x values`
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const off = p.compare.points.findIndex((c, i) => c.x !== p.points[i]?.x);
|
|
461
|
+
if (off >= 0) {
|
|
462
|
+
ctx.addIssue({
|
|
463
|
+
code: z.ZodIssueCode.custom,
|
|
464
|
+
path: ["compare", "points", off, "x"],
|
|
465
|
+
message: `compare point ${off} is "${p.compare.points[off]?.x}" where points has "${p.points[off]?.x}"`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
430
468
|
});
|
|
431
469
|
var calloutParamsSchema = z.object({
|
|
432
470
|
eyebrow: z.string().optional(),
|
|
@@ -1206,8 +1244,8 @@ async function bundleFont(lang, glyphs2, dir) {
|
|
|
1206
1244
|
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1207
1245
|
await mkdir(dir, { recursive: true });
|
|
1208
1246
|
const cssPath = join(dir, "fonts.css");
|
|
1209
|
-
const
|
|
1210
|
-
if (
|
|
1247
|
+
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1248
|
+
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1211
1249
|
const res = await fetch(
|
|
1212
1250
|
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
1213
1251
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1420,6 +1458,21 @@ function nv(v) {
|
|
|
1420
1458
|
}
|
|
1421
1459
|
var DRAW_FROM = { drawSVG: "0%" };
|
|
1422
1460
|
var DRAW_TO = { drawSVG: "100%" };
|
|
1461
|
+
var SHAPE_INDEX = 0;
|
|
1462
|
+
function reshape(target, to, at, seconds, first) {
|
|
1463
|
+
if (!(seconds > 0)) throw new Error(`reshape ${target}: ${seconds}s is no time to reshape in`);
|
|
1464
|
+
return fromTo(
|
|
1465
|
+
target,
|
|
1466
|
+
{ morphSVG: { shape: target, shapeIndex: SHAPE_INDEX } },
|
|
1467
|
+
{
|
|
1468
|
+
morphSVG: { shape: to, shapeIndex: SHAPE_INDEX },
|
|
1469
|
+
duration: sec(seconds),
|
|
1470
|
+
ease: "power2.inOut",
|
|
1471
|
+
...first ? {} : { immediateRender: false }
|
|
1472
|
+
},
|
|
1473
|
+
sec(at)
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1423
1476
|
function travel(target, route, at, seconds) {
|
|
1424
1477
|
if (!(seconds > 0)) throw new Error(`travel ${target}: ${seconds}s is no time to travel in`);
|
|
1425
1478
|
const legs = [];
|
|
@@ -4101,6 +4154,12 @@ var CHART_GAP = 60;
|
|
|
4101
4154
|
var READOUT_W = 460;
|
|
4102
4155
|
var READOUT_LH = 1.35;
|
|
4103
4156
|
var TALL_ASPECT = 1.15;
|
|
4157
|
+
var RESHAPE_SECONDS = 1.2;
|
|
4158
|
+
var GHOST_OPACITY = 0.28;
|
|
4159
|
+
var DRAW_SECONDS = 1.8;
|
|
4160
|
+
var DRAW_FLOOR = 1;
|
|
4161
|
+
var SEPARATE = 0.5;
|
|
4162
|
+
var STEP_FLOOR = 0.15;
|
|
4104
4163
|
function chartScale(values) {
|
|
4105
4164
|
const lo = Math.min(...values);
|
|
4106
4165
|
const hi = Math.max(...values);
|
|
@@ -4131,7 +4190,8 @@ var lineChart = (beat, ctx) => {
|
|
|
4131
4190
|
const { sid, theme } = ctx;
|
|
4132
4191
|
const p = beat.params;
|
|
4133
4192
|
const face = faceOf(theme.fontStack);
|
|
4134
|
-
const
|
|
4193
|
+
const cmp = p.compare;
|
|
4194
|
+
const scale = chartScale([...p.points, ...cmp?.points ?? []].map((pt) => pt.y));
|
|
4135
4195
|
const box = contentW(ctx.format);
|
|
4136
4196
|
const tall = isPortrait(ctx.format);
|
|
4137
4197
|
const width = tall ? box : box - (p.readout ? CHART_GAP + READOUT_W : 0);
|
|
@@ -4174,11 +4234,15 @@ var lineChart = (beat, ctx) => {
|
|
|
4174
4234
|
const LABEL_SIZE3 = 40;
|
|
4175
4235
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4176
4236
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4237
|
+
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
4238
|
+
const ghostW = (s) => textWidth(s, LABEL_SIZE3, 600, 0, false, face);
|
|
4177
4239
|
const shownX = fitIndices((i) => catW(p.points[i]?.x ?? ""), false);
|
|
4178
4240
|
const xLabels = p.points.map(
|
|
4179
4241
|
(pt, i) => shownX.has(i) ? `<text x="${n2(x(i))}" y="${H - PAD2.b + 56}">${esc(pt.x)}</text>` : ""
|
|
4180
4242
|
).join("");
|
|
4181
|
-
const
|
|
4243
|
+
const pathOf = (pts) => pts.map((pt, i) => `${i === 0 ? "M" : "L"}${n2(x(i))},${n2(y(pt.y))}`).join(" ");
|
|
4244
|
+
const path2 = pathOf(p.points);
|
|
4245
|
+
const basePath = cmp ? pathOf(cmp.points) : "";
|
|
4182
4246
|
const dots = p.points.map(
|
|
4183
4247
|
(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}" />`
|
|
4184
4248
|
).join("");
|
|
@@ -4214,6 +4278,75 @@ var lineChart = (beat, ctx) => {
|
|
|
4214
4278
|
}).join("");
|
|
4215
4279
|
const first = { x: x(0), y: y(p.points[0]?.y ?? 0) };
|
|
4216
4280
|
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" />` : "";
|
|
4281
|
+
const lineTag = (part, d, extra = "") => `<path class="chartline" id="${sid}-${part}" d="${d}" fill="none" stroke="${theme.accent}"${extra} />`;
|
|
4282
|
+
const chartlines = cmp ? [
|
|
4283
|
+
lineTag("base", basePath),
|
|
4284
|
+
lineTag("line", basePath, ' opacity="0"'),
|
|
4285
|
+
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
4286
|
+
].join("\n ") : lineTag("line", path2);
|
|
4287
|
+
const curveBand = (pts, x0, x1) => {
|
|
4288
|
+
const ys = [];
|
|
4289
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
4290
|
+
for (let i = 0; i < pts.length; i++) {
|
|
4291
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
4292
|
+
}
|
|
4293
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
4294
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
4295
|
+
for (const edge of [x0, x1]) {
|
|
4296
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
4297
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
4302
|
+
};
|
|
4303
|
+
const fixedBoxes = [
|
|
4304
|
+
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
4305
|
+
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
4306
|
+
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
4307
|
+
{ x: 0, y: 42 - LABEL_SIZE3, w: nameW(p.yLabel), h: LABEL_SIZE3 },
|
|
4308
|
+
{
|
|
4309
|
+
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
4310
|
+
y: H - 16 - LABEL_SIZE3,
|
|
4311
|
+
w: nameW(p.xLabel),
|
|
4312
|
+
h: LABEL_SIZE3
|
|
4313
|
+
},
|
|
4314
|
+
...ticks.map((v) => {
|
|
4315
|
+
const w = runW(v.toFixed(scale.decimals));
|
|
4316
|
+
return { x: PAD2.l - 22 - w, y: y(v) + 13 - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4317
|
+
}),
|
|
4318
|
+
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
4319
|
+
];
|
|
4320
|
+
const lastI = p.points.length - 1;
|
|
4321
|
+
const ghost = (() => {
|
|
4322
|
+
if (!cmp) return void 0;
|
|
4323
|
+
const w = ghostW(cmp.label);
|
|
4324
|
+
if (w > plotW) {
|
|
4325
|
+
throw new Error(
|
|
4326
|
+
`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.`
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
const atEnd = (i, start) => {
|
|
4330
|
+
const cy = y(cmp.points[i]?.y ?? 0);
|
|
4331
|
+
const away = Math.max(44, cy - 26);
|
|
4332
|
+
const toward = Math.min(cy + 52, H - PAD2.b + 4);
|
|
4333
|
+
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
4334
|
+
return sides.map((by) => ({ i, start, by }));
|
|
4335
|
+
};
|
|
4336
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
4337
|
+
return [...atEnd(lastI, false), ...atEnd(0, true)].find((c) => {
|
|
4338
|
+
const box2 = { x: c.start ? x(c.i) : x(c.i) - w, y: c.by - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4339
|
+
if (fixedBoxes.some((f) => overlaps(box2, f))) return false;
|
|
4340
|
+
if (valueBoxes.some((v) => overlaps(box2, v))) return false;
|
|
4341
|
+
if (drawnDeltas.some((d) => d !== null && overlaps(box2, d))) return false;
|
|
4342
|
+
return ![p.points, cmp.points].some((series) => {
|
|
4343
|
+
const [top, bottom] = curveBand(series, box2.x, box2.x + box2.w);
|
|
4344
|
+
return Math.min(box2.y + box2.h, bottom) - Math.max(box2.y, top) > 8;
|
|
4345
|
+
});
|
|
4346
|
+
});
|
|
4347
|
+
})();
|
|
4348
|
+
const ghostLabel = cmp && ghost ? `
|
|
4349
|
+
<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>` : "";
|
|
4217
4350
|
const readout = p.readout ? `
|
|
4218
4351
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
4219
4352
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -4226,7 +4359,7 @@ var lineChart = (beat, ctx) => {
|
|
|
4226
4359
|
the svg and the layout gate reports container_overflow. -->
|
|
4227
4360
|
<text class="axname" x="0" y="42">${esc(p.yLabel)}</text>
|
|
4228
4361
|
<text class="axname" x="${n2(PAD2.l + plotW / 2)}" y="${H - 16}" text-anchor="middle">${esc(p.xLabel)}</text>
|
|
4229
|
-
|
|
4362
|
+
${chartlines}${ghostLabel}
|
|
4230
4363
|
<g>${dots}</g>
|
|
4231
4364
|
${ring}
|
|
4232
4365
|
<g class="ptlab" text-anchor="middle">${values}</g>
|
|
@@ -4234,10 +4367,52 @@ var lineChart = (beat, ctx) => {
|
|
|
4234
4367
|
</svg>${readout}
|
|
4235
4368
|
</div>`;
|
|
4236
4369
|
const draw2 = 0.8;
|
|
4237
|
-
const
|
|
4370
|
+
const count = p.points.length;
|
|
4371
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count);
|
|
4372
|
+
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
4373
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count - 1) : 0;
|
|
4374
|
+
const tailAfter = (s) => Math.max(
|
|
4375
|
+
s * count + 0.4,
|
|
4376
|
+
// the ring: its walk, then the 0.4s it takes to leave
|
|
4377
|
+
0.3 + s * (count - 1),
|
|
4378
|
+
// the dots
|
|
4379
|
+
0.5 + s * (count - 1),
|
|
4380
|
+
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
4381
|
+
);
|
|
4382
|
+
const room = beat.seconds - spine;
|
|
4383
|
+
let drawCents = Math.round(DRAW_SECONDS * 100);
|
|
4384
|
+
let stepCents = Math.floor(idealStep * 100);
|
|
4385
|
+
if (cmp) {
|
|
4386
|
+
while (drawCents / 100 + tailAfter(stepCents / 100) > room) {
|
|
4387
|
+
if (drawCents > Math.round(DRAW_FLOOR * 100)) drawCents--;
|
|
4388
|
+
else if (stepCents > Math.round(STEP_FLOOR * 100)) stepCents--;
|
|
4389
|
+
else break;
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
const drawFor = cmp ? drawCents / 100 : DRAW_SECONDS;
|
|
4393
|
+
const step2 = cmp ? stepCents / 100 : idealStep;
|
|
4394
|
+
const lift2 = sec(draw2 + drawFor);
|
|
4395
|
+
const settled = cmp ? sec(lift2 + SEPARATE + RESHAPE_SECONDS) : draw2;
|
|
4238
4396
|
const tl = [
|
|
4239
4397
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
4240
|
-
tween(
|
|
4398
|
+
tween(
|
|
4399
|
+
`#${sid}-${cmp ? "base" : "line"}`,
|
|
4400
|
+
DRAW_FROM,
|
|
4401
|
+
{ ...DRAW_TO, duration: drawFor, ease: "none" },
|
|
4402
|
+
draw2
|
|
4403
|
+
)
|
|
4404
|
+
];
|
|
4405
|
+
if (cmp) {
|
|
4406
|
+
tl.push(
|
|
4407
|
+
...ghost ? [tween(`#${sid}-ghostlab`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, draw2 + 0.4)] : [],
|
|
4408
|
+
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
4409
|
+
// separating from itself rather than as a second line arriving.
|
|
4410
|
+
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
4411
|
+
tween(`#${sid}-base`, { opacity: 1 }, { opacity: GHOST_OPACITY, duration: SEPARATE }, lift2),
|
|
4412
|
+
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
tl.push(
|
|
4241
4416
|
tween(
|
|
4242
4417
|
`#${sid} .dot`,
|
|
4243
4418
|
// Origin in both halves, or GSAP's smoothOrigin compensates the change with
|
|
@@ -4245,37 +4420,43 @@ var lineChart = (beat, ctx) => {
|
|
|
4245
4420
|
// polyline they mark, inside the frame and so invisible to every gate.
|
|
4246
4421
|
{ opacity: 0, scale: 0, transformOrigin: "center" },
|
|
4247
4422
|
{ opacity: 1, scale: 1, transformOrigin: "center", duration: 0.3, stagger: step2 },
|
|
4248
|
-
|
|
4423
|
+
settled
|
|
4249
4424
|
),
|
|
4250
|
-
tween(
|
|
4251
|
-
|
|
4425
|
+
tween(
|
|
4426
|
+
`#${sid} .pv`,
|
|
4427
|
+
{ opacity: 0 },
|
|
4428
|
+
{ opacity: 1, duration: 0.3, stagger: step2 },
|
|
4429
|
+
settled + 0.2
|
|
4430
|
+
)
|
|
4431
|
+
);
|
|
4252
4432
|
if (deltas) {
|
|
4253
4433
|
tl.push(
|
|
4254
4434
|
tween(
|
|
4255
4435
|
`#${sid} .dv`,
|
|
4256
4436
|
{ opacity: 0, y: -10 },
|
|
4257
4437
|
{ opacity: 1, y: 0, duration: 0.35, stagger: step2 },
|
|
4258
|
-
|
|
4438
|
+
settled + 0.6
|
|
4259
4439
|
)
|
|
4260
4440
|
);
|
|
4261
4441
|
}
|
|
4262
|
-
|
|
4442
|
+
const walk = cmp ? sec(step2 * count) : DRAW_SECONDS;
|
|
4443
|
+
if (count > 1) {
|
|
4263
4444
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
4264
4445
|
tl.push(
|
|
4265
|
-
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 },
|
|
4266
|
-
...travel(`#${sid}-ring`, route,
|
|
4446
|
+
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
4447
|
+
...travel(`#${sid}-ring`, route, settled, walk),
|
|
4267
4448
|
// And it leaves once the curve is whole: a marker parked on the last
|
|
4268
4449
|
// point for the rest of the beat reads as a defect, not as emphasis.
|
|
4269
4450
|
tween(
|
|
4270
4451
|
`#${sid}-ring`,
|
|
4271
4452
|
{ opacity: 1 },
|
|
4272
4453
|
{ opacity: 0, duration: 0.4, immediateRender: false },
|
|
4273
|
-
|
|
4454
|
+
settled + walk
|
|
4274
4455
|
)
|
|
4275
4456
|
);
|
|
4276
4457
|
}
|
|
4277
|
-
const drawn =
|
|
4278
|
-
const holds = [drawn];
|
|
4458
|
+
const drawn = settled + (cmp ? tailAfter(step2) : step2 * count + 0.4);
|
|
4459
|
+
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
4279
4460
|
if (p.readout) {
|
|
4280
4461
|
tl.push(
|
|
4281
4462
|
// It enters from wherever it sits: from the right when it is beside the
|
|
@@ -4284,9 +4465,22 @@ var lineChart = (beat, ctx) => {
|
|
|
4284
4465
|
);
|
|
4285
4466
|
holds.push(drawn + 0.8);
|
|
4286
4467
|
}
|
|
4468
|
+
const end = holds[holds.length - 1] ?? 0;
|
|
4469
|
+
const need = sec(end + 0.15);
|
|
4470
|
+
if (cmp && need > beat.seconds + 1e-9) {
|
|
4471
|
+
return {
|
|
4472
|
+
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
4473
|
+
warnings: [
|
|
4474
|
+
`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.`
|
|
4475
|
+
]
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4287
4478
|
return {
|
|
4288
4479
|
html,
|
|
4289
4480
|
tl,
|
|
4481
|
+
// Named only when a beat actually reshapes, which is what keeps MorphSVG's
|
|
4482
|
+
// 21,195 bytes off every deck that does not. See `PLUGINS` in composition.ts.
|
|
4483
|
+
...cmp ? { plugins: ["morphSVG"] } : {},
|
|
4290
4484
|
holds: holdsWithin(holds, beat.seconds),
|
|
4291
4485
|
css: [
|
|
4292
4486
|
chromeCss(theme),
|
|
@@ -4311,6 +4505,14 @@ var lineChart = (beat, ctx) => {
|
|
|
4311
4505
|
`.axname{font-size:40px;fill:${theme.muted};font-weight:500}`,
|
|
4312
4506
|
`.ptlab{font-size:40px;fill:${theme.fg};font-weight:600}`,
|
|
4313
4507
|
`.delta{font-size:40px;fill:${theme.tones.b};font-weight:600}`,
|
|
4508
|
+
// Only when a ghost was actually named — not merely when there is a ghost,
|
|
4509
|
+
// since the label is dropped where neither side of the baseline is clear.
|
|
4510
|
+
// An unconditional rule would move the stylesheet bytes of every line chart
|
|
4511
|
+
// ever built for a part they do not draw, which is the whole thing
|
|
4512
|
+
// `Scene.plugins` is careful about one level up. `muted`, not `dim`: it
|
|
4513
|
+
// names a series, so it is read, and the curve it names is the thing that
|
|
4514
|
+
// has been faded, not its label.
|
|
4515
|
+
...ghost ? [`.ghostlab{font-size:40px;fill:${theme.muted};font-weight:600}`] : [],
|
|
4314
4516
|
// 1.7 set the two lines of a wrapped readout 68px apart, which reads as two
|
|
4315
4517
|
// unrelated fragments rather than one sentence. 1.35 keeps it a paragraph.
|
|
4316
4518
|
`.readout{font-size:${BODY_SIZE}px;line-height:${READOUT_LH};color:${theme.muted};max-width:${READOUT_W}px}`,
|
|
@@ -5556,7 +5758,10 @@ function round4(n3) {
|
|
|
5556
5758
|
// src/emit/composition.ts
|
|
5557
5759
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5558
5760
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5559
|
-
var
|
|
5761
|
+
var PLUGINS = {
|
|
5762
|
+
dsMorph: { src: "./vendor/ds-morph.js", global: "DSMorphPlugin" },
|
|
5763
|
+
morphSVG: { src: "./vendor/MorphSVGPlugin.min.js", global: "MorphSVGPlugin" }
|
|
5764
|
+
};
|
|
5560
5765
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5561
5766
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5562
5767
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5664,7 +5869,15 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5664
5869
|
}
|
|
5665
5870
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5666
5871
|
if (scene.measure?.length) builds = true;
|
|
5667
|
-
for (const
|
|
5872
|
+
for (const w of cut2.scene.warnings ?? []) opts.onBeatWarning?.(beat.id, w);
|
|
5873
|
+
for (const p of scene.plugins ?? []) {
|
|
5874
|
+
if (!Object.hasOwn(PLUGINS, p)) {
|
|
5875
|
+
throw new Error(
|
|
5876
|
+
`${beat.archetype} ${beat.id}: no vendored plugin named "${p}" \u2014 Scene.plugins takes ${Object.keys(PLUGINS).join(" or ")}`
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
plugins.add(p);
|
|
5880
|
+
}
|
|
5668
5881
|
scenes.push(
|
|
5669
5882
|
sceneHtml(
|
|
5670
5883
|
sid,
|
|
@@ -5781,9 +5994,11 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5781
5994
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5782
5995
|
const island = format.navigable ? `
|
|
5783
5996
|
${emitIsland(slides)}` : "";
|
|
5784
|
-
const
|
|
5785
|
-
|
|
5786
|
-
<script
|
|
5997
|
+
const plugins = Object.entries(PLUGINS).filter(([name]) => laid.plugins.has(name)).map(
|
|
5998
|
+
([, p]) => `
|
|
5999
|
+
<script src="${p.src}"></script>
|
|
6000
|
+
<script>gsap.registerPlugin(${p.global});</script>`
|
|
6001
|
+
).join("");
|
|
5787
6002
|
return `<!doctype html>
|
|
5788
6003
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5789
6004
|
<head>
|
|
@@ -5792,7 +6007,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5792
6007
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5793
6008
|
<script src="${GSAP_SRC}"></script>
|
|
5794
6009
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5795
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${
|
|
6010
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${plugins}
|
|
5796
6011
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5797
6012
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5798
6013
|
<style>
|
|
@@ -6187,8 +6402,8 @@ async function send(url2, target, headers, signal, opts) {
|
|
|
6187
6402
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6188
6403
|
};
|
|
6189
6404
|
try {
|
|
6190
|
-
return await new Promise((
|
|
6191
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
6405
|
+
return await new Promise((resolve7, reject) => {
|
|
6406
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve7);
|
|
6192
6407
|
req.on("error", reject);
|
|
6193
6408
|
req.end();
|
|
6194
6409
|
});
|
|
@@ -6274,7 +6489,7 @@ function message(err) {
|
|
|
6274
6489
|
async function fetchFigures(source, dir, warnings) {
|
|
6275
6490
|
const drops = warnings ?? [];
|
|
6276
6491
|
await mkdir2(dir, { recursive: true });
|
|
6277
|
-
const
|
|
6492
|
+
const cached2 = await readdir(dir).catch(() => []);
|
|
6278
6493
|
const figures = [];
|
|
6279
6494
|
for (const figure of source.figures) {
|
|
6280
6495
|
if (figure.kind === "clip") {
|
|
@@ -6282,7 +6497,7 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6282
6497
|
continue;
|
|
6283
6498
|
}
|
|
6284
6499
|
try {
|
|
6285
|
-
figures.push(figureSchema.parse(await localize(figure, dir,
|
|
6500
|
+
figures.push(figureSchema.parse(await localize(figure, dir, cached2)));
|
|
6286
6501
|
} catch (err) {
|
|
6287
6502
|
drops.push(`figure ${figure.id} was left out: ${reason(err)}`);
|
|
6288
6503
|
}
|
|
@@ -6296,15 +6511,15 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6296
6511
|
}
|
|
6297
6512
|
return parsed.data;
|
|
6298
6513
|
}
|
|
6299
|
-
async function localize(figure, dir,
|
|
6514
|
+
async function localize(figure, dir, cached2) {
|
|
6300
6515
|
const stem = assetStem(figure.id, figure.src);
|
|
6301
|
-
const hit =
|
|
6516
|
+
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
6302
6517
|
const bytes = hit ? await readFile3(join2(dir, hit)) : await load(figure.src);
|
|
6303
6518
|
const size3 = imageSize(bytes);
|
|
6304
6519
|
if (hit) return { ...figure, src: hit, ...size3 };
|
|
6305
6520
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6306
6521
|
await writeFile2(join2(dir, src), bytes);
|
|
6307
|
-
|
|
6522
|
+
cached2.push(src);
|
|
6308
6523
|
return { ...figure, src, ...size3 };
|
|
6309
6524
|
}
|
|
6310
6525
|
function assetStem(id2, src) {
|
|
@@ -6853,7 +7068,10 @@ var REVEALS = {
|
|
|
6853
7068
|
"equation-walk": "one per term",
|
|
6854
7069
|
"equation-morph": "2",
|
|
6855
7070
|
"data-table": "one per highlighted row, plus 1",
|
|
6856
|
-
"
|
|
7071
|
+
// "long enough" is not a hedge: below the emitter's own floor the comparison
|
|
7072
|
+
// is dropped and the chart stops twice becoming a chart that stops once. See
|
|
7073
|
+
// the line-chart entry under THE THIRTEEN ARCHETYPES for what that costs.
|
|
7074
|
+
"line-chart": "1, or 2 when compare is given and the beat is long enough for it",
|
|
6857
7075
|
callout: "one per panel",
|
|
6858
7076
|
pipeline: "one per stage",
|
|
6859
7077
|
"annotated-figure": "one per note, plus 1",
|
|
@@ -6976,6 +7194,29 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
6976
7194
|
scale, over training. Points come from the source's numbers;
|
|
6977
7195
|
deltas, if given, are the steps between consecutive points, so
|
|
6978
7196
|
there is always one fewer.
|
|
7197
|
+
\`compare\` adds a SECOND CONDITION over the same axis \u2014 the tell
|
|
7198
|
+
there is THE SAME QUANTITY MEASURED TWO WAYS, a baseline and the
|
|
7199
|
+
result, where what the source is claiming is the change in the
|
|
7200
|
+
SHAPE of the curve and not two numbers. Its \`label\` names the
|
|
7201
|
+
baseline ("without pretraining", "FP32") and is drawn on the
|
|
7202
|
+
slide where the chart has room for it. Keep it SHORT \u2014 two or
|
|
7203
|
+
three words. A label wider than the plot is refused and the
|
|
7204
|
+
whole slide goes with it, and one that cannot be placed clear of
|
|
7205
|
+
the axis names, the values and both curves is dropped without a
|
|
7206
|
+
word, leaving the baseline unnamed. A \`readout\` takes the plot
|
|
7207
|
+
down to about two thirds of its width, so a beat that has one
|
|
7208
|
+
has room for a shorter label still.
|
|
7209
|
+
\`compare.points\` must carry the SAME x values, in the
|
|
7210
|
+
same order, as \`points\`; the baseline is drawn first, then the
|
|
7211
|
+
curve reshapes into \`points\` and leaves the baseline behind. Do
|
|
7212
|
+
not use it for two unrelated series that happen to share an
|
|
7213
|
+
axis \u2014 nothing reshapes into something it is not a version of.
|
|
7214
|
+
A compare beat also costs TIME \u2014 the baseline draws, is held,
|
|
7215
|
+
then reshapes \u2014 so give it \`seconds\` of 7, or 8 with a
|
|
7216
|
+
\`readout\`. Give it too few and the emitter draws the chart
|
|
7217
|
+
without the comparison rather than stopping on a half-drawn one:
|
|
7218
|
+
the slide is still there, but the point about the baseline is
|
|
7219
|
+
gone.
|
|
6979
7220
|
|
|
6980
7221
|
DESCRIBING ARCHETYPES \u2014 the fallbacks
|
|
6981
7222
|
|
|
@@ -7760,7 +8001,7 @@ function codexCommand(args) {
|
|
|
7760
8001
|
}
|
|
7761
8002
|
function runCodex(args) {
|
|
7762
8003
|
const { argv, env } = codexCommand(args);
|
|
7763
|
-
return new Promise((
|
|
8004
|
+
return new Promise((resolve7, reject) => {
|
|
7764
8005
|
const child = spawn("codex", argv, {
|
|
7765
8006
|
stdio: ["pipe", "ignore", "pipe"],
|
|
7766
8007
|
...env === void 0 ? {} : { env }
|
|
@@ -7783,7 +8024,7 @@ function runCodex(args) {
|
|
|
7783
8024
|
});
|
|
7784
8025
|
child.on("close", (code) => {
|
|
7785
8026
|
clearTimeout(timer);
|
|
7786
|
-
if (code === 0) return
|
|
8027
|
+
if (code === 0) return resolve7();
|
|
7787
8028
|
reject(
|
|
7788
8029
|
new Error(`codex exec exited ${code}.
|
|
7789
8030
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -8244,7 +8485,7 @@ async function find2(can) {
|
|
|
8244
8485
|
throw new Error(MISSING);
|
|
8245
8486
|
}
|
|
8246
8487
|
function runArgv(cmd, args) {
|
|
8247
|
-
return new Promise((
|
|
8488
|
+
return new Promise((resolve7) => {
|
|
8248
8489
|
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8249
8490
|
let stdout = "";
|
|
8250
8491
|
let stderr = "";
|
|
@@ -8254,8 +8495,8 @@ function runArgv(cmd, args) {
|
|
|
8254
8495
|
child.stderr.on("data", (b) => {
|
|
8255
8496
|
stderr += b.toString();
|
|
8256
8497
|
});
|
|
8257
|
-
child.on("error", (e) =>
|
|
8258
|
-
child.on("close", (code) =>
|
|
8498
|
+
child.on("error", (e) => resolve7({ code: -1, stderr: String(e), stdout }));
|
|
8499
|
+
child.on("close", (code) => resolve7({ code: code ?? -1, stderr, stdout }));
|
|
8259
8500
|
});
|
|
8260
8501
|
}
|
|
8261
8502
|
var edgeTts = {
|
|
@@ -8342,8 +8583,8 @@ async function synthesize(text2, opts) {
|
|
|
8342
8583
|
const file = `${key}.mp3`;
|
|
8343
8584
|
const audio = join6(opts.dir, file);
|
|
8344
8585
|
const sidecar = join6(opts.dir, `${key}.json`);
|
|
8345
|
-
const
|
|
8346
|
-
if (
|
|
8586
|
+
const cached2 = await readSidecar(sidecar);
|
|
8587
|
+
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
8347
8588
|
await mkdir4(opts.dir, { recursive: true });
|
|
8348
8589
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
8349
8590
|
const { cues, seconds } = spoken;
|
|
@@ -9251,7 +9492,7 @@ ${tail3}`);
|
|
|
9251
9492
|
}
|
|
9252
9493
|
}
|
|
9253
9494
|
function runLive(file, args) {
|
|
9254
|
-
return new Promise((
|
|
9495
|
+
return new Promise((resolve7, reject) => {
|
|
9255
9496
|
const child = spawn3(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
9256
9497
|
child.on("error", (err) => {
|
|
9257
9498
|
reject(
|
|
@@ -9259,7 +9500,7 @@ function runLive(file, args) {
|
|
|
9259
9500
|
);
|
|
9260
9501
|
});
|
|
9261
9502
|
child.on("close", (code, signal) => {
|
|
9262
|
-
if (code === 0)
|
|
9503
|
+
if (code === 0) resolve7();
|
|
9263
9504
|
else if (signal) {
|
|
9264
9505
|
reject(
|
|
9265
9506
|
new Error(
|
|
@@ -11052,9 +11293,57 @@ function textOf(node) {
|
|
|
11052
11293
|
}
|
|
11053
11294
|
}
|
|
11054
11295
|
|
|
11296
|
+
// src/tmpdir.ts
|
|
11297
|
+
import { realpathSync } from "node:fs";
|
|
11298
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
11299
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
11300
|
+
import { basename as basename4, dirname as dirname4, join as join12, resolve as resolve5, sep } from "node:path";
|
|
11301
|
+
var cached;
|
|
11302
|
+
function real(dir) {
|
|
11303
|
+
let at = resolve5(dir);
|
|
11304
|
+
const tail3 = [];
|
|
11305
|
+
for (; ; ) {
|
|
11306
|
+
try {
|
|
11307
|
+
return join12(realpathSync(at), ...tail3);
|
|
11308
|
+
} catch {
|
|
11309
|
+
const up = dirname4(at);
|
|
11310
|
+
if (up === at) return resolve5(dir);
|
|
11311
|
+
tail3.unshift(basename4(at));
|
|
11312
|
+
at = up;
|
|
11313
|
+
}
|
|
11314
|
+
}
|
|
11315
|
+
}
|
|
11316
|
+
function packageRoot() {
|
|
11317
|
+
cached ??= real(dirname4(createRequire3(import.meta.url).resolve("../package.json")));
|
|
11318
|
+
return cached;
|
|
11319
|
+
}
|
|
11320
|
+
function insideRoot(dir) {
|
|
11321
|
+
const root = packageRoot();
|
|
11322
|
+
const at = real(dir);
|
|
11323
|
+
return at === root || at.startsWith(root + sep);
|
|
11324
|
+
}
|
|
11325
|
+
function guardTmpdir() {
|
|
11326
|
+
const before = tmpdir3();
|
|
11327
|
+
if (!insideRoot(before)) return;
|
|
11328
|
+
delete process.env.TMPDIR;
|
|
11329
|
+
const after = tmpdir3();
|
|
11330
|
+
if (insideRoot(after)) {
|
|
11331
|
+
throw new Error(
|
|
11332
|
+
`tmpdir: ${after} is still inside ${packageRoot()}. Set TMPDIR to a real temp directory.`
|
|
11333
|
+
);
|
|
11334
|
+
}
|
|
11335
|
+
const worker = process.env.VITEST_WORKER_ID;
|
|
11336
|
+
if (worker === void 0 || worker === "1") {
|
|
11337
|
+
process.stderr.write(
|
|
11338
|
+
`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.
|
|
11339
|
+
`
|
|
11340
|
+
);
|
|
11341
|
+
}
|
|
11342
|
+
}
|
|
11343
|
+
|
|
11055
11344
|
// src/verify/index.ts
|
|
11056
11345
|
import { readdir as readdir3, readFile as readFile16 } from "node:fs/promises";
|
|
11057
|
-
import { join as
|
|
11346
|
+
import { join as join16, relative, sep as sep2 } from "node:path";
|
|
11058
11347
|
|
|
11059
11348
|
// src/verify/budget.ts
|
|
11060
11349
|
function readCanvas(html) {
|
|
@@ -11162,7 +11451,7 @@ function clock2(seconds) {
|
|
|
11162
11451
|
// src/verify/check.ts
|
|
11163
11452
|
import { execFile as execFile2 } from "node:child_process";
|
|
11164
11453
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
11165
|
-
import { join as
|
|
11454
|
+
import { join as join13 } from "node:path";
|
|
11166
11455
|
import { promisify as promisify2 } from "node:util";
|
|
11167
11456
|
var run2 = promisify2(execFile2);
|
|
11168
11457
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -11192,7 +11481,7 @@ async function check2(dir, opts = {}) {
|
|
|
11192
11481
|
async function readComposition(dir) {
|
|
11193
11482
|
let html;
|
|
11194
11483
|
try {
|
|
11195
|
-
html = await readFile13(
|
|
11484
|
+
html = await readFile13(join13(dir, "index.html"), "utf8");
|
|
11196
11485
|
} catch {
|
|
11197
11486
|
return { transit: [], duration: 0 };
|
|
11198
11487
|
}
|
|
@@ -11341,7 +11630,7 @@ function tail(s) {
|
|
|
11341
11630
|
|
|
11342
11631
|
// src/verify/fidelity.ts
|
|
11343
11632
|
import { readFile as readFile14 } from "node:fs/promises";
|
|
11344
|
-
import { join as
|
|
11633
|
+
import { join as join14 } from "node:path";
|
|
11345
11634
|
|
|
11346
11635
|
// src/verify/typefloor.ts
|
|
11347
11636
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11788,8 +12077,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
11788
12077
|
elapsedMs: Date.now() - started
|
|
11789
12078
|
});
|
|
11790
12079
|
const stops = opts.stops ?? readStops(
|
|
11791
|
-
await readFile14(
|
|
11792
|
-
await readFile14(
|
|
12080
|
+
await readFile14(join14(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12081
|
+
await readFile14(join14(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11793
12082
|
);
|
|
11794
12083
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
11795
12084
|
let deck = null;
|
|
@@ -11848,8 +12137,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
11848
12137
|
import { execFile as execFile3 } from "node:child_process";
|
|
11849
12138
|
import { createHash as createHash9 } from "node:crypto";
|
|
11850
12139
|
import { mkdtemp as mkdtemp3, readdir as readdir2, readFile as readFile15, rm as rm8 } from "node:fs/promises";
|
|
11851
|
-
import { tmpdir as
|
|
11852
|
-
import { join as
|
|
12140
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
12141
|
+
import { join as join15 } from "node:path";
|
|
11853
12142
|
import { promisify as promisify3 } from "node:util";
|
|
11854
12143
|
var run3 = promisify3(execFile3);
|
|
11855
12144
|
var FLOOR_DB = 40;
|
|
@@ -11888,9 +12177,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
11888
12177
|
async function drift(dir, opts = {}) {
|
|
11889
12178
|
const mode = opts.mode ?? "psnr";
|
|
11890
12179
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
11891
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
11892
|
-
const a =
|
|
11893
|
-
const b =
|
|
12180
|
+
const work = opts.workDir ?? await mkdtemp3(join15(tmpdir4(), "decksmith-drift-"));
|
|
12181
|
+
const a = join15(work, "a");
|
|
12182
|
+
const b = join15(work, "b");
|
|
11894
12183
|
const report2 = await compare(dir, a, b, mode, floorDb, opts);
|
|
11895
12184
|
const keep = opts.keep || !report2.passed;
|
|
11896
12185
|
if (keep) report2.kept = { a, b };
|
|
@@ -11945,11 +12234,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
11945
12234
|
const differing = [];
|
|
11946
12235
|
const ha = [];
|
|
11947
12236
|
for (const [i, name] of fa.entries()) {
|
|
11948
|
-
const one = await sha(
|
|
12237
|
+
const one = await sha(join15(a, name));
|
|
11949
12238
|
ha.push(one);
|
|
11950
|
-
if (one !== await sha(
|
|
12239
|
+
if (one !== await sha(join15(b, name))) differing.push(i + 1);
|
|
11951
12240
|
}
|
|
11952
|
-
const html = await readFile15(
|
|
12241
|
+
const html = await readFile15(join15(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
11953
12242
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
11954
12243
|
let worst;
|
|
11955
12244
|
if (differing.length > 0) {
|
|
@@ -12097,11 +12386,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
12097
12386
|
"-start_number",
|
|
12098
12387
|
String(seq.start),
|
|
12099
12388
|
"-i",
|
|
12100
|
-
|
|
12389
|
+
join15(a, seq.pattern),
|
|
12101
12390
|
"-start_number",
|
|
12102
12391
|
String(seq.start),
|
|
12103
12392
|
"-i",
|
|
12104
|
-
|
|
12393
|
+
join15(b, seq.pattern),
|
|
12105
12394
|
"-lavfi",
|
|
12106
12395
|
"psnr=stats_file=-",
|
|
12107
12396
|
"-f",
|
|
@@ -12176,7 +12465,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12176
12465
|
const html = await readCompositions(dir);
|
|
12177
12466
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
12178
12467
|
const narration = scanNarration(
|
|
12179
|
-
await readFile16(
|
|
12468
|
+
await readFile16(join16(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12180
12469
|
await listFiles(dir)
|
|
12181
12470
|
);
|
|
12182
12471
|
const budget3 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -12213,8 +12502,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12213
12502
|
}
|
|
12214
12503
|
async function declaredStops(dir) {
|
|
12215
12504
|
return readStops(
|
|
12216
|
-
await readFile16(
|
|
12217
|
-
await readFile16(
|
|
12505
|
+
await readFile16(join16(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12506
|
+
await readFile16(join16(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
12218
12507
|
);
|
|
12219
12508
|
}
|
|
12220
12509
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -12254,7 +12543,7 @@ function scanNarration(page, files) {
|
|
|
12254
12543
|
];
|
|
12255
12544
|
}
|
|
12256
12545
|
async function readTiming2(dir) {
|
|
12257
|
-
const raw2 = await readFile16(
|
|
12546
|
+
const raw2 = await readFile16(join16(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
12258
12547
|
if (!raw2) return void 0;
|
|
12259
12548
|
try {
|
|
12260
12549
|
const parsed = JSON.parse(raw2);
|
|
@@ -12266,7 +12555,7 @@ async function readTiming2(dir) {
|
|
|
12266
12555
|
async function listFiles(dir) {
|
|
12267
12556
|
const entries = await readdir3(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12268
12557
|
return new Set(
|
|
12269
|
-
entries.filter((e) => e.isFile()).map((e) => relative(dir,
|
|
12558
|
+
entries.filter((e) => e.isFile()).map((e) => relative(dir, join16(e.parentPath, e.name)).split(sep2).join("/"))
|
|
12270
12559
|
);
|
|
12271
12560
|
}
|
|
12272
12561
|
var INSTEAD = {
|
|
@@ -12525,13 +12814,14 @@ async function readCompositions(dir) {
|
|
|
12525
12814
|
const entries = await readdir3(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12526
12815
|
const files = entries.filter(
|
|
12527
12816
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12528
|
-
).map((e) =>
|
|
12817
|
+
).map((e) => join16(e.parentPath, e.name));
|
|
12529
12818
|
return Promise.all(
|
|
12530
12819
|
files.map(async (f) => [relative(dir, f), await readFile16(f, "utf8")])
|
|
12531
12820
|
);
|
|
12532
12821
|
}
|
|
12533
12822
|
|
|
12534
12823
|
// src/cli.ts
|
|
12824
|
+
guardTmpdir();
|
|
12535
12825
|
var AUDIO_DIR = "audio";
|
|
12536
12826
|
var NARRATION_FILE = "narration.json";
|
|
12537
12827
|
var DEFAULTS = prefsSchema.parse({});
|
|
@@ -12544,43 +12834,48 @@ async function deckRuntime() {
|
|
|
12544
12834
|
}
|
|
12545
12835
|
}
|
|
12546
12836
|
function playerBundle() {
|
|
12547
|
-
const require2 =
|
|
12837
|
+
const require2 = createRequire4(import.meta.url);
|
|
12548
12838
|
try {
|
|
12549
|
-
return
|
|
12839
|
+
return join17(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12550
12840
|
} catch {
|
|
12551
12841
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12552
12842
|
}
|
|
12553
12843
|
}
|
|
12554
12844
|
async function vendorKatex(out) {
|
|
12555
|
-
const require2 =
|
|
12556
|
-
const dist =
|
|
12557
|
-
const css = await readFile17(
|
|
12558
|
-
await mkdir10(
|
|
12559
|
-
for (const file of await readdir4(
|
|
12845
|
+
const require2 = createRequire4(import.meta.url);
|
|
12846
|
+
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12847
|
+
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12848
|
+
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12849
|
+
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12560
12850
|
if (file.endsWith(".woff2"))
|
|
12561
|
-
await cp(
|
|
12851
|
+
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12562
12852
|
}
|
|
12563
12853
|
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12564
12854
|
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12565
12855
|
return kept ? `src:${kept}` : whole;
|
|
12566
12856
|
});
|
|
12567
|
-
await writeFile12(
|
|
12857
|
+
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12568
12858
|
}
|
|
12569
|
-
async function vendorScripts(out) {
|
|
12570
|
-
const require2 =
|
|
12571
|
-
await mkdir10(
|
|
12859
|
+
async function vendorScripts(out, composition) {
|
|
12860
|
+
const require2 = createRequire4(import.meta.url);
|
|
12861
|
+
await mkdir10(join17(out, "vendor"), { recursive: true });
|
|
12862
|
+
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
12572
12863
|
for (const [pkg, rel, name] of [
|
|
12573
12864
|
["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
|
|
12574
12865
|
["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
|
|
12866
|
+
["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
|
|
12575
12867
|
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
12576
12868
|
]) {
|
|
12577
|
-
|
|
12578
|
-
|
|
12869
|
+
if (!wanted(name)) continue;
|
|
12870
|
+
const from = join17(dirname5(require2.resolve(pkg)), rel);
|
|
12871
|
+
await cp(from, join17(out, "vendor", name));
|
|
12872
|
+
}
|
|
12873
|
+
if (wanted("ds-morph.js")) {
|
|
12874
|
+
await cp(
|
|
12875
|
+
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
12876
|
+
join17(out, "vendor", "ds-morph.js")
|
|
12877
|
+
);
|
|
12579
12878
|
}
|
|
12580
|
-
await cp(
|
|
12581
|
-
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
12582
|
-
join16(out, "vendor", "ds-morph.js")
|
|
12583
|
-
);
|
|
12584
12879
|
}
|
|
12585
12880
|
var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
12586
12881
|
{
|
|
@@ -12683,13 +12978,13 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
12683
12978
|
"--max-clip-seconds <n>",
|
|
12684
12979
|
"URL only: seconds of each clip kept, the rest trimmed (maxClipSeconds; default 60)"
|
|
12685
12980
|
).option("--no-transcode", "URL only: ship each clip as the page served it, unshrunk").action(async (input, o) => {
|
|
12686
|
-
const assets =
|
|
12687
|
-
const scratch = url(input) ? await mkdtemp4(
|
|
12981
|
+
const assets = join17(dirname5(resolve6(o.out)), "assets");
|
|
12982
|
+
const scratch = url(input) ? await mkdtemp4(join17(tmpdir5(), "decksmith-harvest-")) : void 0;
|
|
12688
12983
|
try {
|
|
12689
12984
|
let md;
|
|
12690
12985
|
let clips = [];
|
|
12691
12986
|
if (scratch === void 0) {
|
|
12692
|
-
md = await readFile17(
|
|
12987
|
+
md = await readFile17(resolve6(input), "utf8").catch(() => {
|
|
12693
12988
|
throw new Error(`Cannot read ${input}.`);
|
|
12694
12989
|
});
|
|
12695
12990
|
} else {
|
|
@@ -12710,7 +13005,7 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
12710
13005
|
const dropped = [];
|
|
12711
13006
|
const source = await fetchFigures(withClips, assets, dropped);
|
|
12712
13007
|
for (const why3 of dropped) step(`ingest: ${why3}`);
|
|
12713
|
-
const bundle = await bundleFont(source.lang, glyphs(source),
|
|
13008
|
+
const bundle = await bundleFont(source.lang, glyphs(source), join17(assets, "fonts"));
|
|
12714
13009
|
if (bundle) step(`ingest: bundled ${bundle.family} for ${source.lang}`);
|
|
12715
13010
|
await writeJson(o.out, source);
|
|
12716
13011
|
step(`ingest: wrote ${o.out}`);
|
|
@@ -12771,7 +13066,7 @@ imageFlags(
|
|
|
12771
13066
|
program.command("illustrate").description("Draw the pictures the plan asked for, and point the storyboard at them.").argument("<storyboard>", "storyboard.json carrying illustration briefs").requiredOption("--source <file>", "source.json the storyboard was planned from")
|
|
12772
13067
|
).action(async (sbPath, o) => {
|
|
12773
13068
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
12774
|
-
const sourcePath =
|
|
13069
|
+
const sourcePath = resolve6(String(o.source));
|
|
12775
13070
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
12776
13071
|
assertRefsResolve(storyboard, source, { pending: "allow" });
|
|
12777
13072
|
const prefs = await loadPrefs(prefsFromFlags({ ...flags(o), images: true }));
|
|
@@ -12785,7 +13080,7 @@ imageFlags(
|
|
|
12785
13080
|
);
|
|
12786
13081
|
const drawn = await illustrate(storyboard, source, {
|
|
12787
13082
|
prefs,
|
|
12788
|
-
assetsDir:
|
|
13083
|
+
assetsDir: join17(dirname5(sourcePath), "assets"),
|
|
12789
13084
|
onStep: step
|
|
12790
13085
|
});
|
|
12791
13086
|
for (const p of drawn.illustrated) {
|
|
@@ -12795,9 +13090,9 @@ imageFlags(
|
|
|
12795
13090
|
}
|
|
12796
13091
|
await writeJson(sourcePath, drawn.source);
|
|
12797
13092
|
await writeJson(sbPath, drawn.storyboard);
|
|
12798
|
-
const
|
|
13093
|
+
const cached2 = drawn.illustrated.filter((p) => p.cached).length;
|
|
12799
13094
|
step(
|
|
12800
|
-
`illustrate: ${drawn.illustrated.length} picture(s)${
|
|
13095
|
+
`illustrate: ${drawn.illustrated.length} picture(s)${cached2 ? `, ${cached2} cached` : ""} \u2192 ${o.source}, ${sbPath}`
|
|
12801
13096
|
);
|
|
12802
13097
|
});
|
|
12803
13098
|
voiceFlags(
|
|
@@ -12810,18 +13105,18 @@ voiceFlags(
|
|
|
12810
13105
|
const format = pickFormat(String(o.format), o.width, o.height);
|
|
12811
13106
|
const chosen = await loadPrefs(prefsFromFlags(flags(o)));
|
|
12812
13107
|
const prefs = stated(chosen, "lang") ? chosen : { ...chosen, lang: storyboard.lang };
|
|
12813
|
-
const dir =
|
|
13108
|
+
const dir = resolve6(String(o.out));
|
|
12814
13109
|
const speaking = storyboard.beats.filter((b) => b.narration?.trim()).length;
|
|
12815
13110
|
if (speaking === 0) {
|
|
12816
13111
|
throw new Error(`No beat in ${sbPath} has a "narration" field, so there is nothing to speak.`);
|
|
12817
13112
|
}
|
|
12818
13113
|
step(`narrate: ${speaking} of ${storyboard.beats.length} beats have narration`);
|
|
12819
13114
|
const narration = await narrate(storyboard, source, prefs, { dir, format });
|
|
12820
|
-
await writeJson(
|
|
13115
|
+
await writeJson(join17(dir, NARRATION_FILE), narration);
|
|
12821
13116
|
const segments2 = Object.values(narration.beats).flat();
|
|
12822
13117
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
12823
13118
|
step(
|
|
12824
|
-
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${
|
|
13119
|
+
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${join17(dir, NARRATION_FILE)}`
|
|
12825
13120
|
);
|
|
12826
13121
|
});
|
|
12827
13122
|
lookFlags(
|
|
@@ -12836,7 +13131,7 @@ lookFlags(
|
|
|
12836
13131
|
const format = withMinWeight(pickFormat(o.format, o.width, o.height), o.minWeight);
|
|
12837
13132
|
const prefs = await loadPrefs(prefsFromFlags(flags(o)), process.cwd(), source);
|
|
12838
13133
|
const theme = stated(prefs, "theme") ?? storyboard.theme;
|
|
12839
|
-
const out =
|
|
13134
|
+
const out = resolve6(o.out);
|
|
12840
13135
|
await mkdir10(out, { recursive: true });
|
|
12841
13136
|
const found = await findNarration(sbPath, o.narration);
|
|
12842
13137
|
const narration = found ? await loadNarration(found) : void 0;
|
|
@@ -12861,10 +13156,14 @@ lookFlags(
|
|
|
12861
13156
|
// takes eleven good ones with it at the last stage. Printed, not swallowed:
|
|
12862
13157
|
// a missing slide the terminal never mentioned is the failure this project
|
|
12863
13158
|
// keeps finding by eye.
|
|
12864
|
-
onBeatError: (id2, err) => step(`build: left out ${id2} \u2014 ${err.message}`)
|
|
13159
|
+
onBeatError: (id2, err) => step(`build: left out ${id2} \u2014 ${err.message}`),
|
|
13160
|
+
// And the slides that ARE here but not as they were planned. A beat drawn
|
|
13161
|
+
// with one of its parts dropped looks finished, so this line is the only
|
|
13162
|
+
// place anyone learns it is not.
|
|
13163
|
+
onBeatWarning: (id2, warning) => step(`build: kept ${id2} \u2014 ${warning}`)
|
|
12865
13164
|
});
|
|
12866
|
-
await writeFile12(
|
|
12867
|
-
await writeFile12(
|
|
13165
|
+
await writeFile12(join17(out, "index.html"), deck.composition);
|
|
13166
|
+
await writeFile12(join17(out, "hyperframes.json"), HYPERFRAMES_JSON);
|
|
12868
13167
|
await writeTiming(out, {
|
|
12869
13168
|
storyboard,
|
|
12870
13169
|
source,
|
|
@@ -12880,36 +13179,36 @@ lookFlags(
|
|
|
12880
13179
|
...narration ? { narration } : {}
|
|
12881
13180
|
});
|
|
12882
13181
|
if (deck.page) {
|
|
12883
|
-
await writeFile12(
|
|
12884
|
-
await cp(playerBundle(),
|
|
13182
|
+
await writeFile12(join17(out, DECK_PAGE), deck.page);
|
|
13183
|
+
await cp(playerBundle(), join17(out, PLAYER_FILE));
|
|
12885
13184
|
}
|
|
12886
13185
|
await vendorKatex(out);
|
|
12887
|
-
await vendorScripts(out);
|
|
12888
|
-
await copyAssets(
|
|
12889
|
-
if (found && narration) await copyAudio(
|
|
13186
|
+
await vendorScripts(out, deck.composition);
|
|
13187
|
+
await copyAssets(dirname5(resolve6(o.source)), out, source.figures);
|
|
13188
|
+
if (found && narration) await copyAudio(dirname5(found), narration, out);
|
|
12890
13189
|
const look = [theme, paced.speed === 1 ? "" : `${paced.speed}\xD7 speed`].filter(Boolean).join(", ");
|
|
12891
13190
|
const { cut } = deck;
|
|
12892
13191
|
const floor = cut.dropped.filter((d) => d.rule === "below_min_weight").length;
|
|
12893
13192
|
const of = cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
12894
13193
|
step(
|
|
12895
|
-
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${
|
|
13194
|
+
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${join17(out, "index.html")}`
|
|
12896
13195
|
);
|
|
12897
13196
|
for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
|
|
12898
13197
|
step(`build: ${f.message}`);
|
|
12899
13198
|
reportCut(cut);
|
|
12900
|
-
if (deck.page) step(`build: navigable deck \u2192 ${
|
|
13199
|
+
if (deck.page) step(`build: navigable deck \u2192 ${join17(out, DECK_PAGE)}`);
|
|
12901
13200
|
await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
|
|
12902
13201
|
}
|
|
12903
13202
|
);
|
|
12904
13203
|
program.command("verify").description("Re-run the gates on a built deck.").argument("<dir>", "a built deck directory").option("--snapshots", "also write the contrast-pass PNGs to <dir>/snapshots", false).option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser").action(async (dir, o) => {
|
|
12905
|
-
await gate(
|
|
13204
|
+
await gate(resolve6(dir), o.snapshots, void 0, void 0, o.fidelity !== false);
|
|
12906
13205
|
});
|
|
12907
13206
|
program.command("frames").description("Write PNGs of a built deck through the capture path the renderer uses.").argument("<dir>", "a built deck directory").option(
|
|
12908
13207
|
"--at <seconds...>",
|
|
12909
13208
|
"absolute times to photograph; defaults to every hold the deck declares"
|
|
12910
13209
|
).option("--out <dir>", "where to write the PNGs (default: <dir>/frames)").action(async (dir, o) => {
|
|
12911
|
-
const deck =
|
|
12912
|
-
const out =
|
|
13210
|
+
const deck = resolve6(dir);
|
|
13211
|
+
const out = resolve6(o.out ?? join17(deck, "frames"));
|
|
12913
13212
|
let times;
|
|
12914
13213
|
if (o.at?.length) {
|
|
12915
13214
|
times = o.at.map((raw2) => {
|
|
@@ -12919,8 +13218,8 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
12919
13218
|
});
|
|
12920
13219
|
} else {
|
|
12921
13220
|
times = readStops(
|
|
12922
|
-
await readFile17(
|
|
12923
|
-
await readFile17(
|
|
13221
|
+
await readFile17(join17(deck, TIMING_FILE), "utf8").catch(() => null),
|
|
13222
|
+
await readFile17(join17(deck, DECK_PAGE), "utf8").catch(() => null)
|
|
12924
13223
|
).map((s) => s.t);
|
|
12925
13224
|
if (times.length === 0)
|
|
12926
13225
|
throw new Error(
|
|
@@ -12934,7 +13233,7 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
12934
13233
|
program.command("drift").description("Render a built deck twice and compare, frame by frame. Minutes, not seconds.").argument("<dir>", "a built deck directory").option("--identical", "require every frame byte-identical \u2014 image-free decks only", false).option("--floor <dB>", "per-frame PSNR floor", String(FLOOR_DB)).option("--keep", "keep both frame directories even when they agree").option("--workers <n>", "pin both renders to one worker count (default: 1 vs 3)").action(
|
|
12935
13234
|
async (dir, o) => {
|
|
12936
13235
|
step("drift: rendering twice, several minutes a render");
|
|
12937
|
-
const verdict = await drift(
|
|
13236
|
+
const verdict = await drift(resolve6(dir), {
|
|
12938
13237
|
mode: o.identical ? "identical" : "psnr",
|
|
12939
13238
|
floorDb: Number(o.floor),
|
|
12940
13239
|
...o.keep ? { keep: true } : {},
|
|
@@ -12961,8 +13260,8 @@ program.command("render").description("Render a built deck to a finished video:
|
|
|
12961
13260
|
throw new Error(`Unknown --subtitles "${o.subtitles}". Use sidecar, burn or none.`);
|
|
12962
13261
|
}
|
|
12963
13262
|
const result = await render({
|
|
12964
|
-
deck:
|
|
12965
|
-
out:
|
|
13263
|
+
deck: resolve6(dir),
|
|
13264
|
+
out: resolve6(o.out),
|
|
12966
13265
|
subtitles: o.subtitles,
|
|
12967
13266
|
protocolTimeoutMs: Number(o.protocolTimeout),
|
|
12968
13267
|
log: step,
|
|
@@ -12990,7 +13289,7 @@ voiceFlags(
|
|
|
12990
13289
|
)
|
|
12991
13290
|
).action(async (sbPath, o) => {
|
|
12992
13291
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
12993
|
-
const sourcePath =
|
|
13292
|
+
const sourcePath = resolve6(String(o.source));
|
|
12994
13293
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
12995
13294
|
assertRefsResolve(storyboard, source);
|
|
12996
13295
|
if (o.bake && o.link) throw new Error("Choose --bake or --link, not both.");
|
|
@@ -13005,7 +13304,7 @@ voiceFlags(
|
|
|
13005
13304
|
const prefer = o.link ? "link" : "bake";
|
|
13006
13305
|
const plan = await planMedia(figureAssets(source, sourcePath, prefer));
|
|
13007
13306
|
const files = { ...plan.files };
|
|
13008
|
-
if (found && narration) Object.assign(files, await audioFiles(
|
|
13307
|
+
if (found && narration) Object.assign(files, await audioFiles(dirname5(found), narration));
|
|
13009
13308
|
const pack2 = {
|
|
13010
13309
|
version: PACK_VERSION,
|
|
13011
13310
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13020,7 +13319,7 @@ voiceFlags(
|
|
|
13020
13319
|
...narration ? { narration } : {},
|
|
13021
13320
|
media: plan.media
|
|
13022
13321
|
};
|
|
13023
|
-
const out =
|
|
13322
|
+
const out = resolve6(String(o.out));
|
|
13024
13323
|
const bytes = await writePack(pack2, files, out);
|
|
13025
13324
|
if (plan.demoted.length) {
|
|
13026
13325
|
step(`pack: kept as links, not bakeable \u2014 ${plan.demoted.join(", ")}`);
|
|
@@ -13034,19 +13333,19 @@ voiceFlags(
|
|
|
13034
13333
|
step(`pack: ${size2(bytes)} \u2192 ${out}`);
|
|
13035
13334
|
});
|
|
13036
13335
|
program.command("unpack").description("Open a .deck back into the files build reads.").argument("<file>", "a .deck archive").requiredOption("-o, --out <dir>", "directory to open it into").action(async (file, o) => {
|
|
13037
|
-
const { pack: pack2, files } = await readPack(
|
|
13038
|
-
const out =
|
|
13039
|
-
await writeJson(
|
|
13040
|
-
await writeJson(
|
|
13041
|
-
await writeJson(
|
|
13042
|
-
if (pack2.narration) await writeJson(
|
|
13336
|
+
const { pack: pack2, files } = await readPack(resolve6(file));
|
|
13337
|
+
const out = resolve6(o.out);
|
|
13338
|
+
await writeJson(join17(out, "source.json"), pack2.source);
|
|
13339
|
+
await writeJson(join17(out, "storyboard.json"), pack2.storyboard);
|
|
13340
|
+
await writeJson(join17(out, "decksmith.config.json"), pack2.prefs);
|
|
13341
|
+
if (pack2.narration) await writeJson(join17(out, AUDIO_DIR, NARRATION_FILE), pack2.narration);
|
|
13043
13342
|
const figures = new Map(pack2.source.figures.map((f) => [f.id, f.src]));
|
|
13044
13343
|
for (const [path2, bytes] of Object.entries(files)) {
|
|
13045
13344
|
const media = pack2.media.find((m) => m.path === path2);
|
|
13046
13345
|
const src = media && figures.get(media.id);
|
|
13047
|
-
const to = src ?
|
|
13048
|
-
await mkdir10(
|
|
13049
|
-
await writeFile12(
|
|
13346
|
+
const to = src ? join17("assets", src) : path2;
|
|
13347
|
+
await mkdir10(dirname5(join17(out, to)), { recursive: true });
|
|
13348
|
+
await writeFile12(join17(out, to), bytes);
|
|
13050
13349
|
}
|
|
13051
13350
|
const linked = pack2.media.filter((m) => m.policy !== "bake");
|
|
13052
13351
|
step(`unpack: "${pack2.title}", ${pack2.storyboard.beats.length} beats \u2192 ${out}`);
|
|
@@ -13056,7 +13355,7 @@ program.command("unpack").description("Open a .deck back into the files build re
|
|
|
13056
13355
|
);
|
|
13057
13356
|
}
|
|
13058
13357
|
step(
|
|
13059
|
-
`unpack: decksmith build ${
|
|
13358
|
+
`unpack: decksmith build ${join17(relative2(process.cwd(), out) || ".", "storyboard.json")} --source ${join17(relative2(process.cwd(), out) || ".", "source.json")} -o deck`
|
|
13060
13359
|
);
|
|
13061
13360
|
});
|
|
13062
13361
|
program.parseAsync(process.argv).catch((err) => {
|
|
@@ -13077,7 +13376,7 @@ async function gate(dir, snapshots = false, storyboard, kept, fidelity2 = true,
|
|
|
13077
13376
|
);
|
|
13078
13377
|
const verdict = await verify(dir, { snapshots, fidelity: fidelity2 }, storyboard, kept, source);
|
|
13079
13378
|
process.stdout.write(report(verdict));
|
|
13080
|
-
if (snapshots) step(`verify: snapshots in ${
|
|
13379
|
+
if (snapshots) step(`verify: snapshots in ${join17(dir, "snapshots")}`);
|
|
13081
13380
|
if (!verdict.passed) process.exitCode = 1;
|
|
13082
13381
|
}
|
|
13083
13382
|
function report(v) {
|
|
@@ -13094,12 +13393,12 @@ function report(v) {
|
|
|
13094
13393
|
async function findNarration(sbPath, flag) {
|
|
13095
13394
|
if (flag === false) return void 0;
|
|
13096
13395
|
if (typeof flag === "string") {
|
|
13097
|
-
const path2 =
|
|
13396
|
+
const path2 = resolve6(flag);
|
|
13098
13397
|
if (!await stat2(path2).catch(() => null)) throw new Error(`Cannot read narration ${flag}.`);
|
|
13099
13398
|
return path2;
|
|
13100
13399
|
}
|
|
13101
|
-
const beside =
|
|
13102
|
-
for (const candidate of [
|
|
13400
|
+
const beside = dirname5(resolve6(sbPath));
|
|
13401
|
+
for (const candidate of [join17(beside, AUDIO_DIR, NARRATION_FILE), join17(beside, NARRATION_FILE)]) {
|
|
13103
13402
|
if (await stat2(candidate).catch(() => null)) {
|
|
13104
13403
|
step(`narration: using ${candidate}`);
|
|
13105
13404
|
return candidate;
|
|
@@ -13119,11 +13418,11 @@ function audioNames(narration) {
|
|
|
13119
13418
|
].sort();
|
|
13120
13419
|
}
|
|
13121
13420
|
async function copyAudio(from, narration, out) {
|
|
13122
|
-
const dir =
|
|
13421
|
+
const dir = join17(out, AUDIO_DIR);
|
|
13123
13422
|
await mkdir10(dir, { recursive: true });
|
|
13124
13423
|
const names = audioNames(narration);
|
|
13125
13424
|
for (const name of names) {
|
|
13126
|
-
await cp(
|
|
13425
|
+
await cp(join17(from, name), join17(dir, name)).catch(() => {
|
|
13127
13426
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13128
13427
|
});
|
|
13129
13428
|
}
|
|
@@ -13132,7 +13431,7 @@ async function copyAudio(from, narration, out) {
|
|
|
13132
13431
|
async function audioFiles(from, narration) {
|
|
13133
13432
|
const files = {};
|
|
13134
13433
|
for (const name of audioNames(narration)) {
|
|
13135
|
-
const bytes = await readFile17(
|
|
13434
|
+
const bytes = await readFile17(join17(from, name)).catch(() => {
|
|
13136
13435
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13137
13436
|
});
|
|
13138
13437
|
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(bytes);
|
|
@@ -13154,9 +13453,9 @@ function wrapScript(text2, width) {
|
|
|
13154
13453
|
async function writeTiming(out, input) {
|
|
13155
13454
|
try {
|
|
13156
13455
|
const timing = planTiming(input);
|
|
13157
|
-
await writeJson(
|
|
13456
|
+
await writeJson(join17(out, TIMING_FILE), timing);
|
|
13158
13457
|
step(
|
|
13159
|
-
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${
|
|
13458
|
+
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${join17(out, TIMING_FILE)}`
|
|
13160
13459
|
);
|
|
13161
13460
|
} catch (err) {
|
|
13162
13461
|
step(
|
|
@@ -13165,10 +13464,10 @@ async function writeTiming(out, input) {
|
|
|
13165
13464
|
}
|
|
13166
13465
|
}
|
|
13167
13466
|
function figureAssets(source, sourcePath, prefer) {
|
|
13168
|
-
const assets =
|
|
13467
|
+
const assets = join17(dirname5(sourcePath), "assets");
|
|
13169
13468
|
return source.figures.map((f) => ({
|
|
13170
13469
|
id: f.id,
|
|
13171
|
-
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src :
|
|
13470
|
+
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src : join17(assets, f.src),
|
|
13172
13471
|
prefer
|
|
13173
13472
|
}));
|
|
13174
13473
|
}
|
|
@@ -13197,7 +13496,7 @@ function withMinWeight(format, raw2) {
|
|
|
13197
13496
|
return { ...format, minWeight };
|
|
13198
13497
|
}
|
|
13199
13498
|
async function copyAssets(sourceDir, out, figures) {
|
|
13200
|
-
const from =
|
|
13499
|
+
const from = join17(sourceDir, "assets");
|
|
13201
13500
|
if (!await stat2(from).catch(() => null)) {
|
|
13202
13501
|
step(`build: no assets/ beside source.json, skipping`);
|
|
13203
13502
|
return;
|
|
@@ -13205,19 +13504,19 @@ async function copyAssets(sourceDir, out, figures) {
|
|
|
13205
13504
|
const wanted = new Set(
|
|
13206
13505
|
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
13207
13506
|
);
|
|
13208
|
-
await mkdir10(
|
|
13507
|
+
await mkdir10(join17(out, "assets"), { recursive: true });
|
|
13209
13508
|
let copied = 0;
|
|
13210
13509
|
for (const name of wanted) {
|
|
13211
|
-
const src =
|
|
13212
|
-
if (!src.startsWith(`${
|
|
13510
|
+
const src = resolve6(join17(from, name));
|
|
13511
|
+
if (!src.startsWith(`${resolve6(from)}/`)) continue;
|
|
13213
13512
|
if (!await stat2(src).catch(() => null)) continue;
|
|
13214
|
-
await mkdir10(
|
|
13215
|
-
await cp(src,
|
|
13513
|
+
await mkdir10(dirname5(join17(out, "assets", name)), { recursive: true });
|
|
13514
|
+
await cp(src, join17(out, "assets", name));
|
|
13216
13515
|
copied++;
|
|
13217
13516
|
}
|
|
13218
|
-
const fonts =
|
|
13517
|
+
const fonts = join17(from, "fonts");
|
|
13219
13518
|
if (await stat2(fonts).catch(() => null)) {
|
|
13220
|
-
await cp(fonts,
|
|
13519
|
+
await cp(fonts, join17(out, "assets", "fonts"), { recursive: true });
|
|
13221
13520
|
}
|
|
13222
13521
|
step(`build: copied ${copied} referenced figure(s)`);
|
|
13223
13522
|
}
|
|
@@ -13226,7 +13525,7 @@ async function refreshFont(storyboard, source, out) {
|
|
|
13226
13525
|
const bundle = await bundleFont(
|
|
13227
13526
|
storyboard.lang,
|
|
13228
13527
|
glyphs(source) + glyphs(storyboard),
|
|
13229
|
-
|
|
13528
|
+
join17(out, "assets", "fonts")
|
|
13230
13529
|
);
|
|
13231
13530
|
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
13232
13531
|
return bundle?.css;
|
|
@@ -13241,7 +13540,7 @@ function glyphs(value) {
|
|
|
13241
13540
|
return JSON.stringify(value);
|
|
13242
13541
|
}
|
|
13243
13542
|
async function readValidated(file, schema, label) {
|
|
13244
|
-
const text2 = await readFile17(
|
|
13543
|
+
const text2 = await readFile17(resolve6(file), "utf8").catch(() => {
|
|
13245
13544
|
throw new Error(`Cannot read ${label} file ${file}.`);
|
|
13246
13545
|
});
|
|
13247
13546
|
let json;
|
|
@@ -13259,8 +13558,8 @@ ${issues}`);
|
|
|
13259
13558
|
return parsed.data;
|
|
13260
13559
|
}
|
|
13261
13560
|
async function writeJson(file, value) {
|
|
13262
|
-
const path2 =
|
|
13263
|
-
await mkdir10(
|
|
13561
|
+
const path2 = resolve6(file);
|
|
13562
|
+
await mkdir10(dirname5(path2), { recursive: true });
|
|
13264
13563
|
await writeFile12(path2, `${JSON.stringify(value, null, 2)}
|
|
13265
13564
|
`);
|
|
13266
13565
|
}
|