@jokerized/decksmith 0.4.0 → 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/README.md +13 -3
- package/dist/cli.js +529 -388
- package/dist/deck-runtime.js +5 -2
- package/dist/index.js +490 -316
- package/dist/mcp.js +409 -307
- package/dist/types/build/files.d.ts +102 -0
- package/dist/types/deck/runtime.d.ts +4 -2
- package/dist/types/emit/kit.d.ts +32 -0
- package/dist/types/index.d.ts +10 -6
- package/dist/types/render/capture.d.ts +27 -1
- package/dist/types/render/render.d.ts +40 -12
- package/dist/types/render/timing.d.ts +11 -0
- package/dist/types/types.d.ts +64 -0
- package/dist/types/verify/fidelity.d.ts +55 -0
- package/dist/types/verify/index.d.ts +8 -3
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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
|
|
1823
|
-
for (const [lo, hi, units] of BLOCK_ADVANCE) if (
|
|
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: ${
|
|
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
|
|
6709
|
-
import { basename as basename3, join as
|
|
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((
|
|
6910
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
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,29 +7127,37 @@ function message(err) {
|
|
|
6991
7127
|
|
|
6992
7128
|
// src/render/capture.ts
|
|
6993
7129
|
import { homedir } from "node:os";
|
|
6994
|
-
import { join as
|
|
6995
|
-
async function
|
|
6996
|
-
const
|
|
6997
|
-
if (
|
|
7130
|
+
import { join as join4 } from "node:path";
|
|
7131
|
+
async function resolveChrome(need = "open the deck with") {
|
|
7132
|
+
const env2 = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
7133
|
+
if (env2) return { path: env2, source: "env" };
|
|
6998
7134
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6999
|
-
const
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7135
|
+
const pick = async (cacheDir) => {
|
|
7136
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
7137
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
7138
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
7139
|
+
};
|
|
7140
|
+
const hf = await pick(join4(homedir(), ".cache", "hyperframes", "chrome"));
|
|
7141
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
7142
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join4(homedir(), ".cache", "puppeteer"));
|
|
7143
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
7003
7144
|
throw new Error(
|
|
7004
7145
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
7005
7146
|
);
|
|
7006
7147
|
}
|
|
7148
|
+
async function chromePath(need = "open the deck with") {
|
|
7149
|
+
return (await resolveChrome(need)).path;
|
|
7150
|
+
}
|
|
7007
7151
|
|
|
7008
7152
|
// src/source/assets.ts
|
|
7009
7153
|
import { createHash as createHash4 } from "node:crypto";
|
|
7010
|
-
import { mkdir as
|
|
7011
|
-
import { extname as extname2, join as
|
|
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";
|
|
7012
7156
|
import { z as z2 } from "zod";
|
|
7013
7157
|
async function fetchFigures(source, dir, warnings) {
|
|
7014
7158
|
const drops = warnings ?? [];
|
|
7015
|
-
await
|
|
7016
|
-
const cached2 = await
|
|
7159
|
+
await mkdir3(dir, { recursive: true });
|
|
7160
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
7017
7161
|
const figures = [];
|
|
7018
7162
|
for (const figure of source.figures) {
|
|
7019
7163
|
if (figure.kind === "clip") {
|
|
@@ -7038,11 +7182,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
7038
7182
|
async function localize(figure, dir, cached2) {
|
|
7039
7183
|
const stem = assetStem(figure.id, figure.src);
|
|
7040
7184
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7041
|
-
const bytes = hit ? await
|
|
7185
|
+
const bytes = hit ? await readFile4(join5(dir, hit)) : await load(figure.src);
|
|
7042
7186
|
const size = imageSize(bytes);
|
|
7043
7187
|
if (hit) return { ...figure, src: hit, ...size };
|
|
7044
7188
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
7045
|
-
await
|
|
7189
|
+
await writeFile3(join5(dir, src), bytes);
|
|
7046
7190
|
cached2.push(src);
|
|
7047
7191
|
return { ...figure, src, ...size };
|
|
7048
7192
|
}
|
|
@@ -7067,7 +7211,7 @@ function assetExt(bytes, src) {
|
|
|
7067
7211
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
7068
7212
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
7069
7213
|
async function load(src) {
|
|
7070
|
-
if (!/^https?:/i.test(src)) return
|
|
7214
|
+
if (!/^https?:/i.test(src)) return readFile4(src);
|
|
7071
7215
|
const got = await fetchGuarded(src, {
|
|
7072
7216
|
maxBytes: FIGURE_MAX_BYTES,
|
|
7073
7217
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -7603,7 +7747,7 @@ function readContentRegion() {
|
|
|
7603
7747
|
}
|
|
7604
7748
|
|
|
7605
7749
|
// src/source/transcode.ts
|
|
7606
|
-
import { readFile as
|
|
7750
|
+
import { readFile as readFile5, rm } from "node:fs/promises";
|
|
7607
7751
|
import { basename as basename2 } from "node:path";
|
|
7608
7752
|
|
|
7609
7753
|
// src/render/ffmpeg.ts
|
|
@@ -7629,7 +7773,7 @@ ${tail}`);
|
|
|
7629
7773
|
}
|
|
7630
7774
|
}
|
|
7631
7775
|
function runLive(file, args) {
|
|
7632
|
-
return new Promise((
|
|
7776
|
+
return new Promise((resolve9, reject) => {
|
|
7633
7777
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7634
7778
|
child.on("error", (err) => {
|
|
7635
7779
|
reject(
|
|
@@ -7637,7 +7781,7 @@ function runLive(file, args) {
|
|
|
7637
7781
|
);
|
|
7638
7782
|
});
|
|
7639
7783
|
child.on("close", (code, signal) => {
|
|
7640
|
-
if (code === 0)
|
|
7784
|
+
if (code === 0) resolve9();
|
|
7641
7785
|
else if (signal) {
|
|
7642
7786
|
reject(
|
|
7643
7787
|
new Error(
|
|
@@ -7764,11 +7908,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
7764
7908
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
7765
7909
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
7766
7910
|
// three-line band covers the bottom of the slide.
|
|
7767
|
-
|
|
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),
|
|
7768
7917
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
7769
7918
|
// player draws across the bottom eighth of the frame.
|
|
7770
|
-
marginV: Math.round(height *
|
|
7771
|
-
marginX: Math.round(width *
|
|
7919
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
7920
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
7772
7921
|
};
|
|
7773
7922
|
}
|
|
7774
7923
|
|
|
@@ -7834,7 +7983,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7834
7983
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7835
7984
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7836
7985
|
const name = basename2(input);
|
|
7837
|
-
const source = videoSize(await
|
|
7986
|
+
const source = videoSize(await readFile5(input));
|
|
7838
7987
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7839
7988
|
const kept = (why2) => ({
|
|
7840
7989
|
path: input,
|
|
@@ -7866,7 +8015,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7866
8015
|
}
|
|
7867
8016
|
let measured;
|
|
7868
8017
|
try {
|
|
7869
|
-
measured = videoSize(await
|
|
8018
|
+
measured = videoSize(await readFile5(out));
|
|
7870
8019
|
} catch (err) {
|
|
7871
8020
|
await rm(out, { force: true });
|
|
7872
8021
|
return kept(
|
|
@@ -7902,8 +8051,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7902
8051
|
async function harvest(url, dir, opts = {}) {
|
|
7903
8052
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7904
8053
|
const warnings = [];
|
|
7905
|
-
const assets =
|
|
7906
|
-
await
|
|
8054
|
+
const assets = resolve3(dir);
|
|
8055
|
+
await mkdir4(assets, { recursive: true });
|
|
7907
8056
|
const page = await fetchGuarded(url, {
|
|
7908
8057
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
7909
8058
|
timeoutMs,
|
|
@@ -7964,7 +8113,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
7964
8113
|
"--disable-background-networking",
|
|
7965
8114
|
"--disable-extensions",
|
|
7966
8115
|
"--no-default-browser-check",
|
|
7967
|
-
"--no-first-run"
|
|
8116
|
+
"--no-first-run",
|
|
8117
|
+
// The floor under interception — see the note above. Not a hardening
|
|
8118
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
8119
|
+
// happens to be that run.
|
|
8120
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
7968
8121
|
]
|
|
7969
8122
|
});
|
|
7970
8123
|
try {
|
|
@@ -8281,8 +8434,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8281
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`
|
|
8282
8435
|
);
|
|
8283
8436
|
}
|
|
8284
|
-
const path2 =
|
|
8285
|
-
await
|
|
8437
|
+
const path2 = join6(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8438
|
+
await writeFile4(path2, bytes);
|
|
8286
8439
|
assets.push(path2);
|
|
8287
8440
|
got = { path: path2, width: size.width, height: size.height };
|
|
8288
8441
|
} catch (err) {
|
|
@@ -8294,7 +8447,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8294
8447
|
};
|
|
8295
8448
|
const shrink = async (url, path2, measured, what) => {
|
|
8296
8449
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8297
|
-
const out2 =
|
|
8450
|
+
const out2 = join6(dir, assetName(url, ".vp9.webm"));
|
|
8298
8451
|
let small;
|
|
8299
8452
|
try {
|
|
8300
8453
|
small = await transcode(path2, out2, {
|
|
@@ -8336,8 +8489,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8336
8489
|
try {
|
|
8337
8490
|
const bytes = await bytesOf(url);
|
|
8338
8491
|
const measured = videoSize(bytes);
|
|
8339
|
-
const path2 =
|
|
8340
|
-
await
|
|
8492
|
+
const path2 = join6(dir, assetName(url, `.${measured.container}`));
|
|
8493
|
+
await writeFile4(path2, bytes);
|
|
8341
8494
|
return await shrink(url, path2, measured, what);
|
|
8342
8495
|
} catch (err) {
|
|
8343
8496
|
warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
|
|
@@ -8426,7 +8579,7 @@ function unheld(policy) {
|
|
|
8426
8579
|
return "the file could not be held";
|
|
8427
8580
|
}
|
|
8428
8581
|
async function sizeOf(path2) {
|
|
8429
|
-
return await
|
|
8582
|
+
return await stat2(path2).then(
|
|
8430
8583
|
(s) => s.size,
|
|
8431
8584
|
() => 0
|
|
8432
8585
|
);
|
|
@@ -8700,9 +8853,9 @@ ${fence}`;
|
|
|
8700
8853
|
|
|
8701
8854
|
// src/plan/codex.ts
|
|
8702
8855
|
import { spawn as spawn2 } from "node:child_process";
|
|
8703
|
-
import { mkdtemp, readFile as
|
|
8856
|
+
import { mkdtemp, readFile as readFile6, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
8704
8857
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8705
|
-
import { join as
|
|
8858
|
+
import { join as join7 } from "node:path";
|
|
8706
8859
|
import { z as z3 } from "zod";
|
|
8707
8860
|
|
|
8708
8861
|
// src/plan/arc.ts
|
|
@@ -9720,11 +9873,11 @@ function schemaFor(prefs) {
|
|
|
9720
9873
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9721
9874
|
async function codexPlanner(source, opts = {}) {
|
|
9722
9875
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9723
|
-
const dir = await mkdtemp(
|
|
9876
|
+
const dir = await mkdtemp(join7(tmpdir2(), "decksmith-plan-"));
|
|
9724
9877
|
try {
|
|
9725
|
-
const schemaPath =
|
|
9726
|
-
const outPath =
|
|
9727
|
-
await
|
|
9878
|
+
const schemaPath = join7(dir, "storyboard.schema.json");
|
|
9879
|
+
const outPath = join7(dir, "storyboard.json");
|
|
9880
|
+
await writeFile5(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9728
9881
|
await (opts.run ?? runCodex)({
|
|
9729
9882
|
prompt: buildPrompt(source, prefs),
|
|
9730
9883
|
schemaPath,
|
|
@@ -9732,7 +9885,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
9732
9885
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
9733
9886
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
9734
9887
|
});
|
|
9735
|
-
const raw2 = await
|
|
9888
|
+
const raw2 = await readFile6(outPath, "utf8").catch(() => "");
|
|
9736
9889
|
if (raw2.trim() === "") {
|
|
9737
9890
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
9738
9891
|
}
|
|
@@ -9805,7 +9958,7 @@ function codexCommand(args) {
|
|
|
9805
9958
|
}
|
|
9806
9959
|
function runCodex(args) {
|
|
9807
9960
|
const { argv, env: env2 } = codexCommand(args);
|
|
9808
|
-
return new Promise((
|
|
9961
|
+
return new Promise((resolve9, reject) => {
|
|
9809
9962
|
const child = spawn2("codex", argv, {
|
|
9810
9963
|
stdio: ["pipe", "ignore", "pipe"],
|
|
9811
9964
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -9828,7 +9981,7 @@ function runCodex(args) {
|
|
|
9828
9981
|
});
|
|
9829
9982
|
child.on("close", (code) => {
|
|
9830
9983
|
clearTimeout(timer);
|
|
9831
|
-
if (code === 0) return
|
|
9984
|
+
if (code === 0) return resolve9();
|
|
9832
9985
|
reject(
|
|
9833
9986
|
new Error(`codex exec exited ${code}.
|
|
9834
9987
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -9840,14 +9993,14 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
9840
9993
|
|
|
9841
9994
|
// src/images/illustrate.ts
|
|
9842
9995
|
import { createHash as createHash7 } from "node:crypto";
|
|
9843
|
-
import { mkdir as
|
|
9844
|
-
import { join as
|
|
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";
|
|
9845
9998
|
|
|
9846
9999
|
// src/images/providers.ts
|
|
9847
10000
|
import { createHash as createHash6 } from "node:crypto";
|
|
9848
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
10001
|
+
import { mkdtemp as mkdtemp2, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
9849
10002
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
9850
|
-
import { join as
|
|
10003
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
9851
10004
|
import { z as z4 } from "zod";
|
|
9852
10005
|
var SIZE = {
|
|
9853
10006
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -9979,11 +10132,11 @@ function codexImages(opts = {}) {
|
|
|
9979
10132
|
async check() {
|
|
9980
10133
|
},
|
|
9981
10134
|
async generate(req) {
|
|
9982
|
-
const dir = await mkdtemp2(
|
|
10135
|
+
const dir = await mkdtemp2(join8(tmpdir3(), "decksmith-image-"));
|
|
9983
10136
|
try {
|
|
9984
|
-
const schemaPath =
|
|
9985
|
-
const outPath =
|
|
9986
|
-
await
|
|
10137
|
+
const schemaPath = join8(dir, "answer.schema.json");
|
|
10138
|
+
const outPath = join8(dir, "answer.json");
|
|
10139
|
+
await writeFile6(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
9987
10140
|
await run5({
|
|
9988
10141
|
prompt: codexPrompt(req),
|
|
9989
10142
|
schemaPath,
|
|
@@ -9992,7 +10145,7 @@ function codexImages(opts = {}) {
|
|
|
9992
10145
|
cwd: dir,
|
|
9993
10146
|
sandbox: "workspace-write"
|
|
9994
10147
|
});
|
|
9995
|
-
const raw2 = await
|
|
10148
|
+
const raw2 = await readFile7(outPath, "utf8").catch(() => "");
|
|
9996
10149
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
9997
10150
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
9998
10151
|
if (!answer.data.ok) {
|
|
@@ -10000,10 +10153,10 @@ function codexImages(opts = {}) {
|
|
|
10000
10153
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
10001
10154
|
);
|
|
10002
10155
|
}
|
|
10003
|
-
const candidates2 = [
|
|
10004
|
-
if (answer.data.file) candidates2.push(
|
|
10156
|
+
const candidates2 = [join8(dir, "picture.png")];
|
|
10157
|
+
if (answer.data.file) candidates2.push(resolve4(dir, answer.data.file));
|
|
10005
10158
|
for (const path2 of candidates2) {
|
|
10006
|
-
const bytes = await
|
|
10159
|
+
const bytes = await readFile7(path2).catch(() => null);
|
|
10007
10160
|
if (bytes) return raster(bytes, "codex");
|
|
10008
10161
|
}
|
|
10009
10162
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -10135,7 +10288,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
10135
10288
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
10136
10289
|
if (tool !== last) chain.push(tool);
|
|
10137
10290
|
const dropped = /* @__PURE__ */ new Set();
|
|
10138
|
-
if (pending.length > 0) await
|
|
10291
|
+
if (pending.length > 0) await mkdir5(opts.assetsDir, { recursive: true });
|
|
10139
10292
|
for (const [i, slot] of pending.entries()) {
|
|
10140
10293
|
let rungs = chain;
|
|
10141
10294
|
if (i >= images.max) {
|
|
@@ -10232,16 +10385,16 @@ function cacheKey(providerId, req) {
|
|
|
10232
10385
|
async function draw(provider, req, name, dir) {
|
|
10233
10386
|
for (const ext of Object.values(EXT2)) {
|
|
10234
10387
|
const src2 = `${name}${ext}`;
|
|
10235
|
-
const bytes = await
|
|
10388
|
+
const bytes = await readFile8(join9(dir, src2)).catch(() => null);
|
|
10236
10389
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
10237
10390
|
}
|
|
10238
10391
|
await provider.check();
|
|
10239
10392
|
const img = await provider.generate(req);
|
|
10240
10393
|
const src = `${name}${EXT2[img.mime]}`;
|
|
10241
|
-
const tmp =
|
|
10394
|
+
const tmp = join9(dir, `.${name}.tmp`);
|
|
10242
10395
|
try {
|
|
10243
|
-
await
|
|
10244
|
-
await rename(tmp,
|
|
10396
|
+
await writeFile7(tmp, img.bytes);
|
|
10397
|
+
await rename(tmp, join9(dir, src));
|
|
10245
10398
|
} finally {
|
|
10246
10399
|
await rm5(tmp, { force: true });
|
|
10247
10400
|
}
|
|
@@ -10254,9 +10407,9 @@ function sizeOf2(bytes, ext) {
|
|
|
10254
10407
|
// src/narrate/tts.ts
|
|
10255
10408
|
import { spawn as spawn3 } from "node:child_process";
|
|
10256
10409
|
import { createHash as createHash8 } from "node:crypto";
|
|
10257
|
-
import { mkdir as
|
|
10410
|
+
import { mkdir as mkdir6, readFile as readFile9, rm as rm6, writeFile as writeFile8 } from "node:fs/promises";
|
|
10258
10411
|
import { homedir as homedir2 } from "node:os";
|
|
10259
|
-
import { join as
|
|
10412
|
+
import { join as join10 } from "node:path";
|
|
10260
10413
|
var MISSING = [
|
|
10261
10414
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
10262
10415
|
"",
|
|
@@ -10273,8 +10426,8 @@ function candidates() {
|
|
|
10273
10426
|
["edge-tts"],
|
|
10274
10427
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
10275
10428
|
// default, and both are where this actually lands in practice.
|
|
10276
|
-
[
|
|
10277
|
-
[
|
|
10429
|
+
[join10(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10430
|
+
[join10(home, ".local", "bin", "edge-tts")],
|
|
10278
10431
|
// Last resort: the module is installed even though its console script is
|
|
10279
10432
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
10280
10433
|
["python3", "-m", "edge_tts"]
|
|
@@ -10299,7 +10452,7 @@ async function find2(can) {
|
|
|
10299
10452
|
throw new Error(MISSING);
|
|
10300
10453
|
}
|
|
10301
10454
|
function runArgv(cmd, args) {
|
|
10302
|
-
return new Promise((
|
|
10455
|
+
return new Promise((resolve9) => {
|
|
10303
10456
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10304
10457
|
let stdout = "";
|
|
10305
10458
|
let stderr = "";
|
|
@@ -10309,8 +10462,8 @@ function runArgv(cmd, args) {
|
|
|
10309
10462
|
child.stderr.on("data", (b) => {
|
|
10310
10463
|
stderr += b.toString();
|
|
10311
10464
|
});
|
|
10312
|
-
child.on("error", (e) =>
|
|
10313
|
-
child.on("close", (code) =>
|
|
10465
|
+
child.on("error", (e) => resolve9({ code: -1, stderr: String(e), stdout }));
|
|
10466
|
+
child.on("close", (code) => resolve9({ code: code ?? -1, stderr, stdout }));
|
|
10314
10467
|
});
|
|
10315
10468
|
}
|
|
10316
10469
|
var edgeTts = {
|
|
@@ -10377,7 +10530,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
10377
10530
|
async speak(req) {
|
|
10378
10531
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
10379
10532
|
await runner.speak({ ...req, subs });
|
|
10380
|
-
const raw2 = await
|
|
10533
|
+
const raw2 = await readFile9(subs, "utf8").catch(() => "");
|
|
10381
10534
|
await rm6(subs, { force: true });
|
|
10382
10535
|
const cues = parseCues(raw2);
|
|
10383
10536
|
const measured = await runner.measure(req.audio);
|
|
@@ -10395,11 +10548,11 @@ async function synthesize(text2, opts) {
|
|
|
10395
10548
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10396
10549
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10397
10550
|
const file = `${key}.mp3`;
|
|
10398
|
-
const audio =
|
|
10399
|
-
const sidecar =
|
|
10551
|
+
const audio = join10(opts.dir, file);
|
|
10552
|
+
const sidecar = join10(opts.dir, `${key}.json`);
|
|
10400
10553
|
const cached2 = await readSidecar(sidecar);
|
|
10401
10554
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10402
|
-
await
|
|
10555
|
+
await mkdir6(opts.dir, { recursive: true });
|
|
10403
10556
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10404
10557
|
const { cues, seconds } = spoken;
|
|
10405
10558
|
const clamped = cues.map((c) => ({
|
|
@@ -10408,13 +10561,13 @@ async function synthesize(text2, opts) {
|
|
|
10408
10561
|
text: c.text
|
|
10409
10562
|
}));
|
|
10410
10563
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
10411
|
-
await
|
|
10564
|
+
await writeFile8(sidecar, `${JSON.stringify(body, null, 2)}
|
|
10412
10565
|
`);
|
|
10413
10566
|
return { audio, file, seconds, cues: clamped };
|
|
10414
10567
|
}
|
|
10415
10568
|
async function readSidecar(path2) {
|
|
10416
10569
|
try {
|
|
10417
|
-
const parsed = JSON.parse(await
|
|
10570
|
+
const parsed = JSON.parse(await readFile9(path2, "utf8"));
|
|
10418
10571
|
const s = parsed;
|
|
10419
10572
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
10420
10573
|
} catch {
|
|
@@ -10561,12 +10714,12 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
10561
10714
|
}
|
|
10562
10715
|
|
|
10563
10716
|
// src/render/render.ts
|
|
10564
|
-
import { mkdir as
|
|
10565
|
-
import { basename as basename4, dirname as
|
|
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";
|
|
10566
10719
|
|
|
10567
10720
|
// src/render/captions.ts
|
|
10568
|
-
import { mkdir as
|
|
10569
|
-
import { join as
|
|
10721
|
+
import { mkdir as mkdir7, writeFile as writeFile9 } from "node:fs/promises";
|
|
10722
|
+
import { join as join11 } from "node:path";
|
|
10570
10723
|
import { pathToFileURL } from "node:url";
|
|
10571
10724
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
10572
10725
|
function cssString(value) {
|
|
@@ -10657,11 +10810,11 @@ async function captionBlocker() {
|
|
|
10657
10810
|
}
|
|
10658
10811
|
async function renderCaptions(cues, style, deck, work2) {
|
|
10659
10812
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
10660
|
-
const fontCss =
|
|
10813
|
+
const fontCss = join11(deck, DECK_FONT_CSS);
|
|
10661
10814
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL(fontCss).href : null;
|
|
10662
|
-
const page =
|
|
10663
|
-
await
|
|
10664
|
-
await
|
|
10815
|
+
const page = join11(work2, "captions.html");
|
|
10816
|
+
await mkdir7(work2, { recursive: true });
|
|
10817
|
+
await writeFile9(page, captionPage(cues, style, href));
|
|
10665
10818
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
10666
10819
|
const browser = await puppeteer.launch({
|
|
10667
10820
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -10681,7 +10834,7 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
10681
10834
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
10682
10835
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
10683
10836
|
await tab.screenshot({
|
|
10684
|
-
path:
|
|
10837
|
+
path: join11(work2, name),
|
|
10685
10838
|
type: "png",
|
|
10686
10839
|
omitBackground: true,
|
|
10687
10840
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -10722,9 +10875,9 @@ function subtitlePlan(mode) {
|
|
|
10722
10875
|
async function render(opts) {
|
|
10723
10876
|
const log = opts.log ?? (() => {
|
|
10724
10877
|
});
|
|
10725
|
-
const deck =
|
|
10726
|
-
const out =
|
|
10727
|
-
const work2 =
|
|
10878
|
+
const deck = resolve5(opts.deck);
|
|
10879
|
+
const out = resolve5(opts.out);
|
|
10880
|
+
const work2 = join12(dirname3(out), `.${basename4(out)}.parts`);
|
|
10728
10881
|
const timing = await readTiming(deck);
|
|
10729
10882
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
10730
10883
|
const refusal = playbackRefusal(
|
|
@@ -10736,13 +10889,17 @@ async function render(opts) {
|
|
|
10736
10889
|
}
|
|
10737
10890
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
10738
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
|
+
);
|
|
10739
10896
|
const blocker = await captionBlocker();
|
|
10740
10897
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
10741
10898
|
}
|
|
10742
10899
|
const burnable = plan0.burn;
|
|
10743
|
-
await
|
|
10900
|
+
await mkdir8(work2, { recursive: true });
|
|
10744
10901
|
try {
|
|
10745
|
-
const raw2 = opts.video ?
|
|
10902
|
+
const raw2 = opts.video ? resolve5(opts.video) : await capture(deck, join12(work2, "raw.mp4"), opts, log);
|
|
10746
10903
|
const shot = await probe(raw2);
|
|
10747
10904
|
log(
|
|
10748
10905
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -10755,18 +10912,18 @@ async function render(opts) {
|
|
|
10755
10912
|
);
|
|
10756
10913
|
}
|
|
10757
10914
|
const retimed = await retime(raw2, plan, work2, log);
|
|
10758
|
-
const srtPath =
|
|
10915
|
+
const srtPath = join12(dirname3(out), SRT_NAME(out));
|
|
10759
10916
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
10760
10917
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
10761
10918
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
10762
|
-
if (srt) await
|
|
10919
|
+
if (srt) await writeFile10(srtPath, srt);
|
|
10763
10920
|
const burn = burnable && srt.length > 0;
|
|
10764
10921
|
await mux(retimed, timing, plan, deck, out, work2, burn ? plan.cues : void 0, shot.fps, log);
|
|
10765
10922
|
if (playback > 1) {
|
|
10766
10923
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
10767
10924
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
10768
10925
|
if (warning) log(`render: ${warning}`);
|
|
10769
|
-
const fast =
|
|
10926
|
+
const fast = join12(work2, `fast.${basename4(out)}`);
|
|
10770
10927
|
const muxed = await probe(out);
|
|
10771
10928
|
await runTool(
|
|
10772
10929
|
"ffmpeg",
|
|
@@ -10796,8 +10953,8 @@ async function render(opts) {
|
|
|
10796
10953
|
}
|
|
10797
10954
|
}
|
|
10798
10955
|
async function readTiming(deck) {
|
|
10799
|
-
const path2 =
|
|
10800
|
-
const text2 = await
|
|
10956
|
+
const path2 = join12(deck, TIMING_FILE);
|
|
10957
|
+
const text2 = await readFile10(path2, "utf8").catch(() => {
|
|
10801
10958
|
throw new Error(
|
|
10802
10959
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
10803
10960
|
);
|
|
@@ -10836,20 +10993,20 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10836
10993
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
10837
10994
|
const list = [];
|
|
10838
10995
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
10839
|
-
const file =
|
|
10996
|
+
const file = join12(work2, `p${String(i).padStart(4, "0")}.ts`);
|
|
10840
10997
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
10841
10998
|
list.push(file);
|
|
10842
10999
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
10843
11000
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
10844
11001
|
}
|
|
10845
11002
|
}
|
|
10846
|
-
const listFile =
|
|
10847
|
-
await
|
|
11003
|
+
const listFile = join12(work2, "pieces.txt");
|
|
11004
|
+
await writeFile10(
|
|
10848
11005
|
listFile,
|
|
10849
11006
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
10850
11007
|
`
|
|
10851
11008
|
);
|
|
10852
|
-
const joined =
|
|
11009
|
+
const joined = join12(work2, "retimed.mp4");
|
|
10853
11010
|
await runTool("ffmpeg", [
|
|
10854
11011
|
"-y",
|
|
10855
11012
|
"-hide_banner",
|
|
@@ -10871,7 +11028,7 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10871
11028
|
}
|
|
10872
11029
|
async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
10873
11030
|
const inputs = plan.audio.map((a) => ({
|
|
10874
|
-
file:
|
|
11031
|
+
file: join12(deck, timing.audioDir, a.audio),
|
|
10875
11032
|
delayMs: a.delayMs
|
|
10876
11033
|
}));
|
|
10877
11034
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -10892,8 +11049,8 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10892
11049
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
10893
11050
|
}
|
|
10894
11051
|
if (graph.length > 0) {
|
|
10895
|
-
const script =
|
|
10896
|
-
await
|
|
11052
|
+
const script = join12(work2, "mux.filter");
|
|
11053
|
+
await writeFile10(script, `${graph.join(";\n")}
|
|
10897
11054
|
`);
|
|
10898
11055
|
args.push("-filter_complex_script", script);
|
|
10899
11056
|
}
|
|
@@ -10908,13 +11065,13 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10908
11065
|
log(
|
|
10909
11066
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
10910
11067
|
);
|
|
10911
|
-
await
|
|
11068
|
+
await mkdir8(dirname3(out), { recursive: true });
|
|
10912
11069
|
await runTool("ffmpeg", args, { cwd: work2 });
|
|
10913
11070
|
}
|
|
10914
11071
|
|
|
10915
11072
|
// src/pack/pack.ts
|
|
10916
|
-
import { mkdir as
|
|
10917
|
-
import { dirname as
|
|
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";
|
|
10918
11075
|
import { unzipSync, zipSync } from "fflate";
|
|
10919
11076
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
10920
11077
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -10953,8 +11110,8 @@ async function writePack(pack3, files, out) {
|
|
|
10953
11110
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
10954
11111
|
}
|
|
10955
11112
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
10956
|
-
await
|
|
10957
|
-
await
|
|
11113
|
+
await mkdir9(dirname4(out), { recursive: true });
|
|
11114
|
+
await writeFile11(out, zip);
|
|
10958
11115
|
return zip.length;
|
|
10959
11116
|
}
|
|
10960
11117
|
function check2(label, path2 = label) {
|
|
@@ -10973,8 +11130,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10973
11130
|
if (!format) throw new Error("no deck-16x9 format");
|
|
10974
11131
|
const step = opts.onStep ?? (() => {
|
|
10975
11132
|
});
|
|
10976
|
-
const out =
|
|
10977
|
-
await
|
|
11133
|
+
const out = resolve6(outDir);
|
|
11134
|
+
await mkdir10(out, { recursive: true });
|
|
10978
11135
|
const speed = opts.speed ?? 1;
|
|
10979
11136
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
10980
11137
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
@@ -10987,47 +11144,48 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10987
11144
|
});
|
|
10988
11145
|
const files = [];
|
|
10989
11146
|
const write = async (name, text2) => {
|
|
10990
|
-
await
|
|
10991
|
-
files.push(
|
|
11147
|
+
await writeFile12(join13(out, name), text2);
|
|
11148
|
+
files.push(join13(out, name));
|
|
10992
11149
|
};
|
|
10993
11150
|
await write("index.html", deck.composition);
|
|
10994
11151
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
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)}
|
|
11010
11166
|
`);
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
}
|
|
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
|
+
);
|
|
11017
11172
|
}
|
|
11018
11173
|
if (deck.page) {
|
|
11019
11174
|
await write(DECK_PAGE, deck.page);
|
|
11020
|
-
await
|
|
11021
|
-
files.push(
|
|
11175
|
+
await cp2(playerBundle(), join13(out, PLAYER_FILE));
|
|
11176
|
+
files.push(join13(out, PLAYER_FILE));
|
|
11022
11177
|
}
|
|
11023
11178
|
files.push(...await vendorKatex(out));
|
|
11024
|
-
|
|
11179
|
+
files.push(...await vendorScripts(out, deck.composition));
|
|
11180
|
+
if (opts.assetsFrom) {
|
|
11181
|
+
files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
|
|
11182
|
+
}
|
|
11025
11183
|
if (opts.narration && opts.audioFrom) {
|
|
11026
|
-
files.push(...await copyAudio(opts.audioFrom, opts.narration, out));
|
|
11184
|
+
files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
|
|
11027
11185
|
}
|
|
11028
11186
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
11029
11187
|
step(
|
|
11030
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
11188
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join13(out, "index.html")}`
|
|
11031
11189
|
);
|
|
11032
11190
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
11033
11191
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -11043,77 +11201,21 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
11043
11201
|
)}
|
|
11044
11202
|
`;
|
|
11045
11203
|
async function deckRuntime() {
|
|
11046
|
-
const path2 =
|
|
11204
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
11047
11205
|
try {
|
|
11048
|
-
return await
|
|
11206
|
+
return await readFile12(path2, "utf8");
|
|
11049
11207
|
} catch {
|
|
11050
11208
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
11051
11209
|
}
|
|
11052
11210
|
}
|
|
11053
11211
|
function playerBundle() {
|
|
11054
|
-
const require2 =
|
|
11212
|
+
const require2 = createRequire4(import.meta.url);
|
|
11055
11213
|
try {
|
|
11056
|
-
return
|
|
11214
|
+
return join13(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
11057
11215
|
} catch {
|
|
11058
11216
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
11059
11217
|
}
|
|
11060
11218
|
}
|
|
11061
|
-
async function vendorKatex(out) {
|
|
11062
|
-
const require2 = createRequire3(import.meta.url);
|
|
11063
|
-
const dist = join12(dirname4(require2.resolve("katex/package.json")), "dist");
|
|
11064
|
-
const css = await readFile11(join12(dist, "katex.min.css"), "utf8");
|
|
11065
|
-
const written = [];
|
|
11066
|
-
await mkdir9(join12(out, "katex/fonts"), { recursive: true });
|
|
11067
|
-
for (const file of await readdir2(join12(dist, "fonts"))) {
|
|
11068
|
-
if (!file.endsWith(".woff2")) continue;
|
|
11069
|
-
await cp(join12(dist, "fonts", file), join12(out, "katex/fonts", file));
|
|
11070
|
-
written.push(join12(out, "katex/fonts", file));
|
|
11071
|
-
}
|
|
11072
|
-
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
11073
|
-
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
11074
|
-
return kept ? `src:${kept}` : whole;
|
|
11075
|
-
});
|
|
11076
|
-
await writeFile11(join12(out, "katex/katex.min.css"), woff2Only);
|
|
11077
|
-
written.push(join12(out, "katex/katex.min.css"));
|
|
11078
|
-
return written;
|
|
11079
|
-
}
|
|
11080
|
-
async function copyAssets(sourceDir, out) {
|
|
11081
|
-
const from = join12(resolve5(sourceDir), "assets");
|
|
11082
|
-
if (!await stat2(from).catch(() => null)) return [];
|
|
11083
|
-
await cp(from, join12(out, "assets"), { recursive: true });
|
|
11084
|
-
return [join12(out, "assets")];
|
|
11085
|
-
}
|
|
11086
|
-
async function copyAudio(from, narration, out) {
|
|
11087
|
-
const dir = join12(out, narration.dir);
|
|
11088
|
-
await mkdir9(dir, { recursive: true });
|
|
11089
|
-
const names = [
|
|
11090
|
-
...new Set(
|
|
11091
|
-
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
11092
|
-
)
|
|
11093
|
-
].sort();
|
|
11094
|
-
for (const name of names) {
|
|
11095
|
-
await cp(join12(resolve5(from), name), join12(dir, name)).catch(() => {
|
|
11096
|
-
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
11097
|
-
});
|
|
11098
|
-
}
|
|
11099
|
-
return names.map((n3) => join12(dir, n3));
|
|
11100
|
-
}
|
|
11101
|
-
async function refreshFont(storyboard, source, out, step) {
|
|
11102
|
-
try {
|
|
11103
|
-
const bundle = await bundleFont(
|
|
11104
|
-
storyboard.lang,
|
|
11105
|
-
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
11106
|
-
join12(out, "assets", "fonts")
|
|
11107
|
-
);
|
|
11108
|
-
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
11109
|
-
return bundle?.css;
|
|
11110
|
-
} catch (err) {
|
|
11111
|
-
step(
|
|
11112
|
-
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
11113
|
-
);
|
|
11114
|
-
return void 0;
|
|
11115
|
-
}
|
|
11116
|
-
}
|
|
11117
11219
|
|
|
11118
11220
|
// src/server/upload.ts
|
|
11119
11221
|
import { posix, sep as sep2 } from "node:path";
|
|
@@ -11458,8 +11560,8 @@ function int(name, raw2) {
|
|
|
11458
11560
|
}
|
|
11459
11561
|
|
|
11460
11562
|
// src/server/pipeline.ts
|
|
11461
|
-
import { mkdir as
|
|
11462
|
-
import { dirname as
|
|
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";
|
|
11463
11565
|
var AUDIO_DIR = "audio";
|
|
11464
11566
|
var NARRATION_FILE = "narration.json";
|
|
11465
11567
|
var MAX_REMOTE_FIGURES = 40;
|
|
@@ -11482,10 +11584,10 @@ function stagesFor(options) {
|
|
|
11482
11584
|
async function runPipeline(job, input) {
|
|
11483
11585
|
const { options } = input;
|
|
11484
11586
|
const dirs = {
|
|
11485
|
-
upload:
|
|
11486
|
-
src:
|
|
11487
|
-
audio:
|
|
11488
|
-
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")
|
|
11489
11591
|
};
|
|
11490
11592
|
const warnings = [...options.warnings];
|
|
11491
11593
|
const url = (rel) => `/d/${job.id}/${rel}`;
|
|
@@ -11509,21 +11611,21 @@ async function runPipeline(job, input) {
|
|
|
11509
11611
|
theme: options.stated.theme ? prefs.theme : planned.theme
|
|
11510
11612
|
};
|
|
11511
11613
|
assertRefsResolve(storyboard, source, { pending: options.images ? "allow" : "refuse" });
|
|
11512
|
-
await writeJson(
|
|
11614
|
+
await writeJson(join14(job.dir, "storyboard.json"), storyboard);
|
|
11513
11615
|
job.done("plan", `${storyboard.beats.length} beats`);
|
|
11514
11616
|
for (const f of scanBeatCount(storyboard, prefs)) warnings.push(f.message);
|
|
11515
11617
|
if (options.images) {
|
|
11516
11618
|
job.begin("illustrate");
|
|
11517
11619
|
const drawn = await illustrate(storyboard, source, {
|
|
11518
11620
|
prefs,
|
|
11519
|
-
assetsDir:
|
|
11621
|
+
assetsDir: join14(dirs.src, "assets"),
|
|
11520
11622
|
...input.imageChain ? { chain: input.imageChain } : {},
|
|
11521
11623
|
onStep: (line2) => job.log(line2)
|
|
11522
11624
|
});
|
|
11523
11625
|
storyboard = drawn.storyboard;
|
|
11524
11626
|
source = drawn.source;
|
|
11525
|
-
await writeJson(
|
|
11526
|
-
await writeJson(
|
|
11627
|
+
await writeJson(join14(dirs.src, "source.json"), source);
|
|
11628
|
+
await writeJson(join14(job.dir, "storyboard.json"), storyboard);
|
|
11527
11629
|
for (const p of drawn.illustrated) {
|
|
11528
11630
|
job.log(
|
|
11529
11631
|
`illustrate: ${p.beatId} \u2192 assets/${p.src} via ${p.provider}${p.cached ? " (cached)" : ""}`
|
|
@@ -11545,12 +11647,12 @@ async function runPipeline(job, input) {
|
|
|
11545
11647
|
warnings.push("the plan had nothing to say aloud, so the deck is silent");
|
|
11546
11648
|
} else {
|
|
11547
11649
|
job.log(`narrate: ${speaking} of ${storyboard.beats.length} beats speak`);
|
|
11548
|
-
await
|
|
11650
|
+
await mkdir11(dirs.audio, { recursive: true });
|
|
11549
11651
|
const spoken = await narrate(storyboard, source, prefs, {
|
|
11550
11652
|
dir: dirs.audio,
|
|
11551
11653
|
format: options.format
|
|
11552
11654
|
});
|
|
11553
|
-
await writeJson(
|
|
11655
|
+
await writeJson(join14(dirs.audio, NARRATION_FILE), spoken);
|
|
11554
11656
|
narration = { voice: spoken.voice, dir: AUDIO_DIR, beats: spoken.beats };
|
|
11555
11657
|
const segments2 = Object.values(spoken.beats).flat();
|
|
11556
11658
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
@@ -11604,7 +11706,7 @@ async function runPipeline(job, input) {
|
|
|
11604
11706
|
job.log("render: capturing frames \u2014 two minutes for a four-minute video");
|
|
11605
11707
|
const out = await render({
|
|
11606
11708
|
deck: dirs.deck,
|
|
11607
|
-
out:
|
|
11709
|
+
out: join14(dirs.deck, "video.mp4"),
|
|
11608
11710
|
subtitles: "sidecar",
|
|
11609
11711
|
log: (line2) => job.log(line2),
|
|
11610
11712
|
// The last of the three length levers, and the only one that acts on a
|
|
@@ -11653,8 +11755,8 @@ async function runPipeline(job, input) {
|
|
|
11653
11755
|
};
|
|
11654
11756
|
}
|
|
11655
11757
|
async function ingest(job, input, dirs, warnings) {
|
|
11656
|
-
await
|
|
11657
|
-
const root2 =
|
|
11758
|
+
await mkdir11(dirs.upload, { recursive: true });
|
|
11759
|
+
const root2 = resolve7(dirs.upload);
|
|
11658
11760
|
let docPath;
|
|
11659
11761
|
const upload = input.upload;
|
|
11660
11762
|
const bothOrNeither = new UploadError(
|
|
@@ -11664,13 +11766,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11664
11766
|
);
|
|
11665
11767
|
if (input.url !== void 0) {
|
|
11666
11768
|
if (upload !== void 0) throw bothOrNeither;
|
|
11667
|
-
const harvested = await harvestBounded(input.url,
|
|
11769
|
+
const harvested = await harvestBounded(input.url, join14(root2, "assets"), {
|
|
11668
11770
|
...HARVEST_LIMITS,
|
|
11669
11771
|
...input.harvest
|
|
11670
11772
|
});
|
|
11671
11773
|
warnings.push(...harvested.warnings);
|
|
11672
|
-
docPath =
|
|
11673
|
-
await
|
|
11774
|
+
docPath = join14(root2, "document.md");
|
|
11775
|
+
await writeFile13(docPath, harvested.markdown);
|
|
11674
11776
|
job.log(
|
|
11675
11777
|
`ingest: harvested ${input.url} \u2014 "${harvested.title}", ${harvested.assets.length} asset(s)`
|
|
11676
11778
|
);
|
|
@@ -11686,13 +11788,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11686
11788
|
const { files, warnings: zipWarnings } = readZip(upload.bytes);
|
|
11687
11789
|
warnings.push(...zipWarnings);
|
|
11688
11790
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
11689
|
-
const to =
|
|
11791
|
+
const to = resolve7(join14(root2, rel));
|
|
11690
11792
|
if (!insideRoot2(root2, to))
|
|
11691
11793
|
throw new UploadError(`Refusing to write ${rel}.`, "Re-zip from inside the folder.");
|
|
11692
|
-
await
|
|
11693
|
-
await
|
|
11794
|
+
await mkdir11(dirname6(to), { recursive: true });
|
|
11795
|
+
await writeFile13(to, bytes);
|
|
11694
11796
|
}
|
|
11695
|
-
docPath =
|
|
11797
|
+
docPath = join14(root2, pickMarkdown(files));
|
|
11696
11798
|
job.log(
|
|
11697
11799
|
`ingest: unpacked ${Object.keys(files).length} file(s), reading ${pickMarkdown(files)}`
|
|
11698
11800
|
);
|
|
@@ -11704,10 +11806,10 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11704
11806
|
`DeckSmith reads ${MARKDOWN_EXTS.join(", ")} or a .zip containing one. Export the document to markdown first.`
|
|
11705
11807
|
);
|
|
11706
11808
|
}
|
|
11707
|
-
docPath =
|
|
11708
|
-
await
|
|
11809
|
+
docPath = join14(root2, "document.md");
|
|
11810
|
+
await writeFile13(docPath, upload.bytes);
|
|
11709
11811
|
}
|
|
11710
|
-
const text2 = await
|
|
11812
|
+
const text2 = await readFile13(docPath, "utf8");
|
|
11711
11813
|
const parsed = parseMarkdown(text2, {
|
|
11712
11814
|
lang: input.options.stated.lang ? input.options.prefs.lang : void 0
|
|
11713
11815
|
});
|
|
@@ -11727,8 +11829,8 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11727
11829
|
`ingest: ${parsed.sections.length} sections, ${parsed.figures.length} figures, ${parsed.equations.length} equations, ${parsed.tables.length} tables`
|
|
11728
11830
|
);
|
|
11729
11831
|
const guarded = await guardFigures(parsed, root2, input.fetchRemoteFigures, warnings);
|
|
11730
|
-
const source = await fetchFigures(guarded,
|
|
11731
|
-
await writeJson(
|
|
11832
|
+
const source = await fetchFigures(guarded, join14(resolve7(dirs.src), "assets"), warnings);
|
|
11833
|
+
await writeJson(join14(dirs.src, "source.json"), source);
|
|
11732
11834
|
return source;
|
|
11733
11835
|
}
|
|
11734
11836
|
async function harvestBounded(url, dir, opts) {
|
|
@@ -11804,7 +11906,7 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11804
11906
|
warnings.push(`figure ${figure.id} was left out: "${scheme}:" figures are not read`);
|
|
11805
11907
|
continue;
|
|
11806
11908
|
}
|
|
11807
|
-
const abs = isAbsolute(src) ?
|
|
11909
|
+
const abs = isAbsolute(src) ? resolve7(src) : resolve7(join14(root2, src.replace(/^\.?\//, "")));
|
|
11808
11910
|
if (!insideRoot2(root2, abs)) {
|
|
11809
11911
|
warnings.push(`figure ${figure.id} was left out: "${src}" points outside the upload`);
|
|
11810
11912
|
continue;
|
|
@@ -11819,19 +11921,19 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11819
11921
|
}
|
|
11820
11922
|
async function pack2(job, ctx) {
|
|
11821
11923
|
try {
|
|
11822
|
-
const assets =
|
|
11924
|
+
const assets = join14(resolve7(ctx.dirs.src), "assets");
|
|
11823
11925
|
const requests = ctx.source.figures.map((f) => ({
|
|
11824
11926
|
id: f.id,
|
|
11825
|
-
url:
|
|
11927
|
+
url: join14(assets, f.src),
|
|
11826
11928
|
prefer: "bake"
|
|
11827
11929
|
}));
|
|
11828
11930
|
const plan = await planMedia(requests, async (url) => ({
|
|
11829
|
-
bytes: new Uint8Array(await
|
|
11931
|
+
bytes: new Uint8Array(await readFile13(url))
|
|
11830
11932
|
}));
|
|
11831
11933
|
const files = { ...plan.files };
|
|
11832
11934
|
if (ctx.narration) {
|
|
11833
|
-
for (const name of
|
|
11834
|
-
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await
|
|
11935
|
+
for (const name of audioNames2(ctx.narration)) {
|
|
11936
|
+
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile13(join14(ctx.dirs.audio, name)));
|
|
11835
11937
|
}
|
|
11836
11938
|
}
|
|
11837
11939
|
const container = {
|
|
@@ -11848,10 +11950,10 @@ async function pack2(job, ctx) {
|
|
|
11848
11950
|
},
|
|
11849
11951
|
source: ctx.source,
|
|
11850
11952
|
storyboard: ctx.storyboard,
|
|
11851
|
-
...ctx.narration ? { narration: await readJson(
|
|
11953
|
+
...ctx.narration ? { narration: await readJson(join14(ctx.dirs.audio, NARRATION_FILE)) } : {},
|
|
11852
11954
|
media: plan.media
|
|
11853
11955
|
};
|
|
11854
|
-
const bytes = await writePack(container, files,
|
|
11956
|
+
const bytes = await writePack(container, files, join14(ctx.dirs.deck, "deck.deck"));
|
|
11855
11957
|
job.log(`pack: ${Math.round(bytes / 1024)} KB \u2192 deck.deck`);
|
|
11856
11958
|
return ctx.url("deck.deck");
|
|
11857
11959
|
} catch (err) {
|
|
@@ -11859,7 +11961,7 @@ async function pack2(job, ctx) {
|
|
|
11859
11961
|
return void 0;
|
|
11860
11962
|
}
|
|
11861
11963
|
}
|
|
11862
|
-
function
|
|
11964
|
+
function audioNames2(narration) {
|
|
11863
11965
|
return [
|
|
11864
11966
|
...new Set(
|
|
11865
11967
|
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
@@ -11874,12 +11976,12 @@ function host(src) {
|
|
|
11874
11976
|
}
|
|
11875
11977
|
}
|
|
11876
11978
|
async function writeJson(path2, value) {
|
|
11877
|
-
await
|
|
11878
|
-
await
|
|
11979
|
+
await mkdir11(dirname6(path2), { recursive: true });
|
|
11980
|
+
await writeFile13(path2, `${JSON.stringify(value, null, 2)}
|
|
11879
11981
|
`);
|
|
11880
11982
|
}
|
|
11881
11983
|
async function readJson(path2) {
|
|
11882
|
-
return JSON.parse(await
|
|
11984
|
+
return JSON.parse(await readFile13(path2, "utf8"));
|
|
11883
11985
|
}
|
|
11884
11986
|
|
|
11885
11987
|
// src/server/errors.ts
|
|
@@ -12285,14 +12387,14 @@ var PAGE_BUDGET = {
|
|
|
12285
12387
|
refs: "relative"
|
|
12286
12388
|
};
|
|
12287
12389
|
async function readPage(url, work2) {
|
|
12288
|
-
await
|
|
12289
|
-
const dir = await mkdtemp3(
|
|
12390
|
+
await mkdir12(work2, { recursive: true });
|
|
12391
|
+
const dir = await mkdtemp3(join15(work2, "harvest-"));
|
|
12290
12392
|
try {
|
|
12291
12393
|
const page = await harvest(url, dir, PAGE_BUDGET);
|
|
12292
12394
|
const files = {
|
|
12293
12395
|
"document.md": new TextEncoder().encode(page.markdown)
|
|
12294
12396
|
};
|
|
12295
|
-
for (const asset of page.assets) files[basename5(asset)] = await
|
|
12397
|
+
for (const asset of page.assets) files[basename5(asset)] = await readFile14(asset);
|
|
12296
12398
|
const warnings = [...page.warnings];
|
|
12297
12399
|
if (page.clips.length > 0) {
|
|
12298
12400
|
warnings.push(
|
|
@@ -12326,7 +12428,7 @@ var statusSchema = z5.object({
|
|
|
12326
12428
|
var DEFAULT_WAIT = 45;
|
|
12327
12429
|
function deckTools(opts) {
|
|
12328
12430
|
const queue = new Queue({ maxQueued: 8 });
|
|
12329
|
-
const root2 =
|
|
12431
|
+
const root2 = resolve8(opts.root);
|
|
12330
12432
|
let cached2;
|
|
12331
12433
|
const found = async () => {
|
|
12332
12434
|
cached2 ??= await (opts.probe ?? prereqs)();
|
|
@@ -12334,7 +12436,7 @@ function deckTools(opts) {
|
|
|
12334
12436
|
};
|
|
12335
12437
|
const insideRoot3 = (p) => {
|
|
12336
12438
|
if (!isAbsolute2(p)) throw new Error(`document_path must be absolute; got "${p}".`);
|
|
12337
|
-
const full =
|
|
12439
|
+
const full = resolve8(p);
|
|
12338
12440
|
if (full !== root2 && !full.startsWith(root2 + sep3)) {
|
|
12339
12441
|
throw new Error(
|
|
12340
12442
|
`document_path is outside the server's root. It may only read files under ${root2}.`
|
|
@@ -12413,15 +12515,15 @@ function deckTools(opts) {
|
|
|
12413
12515
|
}
|
|
12414
12516
|
const page = input.document_url === void 0 ? void 0 : await readPage(input.document_url, opts.work);
|
|
12415
12517
|
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(sep3).pop() : "document.md";
|
|
12416
|
-
const bytes = page ? page.zip : file ? await
|
|
12518
|
+
const bytes = page ? page.zip : file ? await readFile14(file) : new TextEncoder().encode(input.document_text);
|
|
12417
12519
|
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
12418
12520
|
throw new Error(
|
|
12419
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." : "")
|
|
12420
12522
|
);
|
|
12421
12523
|
}
|
|
12422
12524
|
const id2 = randomBytes(16).toString("base64url");
|
|
12423
|
-
const dir =
|
|
12424
|
-
await
|
|
12525
|
+
const dir = join15(opts.work, id2);
|
|
12526
|
+
await mkdir12(dir, { recursive: true });
|
|
12425
12527
|
queue.submit({
|
|
12426
12528
|
id: id2,
|
|
12427
12529
|
dir,
|
|
@@ -12448,7 +12550,7 @@ function deckTools(opts) {
|
|
|
12448
12550
|
};
|
|
12449
12551
|
}
|
|
12450
12552
|
function dirOf(work2, id2) {
|
|
12451
|
-
return
|
|
12553
|
+
return join15(work2, id2);
|
|
12452
12554
|
}
|
|
12453
12555
|
function waitFor(queue, id2, seconds) {
|
|
12454
12556
|
const now = queue.view(id2);
|
|
@@ -12480,9 +12582,9 @@ function report(view, dir) {
|
|
|
12480
12582
|
// (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
|
|
12481
12583
|
// is the shape of the CLI's output directory and not the server's, and an
|
|
12482
12584
|
// agent told to read it would have found nothing there.
|
|
12483
|
-
storyboard_path:
|
|
12585
|
+
storyboard_path: join15(dir, "storyboard.json"),
|
|
12484
12586
|
...done && view.result ? {
|
|
12485
|
-
deck_path:
|
|
12587
|
+
deck_path: join15(dir, "deck"),
|
|
12486
12588
|
slides: view.result.slides,
|
|
12487
12589
|
duration_seconds: view.result.duration,
|
|
12488
12590
|
warnings: view.result.warnings
|
|
@@ -12491,7 +12593,7 @@ function report(view, dir) {
|
|
|
12491
12593
|
};
|
|
12492
12594
|
}
|
|
12493
12595
|
function defaultWork() {
|
|
12494
|
-
return
|
|
12596
|
+
return join15(tmpdir4(), "decksmith-mcp");
|
|
12495
12597
|
}
|
|
12496
12598
|
|
|
12497
12599
|
// src/mcp/main.ts
|