@jokerized/decksmith 0.4.1 → 0.5.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/dist/mcp.js CHANGED
@@ -61,21 +61,185 @@ var VERSION = createRequire2(import.meta.url)("../package.json").version;
61
61
 
62
62
  // src/mcp/tools.ts
63
63
  import { randomBytes } from "node:crypto";
64
- import { mkdir as mkdir11, mkdtemp as mkdtemp3, readFile as readFile13, rm as rm8 } from "node:fs/promises";
64
+ import { mkdir as mkdir12, mkdtemp as mkdtemp3, readFile as readFile14, rm as rm8 } from "node:fs/promises";
65
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";
66
+ import { basename as basename5, isAbsolute as isAbsolute2, join as join15, resolve as resolve8, sep as sep3 } from "node:path";
67
67
  import { zipSync as zipSync2 } from "fflate";
68
68
  import { z as z5 } from "zod";
69
69
 
70
70
  // src/index.ts
71
- import { cp, mkdir as mkdir9, readdir as readdir2, readFile as readFile11, stat as stat2, writeFile as writeFile11 } from "node:fs/promises";
71
+ import { cp as cp2, mkdir as mkdir10, readFile as readFile12, writeFile as writeFile12 } from "node:fs/promises";
72
+ import { createRequire as createRequire4 } from "node:module";
73
+ import { dirname as dirname5, join as join13, resolve as resolve6 } from "node:path";
74
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
75
+
76
+ // src/build/files.ts
77
+ import { cp, mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
72
78
  import { createRequire as createRequire3 } from "node:module";
73
- import { dirname as dirname4, join as join12, resolve as resolve5 } from "node:path";
79
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
74
80
  import { fileURLToPath } from "node:url";
75
81
 
76
- // src/pack/media.ts
82
+ // src/source/fonts.ts
77
83
  import { createHash } from "node:crypto";
78
- import { readFile } from "node:fs/promises";
84
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
85
+ import { join as join2 } from "node:path";
86
+ function familyFor(lang) {
87
+ const tag = lang.toLowerCase();
88
+ if (tag.startsWith("ko")) return "Noto Sans KR";
89
+ if (tag.startsWith("ja")) return "Noto Sans JP";
90
+ if (tag.startsWith("zh")) return /hant|-tw|-hk|-mo/.test(tag) ? "Noto Sans TC" : "Noto Sans SC";
91
+ return null;
92
+ }
93
+ var UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
94
+ async function bundleFont(lang, glyphs, dir) {
95
+ const family = familyFor(lang);
96
+ if (!family) return null;
97
+ const text2 = [...new Set(glyphs)].filter((c) => c > " ").sort().join("");
98
+ const stamp = `/* decksmith ${createHash("sha256").update(`${family}
99
+ ${text2}`).digest("hex").slice(0, 16)} */`;
100
+ await mkdir(dir, { recursive: true });
101
+ const cssPath = join2(dir, "fonts.css");
102
+ const cached2 = await readFile(cssPath, "utf8").catch(() => "");
103
+ if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
104
+ const res = await fetch(
105
+ `https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
106
+ { headers: { "User-Agent": UA } }
107
+ );
108
+ if (!res.ok) throw new Error(`google fonts: HTTP ${res.status}`);
109
+ let css = await res.text();
110
+ const remote = [...new Set([...css.matchAll(/url\((https:[^)]+)\)/g)].map((m) => m[1] ?? ""))];
111
+ const slug = family.toLowerCase().replace(/[^a-z0-9]/g, "");
112
+ const files = [];
113
+ for (const [i, url] of remote.entries()) {
114
+ const font = await fetch(url);
115
+ if (!font.ok) throw new Error(`${url}: HTTP ${font.status}`);
116
+ const name = `${slug}-${i}.woff2`;
117
+ await writeFile(join2(dir, name), Buffer.from(await font.arrayBuffer()));
118
+ css = css.replaceAll(url, name);
119
+ files.push(name);
120
+ }
121
+ css = `${stamp}
122
+ ${css}`;
123
+ await writeFile(cssPath, css);
124
+ return { family, css, files };
125
+ }
126
+ function localNames(css) {
127
+ return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
128
+ }
129
+
130
+ // src/build/files.ts
131
+ function audioNames(narration) {
132
+ return [
133
+ ...new Set(
134
+ Object.values(narration.beats).flat().map((s) => s.audio)
135
+ )
136
+ ].sort();
137
+ }
138
+ async function vendorKatex(out) {
139
+ const require2 = createRequire3(import.meta.url);
140
+ const dist = join3(dirname2(require2.resolve("katex/package.json")), "dist");
141
+ const css = await readFile2(join3(dist, "katex.min.css"), "utf8");
142
+ const written = [];
143
+ await mkdir2(join3(out, "katex/fonts"), { recursive: true });
144
+ for (const file of await readdir(join3(dist, "fonts"))) {
145
+ if (!file.endsWith(".woff2")) continue;
146
+ await cp(join3(dist, "fonts", file), join3(out, "katex/fonts", file));
147
+ written.push(join3(out, "katex/fonts", file));
148
+ }
149
+ const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
150
+ const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
151
+ return kept ? `src:${kept}` : whole;
152
+ });
153
+ await writeFile2(join3(out, "katex/katex.min.css"), woff2Only);
154
+ written.push(join3(out, "katex/katex.min.css"));
155
+ return written;
156
+ }
157
+ async function vendorScripts(out, composition) {
158
+ const require2 = createRequire3(import.meta.url);
159
+ await mkdir2(join3(out, "vendor"), { recursive: true });
160
+ const wanted = (name) => composition.includes(`./vendor/${name}`);
161
+ const written = [];
162
+ for (const [pkg, rel, name] of [
163
+ ["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
164
+ ["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
165
+ ["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
166
+ ["katex/package.json", "dist/katex.min.js", "katex.min.js"]
167
+ ]) {
168
+ if (!wanted(name)) continue;
169
+ const from = join3(dirname2(require2.resolve(pkg)), rel);
170
+ await cp(from, join3(out, "vendor", name));
171
+ written.push(join3(out, "vendor", name));
172
+ }
173
+ if (wanted("ds-morph.js")) {
174
+ await cp(
175
+ fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
176
+ join3(out, "vendor", "ds-morph.js")
177
+ );
178
+ written.push(join3(out, "vendor", "ds-morph.js"));
179
+ }
180
+ return written;
181
+ }
182
+ async function copyAssets(sourceDir, out, figures, step) {
183
+ const from = join3(resolve2(sourceDir), "assets");
184
+ if (!await stat(from).catch(() => null)) {
185
+ step(`build: no assets/ beside source.json, skipping`);
186
+ return [];
187
+ }
188
+ const wanted = new Set(
189
+ figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
190
+ );
191
+ await mkdir2(join3(out, "assets"), { recursive: true });
192
+ const written = [];
193
+ let copied = 0;
194
+ for (const name of wanted) {
195
+ const src = resolve2(join3(from, name));
196
+ if (!src.startsWith(`${from}/`)) continue;
197
+ if (!await stat(src).catch(() => null)) continue;
198
+ await mkdir2(dirname2(join3(out, "assets", name)), { recursive: true });
199
+ await cp(src, join3(out, "assets", name));
200
+ written.push(join3(out, "assets", name));
201
+ copied++;
202
+ }
203
+ const fonts = join3(from, "fonts");
204
+ if (await stat(fonts).catch(() => null)) {
205
+ await cp(fonts, join3(out, "assets", "fonts"), { recursive: true });
206
+ written.push(join3(out, "assets", "fonts"));
207
+ }
208
+ step(`build: copied ${copied} referenced figure(s)`);
209
+ return written;
210
+ }
211
+ async function copyAudio(from, narration, out, step) {
212
+ const dir = join3(out, narration.dir);
213
+ await mkdir2(dir, { recursive: true });
214
+ const names = audioNames(narration);
215
+ for (const name of names) {
216
+ await cp(join3(resolve2(from), name), join3(dir, name)).catch(() => {
217
+ throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
218
+ });
219
+ }
220
+ step(`narration: ${names.length} audio file(s) \u2192 ${dir}`);
221
+ return names.map((n3) => join3(dir, n3));
222
+ }
223
+ async function refreshFont(storyboard, source, out, step) {
224
+ try {
225
+ const bundle = await bundleFont(
226
+ storyboard.lang,
227
+ JSON.stringify(source) + JSON.stringify(storyboard),
228
+ join3(out, "assets", "fonts")
229
+ );
230
+ if (bundle) step(`build: font bundle covers ${bundle.family}`);
231
+ return bundle?.css;
232
+ } catch (err) {
233
+ step(
234
+ `build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
235
+ );
236
+ return void 0;
237
+ }
238
+ }
239
+
240
+ // src/pack/media.ts
241
+ import { createHash as createHash2 } from "node:crypto";
242
+ import { readFile as readFile3 } from "node:fs/promises";
79
243
  import { extname } from "node:path";
80
244
  var PLAYERS = [
81
245
  "youtube.com",
@@ -240,7 +404,7 @@ async function planMedia(assets, fetcher = fetchAsset) {
240
404
  }
241
405
  function bakedName(id2, url, mime) {
242
406
  const slug = id2.toLowerCase().replace(/[^a-z0-9_-]+/g, "-") || "asset";
243
- const hash = createHash("sha256").update(url).digest("hex").slice(0, 8);
407
+ const hash = createHash2("sha256").update(url).digest("hex").slice(0, 8);
244
408
  return `${slug}-${hash}${extOf(url) || extFor(mime)}`;
245
409
  }
246
410
  function extOf(url) {
@@ -284,7 +448,7 @@ async function fetchAsset(url) {
284
448
  mime: res.headers.get("content-type") ?? void 0
285
449
  };
286
450
  }
287
- return { bytes: new Uint8Array(await readFile(url)) };
451
+ return { bytes: new Uint8Array(await readFile3(url)) };
288
452
  }
289
453
 
290
454
  // src/types.ts
@@ -941,6 +1105,12 @@ var packSchema = z.object({
941
1105
  narration: narrationSchema.optional(),
942
1106
  media: z.array(mediaSchema).default([])
943
1107
  });
1108
+ var CAPTION_FONT_RATIO = 0.037;
1109
+ var CAPTION_MARGIN_V_RATIO = 0.09;
1110
+ var CAPTION_MARGIN_X_RATIO = 0.04;
1111
+ function captionFontSize(width) {
1112
+ return Math.round(width * CAPTION_FONT_RATIO);
1113
+ }
944
1114
  var DESTINATIONS = {
945
1115
  // A presented deck is paced by the person clicking through it, and long-form
946
1116
  // video has no ceiling worth encoding. An empty list is "unbudgeted", stated.
@@ -1254,54 +1424,6 @@ function clock(seconds) {
1254
1424
  return m === 0 ? `${whole}s` : `${m}m${String(whole % 60).padStart(2, "0")}s`;
1255
1425
  }
1256
1426
 
1257
- // src/source/fonts.ts
1258
- import { createHash as createHash2 } from "node:crypto";
1259
- import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
1260
- import { join as join2 } from "node:path";
1261
- function familyFor(lang) {
1262
- const tag = lang.toLowerCase();
1263
- if (tag.startsWith("ko")) return "Noto Sans KR";
1264
- if (tag.startsWith("ja")) return "Noto Sans JP";
1265
- if (tag.startsWith("zh")) return /hant|-tw|-hk|-mo/.test(tag) ? "Noto Sans TC" : "Noto Sans SC";
1266
- return null;
1267
- }
1268
- var UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
1269
- async function bundleFont(lang, glyphs, dir) {
1270
- const family = familyFor(lang);
1271
- if (!family) return null;
1272
- const text2 = [...new Set(glyphs)].filter((c) => c > " ").sort().join("");
1273
- const stamp = `/* decksmith ${createHash2("sha256").update(`${family}
1274
- ${text2}`).digest("hex").slice(0, 16)} */`;
1275
- await mkdir(dir, { recursive: true });
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) };
1279
- const res = await fetch(
1280
- `https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
1281
- { headers: { "User-Agent": UA } }
1282
- );
1283
- if (!res.ok) throw new Error(`google fonts: HTTP ${res.status}`);
1284
- let css = await res.text();
1285
- const remote = [...new Set([...css.matchAll(/url\((https:[^)]+)\)/g)].map((m) => m[1] ?? ""))];
1286
- const slug = family.toLowerCase().replace(/[^a-z0-9]/g, "");
1287
- const files = [];
1288
- for (const [i, url] of remote.entries()) {
1289
- const font = await fetch(url);
1290
- if (!font.ok) throw new Error(`${url}: HTTP ${font.status}`);
1291
- const name = `${slug}-${i}.woff2`;
1292
- await writeFile(join2(dir, name), Buffer.from(await font.arrayBuffer()));
1293
- css = css.replaceAll(url, name);
1294
- files.push(name);
1295
- }
1296
- css = `${stamp}
1297
- ${css}`;
1298
- await writeFile(cssPath, css);
1299
- return { family, css, files };
1300
- }
1301
- function localNames(css) {
1302
- return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
1303
- }
1304
-
1305
1427
  // src/emit/kit.ts
1306
1428
  var PAD_X = 110;
1307
1429
  var PAD_Y = 84;
@@ -1320,7 +1442,12 @@ function contentW(format) {
1320
1442
  return refWidth(format) - 2 * PAD_X;
1321
1443
  }
1322
1444
  function contentH(format) {
1323
- return refHeight(format) - 2 * PAD_Y;
1445
+ return refHeight(format) - 2 * PAD_Y - reserveRef(format);
1446
+ }
1447
+ function reserveRef(format) {
1448
+ const reserve = format.captionReserve ?? 0;
1449
+ if (reserve <= 0) return 0;
1450
+ return Math.ceil(reserve * refHeight(format) / format.height);
1324
1451
  }
1325
1452
  function raw(js2) {
1326
1453
  return { __raw: js2 };
@@ -1819,8 +1946,8 @@ function charUnits(c, tabular, cjkRun, hangul) {
1819
1946
  }
1820
1947
  const measured = ADVANCE[c];
1821
1948
  if (measured !== void 0) return [measured, true];
1822
- const cp2 = c.codePointAt(0) ?? 0;
1823
- for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp2 >= lo && cp2 <= hi) return [units, false];
1949
+ const cp3 = c.codePointAt(0) ?? 0;
1950
+ for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp3 >= lo && cp3 <= hi) return [units, false];
1824
1951
  return [UNMEASURED, true];
1825
1952
  }
1826
1953
  function weightFactor(weight) {
@@ -2081,11 +2208,16 @@ function baseCss(theme, format) {
2081
2208
  gate still catches anything taller than the canvas; it just now
2082
2209
  overflows symmetrically instead of only downwards. */
2083
2210
  .scene { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
2084
- padding: ${PAD_Y}px ${PAD_X}px; display: flex; flex-direction: column;
2211
+ padding: ${scenePadding(format)}; display: flex; flex-direction: column;
2085
2212
  justify-content: center; }
2086
2213
  ${referenceSpaceCss(format)}
2087
2214
  ${AMBIENT_KEYFRAMES}`;
2088
2215
  }
2216
+ function scenePadding(format) {
2217
+ const reserve = reserveRef(format);
2218
+ const base = `${PAD_Y}px ${PAD_X}px`;
2219
+ return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
2220
+ }
2089
2221
  function referenceSpaceCss(format) {
2090
2222
  const zoom = zoomOf(format);
2091
2223
  if (zoom === 1) return "";
@@ -6413,6 +6545,10 @@ function planTiming(input) {
6413
6545
  lang: storyboard.lang,
6414
6546
  audioDir: segments2.length > 0 ? narration?.dir ?? "" : "",
6415
6547
  voice: segments2.length > 0 ? narration?.voice ?? "" : "",
6548
+ // The format the deck was LAID OUT under, which is the only honest source:
6549
+ // `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
6550
+ // disagree with the box the archetypes actually drew into.
6551
+ captionReserve: format.captionReserve ?? 0,
6416
6552
  scenes,
6417
6553
  segments: segments2
6418
6554
  };
@@ -6705,8 +6841,8 @@ function textOf(node) {
6705
6841
 
6706
6842
  // src/source/harvest.ts
6707
6843
  import { createHash as createHash5 } from "node:crypto";
6708
- import { copyFile, mkdir as mkdir3, rm as rm2, stat, writeFile as writeFile3 } from "node:fs/promises";
6709
- import { basename as basename3, join as join5, resolve as resolve2 } from "node:path";
6844
+ import { copyFile, mkdir as mkdir4, rm as rm2, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
6845
+ import { basename as basename3, join as join6, resolve as resolve3 } from "node:path";
6710
6846
 
6711
6847
  // src/net/fetch.ts
6712
6848
  import { lookup as resolveHost } from "node:dns/promises";
@@ -6906,8 +7042,8 @@ async function send(url, target, headers, signal, opts) {
6906
7042
  ...secure && isIP(host2) === 0 ? { servername: host2 } : {}
6907
7043
  };
6908
7044
  try {
6909
- return await new Promise((resolve8, reject) => {
6910
- const req = (secure ? httpsRequest : httpRequest)(options, resolve8);
7045
+ return await new Promise((resolve9, reject) => {
7046
+ const req = (secure ? httpsRequest : httpRequest)(options, resolve9);
6911
7047
  req.on("error", reject);
6912
7048
  req.end();
6913
7049
  });
@@ -6991,7 +7127,7 @@ function message(err) {
6991
7127
 
6992
7128
  // src/render/capture.ts
6993
7129
  import { homedir } from "node:os";
6994
- import { join as join3 } from "node:path";
7130
+ import { join as join4 } from "node:path";
6995
7131
  async function resolveChrome(need = "open the deck with") {
6996
7132
  const env2 = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
6997
7133
  if (env2) return { path: env2, source: "env" };
@@ -7001,9 +7137,9 @@ async function resolveChrome(need = "open the deck with") {
7001
7137
  const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
7002
7138
  return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
7003
7139
  };
7004
- const hf = await pick(join3(homedir(), ".cache", "hyperframes", "chrome"));
7140
+ const hf = await pick(join4(homedir(), ".cache", "hyperframes", "chrome"));
7005
7141
  if (hf) return { path: hf.executablePath, source: "hyperframes" };
7006
- const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer"));
7142
+ const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join4(homedir(), ".cache", "puppeteer"));
7007
7143
  if (pup) return { path: pup.executablePath, source: "puppeteer" };
7008
7144
  throw new Error(
7009
7145
  `no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
@@ -7015,13 +7151,13 @@ async function chromePath(need = "open the deck with") {
7015
7151
 
7016
7152
  // src/source/assets.ts
7017
7153
  import { createHash as createHash4 } from "node:crypto";
7018
- import { mkdir as mkdir2, readdir, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
7019
- import { extname as extname2, join as join4 } from "node:path";
7154
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
7155
+ import { extname as extname2, join as join5 } from "node:path";
7020
7156
  import { z as z2 } from "zod";
7021
7157
  async function fetchFigures(source, dir, warnings) {
7022
7158
  const drops = warnings ?? [];
7023
- await mkdir2(dir, { recursive: true });
7024
- const cached2 = await readdir(dir).catch(() => []);
7159
+ await mkdir3(dir, { recursive: true });
7160
+ const cached2 = await readdir2(dir).catch(() => []);
7025
7161
  const figures = [];
7026
7162
  for (const figure of source.figures) {
7027
7163
  if (figure.kind === "clip") {
@@ -7046,11 +7182,11 @@ async function fetchFigures(source, dir, warnings) {
7046
7182
  async function localize(figure, dir, cached2) {
7047
7183
  const stem = assetStem(figure.id, figure.src);
7048
7184
  const hit = cached2.find((name) => name.startsWith(`${stem}.`));
7049
- const bytes = hit ? await readFile3(join4(dir, hit)) : await load(figure.src);
7185
+ const bytes = hit ? await readFile4(join5(dir, hit)) : await load(figure.src);
7050
7186
  const size = imageSize(bytes);
7051
7187
  if (hit) return { ...figure, src: hit, ...size };
7052
7188
  const src = `${stem}${assetExt(bytes, figure.src)}`;
7053
- await writeFile2(join4(dir, src), bytes);
7189
+ await writeFile3(join5(dir, src), bytes);
7054
7190
  cached2.push(src);
7055
7191
  return { ...figure, src, ...size };
7056
7192
  }
@@ -7075,7 +7211,7 @@ function assetExt(bytes, src) {
7075
7211
  var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
7076
7212
  var FIGURE_TIMEOUT_MS = 2e4;
7077
7213
  async function load(src) {
7078
- if (!/^https?:/i.test(src)) return readFile3(src);
7214
+ if (!/^https?:/i.test(src)) return readFile4(src);
7079
7215
  const got = await fetchGuarded(src, {
7080
7216
  maxBytes: FIGURE_MAX_BYTES,
7081
7217
  timeoutMs: FIGURE_TIMEOUT_MS
@@ -7611,7 +7747,7 @@ function readContentRegion() {
7611
7747
  }
7612
7748
 
7613
7749
  // src/source/transcode.ts
7614
- import { readFile as readFile4, rm } from "node:fs/promises";
7750
+ import { readFile as readFile5, rm } from "node:fs/promises";
7615
7751
  import { basename as basename2 } from "node:path";
7616
7752
 
7617
7753
  // src/render/ffmpeg.ts
@@ -7637,7 +7773,7 @@ ${tail}`);
7637
7773
  }
7638
7774
  }
7639
7775
  function runLive(file, args) {
7640
- return new Promise((resolve8, reject) => {
7776
+ return new Promise((resolve9, reject) => {
7641
7777
  const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
7642
7778
  child.on("error", (err) => {
7643
7779
  reject(
@@ -7645,7 +7781,7 @@ function runLive(file, args) {
7645
7781
  );
7646
7782
  });
7647
7783
  child.on("close", (code, signal) => {
7648
- if (code === 0) resolve8();
7784
+ if (code === 0) resolve9();
7649
7785
  else if (signal) {
7650
7786
  reject(
7651
7787
  new Error(
@@ -7772,11 +7908,16 @@ function burnStyle(width, height, font = "Arial") {
7772
7908
  // here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
7773
7909
  // "4% of the width" looked like on paper — overflows to a THIRD line, and a
7774
7910
  // three-line band covers the bottom of the slide.
7775
- fontSize: Math.round(width * 0.037),
7911
+ //
7912
+ // THE RATIOS ARE NOT HERE. They are in src/types.ts beside `bandReserve`,
7913
+ // because `src/emit` has to reserve exactly the strip this draws into and
7914
+ // may not import this file. A local copy of 0.037 is how the two drift back
7915
+ // apart, which is the bug this whole path exists to close.
7916
+ fontSize: captionFontSize(width),
7776
7917
  // Clear of the play button, the progress bar and the handle every vertical
7777
7918
  // player draws across the bottom eighth of the frame.
7778
- marginV: Math.round(height * 0.09),
7779
- marginX: Math.round(width * 0.04)
7919
+ marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
7920
+ marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
7780
7921
  };
7781
7922
  }
7782
7923
 
@@ -7842,7 +7983,7 @@ async function transcode(input, out, opts = {}) {
7842
7983
  const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
7843
7984
  const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
7844
7985
  const name = basename2(input);
7845
- const source = videoSize(await readFile4(input));
7986
+ const source = videoSize(await readFile5(input));
7846
7987
  const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
7847
7988
  const kept = (why2) => ({
7848
7989
  path: input,
@@ -7874,7 +8015,7 @@ async function transcode(input, out, opts = {}) {
7874
8015
  }
7875
8016
  let measured;
7876
8017
  try {
7877
- measured = videoSize(await readFile4(out));
8018
+ measured = videoSize(await readFile5(out));
7878
8019
  } catch (err) {
7879
8020
  await rm(out, { force: true });
7880
8021
  return kept(
@@ -7910,8 +8051,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
7910
8051
  async function harvest(url, dir, opts = {}) {
7911
8052
  const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
7912
8053
  const warnings = [];
7913
- const assets = resolve2(dir);
7914
- await mkdir3(assets, { recursive: true });
8054
+ const assets = resolve3(dir);
8055
+ await mkdir4(assets, { recursive: true });
7915
8056
  const page = await fetchGuarded(url, {
7916
8057
  maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
7917
8058
  timeoutMs,
@@ -8293,8 +8434,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
8293
8434
  `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`
8294
8435
  );
8295
8436
  }
8296
- const path2 = join5(dir, assetName(url, extFor2(sniffFormat(bytes))));
8297
- await writeFile3(path2, bytes);
8437
+ const path2 = join6(dir, assetName(url, extFor2(sniffFormat(bytes))));
8438
+ await writeFile4(path2, bytes);
8298
8439
  assets.push(path2);
8299
8440
  got = { path: path2, width: size.width, height: size.height };
8300
8441
  } catch (err) {
@@ -8306,7 +8447,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
8306
8447
  };
8307
8448
  const shrink = async (url, path2, measured, what) => {
8308
8449
  if (opts.transcode === false) return { path: path2, ...measured };
8309
- const out2 = join5(dir, assetName(url, ".vp9.webm"));
8450
+ const out2 = join6(dir, assetName(url, ".vp9.webm"));
8310
8451
  let small;
8311
8452
  try {
8312
8453
  small = await transcode(path2, out2, {
@@ -8348,8 +8489,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
8348
8489
  try {
8349
8490
  const bytes = await bytesOf(url);
8350
8491
  const measured = videoSize(bytes);
8351
- const path2 = join5(dir, assetName(url, `.${measured.container}`));
8352
- await writeFile3(path2, bytes);
8492
+ const path2 = join6(dir, assetName(url, `.${measured.container}`));
8493
+ await writeFile4(path2, bytes);
8353
8494
  return await shrink(url, path2, measured, what);
8354
8495
  } catch (err) {
8355
8496
  warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
@@ -8438,7 +8579,7 @@ function unheld(policy) {
8438
8579
  return "the file could not be held";
8439
8580
  }
8440
8581
  async function sizeOf(path2) {
8441
- return await stat(path2).then(
8582
+ return await stat2(path2).then(
8442
8583
  (s) => s.size,
8443
8584
  () => 0
8444
8585
  );
@@ -8712,9 +8853,9 @@ ${fence}`;
8712
8853
 
8713
8854
  // src/plan/codex.ts
8714
8855
  import { spawn as spawn2 } from "node:child_process";
8715
- import { mkdtemp, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
8856
+ import { mkdtemp, readFile as readFile6, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
8716
8857
  import { tmpdir as tmpdir2 } from "node:os";
8717
- import { join as join6 } from "node:path";
8858
+ import { join as join7 } from "node:path";
8718
8859
  import { z as z3 } from "zod";
8719
8860
 
8720
8861
  // src/plan/arc.ts
@@ -9732,11 +9873,11 @@ function schemaFor(prefs) {
9732
9873
  var SCHEMA = schemaFor({ genre: "general" });
9733
9874
  async function codexPlanner(source, opts = {}) {
9734
9875
  const prefs = opts.prefs ?? prefsSchema.parse({});
9735
- const dir = await mkdtemp(join6(tmpdir2(), "decksmith-plan-"));
9876
+ const dir = await mkdtemp(join7(tmpdir2(), "decksmith-plan-"));
9736
9877
  try {
9737
- const schemaPath = join6(dir, "storyboard.schema.json");
9738
- const outPath = join6(dir, "storyboard.json");
9739
- await writeFile4(schemaPath, JSON.stringify(schemaFor(prefs)));
9878
+ const schemaPath = join7(dir, "storyboard.schema.json");
9879
+ const outPath = join7(dir, "storyboard.json");
9880
+ await writeFile5(schemaPath, JSON.stringify(schemaFor(prefs)));
9740
9881
  await (opts.run ?? runCodex)({
9741
9882
  prompt: buildPrompt(source, prefs),
9742
9883
  schemaPath,
@@ -9744,7 +9885,7 @@ async function codexPlanner(source, opts = {}) {
9744
9885
  ...opts.model === void 0 ? {} : { model: opts.model },
9745
9886
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
9746
9887
  });
9747
- const raw2 = await readFile5(outPath, "utf8").catch(() => "");
9888
+ const raw2 = await readFile6(outPath, "utf8").catch(() => "");
9748
9889
  if (raw2.trim() === "") {
9749
9890
  throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
9750
9891
  }
@@ -9817,7 +9958,7 @@ function codexCommand(args) {
9817
9958
  }
9818
9959
  function runCodex(args) {
9819
9960
  const { argv, env: env2 } = codexCommand(args);
9820
- return new Promise((resolve8, reject) => {
9961
+ return new Promise((resolve9, reject) => {
9821
9962
  const child = spawn2("codex", argv, {
9822
9963
  stdio: ["pipe", "ignore", "pipe"],
9823
9964
  ...env2 === void 0 ? {} : { env: env2 }
@@ -9840,7 +9981,7 @@ function runCodex(args) {
9840
9981
  });
9841
9982
  child.on("close", (code) => {
9842
9983
  clearTimeout(timer);
9843
- if (code === 0) return resolve8();
9984
+ if (code === 0) return resolve9();
9844
9985
  reject(
9845
9986
  new Error(`codex exec exited ${code}.
9846
9987
  ${stderr.trim().split("\n").slice(-8).join("\n")}`)
@@ -9852,14 +9993,14 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
9852
9993
 
9853
9994
  // src/images/illustrate.ts
9854
9995
  import { createHash as createHash7 } from "node:crypto";
9855
- import { mkdir as mkdir4, readFile as readFile7, rename, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
9856
- import { join as join8 } from "node:path";
9996
+ import { mkdir as mkdir5, readFile as readFile8, rename, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
9997
+ import { join as join9 } from "node:path";
9857
9998
 
9858
9999
  // src/images/providers.ts
9859
10000
  import { createHash as createHash6 } from "node:crypto";
9860
- import { mkdtemp as mkdtemp2, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
10001
+ import { mkdtemp as mkdtemp2, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
9861
10002
  import { tmpdir as tmpdir3 } from "node:os";
9862
- import { join as join7, resolve as resolve3 } from "node:path";
10003
+ import { join as join8, resolve as resolve4 } from "node:path";
9863
10004
  import { z as z4 } from "zod";
9864
10005
  var SIZE = {
9865
10006
  landscape: { width: 1536, height: 1024 },
@@ -9991,11 +10132,11 @@ function codexImages(opts = {}) {
9991
10132
  async check() {
9992
10133
  },
9993
10134
  async generate(req) {
9994
- const dir = await mkdtemp2(join7(tmpdir3(), "decksmith-image-"));
10135
+ const dir = await mkdtemp2(join8(tmpdir3(), "decksmith-image-"));
9995
10136
  try {
9996
- const schemaPath = join7(dir, "answer.schema.json");
9997
- const outPath = join7(dir, "answer.json");
9998
- await writeFile5(schemaPath, JSON.stringify(ANSWER_SCHEMA));
10137
+ const schemaPath = join8(dir, "answer.schema.json");
10138
+ const outPath = join8(dir, "answer.json");
10139
+ await writeFile6(schemaPath, JSON.stringify(ANSWER_SCHEMA));
9999
10140
  await run5({
10000
10141
  prompt: codexPrompt(req),
10001
10142
  schemaPath,
@@ -10004,7 +10145,7 @@ function codexImages(opts = {}) {
10004
10145
  cwd: dir,
10005
10146
  sandbox: "workspace-write"
10006
10147
  });
10007
- const raw2 = await readFile6(outPath, "utf8").catch(() => "");
10148
+ const raw2 = await readFile7(outPath, "utf8").catch(() => "");
10008
10149
  const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
10009
10150
  if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
10010
10151
  if (!answer.data.ok) {
@@ -10012,10 +10153,10 @@ function codexImages(opts = {}) {
10012
10153
  `codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
10013
10154
  );
10014
10155
  }
10015
- const candidates2 = [join7(dir, "picture.png")];
10016
- if (answer.data.file) candidates2.push(resolve3(dir, answer.data.file));
10156
+ const candidates2 = [join8(dir, "picture.png")];
10157
+ if (answer.data.file) candidates2.push(resolve4(dir, answer.data.file));
10017
10158
  for (const path2 of candidates2) {
10018
- const bytes = await readFile6(path2).catch(() => null);
10159
+ const bytes = await readFile7(path2).catch(() => null);
10019
10160
  if (bytes) return raster(bytes, "codex");
10020
10161
  }
10021
10162
  throw new Error("codex said ok but wrote no picture.png");
@@ -10147,7 +10288,7 @@ async function illustrate(storyboard, source, opts) {
10147
10288
  const tool = last?.id === "svg" ? last : toolSvg();
10148
10289
  if (tool !== last) chain.push(tool);
10149
10290
  const dropped = /* @__PURE__ */ new Set();
10150
- if (pending.length > 0) await mkdir4(opts.assetsDir, { recursive: true });
10291
+ if (pending.length > 0) await mkdir5(opts.assetsDir, { recursive: true });
10151
10292
  for (const [i, slot] of pending.entries()) {
10152
10293
  let rungs = chain;
10153
10294
  if (i >= images.max) {
@@ -10244,16 +10385,16 @@ function cacheKey(providerId, req) {
10244
10385
  async function draw(provider, req, name, dir) {
10245
10386
  for (const ext of Object.values(EXT2)) {
10246
10387
  const src2 = `${name}${ext}`;
10247
- const bytes = await readFile7(join8(dir, src2)).catch(() => null);
10388
+ const bytes = await readFile8(join9(dir, src2)).catch(() => null);
10248
10389
  if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
10249
10390
  }
10250
10391
  await provider.check();
10251
10392
  const img = await provider.generate(req);
10252
10393
  const src = `${name}${EXT2[img.mime]}`;
10253
- const tmp = join8(dir, `.${name}.tmp`);
10394
+ const tmp = join9(dir, `.${name}.tmp`);
10254
10395
  try {
10255
- await writeFile6(tmp, img.bytes);
10256
- await rename(tmp, join8(dir, src));
10396
+ await writeFile7(tmp, img.bytes);
10397
+ await rename(tmp, join9(dir, src));
10257
10398
  } finally {
10258
10399
  await rm5(tmp, { force: true });
10259
10400
  }
@@ -10266,9 +10407,9 @@ function sizeOf2(bytes, ext) {
10266
10407
  // src/narrate/tts.ts
10267
10408
  import { spawn as spawn3 } from "node:child_process";
10268
10409
  import { createHash as createHash8 } from "node:crypto";
10269
- import { mkdir as mkdir5, readFile as readFile8, rm as rm6, writeFile as writeFile7 } from "node:fs/promises";
10410
+ import { mkdir as mkdir6, readFile as readFile9, rm as rm6, writeFile as writeFile8 } from "node:fs/promises";
10270
10411
  import { homedir as homedir2 } from "node:os";
10271
- import { join as join9 } from "node:path";
10412
+ import { join as join10 } from "node:path";
10272
10413
  var MISSING = [
10273
10414
  "edge-tts is not installed, so narration cannot be synthesised.",
10274
10415
  "",
@@ -10285,8 +10426,8 @@ function candidates() {
10285
10426
  ["edge-tts"],
10286
10427
  // pip --user on macOS and on Linux respectively. Neither is on PATH by
10287
10428
  // default, and both are where this actually lands in practice.
10288
- [join9(home, "Library", "Python", "3.9", "bin", "edge-tts")],
10289
- [join9(home, ".local", "bin", "edge-tts")],
10429
+ [join10(home, "Library", "Python", "3.9", "bin", "edge-tts")],
10430
+ [join10(home, ".local", "bin", "edge-tts")],
10290
10431
  // Last resort: the module is installed even though its console script is
10291
10432
  // nowhere findable, which is the normal state of a pip --user install.
10292
10433
  ["python3", "-m", "edge_tts"]
@@ -10311,7 +10452,7 @@ async function find2(can) {
10311
10452
  throw new Error(MISSING);
10312
10453
  }
10313
10454
  function runArgv(cmd, args) {
10314
- return new Promise((resolve8) => {
10455
+ return new Promise((resolve9) => {
10315
10456
  const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
10316
10457
  let stdout = "";
10317
10458
  let stderr = "";
@@ -10321,8 +10462,8 @@ function runArgv(cmd, args) {
10321
10462
  child.stderr.on("data", (b) => {
10322
10463
  stderr += b.toString();
10323
10464
  });
10324
- child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
10325
- child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
10465
+ child.on("error", (e) => resolve9({ code: -1, stderr: String(e), stdout }));
10466
+ child.on("close", (code) => resolve9({ code: code ?? -1, stderr, stdout }));
10326
10467
  });
10327
10468
  }
10328
10469
  var edgeTts = {
@@ -10389,7 +10530,7 @@ function edgeProvider(runner = edgeTts) {
10389
10530
  async speak(req) {
10390
10531
  const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
10391
10532
  await runner.speak({ ...req, subs });
10392
- const raw2 = await readFile8(subs, "utf8").catch(() => "");
10533
+ const raw2 = await readFile9(subs, "utf8").catch(() => "");
10393
10534
  await rm6(subs, { force: true });
10394
10535
  const cues = parseCues(raw2);
10395
10536
  const measured = await runner.measure(req.audio);
@@ -10407,11 +10548,11 @@ async function synthesize(text2, opts) {
10407
10548
  const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
10408
10549
  const key = cacheKey2(text2, opts.voice, rate, pitch);
10409
10550
  const file = `${key}.mp3`;
10410
- const audio = join9(opts.dir, file);
10411
- const sidecar = join9(opts.dir, `${key}.json`);
10551
+ const audio = join10(opts.dir, file);
10552
+ const sidecar = join10(opts.dir, `${key}.json`);
10412
10553
  const cached2 = await readSidecar(sidecar);
10413
10554
  if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
10414
- await mkdir5(opts.dir, { recursive: true });
10555
+ await mkdir6(opts.dir, { recursive: true });
10415
10556
  const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
10416
10557
  const { cues, seconds } = spoken;
10417
10558
  const clamped = cues.map((c) => ({
@@ -10420,13 +10561,13 @@ async function synthesize(text2, opts) {
10420
10561
  text: c.text
10421
10562
  }));
10422
10563
  const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
10423
- await writeFile7(sidecar, `${JSON.stringify(body, null, 2)}
10564
+ await writeFile8(sidecar, `${JSON.stringify(body, null, 2)}
10424
10565
  `);
10425
10566
  return { audio, file, seconds, cues: clamped };
10426
10567
  }
10427
10568
  async function readSidecar(path2) {
10428
10569
  try {
10429
- const parsed = JSON.parse(await readFile8(path2, "utf8"));
10570
+ const parsed = JSON.parse(await readFile9(path2, "utf8"));
10430
10571
  const s = parsed;
10431
10572
  return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
10432
10573
  } catch {
@@ -10573,12 +10714,12 @@ function scanBeatCount(storyboard, prefs) {
10573
10714
  }
10574
10715
 
10575
10716
  // src/render/render.ts
10576
- import { mkdir as mkdir7, readFile as readFile9, rename as rename2, rm as rm7, writeFile as writeFile9 } from "node:fs/promises";
10577
- import { basename as basename4, dirname as dirname2, join as join11, resolve as resolve4 } from "node:path";
10717
+ import { mkdir as mkdir8, readFile as readFile10, rename as rename2, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
10718
+ import { basename as basename4, dirname as dirname3, join as join12, resolve as resolve5 } from "node:path";
10578
10719
 
10579
10720
  // src/render/captions.ts
10580
- import { mkdir as mkdir6, writeFile as writeFile8 } from "node:fs/promises";
10581
- import { join as join10 } from "node:path";
10721
+ import { mkdir as mkdir7, writeFile as writeFile9 } from "node:fs/promises";
10722
+ import { join as join11 } from "node:path";
10582
10723
  import { pathToFileURL } from "node:url";
10583
10724
  var DECK_FONT_CSS = "assets/fonts/fonts.css";
10584
10725
  function cssString(value) {
@@ -10669,11 +10810,11 @@ async function captionBlocker() {
10669
10810
  }
10670
10811
  async function renderCaptions(cues, style, deck, work2) {
10671
10812
  if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
10672
- const fontCss = join10(deck, DECK_FONT_CSS);
10813
+ const fontCss = join11(deck, DECK_FONT_CSS);
10673
10814
  const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL(fontCss).href : null;
10674
- const page = join10(work2, "captions.html");
10675
- await mkdir6(work2, { recursive: true });
10676
- await writeFile8(page, captionPage(cues, style, href));
10815
+ const page = join11(work2, "captions.html");
10816
+ await mkdir7(work2, { recursive: true });
10817
+ await writeFile9(page, captionPage(cues, style, href));
10677
10818
  const { default: puppeteer } = await import("puppeteer-core");
10678
10819
  const browser = await puppeteer.launch({
10679
10820
  executablePath: await chromePath(CAPTION_NEED),
@@ -10693,7 +10834,7 @@ async function renderCaptions(cues, style, deck, work2) {
10693
10834
  const name = `cap${String(i).padStart(4, "0")}.png`;
10694
10835
  await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
10695
10836
  await tab.screenshot({
10696
- path: join10(work2, name),
10837
+ path: join11(work2, name),
10697
10838
  type: "png",
10698
10839
  omitBackground: true,
10699
10840
  clip: { x: box.x, y: box.y, width: box.width, height: box.height }
@@ -10734,9 +10875,9 @@ function subtitlePlan(mode) {
10734
10875
  async function render(opts) {
10735
10876
  const log = opts.log ?? (() => {
10736
10877
  });
10737
- const deck = resolve4(opts.deck);
10738
- const out = resolve4(opts.out);
10739
- const work2 = join11(dirname2(out), `.${basename4(out)}.parts`);
10878
+ const deck = resolve5(opts.deck);
10879
+ const out = resolve5(opts.out);
10880
+ const work2 = join12(dirname3(out), `.${basename4(out)}.parts`);
10740
10881
  const timing = await readTiming(deck);
10741
10882
  if (opts.targetSeconds && !opts.allowFastPlayback) {
10742
10883
  const refusal = playbackRefusal(
@@ -10748,13 +10889,17 @@ async function render(opts) {
10748
10889
  }
10749
10890
  const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
10750
10891
  if (plan0.burn) {
10892
+ if (!timing.captionReserve)
10893
+ throw new Error(
10894
+ "This deck was built without room for a burned caption, so the band would sit on the slide's own text. Rebuild with `decksmith build --reserve-captions`, or render with `--subtitles sidecar`."
10895
+ );
10751
10896
  const blocker = await captionBlocker();
10752
10897
  if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
10753
10898
  }
10754
10899
  const burnable = plan0.burn;
10755
- await mkdir7(work2, { recursive: true });
10900
+ await mkdir8(work2, { recursive: true });
10756
10901
  try {
10757
- const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join11(work2, "raw.mp4"), opts, log);
10902
+ const raw2 = opts.video ? resolve5(opts.video) : await capture(deck, join12(work2, "raw.mp4"), opts, log);
10758
10903
  const shot = await probe(raw2);
10759
10904
  log(
10760
10905
  `render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
@@ -10767,18 +10912,18 @@ async function render(opts) {
10767
10912
  );
10768
10913
  }
10769
10914
  const retimed = await retime(raw2, plan, work2, log);
10770
- const srtPath = join11(dirname2(out), SRT_NAME(out));
10915
+ const srtPath = join12(dirname3(out), SRT_NAME(out));
10771
10916
  const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
10772
10917
  const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
10773
10918
  const srt = plan0.sidecar ? toSrt(cues) : "";
10774
- if (srt) await writeFile9(srtPath, srt);
10919
+ if (srt) await writeFile10(srtPath, srt);
10775
10920
  const burn = burnable && srt.length > 0;
10776
10921
  await mux(retimed, timing, plan, deck, out, work2, burn ? plan.cues : void 0, shot.fps, log);
10777
10922
  if (playback > 1) {
10778
10923
  log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
10779
10924
  const warning = playbackWarning(playback, p95CueRate(plan.cues));
10780
10925
  if (warning) log(`render: ${warning}`);
10781
- const fast = join11(work2, `fast.${basename4(out)}`);
10926
+ const fast = join12(work2, `fast.${basename4(out)}`);
10782
10927
  const muxed = await probe(out);
10783
10928
  await runTool(
10784
10929
  "ffmpeg",
@@ -10808,8 +10953,8 @@ async function render(opts) {
10808
10953
  }
10809
10954
  }
10810
10955
  async function readTiming(deck) {
10811
- const path2 = join11(deck, TIMING_FILE);
10812
- const text2 = await readFile9(path2, "utf8").catch(() => {
10956
+ const path2 = join12(deck, TIMING_FILE);
10957
+ const text2 = await readFile10(path2, "utf8").catch(() => {
10813
10958
  throw new Error(
10814
10959
  `${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
10815
10960
  );
@@ -10848,20 +10993,20 @@ async function retime(raw2, plan, work2, log) {
10848
10993
  if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
10849
10994
  const list = [];
10850
10995
  for (const [i, piece] of plan.pieces.entries()) {
10851
- const file = join11(work2, `p${String(i).padStart(4, "0")}.ts`);
10996
+ const file = join12(work2, `p${String(i).padStart(4, "0")}.ts`);
10852
10997
  await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
10853
10998
  list.push(file);
10854
10999
  if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
10855
11000
  log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
10856
11001
  }
10857
11002
  }
10858
- const listFile = join11(work2, "pieces.txt");
10859
- await writeFile9(
11003
+ const listFile = join12(work2, "pieces.txt");
11004
+ await writeFile10(
10860
11005
  listFile,
10861
11006
  `${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
10862
11007
  `
10863
11008
  );
10864
- const joined = join11(work2, "retimed.mp4");
11009
+ const joined = join12(work2, "retimed.mp4");
10865
11010
  await runTool("ffmpeg", [
10866
11011
  "-y",
10867
11012
  "-hide_banner",
@@ -10883,7 +11028,7 @@ async function retime(raw2, plan, work2, log) {
10883
11028
  }
10884
11029
  async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
10885
11030
  const inputs = plan.audio.map((a) => ({
10886
- file: join11(deck, timing.audioDir, a.audio),
11031
+ file: join12(deck, timing.audioDir, a.audio),
10887
11032
  delayMs: a.delayMs
10888
11033
  }));
10889
11034
  const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
@@ -10904,8 +11049,8 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
10904
11049
  graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
10905
11050
  }
10906
11051
  if (graph.length > 0) {
10907
- const script = join11(work2, "mux.filter");
10908
- await writeFile9(script, `${graph.join(";\n")}
11052
+ const script = join12(work2, "mux.filter");
11053
+ await writeFile10(script, `${graph.join(";\n")}
10909
11054
  `);
10910
11055
  args.push("-filter_complex_script", script);
10911
11056
  }
@@ -10920,13 +11065,13 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
10920
11065
  log(
10921
11066
  `render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
10922
11067
  );
10923
- await mkdir7(dirname2(out), { recursive: true });
11068
+ await mkdir8(dirname3(out), { recursive: true });
10924
11069
  await runTool("ffmpeg", args, { cwd: work2 });
10925
11070
  }
10926
11071
 
10927
11072
  // src/pack/pack.ts
10928
- import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile10 } from "node:fs/promises";
10929
- import { dirname as dirname3, extname as extname3 } from "node:path";
11073
+ import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile11 } from "node:fs/promises";
11074
+ import { dirname as dirname4, extname as extname3 } from "node:path";
10930
11075
  import { unzipSync, zipSync } from "fflate";
10931
11076
  var MTIME = Date.UTC(1980, 0, 2, 12);
10932
11077
  var STORED = /* @__PURE__ */ new Set([
@@ -10965,8 +11110,8 @@ async function writePack(pack3, files, out) {
10965
11110
  entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
10966
11111
  }
10967
11112
  const zip = zipSync(entries, { mtime: MTIME });
10968
- await mkdir8(dirname3(out), { recursive: true });
10969
- await writeFile10(out, zip);
11113
+ await mkdir9(dirname4(out), { recursive: true });
11114
+ await writeFile11(out, zip);
10970
11115
  return zip.length;
10971
11116
  }
10972
11117
  function check2(label, path2 = label) {
@@ -10985,8 +11130,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
10985
11130
  if (!format) throw new Error("no deck-16x9 format");
10986
11131
  const step = opts.onStep ?? (() => {
10987
11132
  });
10988
- const out = resolve5(outDir);
10989
- await mkdir9(out, { recursive: true });
11133
+ const out = resolve6(outDir);
11134
+ await mkdir10(out, { recursive: true });
10990
11135
  const speed = opts.speed ?? 1;
10991
11136
  const fontCss = await refreshFont(storyboard, source, out, step);
10992
11137
  const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
@@ -10999,47 +11144,48 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
10999
11144
  });
11000
11145
  const files = [];
11001
11146
  const write = async (name, text2) => {
11002
- await writeFile11(join12(out, name), text2);
11003
- files.push(join12(out, name));
11147
+ await writeFile12(join13(out, name), text2);
11148
+ files.push(join13(out, name));
11004
11149
  };
11005
11150
  await write("index.html", deck.composition);
11006
11151
  await write("hyperframes.json", HYPERFRAMES_JSON);
11007
- if (opts.narration) {
11008
- try {
11009
- const timing = planTiming({
11010
- storyboard,
11011
- source,
11012
- format,
11013
- composition: deck.composition,
11014
- // What was drawn, not what the threshold would have kept: a budgeted
11015
- // format cuts to its own length as well, and the two lists differ.
11016
- beats: deck.cut.kept,
11017
- narration: opts.narration,
11018
- speed,
11019
- ...opts.theme ? { theme: opts.theme } : {}
11020
- });
11021
- await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
11152
+ try {
11153
+ const timing = planTiming({
11154
+ storyboard,
11155
+ source,
11156
+ format,
11157
+ composition: deck.composition,
11158
+ // What was drawn, not what the threshold would have kept: a budgeted
11159
+ // format cuts to its own length as well, and the two lists differ.
11160
+ beats: deck.cut.kept,
11161
+ speed,
11162
+ ...opts.narration ? { narration: opts.narration } : {},
11163
+ ...opts.theme ? { theme: opts.theme } : {}
11164
+ });
11165
+ await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
11022
11166
  `);
11023
- step(`build: timing for ${timing.segments.length} narration segment(s)`);
11024
- } catch (err) {
11025
- step(
11026
- `build: cannot place narration on this deck, so no ${TIMING_FILE} was written and \`render\` will refuse. ${err instanceof Error ? err.message : err}`
11027
- );
11028
- }
11167
+ step(`build: timing for ${timing.segments.length} narration segment(s)`);
11168
+ } catch (err) {
11169
+ step(
11170
+ `build: cannot place narration on this deck, so no ${TIMING_FILE} was written and \`render\` will refuse. ${err instanceof Error ? err.message : err}`
11171
+ );
11029
11172
  }
11030
11173
  if (deck.page) {
11031
11174
  await write(DECK_PAGE, deck.page);
11032
- await cp(playerBundle(), join12(out, PLAYER_FILE));
11033
- files.push(join12(out, PLAYER_FILE));
11175
+ await cp2(playerBundle(), join13(out, PLAYER_FILE));
11176
+ files.push(join13(out, PLAYER_FILE));
11034
11177
  }
11035
11178
  files.push(...await vendorKatex(out));
11036
- if (opts.assetsFrom) files.push(...await copyAssets(opts.assetsFrom, out));
11179
+ files.push(...await vendorScripts(out, deck.composition));
11180
+ if (opts.assetsFrom) {
11181
+ files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
11182
+ }
11037
11183
  if (opts.narration && opts.audioFrom) {
11038
- files.push(...await copyAudio(opts.audioFrom, opts.narration, out));
11184
+ files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
11039
11185
  }
11040
11186
  const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
11041
11187
  step(
11042
- `build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join12(out, "index.html")}`
11188
+ `build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join13(out, "index.html")}`
11043
11189
  );
11044
11190
  for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
11045
11191
  for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
@@ -11055,77 +11201,21 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
11055
11201
  )}
11056
11202
  `;
11057
11203
  async function deckRuntime() {
11058
- const path2 = fileURLToPath(new URL(`./${"deck-runtime.js"}`, import.meta.url));
11204
+ const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
11059
11205
  try {
11060
- return await readFile11(path2, "utf8");
11206
+ return await readFile12(path2, "utf8");
11061
11207
  } catch {
11062
11208
  throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
11063
11209
  }
11064
11210
  }
11065
11211
  function playerBundle() {
11066
- const require2 = createRequire3(import.meta.url);
11212
+ const require2 = createRequire4(import.meta.url);
11067
11213
  try {
11068
- return join12(dirname4(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
11214
+ return join13(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
11069
11215
  } catch {
11070
11216
  throw new Error('Cannot locate the hyperframes player. Run "npm install".');
11071
11217
  }
11072
11218
  }
11073
- async function vendorKatex(out) {
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");
11077
- const written = [];
11078
- await mkdir9(join12(out, "katex/fonts"), { recursive: true });
11079
- for (const file of await readdir2(join12(dist, "fonts"))) {
11080
- if (!file.endsWith(".woff2")) continue;
11081
- await cp(join12(dist, "fonts", file), join12(out, "katex/fonts", file));
11082
- written.push(join12(out, "katex/fonts", file));
11083
- }
11084
- const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
11085
- const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
11086
- return kept ? `src:${kept}` : whole;
11087
- });
11088
- await writeFile11(join12(out, "katex/katex.min.css"), woff2Only);
11089
- written.push(join12(out, "katex/katex.min.css"));
11090
- return written;
11091
- }
11092
- async function copyAssets(sourceDir, out) {
11093
- const from = join12(resolve5(sourceDir), "assets");
11094
- if (!await stat2(from).catch(() => null)) return [];
11095
- await cp(from, join12(out, "assets"), { recursive: true });
11096
- return [join12(out, "assets")];
11097
- }
11098
- async function copyAudio(from, narration, out) {
11099
- const dir = join12(out, narration.dir);
11100
- await mkdir9(dir, { recursive: true });
11101
- const names = [
11102
- ...new Set(
11103
- Object.values(narration.beats).flat().map((s) => s.audio)
11104
- )
11105
- ].sort();
11106
- for (const name of names) {
11107
- await cp(join12(resolve5(from), name), join12(dir, name)).catch(() => {
11108
- throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
11109
- });
11110
- }
11111
- return names.map((n3) => join12(dir, n3));
11112
- }
11113
- async function refreshFont(storyboard, source, out, step) {
11114
- try {
11115
- const bundle = await bundleFont(
11116
- storyboard.lang,
11117
- JSON.stringify(source) + JSON.stringify(storyboard),
11118
- join12(out, "assets", "fonts")
11119
- );
11120
- if (bundle) step(`build: font bundle covers ${bundle.family}`);
11121
- return bundle?.css;
11122
- } catch (err) {
11123
- step(
11124
- `build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
11125
- );
11126
- return void 0;
11127
- }
11128
- }
11129
11219
 
11130
11220
  // src/server/upload.ts
11131
11221
  import { posix, sep as sep2 } from "node:path";
@@ -11470,8 +11560,8 @@ function int(name, raw2) {
11470
11560
  }
11471
11561
 
11472
11562
  // src/server/pipeline.ts
11473
- import { mkdir as mkdir10, readFile as readFile12, stat as stat3, writeFile as writeFile12 } from "node:fs/promises";
11474
- import { dirname as dirname5, isAbsolute, join as join13, relative, resolve as resolve6 } from "node:path";
11563
+ import { mkdir as mkdir11, readFile as readFile13, stat as stat3, writeFile as writeFile13 } from "node:fs/promises";
11564
+ import { dirname as dirname6, isAbsolute, join as join14, relative, resolve as resolve7 } from "node:path";
11475
11565
  var AUDIO_DIR = "audio";
11476
11566
  var NARRATION_FILE = "narration.json";
11477
11567
  var MAX_REMOTE_FIGURES = 40;
@@ -11494,10 +11584,10 @@ function stagesFor(options) {
11494
11584
  async function runPipeline(job, input) {
11495
11585
  const { options } = input;
11496
11586
  const dirs = {
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")
11587
+ upload: join14(job.dir, "upload"),
11588
+ src: join14(job.dir, "src"),
11589
+ audio: join14(job.dir, AUDIO_DIR),
11590
+ deck: join14(job.dir, "deck")
11501
11591
  };
11502
11592
  const warnings = [...options.warnings];
11503
11593
  const url = (rel) => `/d/${job.id}/${rel}`;
@@ -11521,21 +11611,21 @@ async function runPipeline(job, input) {
11521
11611
  theme: options.stated.theme ? prefs.theme : planned.theme
11522
11612
  };
11523
11613
  assertRefsResolve(storyboard, source, { pending: options.images ? "allow" : "refuse" });
11524
- await writeJson(join13(job.dir, "storyboard.json"), storyboard);
11614
+ await writeJson(join14(job.dir, "storyboard.json"), storyboard);
11525
11615
  job.done("plan", `${storyboard.beats.length} beats`);
11526
11616
  for (const f of scanBeatCount(storyboard, prefs)) warnings.push(f.message);
11527
11617
  if (options.images) {
11528
11618
  job.begin("illustrate");
11529
11619
  const drawn = await illustrate(storyboard, source, {
11530
11620
  prefs,
11531
- assetsDir: join13(dirs.src, "assets"),
11621
+ assetsDir: join14(dirs.src, "assets"),
11532
11622
  ...input.imageChain ? { chain: input.imageChain } : {},
11533
11623
  onStep: (line2) => job.log(line2)
11534
11624
  });
11535
11625
  storyboard = drawn.storyboard;
11536
11626
  source = drawn.source;
11537
- await writeJson(join13(dirs.src, "source.json"), source);
11538
- await writeJson(join13(job.dir, "storyboard.json"), storyboard);
11627
+ await writeJson(join14(dirs.src, "source.json"), source);
11628
+ await writeJson(join14(job.dir, "storyboard.json"), storyboard);
11539
11629
  for (const p of drawn.illustrated) {
11540
11630
  job.log(
11541
11631
  `illustrate: ${p.beatId} \u2192 assets/${p.src} via ${p.provider}${p.cached ? " (cached)" : ""}`
@@ -11557,12 +11647,12 @@ async function runPipeline(job, input) {
11557
11647
  warnings.push("the plan had nothing to say aloud, so the deck is silent");
11558
11648
  } else {
11559
11649
  job.log(`narrate: ${speaking} of ${storyboard.beats.length} beats speak`);
11560
- await mkdir10(dirs.audio, { recursive: true });
11650
+ await mkdir11(dirs.audio, { recursive: true });
11561
11651
  const spoken = await narrate(storyboard, source, prefs, {
11562
11652
  dir: dirs.audio,
11563
11653
  format: options.format
11564
11654
  });
11565
- await writeJson(join13(dirs.audio, NARRATION_FILE), spoken);
11655
+ await writeJson(join14(dirs.audio, NARRATION_FILE), spoken);
11566
11656
  narration = { voice: spoken.voice, dir: AUDIO_DIR, beats: spoken.beats };
11567
11657
  const segments2 = Object.values(spoken.beats).flat();
11568
11658
  const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
@@ -11616,7 +11706,7 @@ async function runPipeline(job, input) {
11616
11706
  job.log("render: capturing frames \u2014 two minutes for a four-minute video");
11617
11707
  const out = await render({
11618
11708
  deck: dirs.deck,
11619
- out: join13(dirs.deck, "video.mp4"),
11709
+ out: join14(dirs.deck, "video.mp4"),
11620
11710
  subtitles: "sidecar",
11621
11711
  log: (line2) => job.log(line2),
11622
11712
  // The last of the three length levers, and the only one that acts on a
@@ -11665,8 +11755,8 @@ async function runPipeline(job, input) {
11665
11755
  };
11666
11756
  }
11667
11757
  async function ingest(job, input, dirs, warnings) {
11668
- await mkdir10(dirs.upload, { recursive: true });
11669
- const root2 = resolve6(dirs.upload);
11758
+ await mkdir11(dirs.upload, { recursive: true });
11759
+ const root2 = resolve7(dirs.upload);
11670
11760
  let docPath;
11671
11761
  const upload = input.upload;
11672
11762
  const bothOrNeither = new UploadError(
@@ -11676,13 +11766,13 @@ async function ingest(job, input, dirs, warnings) {
11676
11766
  );
11677
11767
  if (input.url !== void 0) {
11678
11768
  if (upload !== void 0) throw bothOrNeither;
11679
- const harvested = await harvestBounded(input.url, join13(root2, "assets"), {
11769
+ const harvested = await harvestBounded(input.url, join14(root2, "assets"), {
11680
11770
  ...HARVEST_LIMITS,
11681
11771
  ...input.harvest
11682
11772
  });
11683
11773
  warnings.push(...harvested.warnings);
11684
- docPath = join13(root2, "document.md");
11685
- await writeFile12(docPath, harvested.markdown);
11774
+ docPath = join14(root2, "document.md");
11775
+ await writeFile13(docPath, harvested.markdown);
11686
11776
  job.log(
11687
11777
  `ingest: harvested ${input.url} \u2014 "${harvested.title}", ${harvested.assets.length} asset(s)`
11688
11778
  );
@@ -11698,13 +11788,13 @@ async function ingest(job, input, dirs, warnings) {
11698
11788
  const { files, warnings: zipWarnings } = readZip(upload.bytes);
11699
11789
  warnings.push(...zipWarnings);
11700
11790
  for (const [rel, bytes] of Object.entries(files)) {
11701
- const to = resolve6(join13(root2, rel));
11791
+ const to = resolve7(join14(root2, rel));
11702
11792
  if (!insideRoot2(root2, to))
11703
11793
  throw new UploadError(`Refusing to write ${rel}.`, "Re-zip from inside the folder.");
11704
- await mkdir10(dirname5(to), { recursive: true });
11705
- await writeFile12(to, bytes);
11794
+ await mkdir11(dirname6(to), { recursive: true });
11795
+ await writeFile13(to, bytes);
11706
11796
  }
11707
- docPath = join13(root2, pickMarkdown(files));
11797
+ docPath = join14(root2, pickMarkdown(files));
11708
11798
  job.log(
11709
11799
  `ingest: unpacked ${Object.keys(files).length} file(s), reading ${pickMarkdown(files)}`
11710
11800
  );
@@ -11716,10 +11806,10 @@ async function ingest(job, input, dirs, warnings) {
11716
11806
  `DeckSmith reads ${MARKDOWN_EXTS.join(", ")} or a .zip containing one. Export the document to markdown first.`
11717
11807
  );
11718
11808
  }
11719
- docPath = join13(root2, "document.md");
11720
- await writeFile12(docPath, upload.bytes);
11809
+ docPath = join14(root2, "document.md");
11810
+ await writeFile13(docPath, upload.bytes);
11721
11811
  }
11722
- const text2 = await readFile12(docPath, "utf8");
11812
+ const text2 = await readFile13(docPath, "utf8");
11723
11813
  const parsed = parseMarkdown(text2, {
11724
11814
  lang: input.options.stated.lang ? input.options.prefs.lang : void 0
11725
11815
  });
@@ -11739,8 +11829,8 @@ async function ingest(job, input, dirs, warnings) {
11739
11829
  `ingest: ${parsed.sections.length} sections, ${parsed.figures.length} figures, ${parsed.equations.length} equations, ${parsed.tables.length} tables`
11740
11830
  );
11741
11831
  const guarded = await guardFigures(parsed, root2, input.fetchRemoteFigures, warnings);
11742
- const source = await fetchFigures(guarded, join13(resolve6(dirs.src), "assets"), warnings);
11743
- await writeJson(join13(dirs.src, "source.json"), source);
11832
+ const source = await fetchFigures(guarded, join14(resolve7(dirs.src), "assets"), warnings);
11833
+ await writeJson(join14(dirs.src, "source.json"), source);
11744
11834
  return source;
11745
11835
  }
11746
11836
  async function harvestBounded(url, dir, opts) {
@@ -11816,7 +11906,7 @@ async function guardFigures(source, root2, allowRemote, warnings) {
11816
11906
  warnings.push(`figure ${figure.id} was left out: "${scheme}:" figures are not read`);
11817
11907
  continue;
11818
11908
  }
11819
- const abs = isAbsolute(src) ? resolve6(src) : resolve6(join13(root2, src.replace(/^\.?\//, "")));
11909
+ const abs = isAbsolute(src) ? resolve7(src) : resolve7(join14(root2, src.replace(/^\.?\//, "")));
11820
11910
  if (!insideRoot2(root2, abs)) {
11821
11911
  warnings.push(`figure ${figure.id} was left out: "${src}" points outside the upload`);
11822
11912
  continue;
@@ -11831,19 +11921,19 @@ async function guardFigures(source, root2, allowRemote, warnings) {
11831
11921
  }
11832
11922
  async function pack2(job, ctx) {
11833
11923
  try {
11834
- const assets = join13(resolve6(ctx.dirs.src), "assets");
11924
+ const assets = join14(resolve7(ctx.dirs.src), "assets");
11835
11925
  const requests = ctx.source.figures.map((f) => ({
11836
11926
  id: f.id,
11837
- url: join13(assets, f.src),
11927
+ url: join14(assets, f.src),
11838
11928
  prefer: "bake"
11839
11929
  }));
11840
11930
  const plan = await planMedia(requests, async (url) => ({
11841
- bytes: new Uint8Array(await readFile12(url))
11931
+ bytes: new Uint8Array(await readFile13(url))
11842
11932
  }));
11843
11933
  const files = { ...plan.files };
11844
11934
  if (ctx.narration) {
11845
- for (const name of audioNames(ctx.narration)) {
11846
- files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile12(join13(ctx.dirs.audio, name)));
11935
+ for (const name of audioNames2(ctx.narration)) {
11936
+ files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile13(join14(ctx.dirs.audio, name)));
11847
11937
  }
11848
11938
  }
11849
11939
  const container = {
@@ -11860,10 +11950,10 @@ async function pack2(job, ctx) {
11860
11950
  },
11861
11951
  source: ctx.source,
11862
11952
  storyboard: ctx.storyboard,
11863
- ...ctx.narration ? { narration: await readJson(join13(ctx.dirs.audio, NARRATION_FILE)) } : {},
11953
+ ...ctx.narration ? { narration: await readJson(join14(ctx.dirs.audio, NARRATION_FILE)) } : {},
11864
11954
  media: plan.media
11865
11955
  };
11866
- const bytes = await writePack(container, files, join13(ctx.dirs.deck, "deck.deck"));
11956
+ const bytes = await writePack(container, files, join14(ctx.dirs.deck, "deck.deck"));
11867
11957
  job.log(`pack: ${Math.round(bytes / 1024)} KB \u2192 deck.deck`);
11868
11958
  return ctx.url("deck.deck");
11869
11959
  } catch (err) {
@@ -11871,7 +11961,7 @@ async function pack2(job, ctx) {
11871
11961
  return void 0;
11872
11962
  }
11873
11963
  }
11874
- function audioNames(narration) {
11964
+ function audioNames2(narration) {
11875
11965
  return [
11876
11966
  ...new Set(
11877
11967
  Object.values(narration.beats).flat().map((s) => s.audio)
@@ -11886,12 +11976,12 @@ function host(src) {
11886
11976
  }
11887
11977
  }
11888
11978
  async function writeJson(path2, value) {
11889
- await mkdir10(dirname5(path2), { recursive: true });
11890
- await writeFile12(path2, `${JSON.stringify(value, null, 2)}
11979
+ await mkdir11(dirname6(path2), { recursive: true });
11980
+ await writeFile13(path2, `${JSON.stringify(value, null, 2)}
11891
11981
  `);
11892
11982
  }
11893
11983
  async function readJson(path2) {
11894
- return JSON.parse(await readFile12(path2, "utf8"));
11984
+ return JSON.parse(await readFile13(path2, "utf8"));
11895
11985
  }
11896
11986
 
11897
11987
  // src/server/errors.ts
@@ -12297,14 +12387,14 @@ var PAGE_BUDGET = {
12297
12387
  refs: "relative"
12298
12388
  };
12299
12389
  async function readPage(url, work2) {
12300
- await mkdir11(work2, { recursive: true });
12301
- const dir = await mkdtemp3(join14(work2, "harvest-"));
12390
+ await mkdir12(work2, { recursive: true });
12391
+ const dir = await mkdtemp3(join15(work2, "harvest-"));
12302
12392
  try {
12303
12393
  const page = await harvest(url, dir, PAGE_BUDGET);
12304
12394
  const files = {
12305
12395
  "document.md": new TextEncoder().encode(page.markdown)
12306
12396
  };
12307
- for (const asset of page.assets) files[basename5(asset)] = await readFile13(asset);
12397
+ for (const asset of page.assets) files[basename5(asset)] = await readFile14(asset);
12308
12398
  const warnings = [...page.warnings];
12309
12399
  if (page.clips.length > 0) {
12310
12400
  warnings.push(
@@ -12338,7 +12428,7 @@ var statusSchema = z5.object({
12338
12428
  var DEFAULT_WAIT = 45;
12339
12429
  function deckTools(opts) {
12340
12430
  const queue = new Queue({ maxQueued: 8 });
12341
- const root2 = resolve7(opts.root);
12431
+ const root2 = resolve8(opts.root);
12342
12432
  let cached2;
12343
12433
  const found = async () => {
12344
12434
  cached2 ??= await (opts.probe ?? prereqs)();
@@ -12346,7 +12436,7 @@ function deckTools(opts) {
12346
12436
  };
12347
12437
  const insideRoot3 = (p) => {
12348
12438
  if (!isAbsolute2(p)) throw new Error(`document_path must be absolute; got "${p}".`);
12349
- const full = resolve7(p);
12439
+ const full = resolve8(p);
12350
12440
  if (full !== root2 && !full.startsWith(root2 + sep3)) {
12351
12441
  throw new Error(
12352
12442
  `document_path is outside the server's root. It may only read files under ${root2}.`
@@ -12425,15 +12515,15 @@ function deckTools(opts) {
12425
12515
  }
12426
12516
  const page = input.document_url === void 0 ? void 0 : await readPage(input.document_url, opts.work);
12427
12517
  const filename = page ? "page.zip" : input.document_path ? input.document_path.split(sep3).pop() : "document.md";
12428
- const bytes = page ? page.zip : file ? await readFile13(file) : new TextEncoder().encode(input.document_text);
12518
+ const bytes = page ? page.zip : file ? await readFile14(file) : new TextEncoder().encode(input.document_text);
12429
12519
  if (bytes.byteLength > MAX_UPLOAD_BYTES) {
12430
12520
  throw new Error(
12431
12521
  `Document is ${(bytes.byteLength / 1e6).toFixed(1)} MB, over the ${(MAX_UPLOAD_BYTES / 1e6).toFixed(0)} MB cap.` + (page ? " Harvest fewer figures with a smaller maxAssets, or save the page and edit it." : "")
12432
12522
  );
12433
12523
  }
12434
12524
  const id2 = randomBytes(16).toString("base64url");
12435
- const dir = join14(opts.work, id2);
12436
- await mkdir11(dir, { recursive: true });
12525
+ const dir = join15(opts.work, id2);
12526
+ await mkdir12(dir, { recursive: true });
12437
12527
  queue.submit({
12438
12528
  id: id2,
12439
12529
  dir,
@@ -12460,7 +12550,7 @@ function deckTools(opts) {
12460
12550
  };
12461
12551
  }
12462
12552
  function dirOf(work2, id2) {
12463
- return join14(work2, id2);
12553
+ return join15(work2, id2);
12464
12554
  }
12465
12555
  function waitFor(queue, id2, seconds) {
12466
12556
  const now = queue.view(id2);
@@ -12492,9 +12582,9 @@ function report(view, dir) {
12492
12582
  // (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
12493
12583
  // is the shape of the CLI's output directory and not the server's, and an
12494
12584
  // agent told to read it would have found nothing there.
12495
- storyboard_path: join14(dir, "storyboard.json"),
12585
+ storyboard_path: join15(dir, "storyboard.json"),
12496
12586
  ...done && view.result ? {
12497
- deck_path: join14(dir, "deck"),
12587
+ deck_path: join15(dir, "deck"),
12498
12588
  slides: view.result.slides,
12499
12589
  duration_seconds: view.result.duration,
12500
12590
  warnings: view.result.warnings
@@ -12503,7 +12593,7 @@ function report(view, dir) {
12503
12593
  };
12504
12594
  }
12505
12595
  function defaultWork() {
12506
- return join14(tmpdir4(), "decksmith-mcp");
12596
+ return join15(tmpdir4(), "decksmith-mcp");
12507
12597
  }
12508
12598
 
12509
12599
  // src/mcp/main.ts