@jokerized/decksmith 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/cli.js +488 -384
- package/dist/deck-runtime.js +5 -2
- package/dist/index.js +449 -312
- package/dist/mcp.js +392 -303
- 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/render.d.ts +40 -12
- package/dist/types/render/timing.d.ts +11 -0
- package/dist/types/types.d.ts +87 -0
- package/dist/types/verify/budget.d.ts +10 -2
- 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.
|
|
@@ -948,8 +1118,7 @@ var DESTINATIONS = {
|
|
|
948
1118
|
"video-16x9": [],
|
|
949
1119
|
"short-9x16": [
|
|
950
1120
|
{ name: "YouTube Shorts", maxSeconds: 180 },
|
|
951
|
-
{ name: "Instagram Reels", maxSeconds: 180 }
|
|
952
|
-
{ name: "Facebook Reels", maxSeconds: 90 }
|
|
1121
|
+
{ name: "Instagram Reels", maxSeconds: 180 }
|
|
953
1122
|
],
|
|
954
1123
|
"post-1x1": [{ name: "X (standard account)", maxSeconds: 140 }]
|
|
955
1124
|
};
|
|
@@ -1254,54 +1423,6 @@ function clock(seconds) {
|
|
|
1254
1423
|
return m === 0 ? `${whole}s` : `${m}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1255
1424
|
}
|
|
1256
1425
|
|
|
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
1426
|
// src/emit/kit.ts
|
|
1306
1427
|
var PAD_X = 110;
|
|
1307
1428
|
var PAD_Y = 84;
|
|
@@ -1320,7 +1441,12 @@ function contentW(format) {
|
|
|
1320
1441
|
return refWidth(format) - 2 * PAD_X;
|
|
1321
1442
|
}
|
|
1322
1443
|
function contentH(format) {
|
|
1323
|
-
return refHeight(format) - 2 * PAD_Y;
|
|
1444
|
+
return refHeight(format) - 2 * PAD_Y - reserveRef(format);
|
|
1445
|
+
}
|
|
1446
|
+
function reserveRef(format) {
|
|
1447
|
+
const reserve = format.captionReserve ?? 0;
|
|
1448
|
+
if (reserve <= 0) return 0;
|
|
1449
|
+
return Math.ceil(reserve * refHeight(format) / format.height);
|
|
1324
1450
|
}
|
|
1325
1451
|
function raw(js2) {
|
|
1326
1452
|
return { __raw: js2 };
|
|
@@ -1819,8 +1945,8 @@ function charUnits(c, tabular, cjkRun, hangul) {
|
|
|
1819
1945
|
}
|
|
1820
1946
|
const measured = ADVANCE[c];
|
|
1821
1947
|
if (measured !== void 0) return [measured, true];
|
|
1822
|
-
const
|
|
1823
|
-
for (const [lo, hi, units] of BLOCK_ADVANCE) if (
|
|
1948
|
+
const cp3 = c.codePointAt(0) ?? 0;
|
|
1949
|
+
for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp3 >= lo && cp3 <= hi) return [units, false];
|
|
1824
1950
|
return [UNMEASURED, true];
|
|
1825
1951
|
}
|
|
1826
1952
|
function weightFactor(weight) {
|
|
@@ -2081,11 +2207,16 @@ function baseCss(theme, format) {
|
|
|
2081
2207
|
gate still catches anything taller than the canvas; it just now
|
|
2082
2208
|
overflows symmetrically instead of only downwards. */
|
|
2083
2209
|
.scene { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
2084
|
-
padding: ${
|
|
2210
|
+
padding: ${scenePadding(format)}; display: flex; flex-direction: column;
|
|
2085
2211
|
justify-content: center; }
|
|
2086
2212
|
${referenceSpaceCss(format)}
|
|
2087
2213
|
${AMBIENT_KEYFRAMES}`;
|
|
2088
2214
|
}
|
|
2215
|
+
function scenePadding(format) {
|
|
2216
|
+
const reserve = reserveRef(format);
|
|
2217
|
+
const base = `${PAD_Y}px ${PAD_X}px`;
|
|
2218
|
+
return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
|
|
2219
|
+
}
|
|
2089
2220
|
function referenceSpaceCss(format) {
|
|
2090
2221
|
const zoom = zoomOf(format);
|
|
2091
2222
|
if (zoom === 1) return "";
|
|
@@ -6413,6 +6544,10 @@ function planTiming(input) {
|
|
|
6413
6544
|
lang: storyboard.lang,
|
|
6414
6545
|
audioDir: segments2.length > 0 ? narration?.dir ?? "" : "",
|
|
6415
6546
|
voice: segments2.length > 0 ? narration?.voice ?? "" : "",
|
|
6547
|
+
// The format the deck was LAID OUT under, which is the only honest source:
|
|
6548
|
+
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
6549
|
+
// disagree with the box the archetypes actually drew into.
|
|
6550
|
+
captionReserve: format.captionReserve ?? 0,
|
|
6416
6551
|
scenes,
|
|
6417
6552
|
segments: segments2
|
|
6418
6553
|
};
|
|
@@ -6705,8 +6840,8 @@ function textOf(node) {
|
|
|
6705
6840
|
|
|
6706
6841
|
// src/source/harvest.ts
|
|
6707
6842
|
import { createHash as createHash5 } from "node:crypto";
|
|
6708
|
-
import { copyFile, mkdir as
|
|
6709
|
-
import { basename as basename3, join as
|
|
6843
|
+
import { copyFile, mkdir as mkdir4, rm as rm2, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
|
|
6844
|
+
import { basename as basename3, join as join6, resolve as resolve3 } from "node:path";
|
|
6710
6845
|
|
|
6711
6846
|
// src/net/fetch.ts
|
|
6712
6847
|
import { lookup as resolveHost } from "node:dns/promises";
|
|
@@ -6906,8 +7041,8 @@ async function send(url, target, headers, signal, opts) {
|
|
|
6906
7041
|
...secure && isIP(host2) === 0 ? { servername: host2 } : {}
|
|
6907
7042
|
};
|
|
6908
7043
|
try {
|
|
6909
|
-
return await new Promise((
|
|
6910
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
7044
|
+
return await new Promise((resolve9, reject) => {
|
|
7045
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve9);
|
|
6911
7046
|
req.on("error", reject);
|
|
6912
7047
|
req.end();
|
|
6913
7048
|
});
|
|
@@ -6991,7 +7126,7 @@ function message(err) {
|
|
|
6991
7126
|
|
|
6992
7127
|
// src/render/capture.ts
|
|
6993
7128
|
import { homedir } from "node:os";
|
|
6994
|
-
import { join as
|
|
7129
|
+
import { join as join4 } from "node:path";
|
|
6995
7130
|
async function resolveChrome(need = "open the deck with") {
|
|
6996
7131
|
const env2 = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
6997
7132
|
if (env2) return { path: env2, source: "env" };
|
|
@@ -7001,9 +7136,9 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
7001
7136
|
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
7002
7137
|
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
7003
7138
|
};
|
|
7004
|
-
const hf = await pick(
|
|
7139
|
+
const hf = await pick(join4(homedir(), ".cache", "hyperframes", "chrome"));
|
|
7005
7140
|
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
7006
|
-
const pup = await pick(process.env.PUPPETEER_CACHE_DIR ||
|
|
7141
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join4(homedir(), ".cache", "puppeteer"));
|
|
7007
7142
|
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
7008
7143
|
throw new Error(
|
|
7009
7144
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
@@ -7015,13 +7150,13 @@ async function chromePath(need = "open the deck with") {
|
|
|
7015
7150
|
|
|
7016
7151
|
// src/source/assets.ts
|
|
7017
7152
|
import { createHash as createHash4 } from "node:crypto";
|
|
7018
|
-
import { mkdir as
|
|
7019
|
-
import { extname as extname2, join as
|
|
7153
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
7154
|
+
import { extname as extname2, join as join5 } from "node:path";
|
|
7020
7155
|
import { z as z2 } from "zod";
|
|
7021
7156
|
async function fetchFigures(source, dir, warnings) {
|
|
7022
7157
|
const drops = warnings ?? [];
|
|
7023
|
-
await
|
|
7024
|
-
const cached2 = await
|
|
7158
|
+
await mkdir3(dir, { recursive: true });
|
|
7159
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
7025
7160
|
const figures = [];
|
|
7026
7161
|
for (const figure of source.figures) {
|
|
7027
7162
|
if (figure.kind === "clip") {
|
|
@@ -7046,11 +7181,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
7046
7181
|
async function localize(figure, dir, cached2) {
|
|
7047
7182
|
const stem = assetStem(figure.id, figure.src);
|
|
7048
7183
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7049
|
-
const bytes = hit ? await
|
|
7184
|
+
const bytes = hit ? await readFile4(join5(dir, hit)) : await load(figure.src);
|
|
7050
7185
|
const size = imageSize(bytes);
|
|
7051
7186
|
if (hit) return { ...figure, src: hit, ...size };
|
|
7052
7187
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
7053
|
-
await
|
|
7188
|
+
await writeFile3(join5(dir, src), bytes);
|
|
7054
7189
|
cached2.push(src);
|
|
7055
7190
|
return { ...figure, src, ...size };
|
|
7056
7191
|
}
|
|
@@ -7075,7 +7210,7 @@ function assetExt(bytes, src) {
|
|
|
7075
7210
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
7076
7211
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
7077
7212
|
async function load(src) {
|
|
7078
|
-
if (!/^https?:/i.test(src)) return
|
|
7213
|
+
if (!/^https?:/i.test(src)) return readFile4(src);
|
|
7079
7214
|
const got = await fetchGuarded(src, {
|
|
7080
7215
|
maxBytes: FIGURE_MAX_BYTES,
|
|
7081
7216
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -7611,7 +7746,7 @@ function readContentRegion() {
|
|
|
7611
7746
|
}
|
|
7612
7747
|
|
|
7613
7748
|
// src/source/transcode.ts
|
|
7614
|
-
import { readFile as
|
|
7749
|
+
import { readFile as readFile5, rm } from "node:fs/promises";
|
|
7615
7750
|
import { basename as basename2 } from "node:path";
|
|
7616
7751
|
|
|
7617
7752
|
// src/render/ffmpeg.ts
|
|
@@ -7637,7 +7772,7 @@ ${tail}`);
|
|
|
7637
7772
|
}
|
|
7638
7773
|
}
|
|
7639
7774
|
function runLive(file, args) {
|
|
7640
|
-
return new Promise((
|
|
7775
|
+
return new Promise((resolve9, reject) => {
|
|
7641
7776
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7642
7777
|
child.on("error", (err) => {
|
|
7643
7778
|
reject(
|
|
@@ -7645,7 +7780,7 @@ function runLive(file, args) {
|
|
|
7645
7780
|
);
|
|
7646
7781
|
});
|
|
7647
7782
|
child.on("close", (code, signal) => {
|
|
7648
|
-
if (code === 0)
|
|
7783
|
+
if (code === 0) resolve9();
|
|
7649
7784
|
else if (signal) {
|
|
7650
7785
|
reject(
|
|
7651
7786
|
new Error(
|
|
@@ -7772,11 +7907,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
7772
7907
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
7773
7908
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
7774
7909
|
// three-line band covers the bottom of the slide.
|
|
7775
|
-
|
|
7910
|
+
//
|
|
7911
|
+
// THE RATIOS ARE NOT HERE. They are in src/types.ts beside `bandReserve`,
|
|
7912
|
+
// because `src/emit` has to reserve exactly the strip this draws into and
|
|
7913
|
+
// may not import this file. A local copy of 0.037 is how the two drift back
|
|
7914
|
+
// apart, which is the bug this whole path exists to close.
|
|
7915
|
+
fontSize: captionFontSize(width),
|
|
7776
7916
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
7777
7917
|
// player draws across the bottom eighth of the frame.
|
|
7778
|
-
marginV: Math.round(height *
|
|
7779
|
-
marginX: Math.round(width *
|
|
7918
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
7919
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
7780
7920
|
};
|
|
7781
7921
|
}
|
|
7782
7922
|
|
|
@@ -7842,7 +7982,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7842
7982
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7843
7983
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7844
7984
|
const name = basename2(input);
|
|
7845
|
-
const source = videoSize(await
|
|
7985
|
+
const source = videoSize(await readFile5(input));
|
|
7846
7986
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7847
7987
|
const kept = (why2) => ({
|
|
7848
7988
|
path: input,
|
|
@@ -7874,7 +8014,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7874
8014
|
}
|
|
7875
8015
|
let measured;
|
|
7876
8016
|
try {
|
|
7877
|
-
measured = videoSize(await
|
|
8017
|
+
measured = videoSize(await readFile5(out));
|
|
7878
8018
|
} catch (err) {
|
|
7879
8019
|
await rm(out, { force: true });
|
|
7880
8020
|
return kept(
|
|
@@ -7910,8 +8050,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7910
8050
|
async function harvest(url, dir, opts = {}) {
|
|
7911
8051
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7912
8052
|
const warnings = [];
|
|
7913
|
-
const assets =
|
|
7914
|
-
await
|
|
8053
|
+
const assets = resolve3(dir);
|
|
8054
|
+
await mkdir4(assets, { recursive: true });
|
|
7915
8055
|
const page = await fetchGuarded(url, {
|
|
7916
8056
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
7917
8057
|
timeoutMs,
|
|
@@ -8293,8 +8433,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8293
8433
|
`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
8434
|
);
|
|
8295
8435
|
}
|
|
8296
|
-
const path2 =
|
|
8297
|
-
await
|
|
8436
|
+
const path2 = join6(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8437
|
+
await writeFile4(path2, bytes);
|
|
8298
8438
|
assets.push(path2);
|
|
8299
8439
|
got = { path: path2, width: size.width, height: size.height };
|
|
8300
8440
|
} catch (err) {
|
|
@@ -8306,7 +8446,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8306
8446
|
};
|
|
8307
8447
|
const shrink = async (url, path2, measured, what) => {
|
|
8308
8448
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8309
|
-
const out2 =
|
|
8449
|
+
const out2 = join6(dir, assetName(url, ".vp9.webm"));
|
|
8310
8450
|
let small;
|
|
8311
8451
|
try {
|
|
8312
8452
|
small = await transcode(path2, out2, {
|
|
@@ -8348,8 +8488,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8348
8488
|
try {
|
|
8349
8489
|
const bytes = await bytesOf(url);
|
|
8350
8490
|
const measured = videoSize(bytes);
|
|
8351
|
-
const path2 =
|
|
8352
|
-
await
|
|
8491
|
+
const path2 = join6(dir, assetName(url, `.${measured.container}`));
|
|
8492
|
+
await writeFile4(path2, bytes);
|
|
8353
8493
|
return await shrink(url, path2, measured, what);
|
|
8354
8494
|
} catch (err) {
|
|
8355
8495
|
warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
|
|
@@ -8438,7 +8578,7 @@ function unheld(policy) {
|
|
|
8438
8578
|
return "the file could not be held";
|
|
8439
8579
|
}
|
|
8440
8580
|
async function sizeOf(path2) {
|
|
8441
|
-
return await
|
|
8581
|
+
return await stat2(path2).then(
|
|
8442
8582
|
(s) => s.size,
|
|
8443
8583
|
() => 0
|
|
8444
8584
|
);
|
|
@@ -8712,9 +8852,9 @@ ${fence}`;
|
|
|
8712
8852
|
|
|
8713
8853
|
// src/plan/codex.ts
|
|
8714
8854
|
import { spawn as spawn2 } from "node:child_process";
|
|
8715
|
-
import { mkdtemp, readFile as
|
|
8855
|
+
import { mkdtemp, readFile as readFile6, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
8716
8856
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8717
|
-
import { join as
|
|
8857
|
+
import { join as join7 } from "node:path";
|
|
8718
8858
|
import { z as z3 } from "zod";
|
|
8719
8859
|
|
|
8720
8860
|
// src/plan/arc.ts
|
|
@@ -9732,11 +9872,11 @@ function schemaFor(prefs) {
|
|
|
9732
9872
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9733
9873
|
async function codexPlanner(source, opts = {}) {
|
|
9734
9874
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9735
|
-
const dir = await mkdtemp(
|
|
9875
|
+
const dir = await mkdtemp(join7(tmpdir2(), "decksmith-plan-"));
|
|
9736
9876
|
try {
|
|
9737
|
-
const schemaPath =
|
|
9738
|
-
const outPath =
|
|
9739
|
-
await
|
|
9877
|
+
const schemaPath = join7(dir, "storyboard.schema.json");
|
|
9878
|
+
const outPath = join7(dir, "storyboard.json");
|
|
9879
|
+
await writeFile5(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9740
9880
|
await (opts.run ?? runCodex)({
|
|
9741
9881
|
prompt: buildPrompt(source, prefs),
|
|
9742
9882
|
schemaPath,
|
|
@@ -9744,7 +9884,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
9744
9884
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
9745
9885
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
9746
9886
|
});
|
|
9747
|
-
const raw2 = await
|
|
9887
|
+
const raw2 = await readFile6(outPath, "utf8").catch(() => "");
|
|
9748
9888
|
if (raw2.trim() === "") {
|
|
9749
9889
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
9750
9890
|
}
|
|
@@ -9817,7 +9957,7 @@ function codexCommand(args) {
|
|
|
9817
9957
|
}
|
|
9818
9958
|
function runCodex(args) {
|
|
9819
9959
|
const { argv, env: env2 } = codexCommand(args);
|
|
9820
|
-
return new Promise((
|
|
9960
|
+
return new Promise((resolve9, reject) => {
|
|
9821
9961
|
const child = spawn2("codex", argv, {
|
|
9822
9962
|
stdio: ["pipe", "ignore", "pipe"],
|
|
9823
9963
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -9840,7 +9980,7 @@ function runCodex(args) {
|
|
|
9840
9980
|
});
|
|
9841
9981
|
child.on("close", (code) => {
|
|
9842
9982
|
clearTimeout(timer);
|
|
9843
|
-
if (code === 0) return
|
|
9983
|
+
if (code === 0) return resolve9();
|
|
9844
9984
|
reject(
|
|
9845
9985
|
new Error(`codex exec exited ${code}.
|
|
9846
9986
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -9852,14 +9992,14 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
9852
9992
|
|
|
9853
9993
|
// src/images/illustrate.ts
|
|
9854
9994
|
import { createHash as createHash7 } from "node:crypto";
|
|
9855
|
-
import { mkdir as
|
|
9856
|
-
import { join as
|
|
9995
|
+
import { mkdir as mkdir5, readFile as readFile8, rename, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
|
|
9996
|
+
import { join as join9 } from "node:path";
|
|
9857
9997
|
|
|
9858
9998
|
// src/images/providers.ts
|
|
9859
9999
|
import { createHash as createHash6 } from "node:crypto";
|
|
9860
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
10000
|
+
import { mkdtemp as mkdtemp2, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
9861
10001
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
9862
|
-
import { join as
|
|
10002
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
9863
10003
|
import { z as z4 } from "zod";
|
|
9864
10004
|
var SIZE = {
|
|
9865
10005
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -9991,11 +10131,11 @@ function codexImages(opts = {}) {
|
|
|
9991
10131
|
async check() {
|
|
9992
10132
|
},
|
|
9993
10133
|
async generate(req) {
|
|
9994
|
-
const dir = await mkdtemp2(
|
|
10134
|
+
const dir = await mkdtemp2(join8(tmpdir3(), "decksmith-image-"));
|
|
9995
10135
|
try {
|
|
9996
|
-
const schemaPath =
|
|
9997
|
-
const outPath =
|
|
9998
|
-
await
|
|
10136
|
+
const schemaPath = join8(dir, "answer.schema.json");
|
|
10137
|
+
const outPath = join8(dir, "answer.json");
|
|
10138
|
+
await writeFile6(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
9999
10139
|
await run5({
|
|
10000
10140
|
prompt: codexPrompt(req),
|
|
10001
10141
|
schemaPath,
|
|
@@ -10004,7 +10144,7 @@ function codexImages(opts = {}) {
|
|
|
10004
10144
|
cwd: dir,
|
|
10005
10145
|
sandbox: "workspace-write"
|
|
10006
10146
|
});
|
|
10007
|
-
const raw2 = await
|
|
10147
|
+
const raw2 = await readFile7(outPath, "utf8").catch(() => "");
|
|
10008
10148
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
10009
10149
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
10010
10150
|
if (!answer.data.ok) {
|
|
@@ -10012,10 +10152,10 @@ function codexImages(opts = {}) {
|
|
|
10012
10152
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
10013
10153
|
);
|
|
10014
10154
|
}
|
|
10015
|
-
const candidates2 = [
|
|
10016
|
-
if (answer.data.file) candidates2.push(
|
|
10155
|
+
const candidates2 = [join8(dir, "picture.png")];
|
|
10156
|
+
if (answer.data.file) candidates2.push(resolve4(dir, answer.data.file));
|
|
10017
10157
|
for (const path2 of candidates2) {
|
|
10018
|
-
const bytes = await
|
|
10158
|
+
const bytes = await readFile7(path2).catch(() => null);
|
|
10019
10159
|
if (bytes) return raster(bytes, "codex");
|
|
10020
10160
|
}
|
|
10021
10161
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -10147,7 +10287,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
10147
10287
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
10148
10288
|
if (tool !== last) chain.push(tool);
|
|
10149
10289
|
const dropped = /* @__PURE__ */ new Set();
|
|
10150
|
-
if (pending.length > 0) await
|
|
10290
|
+
if (pending.length > 0) await mkdir5(opts.assetsDir, { recursive: true });
|
|
10151
10291
|
for (const [i, slot] of pending.entries()) {
|
|
10152
10292
|
let rungs = chain;
|
|
10153
10293
|
if (i >= images.max) {
|
|
@@ -10244,16 +10384,16 @@ function cacheKey(providerId, req) {
|
|
|
10244
10384
|
async function draw(provider, req, name, dir) {
|
|
10245
10385
|
for (const ext of Object.values(EXT2)) {
|
|
10246
10386
|
const src2 = `${name}${ext}`;
|
|
10247
|
-
const bytes = await
|
|
10387
|
+
const bytes = await readFile8(join9(dir, src2)).catch(() => null);
|
|
10248
10388
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
10249
10389
|
}
|
|
10250
10390
|
await provider.check();
|
|
10251
10391
|
const img = await provider.generate(req);
|
|
10252
10392
|
const src = `${name}${EXT2[img.mime]}`;
|
|
10253
|
-
const tmp =
|
|
10393
|
+
const tmp = join9(dir, `.${name}.tmp`);
|
|
10254
10394
|
try {
|
|
10255
|
-
await
|
|
10256
|
-
await rename(tmp,
|
|
10395
|
+
await writeFile7(tmp, img.bytes);
|
|
10396
|
+
await rename(tmp, join9(dir, src));
|
|
10257
10397
|
} finally {
|
|
10258
10398
|
await rm5(tmp, { force: true });
|
|
10259
10399
|
}
|
|
@@ -10266,9 +10406,9 @@ function sizeOf2(bytes, ext) {
|
|
|
10266
10406
|
// src/narrate/tts.ts
|
|
10267
10407
|
import { spawn as spawn3 } from "node:child_process";
|
|
10268
10408
|
import { createHash as createHash8 } from "node:crypto";
|
|
10269
|
-
import { mkdir as
|
|
10409
|
+
import { mkdir as mkdir6, readFile as readFile9, rm as rm6, writeFile as writeFile8 } from "node:fs/promises";
|
|
10270
10410
|
import { homedir as homedir2 } from "node:os";
|
|
10271
|
-
import { join as
|
|
10411
|
+
import { join as join10 } from "node:path";
|
|
10272
10412
|
var MISSING = [
|
|
10273
10413
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
10274
10414
|
"",
|
|
@@ -10285,8 +10425,8 @@ function candidates() {
|
|
|
10285
10425
|
["edge-tts"],
|
|
10286
10426
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
10287
10427
|
// default, and both are where this actually lands in practice.
|
|
10288
|
-
[
|
|
10289
|
-
[
|
|
10428
|
+
[join10(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10429
|
+
[join10(home, ".local", "bin", "edge-tts")],
|
|
10290
10430
|
// Last resort: the module is installed even though its console script is
|
|
10291
10431
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
10292
10432
|
["python3", "-m", "edge_tts"]
|
|
@@ -10311,7 +10451,7 @@ async function find2(can) {
|
|
|
10311
10451
|
throw new Error(MISSING);
|
|
10312
10452
|
}
|
|
10313
10453
|
function runArgv(cmd, args) {
|
|
10314
|
-
return new Promise((
|
|
10454
|
+
return new Promise((resolve9) => {
|
|
10315
10455
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10316
10456
|
let stdout = "";
|
|
10317
10457
|
let stderr = "";
|
|
@@ -10321,8 +10461,8 @@ function runArgv(cmd, args) {
|
|
|
10321
10461
|
child.stderr.on("data", (b) => {
|
|
10322
10462
|
stderr += b.toString();
|
|
10323
10463
|
});
|
|
10324
|
-
child.on("error", (e) =>
|
|
10325
|
-
child.on("close", (code) =>
|
|
10464
|
+
child.on("error", (e) => resolve9({ code: -1, stderr: String(e), stdout }));
|
|
10465
|
+
child.on("close", (code) => resolve9({ code: code ?? -1, stderr, stdout }));
|
|
10326
10466
|
});
|
|
10327
10467
|
}
|
|
10328
10468
|
var edgeTts = {
|
|
@@ -10389,7 +10529,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
10389
10529
|
async speak(req) {
|
|
10390
10530
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
10391
10531
|
await runner.speak({ ...req, subs });
|
|
10392
|
-
const raw2 = await
|
|
10532
|
+
const raw2 = await readFile9(subs, "utf8").catch(() => "");
|
|
10393
10533
|
await rm6(subs, { force: true });
|
|
10394
10534
|
const cues = parseCues(raw2);
|
|
10395
10535
|
const measured = await runner.measure(req.audio);
|
|
@@ -10407,11 +10547,11 @@ async function synthesize(text2, opts) {
|
|
|
10407
10547
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10408
10548
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10409
10549
|
const file = `${key}.mp3`;
|
|
10410
|
-
const audio =
|
|
10411
|
-
const sidecar =
|
|
10550
|
+
const audio = join10(opts.dir, file);
|
|
10551
|
+
const sidecar = join10(opts.dir, `${key}.json`);
|
|
10412
10552
|
const cached2 = await readSidecar(sidecar);
|
|
10413
10553
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10414
|
-
await
|
|
10554
|
+
await mkdir6(opts.dir, { recursive: true });
|
|
10415
10555
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10416
10556
|
const { cues, seconds } = spoken;
|
|
10417
10557
|
const clamped = cues.map((c) => ({
|
|
@@ -10420,13 +10560,13 @@ async function synthesize(text2, opts) {
|
|
|
10420
10560
|
text: c.text
|
|
10421
10561
|
}));
|
|
10422
10562
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
10423
|
-
await
|
|
10563
|
+
await writeFile8(sidecar, `${JSON.stringify(body, null, 2)}
|
|
10424
10564
|
`);
|
|
10425
10565
|
return { audio, file, seconds, cues: clamped };
|
|
10426
10566
|
}
|
|
10427
10567
|
async function readSidecar(path2) {
|
|
10428
10568
|
try {
|
|
10429
|
-
const parsed = JSON.parse(await
|
|
10569
|
+
const parsed = JSON.parse(await readFile9(path2, "utf8"));
|
|
10430
10570
|
const s = parsed;
|
|
10431
10571
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
10432
10572
|
} catch {
|
|
@@ -10573,12 +10713,12 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
10573
10713
|
}
|
|
10574
10714
|
|
|
10575
10715
|
// src/render/render.ts
|
|
10576
|
-
import { mkdir as
|
|
10577
|
-
import { basename as basename4, dirname as
|
|
10716
|
+
import { mkdir as mkdir8, readFile as readFile10, rename as rename2, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
|
|
10717
|
+
import { basename as basename4, dirname as dirname3, join as join12, resolve as resolve5 } from "node:path";
|
|
10578
10718
|
|
|
10579
10719
|
// src/render/captions.ts
|
|
10580
|
-
import { mkdir as
|
|
10581
|
-
import { join as
|
|
10720
|
+
import { mkdir as mkdir7, writeFile as writeFile9 } from "node:fs/promises";
|
|
10721
|
+
import { join as join11 } from "node:path";
|
|
10582
10722
|
import { pathToFileURL } from "node:url";
|
|
10583
10723
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
10584
10724
|
function cssString(value) {
|
|
@@ -10669,11 +10809,11 @@ async function captionBlocker() {
|
|
|
10669
10809
|
}
|
|
10670
10810
|
async function renderCaptions(cues, style, deck, work2) {
|
|
10671
10811
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
10672
|
-
const fontCss =
|
|
10812
|
+
const fontCss = join11(deck, DECK_FONT_CSS);
|
|
10673
10813
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL(fontCss).href : null;
|
|
10674
|
-
const page =
|
|
10675
|
-
await
|
|
10676
|
-
await
|
|
10814
|
+
const page = join11(work2, "captions.html");
|
|
10815
|
+
await mkdir7(work2, { recursive: true });
|
|
10816
|
+
await writeFile9(page, captionPage(cues, style, href));
|
|
10677
10817
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
10678
10818
|
const browser = await puppeteer.launch({
|
|
10679
10819
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -10693,7 +10833,7 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
10693
10833
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
10694
10834
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
10695
10835
|
await tab.screenshot({
|
|
10696
|
-
path:
|
|
10836
|
+
path: join11(work2, name),
|
|
10697
10837
|
type: "png",
|
|
10698
10838
|
omitBackground: true,
|
|
10699
10839
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -10734,9 +10874,9 @@ function subtitlePlan(mode) {
|
|
|
10734
10874
|
async function render(opts) {
|
|
10735
10875
|
const log = opts.log ?? (() => {
|
|
10736
10876
|
});
|
|
10737
|
-
const deck =
|
|
10738
|
-
const out =
|
|
10739
|
-
const work2 =
|
|
10877
|
+
const deck = resolve5(opts.deck);
|
|
10878
|
+
const out = resolve5(opts.out);
|
|
10879
|
+
const work2 = join12(dirname3(out), `.${basename4(out)}.parts`);
|
|
10740
10880
|
const timing = await readTiming(deck);
|
|
10741
10881
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
10742
10882
|
const refusal = playbackRefusal(
|
|
@@ -10748,13 +10888,17 @@ async function render(opts) {
|
|
|
10748
10888
|
}
|
|
10749
10889
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
10750
10890
|
if (plan0.burn) {
|
|
10891
|
+
if (!timing.captionReserve)
|
|
10892
|
+
throw new Error(
|
|
10893
|
+
"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`."
|
|
10894
|
+
);
|
|
10751
10895
|
const blocker = await captionBlocker();
|
|
10752
10896
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
10753
10897
|
}
|
|
10754
10898
|
const burnable = plan0.burn;
|
|
10755
|
-
await
|
|
10899
|
+
await mkdir8(work2, { recursive: true });
|
|
10756
10900
|
try {
|
|
10757
|
-
const raw2 = opts.video ?
|
|
10901
|
+
const raw2 = opts.video ? resolve5(opts.video) : await capture(deck, join12(work2, "raw.mp4"), opts, log);
|
|
10758
10902
|
const shot = await probe(raw2);
|
|
10759
10903
|
log(
|
|
10760
10904
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -10767,18 +10911,18 @@ async function render(opts) {
|
|
|
10767
10911
|
);
|
|
10768
10912
|
}
|
|
10769
10913
|
const retimed = await retime(raw2, plan, work2, log);
|
|
10770
|
-
const srtPath =
|
|
10914
|
+
const srtPath = join12(dirname3(out), SRT_NAME(out));
|
|
10771
10915
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
10772
10916
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
10773
10917
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
10774
|
-
if (srt) await
|
|
10918
|
+
if (srt) await writeFile10(srtPath, srt);
|
|
10775
10919
|
const burn = burnable && srt.length > 0;
|
|
10776
10920
|
await mux(retimed, timing, plan, deck, out, work2, burn ? plan.cues : void 0, shot.fps, log);
|
|
10777
10921
|
if (playback > 1) {
|
|
10778
10922
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
10779
10923
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
10780
10924
|
if (warning) log(`render: ${warning}`);
|
|
10781
|
-
const fast =
|
|
10925
|
+
const fast = join12(work2, `fast.${basename4(out)}`);
|
|
10782
10926
|
const muxed = await probe(out);
|
|
10783
10927
|
await runTool(
|
|
10784
10928
|
"ffmpeg",
|
|
@@ -10808,8 +10952,8 @@ async function render(opts) {
|
|
|
10808
10952
|
}
|
|
10809
10953
|
}
|
|
10810
10954
|
async function readTiming(deck) {
|
|
10811
|
-
const path2 =
|
|
10812
|
-
const text2 = await
|
|
10955
|
+
const path2 = join12(deck, TIMING_FILE);
|
|
10956
|
+
const text2 = await readFile10(path2, "utf8").catch(() => {
|
|
10813
10957
|
throw new Error(
|
|
10814
10958
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
10815
10959
|
);
|
|
@@ -10848,20 +10992,20 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10848
10992
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
10849
10993
|
const list = [];
|
|
10850
10994
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
10851
|
-
const file =
|
|
10995
|
+
const file = join12(work2, `p${String(i).padStart(4, "0")}.ts`);
|
|
10852
10996
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
10853
10997
|
list.push(file);
|
|
10854
10998
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
10855
10999
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
10856
11000
|
}
|
|
10857
11001
|
}
|
|
10858
|
-
const listFile =
|
|
10859
|
-
await
|
|
11002
|
+
const listFile = join12(work2, "pieces.txt");
|
|
11003
|
+
await writeFile10(
|
|
10860
11004
|
listFile,
|
|
10861
11005
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
10862
11006
|
`
|
|
10863
11007
|
);
|
|
10864
|
-
const joined =
|
|
11008
|
+
const joined = join12(work2, "retimed.mp4");
|
|
10865
11009
|
await runTool("ffmpeg", [
|
|
10866
11010
|
"-y",
|
|
10867
11011
|
"-hide_banner",
|
|
@@ -10883,7 +11027,7 @@ async function retime(raw2, plan, work2, log) {
|
|
|
10883
11027
|
}
|
|
10884
11028
|
async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
10885
11029
|
const inputs = plan.audio.map((a) => ({
|
|
10886
|
-
file:
|
|
11030
|
+
file: join12(deck, timing.audioDir, a.audio),
|
|
10887
11031
|
delayMs: a.delayMs
|
|
10888
11032
|
}));
|
|
10889
11033
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -10904,8 +11048,8 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10904
11048
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
10905
11049
|
}
|
|
10906
11050
|
if (graph.length > 0) {
|
|
10907
|
-
const script =
|
|
10908
|
-
await
|
|
11051
|
+
const script = join12(work2, "mux.filter");
|
|
11052
|
+
await writeFile10(script, `${graph.join(";\n")}
|
|
10909
11053
|
`);
|
|
10910
11054
|
args.push("-filter_complex_script", script);
|
|
10911
11055
|
}
|
|
@@ -10920,13 +11064,13 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
10920
11064
|
log(
|
|
10921
11065
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
10922
11066
|
);
|
|
10923
|
-
await
|
|
11067
|
+
await mkdir8(dirname3(out), { recursive: true });
|
|
10924
11068
|
await runTool("ffmpeg", args, { cwd: work2 });
|
|
10925
11069
|
}
|
|
10926
11070
|
|
|
10927
11071
|
// src/pack/pack.ts
|
|
10928
|
-
import { mkdir as
|
|
10929
|
-
import { dirname as
|
|
11072
|
+
import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile11 } from "node:fs/promises";
|
|
11073
|
+
import { dirname as dirname4, extname as extname3 } from "node:path";
|
|
10930
11074
|
import { unzipSync, zipSync } from "fflate";
|
|
10931
11075
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
10932
11076
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -10965,8 +11109,8 @@ async function writePack(pack3, files, out) {
|
|
|
10965
11109
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
10966
11110
|
}
|
|
10967
11111
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
10968
|
-
await
|
|
10969
|
-
await
|
|
11112
|
+
await mkdir9(dirname4(out), { recursive: true });
|
|
11113
|
+
await writeFile11(out, zip);
|
|
10970
11114
|
return zip.length;
|
|
10971
11115
|
}
|
|
10972
11116
|
function check2(label, path2 = label) {
|
|
@@ -10985,8 +11129,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10985
11129
|
if (!format) throw new Error("no deck-16x9 format");
|
|
10986
11130
|
const step = opts.onStep ?? (() => {
|
|
10987
11131
|
});
|
|
10988
|
-
const out =
|
|
10989
|
-
await
|
|
11132
|
+
const out = resolve6(outDir);
|
|
11133
|
+
await mkdir10(out, { recursive: true });
|
|
10990
11134
|
const speed = opts.speed ?? 1;
|
|
10991
11135
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
10992
11136
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
@@ -10999,47 +11143,48 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
10999
11143
|
});
|
|
11000
11144
|
const files = [];
|
|
11001
11145
|
const write = async (name, text2) => {
|
|
11002
|
-
await
|
|
11003
|
-
files.push(
|
|
11146
|
+
await writeFile12(join13(out, name), text2);
|
|
11147
|
+
files.push(join13(out, name));
|
|
11004
11148
|
};
|
|
11005
11149
|
await write("index.html", deck.composition);
|
|
11006
11150
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
11151
|
+
try {
|
|
11152
|
+
const timing = planTiming({
|
|
11153
|
+
storyboard,
|
|
11154
|
+
source,
|
|
11155
|
+
format,
|
|
11156
|
+
composition: deck.composition,
|
|
11157
|
+
// What was drawn, not what the threshold would have kept: a budgeted
|
|
11158
|
+
// format cuts to its own length as well, and the two lists differ.
|
|
11159
|
+
beats: deck.cut.kept,
|
|
11160
|
+
speed,
|
|
11161
|
+
...opts.narration ? { narration: opts.narration } : {},
|
|
11162
|
+
...opts.theme ? { theme: opts.theme } : {}
|
|
11163
|
+
});
|
|
11164
|
+
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
11022
11165
|
`);
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
}
|
|
11166
|
+
step(`build: timing for ${timing.segments.length} narration segment(s)`);
|
|
11167
|
+
} catch (err) {
|
|
11168
|
+
step(
|
|
11169
|
+
`build: cannot place narration on this deck, so no ${TIMING_FILE} was written and \`render\` will refuse. ${err instanceof Error ? err.message : err}`
|
|
11170
|
+
);
|
|
11029
11171
|
}
|
|
11030
11172
|
if (deck.page) {
|
|
11031
11173
|
await write(DECK_PAGE, deck.page);
|
|
11032
|
-
await
|
|
11033
|
-
files.push(
|
|
11174
|
+
await cp2(playerBundle(), join13(out, PLAYER_FILE));
|
|
11175
|
+
files.push(join13(out, PLAYER_FILE));
|
|
11034
11176
|
}
|
|
11035
11177
|
files.push(...await vendorKatex(out));
|
|
11036
|
-
|
|
11178
|
+
files.push(...await vendorScripts(out, deck.composition));
|
|
11179
|
+
if (opts.assetsFrom) {
|
|
11180
|
+
files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
|
|
11181
|
+
}
|
|
11037
11182
|
if (opts.narration && opts.audioFrom) {
|
|
11038
|
-
files.push(...await copyAudio(opts.audioFrom, opts.narration, out));
|
|
11183
|
+
files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
|
|
11039
11184
|
}
|
|
11040
11185
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
11041
11186
|
step(
|
|
11042
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
11187
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join13(out, "index.html")}`
|
|
11043
11188
|
);
|
|
11044
11189
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
11045
11190
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -11055,77 +11200,21 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
11055
11200
|
)}
|
|
11056
11201
|
`;
|
|
11057
11202
|
async function deckRuntime() {
|
|
11058
|
-
const path2 =
|
|
11203
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
11059
11204
|
try {
|
|
11060
|
-
return await
|
|
11205
|
+
return await readFile12(path2, "utf8");
|
|
11061
11206
|
} catch {
|
|
11062
11207
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
11063
11208
|
}
|
|
11064
11209
|
}
|
|
11065
11210
|
function playerBundle() {
|
|
11066
|
-
const require2 =
|
|
11211
|
+
const require2 = createRequire4(import.meta.url);
|
|
11067
11212
|
try {
|
|
11068
|
-
return
|
|
11213
|
+
return join13(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
11069
11214
|
} catch {
|
|
11070
11215
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
11071
11216
|
}
|
|
11072
11217
|
}
|
|
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
11218
|
|
|
11130
11219
|
// src/server/upload.ts
|
|
11131
11220
|
import { posix, sep as sep2 } from "node:path";
|
|
@@ -11470,8 +11559,8 @@ function int(name, raw2) {
|
|
|
11470
11559
|
}
|
|
11471
11560
|
|
|
11472
11561
|
// src/server/pipeline.ts
|
|
11473
|
-
import { mkdir as
|
|
11474
|
-
import { dirname as
|
|
11562
|
+
import { mkdir as mkdir11, readFile as readFile13, stat as stat3, writeFile as writeFile13 } from "node:fs/promises";
|
|
11563
|
+
import { dirname as dirname6, isAbsolute, join as join14, relative, resolve as resolve7 } from "node:path";
|
|
11475
11564
|
var AUDIO_DIR = "audio";
|
|
11476
11565
|
var NARRATION_FILE = "narration.json";
|
|
11477
11566
|
var MAX_REMOTE_FIGURES = 40;
|
|
@@ -11494,10 +11583,10 @@ function stagesFor(options) {
|
|
|
11494
11583
|
async function runPipeline(job, input) {
|
|
11495
11584
|
const { options } = input;
|
|
11496
11585
|
const dirs = {
|
|
11497
|
-
upload:
|
|
11498
|
-
src:
|
|
11499
|
-
audio:
|
|
11500
|
-
deck:
|
|
11586
|
+
upload: join14(job.dir, "upload"),
|
|
11587
|
+
src: join14(job.dir, "src"),
|
|
11588
|
+
audio: join14(job.dir, AUDIO_DIR),
|
|
11589
|
+
deck: join14(job.dir, "deck")
|
|
11501
11590
|
};
|
|
11502
11591
|
const warnings = [...options.warnings];
|
|
11503
11592
|
const url = (rel) => `/d/${job.id}/${rel}`;
|
|
@@ -11521,21 +11610,21 @@ async function runPipeline(job, input) {
|
|
|
11521
11610
|
theme: options.stated.theme ? prefs.theme : planned.theme
|
|
11522
11611
|
};
|
|
11523
11612
|
assertRefsResolve(storyboard, source, { pending: options.images ? "allow" : "refuse" });
|
|
11524
|
-
await writeJson(
|
|
11613
|
+
await writeJson(join14(job.dir, "storyboard.json"), storyboard);
|
|
11525
11614
|
job.done("plan", `${storyboard.beats.length} beats`);
|
|
11526
11615
|
for (const f of scanBeatCount(storyboard, prefs)) warnings.push(f.message);
|
|
11527
11616
|
if (options.images) {
|
|
11528
11617
|
job.begin("illustrate");
|
|
11529
11618
|
const drawn = await illustrate(storyboard, source, {
|
|
11530
11619
|
prefs,
|
|
11531
|
-
assetsDir:
|
|
11620
|
+
assetsDir: join14(dirs.src, "assets"),
|
|
11532
11621
|
...input.imageChain ? { chain: input.imageChain } : {},
|
|
11533
11622
|
onStep: (line2) => job.log(line2)
|
|
11534
11623
|
});
|
|
11535
11624
|
storyboard = drawn.storyboard;
|
|
11536
11625
|
source = drawn.source;
|
|
11537
|
-
await writeJson(
|
|
11538
|
-
await writeJson(
|
|
11626
|
+
await writeJson(join14(dirs.src, "source.json"), source);
|
|
11627
|
+
await writeJson(join14(job.dir, "storyboard.json"), storyboard);
|
|
11539
11628
|
for (const p of drawn.illustrated) {
|
|
11540
11629
|
job.log(
|
|
11541
11630
|
`illustrate: ${p.beatId} \u2192 assets/${p.src} via ${p.provider}${p.cached ? " (cached)" : ""}`
|
|
@@ -11557,12 +11646,12 @@ async function runPipeline(job, input) {
|
|
|
11557
11646
|
warnings.push("the plan had nothing to say aloud, so the deck is silent");
|
|
11558
11647
|
} else {
|
|
11559
11648
|
job.log(`narrate: ${speaking} of ${storyboard.beats.length} beats speak`);
|
|
11560
|
-
await
|
|
11649
|
+
await mkdir11(dirs.audio, { recursive: true });
|
|
11561
11650
|
const spoken = await narrate(storyboard, source, prefs, {
|
|
11562
11651
|
dir: dirs.audio,
|
|
11563
11652
|
format: options.format
|
|
11564
11653
|
});
|
|
11565
|
-
await writeJson(
|
|
11654
|
+
await writeJson(join14(dirs.audio, NARRATION_FILE), spoken);
|
|
11566
11655
|
narration = { voice: spoken.voice, dir: AUDIO_DIR, beats: spoken.beats };
|
|
11567
11656
|
const segments2 = Object.values(spoken.beats).flat();
|
|
11568
11657
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
@@ -11616,7 +11705,7 @@ async function runPipeline(job, input) {
|
|
|
11616
11705
|
job.log("render: capturing frames \u2014 two minutes for a four-minute video");
|
|
11617
11706
|
const out = await render({
|
|
11618
11707
|
deck: dirs.deck,
|
|
11619
|
-
out:
|
|
11708
|
+
out: join14(dirs.deck, "video.mp4"),
|
|
11620
11709
|
subtitles: "sidecar",
|
|
11621
11710
|
log: (line2) => job.log(line2),
|
|
11622
11711
|
// The last of the three length levers, and the only one that acts on a
|
|
@@ -11665,8 +11754,8 @@ async function runPipeline(job, input) {
|
|
|
11665
11754
|
};
|
|
11666
11755
|
}
|
|
11667
11756
|
async function ingest(job, input, dirs, warnings) {
|
|
11668
|
-
await
|
|
11669
|
-
const root2 =
|
|
11757
|
+
await mkdir11(dirs.upload, { recursive: true });
|
|
11758
|
+
const root2 = resolve7(dirs.upload);
|
|
11670
11759
|
let docPath;
|
|
11671
11760
|
const upload = input.upload;
|
|
11672
11761
|
const bothOrNeither = new UploadError(
|
|
@@ -11676,13 +11765,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11676
11765
|
);
|
|
11677
11766
|
if (input.url !== void 0) {
|
|
11678
11767
|
if (upload !== void 0) throw bothOrNeither;
|
|
11679
|
-
const harvested = await harvestBounded(input.url,
|
|
11768
|
+
const harvested = await harvestBounded(input.url, join14(root2, "assets"), {
|
|
11680
11769
|
...HARVEST_LIMITS,
|
|
11681
11770
|
...input.harvest
|
|
11682
11771
|
});
|
|
11683
11772
|
warnings.push(...harvested.warnings);
|
|
11684
|
-
docPath =
|
|
11685
|
-
await
|
|
11773
|
+
docPath = join14(root2, "document.md");
|
|
11774
|
+
await writeFile13(docPath, harvested.markdown);
|
|
11686
11775
|
job.log(
|
|
11687
11776
|
`ingest: harvested ${input.url} \u2014 "${harvested.title}", ${harvested.assets.length} asset(s)`
|
|
11688
11777
|
);
|
|
@@ -11698,13 +11787,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11698
11787
|
const { files, warnings: zipWarnings } = readZip(upload.bytes);
|
|
11699
11788
|
warnings.push(...zipWarnings);
|
|
11700
11789
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
11701
|
-
const to =
|
|
11790
|
+
const to = resolve7(join14(root2, rel));
|
|
11702
11791
|
if (!insideRoot2(root2, to))
|
|
11703
11792
|
throw new UploadError(`Refusing to write ${rel}.`, "Re-zip from inside the folder.");
|
|
11704
|
-
await
|
|
11705
|
-
await
|
|
11793
|
+
await mkdir11(dirname6(to), { recursive: true });
|
|
11794
|
+
await writeFile13(to, bytes);
|
|
11706
11795
|
}
|
|
11707
|
-
docPath =
|
|
11796
|
+
docPath = join14(root2, pickMarkdown(files));
|
|
11708
11797
|
job.log(
|
|
11709
11798
|
`ingest: unpacked ${Object.keys(files).length} file(s), reading ${pickMarkdown(files)}`
|
|
11710
11799
|
);
|
|
@@ -11716,10 +11805,10 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11716
11805
|
`DeckSmith reads ${MARKDOWN_EXTS.join(", ")} or a .zip containing one. Export the document to markdown first.`
|
|
11717
11806
|
);
|
|
11718
11807
|
}
|
|
11719
|
-
docPath =
|
|
11720
|
-
await
|
|
11808
|
+
docPath = join14(root2, "document.md");
|
|
11809
|
+
await writeFile13(docPath, upload.bytes);
|
|
11721
11810
|
}
|
|
11722
|
-
const text2 = await
|
|
11811
|
+
const text2 = await readFile13(docPath, "utf8");
|
|
11723
11812
|
const parsed = parseMarkdown(text2, {
|
|
11724
11813
|
lang: input.options.stated.lang ? input.options.prefs.lang : void 0
|
|
11725
11814
|
});
|
|
@@ -11739,8 +11828,8 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
11739
11828
|
`ingest: ${parsed.sections.length} sections, ${parsed.figures.length} figures, ${parsed.equations.length} equations, ${parsed.tables.length} tables`
|
|
11740
11829
|
);
|
|
11741
11830
|
const guarded = await guardFigures(parsed, root2, input.fetchRemoteFigures, warnings);
|
|
11742
|
-
const source = await fetchFigures(guarded,
|
|
11743
|
-
await writeJson(
|
|
11831
|
+
const source = await fetchFigures(guarded, join14(resolve7(dirs.src), "assets"), warnings);
|
|
11832
|
+
await writeJson(join14(dirs.src, "source.json"), source);
|
|
11744
11833
|
return source;
|
|
11745
11834
|
}
|
|
11746
11835
|
async function harvestBounded(url, dir, opts) {
|
|
@@ -11816,7 +11905,7 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11816
11905
|
warnings.push(`figure ${figure.id} was left out: "${scheme}:" figures are not read`);
|
|
11817
11906
|
continue;
|
|
11818
11907
|
}
|
|
11819
|
-
const abs = isAbsolute(src) ?
|
|
11908
|
+
const abs = isAbsolute(src) ? resolve7(src) : resolve7(join14(root2, src.replace(/^\.?\//, "")));
|
|
11820
11909
|
if (!insideRoot2(root2, abs)) {
|
|
11821
11910
|
warnings.push(`figure ${figure.id} was left out: "${src}" points outside the upload`);
|
|
11822
11911
|
continue;
|
|
@@ -11831,19 +11920,19 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
11831
11920
|
}
|
|
11832
11921
|
async function pack2(job, ctx) {
|
|
11833
11922
|
try {
|
|
11834
|
-
const assets =
|
|
11923
|
+
const assets = join14(resolve7(ctx.dirs.src), "assets");
|
|
11835
11924
|
const requests = ctx.source.figures.map((f) => ({
|
|
11836
11925
|
id: f.id,
|
|
11837
|
-
url:
|
|
11926
|
+
url: join14(assets, f.src),
|
|
11838
11927
|
prefer: "bake"
|
|
11839
11928
|
}));
|
|
11840
11929
|
const plan = await planMedia(requests, async (url) => ({
|
|
11841
|
-
bytes: new Uint8Array(await
|
|
11930
|
+
bytes: new Uint8Array(await readFile13(url))
|
|
11842
11931
|
}));
|
|
11843
11932
|
const files = { ...plan.files };
|
|
11844
11933
|
if (ctx.narration) {
|
|
11845
|
-
for (const name of
|
|
11846
|
-
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await
|
|
11934
|
+
for (const name of audioNames2(ctx.narration)) {
|
|
11935
|
+
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile13(join14(ctx.dirs.audio, name)));
|
|
11847
11936
|
}
|
|
11848
11937
|
}
|
|
11849
11938
|
const container = {
|
|
@@ -11860,10 +11949,10 @@ async function pack2(job, ctx) {
|
|
|
11860
11949
|
},
|
|
11861
11950
|
source: ctx.source,
|
|
11862
11951
|
storyboard: ctx.storyboard,
|
|
11863
|
-
...ctx.narration ? { narration: await readJson(
|
|
11952
|
+
...ctx.narration ? { narration: await readJson(join14(ctx.dirs.audio, NARRATION_FILE)) } : {},
|
|
11864
11953
|
media: plan.media
|
|
11865
11954
|
};
|
|
11866
|
-
const bytes = await writePack(container, files,
|
|
11955
|
+
const bytes = await writePack(container, files, join14(ctx.dirs.deck, "deck.deck"));
|
|
11867
11956
|
job.log(`pack: ${Math.round(bytes / 1024)} KB \u2192 deck.deck`);
|
|
11868
11957
|
return ctx.url("deck.deck");
|
|
11869
11958
|
} catch (err) {
|
|
@@ -11871,7 +11960,7 @@ async function pack2(job, ctx) {
|
|
|
11871
11960
|
return void 0;
|
|
11872
11961
|
}
|
|
11873
11962
|
}
|
|
11874
|
-
function
|
|
11963
|
+
function audioNames2(narration) {
|
|
11875
11964
|
return [
|
|
11876
11965
|
...new Set(
|
|
11877
11966
|
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
@@ -11886,12 +11975,12 @@ function host(src) {
|
|
|
11886
11975
|
}
|
|
11887
11976
|
}
|
|
11888
11977
|
async function writeJson(path2, value) {
|
|
11889
|
-
await
|
|
11890
|
-
await
|
|
11978
|
+
await mkdir11(dirname6(path2), { recursive: true });
|
|
11979
|
+
await writeFile13(path2, `${JSON.stringify(value, null, 2)}
|
|
11891
11980
|
`);
|
|
11892
11981
|
}
|
|
11893
11982
|
async function readJson(path2) {
|
|
11894
|
-
return JSON.parse(await
|
|
11983
|
+
return JSON.parse(await readFile13(path2, "utf8"));
|
|
11895
11984
|
}
|
|
11896
11985
|
|
|
11897
11986
|
// src/server/errors.ts
|
|
@@ -12297,14 +12386,14 @@ var PAGE_BUDGET = {
|
|
|
12297
12386
|
refs: "relative"
|
|
12298
12387
|
};
|
|
12299
12388
|
async function readPage(url, work2) {
|
|
12300
|
-
await
|
|
12301
|
-
const dir = await mkdtemp3(
|
|
12389
|
+
await mkdir12(work2, { recursive: true });
|
|
12390
|
+
const dir = await mkdtemp3(join15(work2, "harvest-"));
|
|
12302
12391
|
try {
|
|
12303
12392
|
const page = await harvest(url, dir, PAGE_BUDGET);
|
|
12304
12393
|
const files = {
|
|
12305
12394
|
"document.md": new TextEncoder().encode(page.markdown)
|
|
12306
12395
|
};
|
|
12307
|
-
for (const asset of page.assets) files[basename5(asset)] = await
|
|
12396
|
+
for (const asset of page.assets) files[basename5(asset)] = await readFile14(asset);
|
|
12308
12397
|
const warnings = [...page.warnings];
|
|
12309
12398
|
if (page.clips.length > 0) {
|
|
12310
12399
|
warnings.push(
|
|
@@ -12338,7 +12427,7 @@ var statusSchema = z5.object({
|
|
|
12338
12427
|
var DEFAULT_WAIT = 45;
|
|
12339
12428
|
function deckTools(opts) {
|
|
12340
12429
|
const queue = new Queue({ maxQueued: 8 });
|
|
12341
|
-
const root2 =
|
|
12430
|
+
const root2 = resolve8(opts.root);
|
|
12342
12431
|
let cached2;
|
|
12343
12432
|
const found = async () => {
|
|
12344
12433
|
cached2 ??= await (opts.probe ?? prereqs)();
|
|
@@ -12346,7 +12435,7 @@ function deckTools(opts) {
|
|
|
12346
12435
|
};
|
|
12347
12436
|
const insideRoot3 = (p) => {
|
|
12348
12437
|
if (!isAbsolute2(p)) throw new Error(`document_path must be absolute; got "${p}".`);
|
|
12349
|
-
const full =
|
|
12438
|
+
const full = resolve8(p);
|
|
12350
12439
|
if (full !== root2 && !full.startsWith(root2 + sep3)) {
|
|
12351
12440
|
throw new Error(
|
|
12352
12441
|
`document_path is outside the server's root. It may only read files under ${root2}.`
|
|
@@ -12425,15 +12514,15 @@ function deckTools(opts) {
|
|
|
12425
12514
|
}
|
|
12426
12515
|
const page = input.document_url === void 0 ? void 0 : await readPage(input.document_url, opts.work);
|
|
12427
12516
|
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(sep3).pop() : "document.md";
|
|
12428
|
-
const bytes = page ? page.zip : file ? await
|
|
12517
|
+
const bytes = page ? page.zip : file ? await readFile14(file) : new TextEncoder().encode(input.document_text);
|
|
12429
12518
|
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
12430
12519
|
throw new Error(
|
|
12431
12520
|
`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
12521
|
);
|
|
12433
12522
|
}
|
|
12434
12523
|
const id2 = randomBytes(16).toString("base64url");
|
|
12435
|
-
const dir =
|
|
12436
|
-
await
|
|
12524
|
+
const dir = join15(opts.work, id2);
|
|
12525
|
+
await mkdir12(dir, { recursive: true });
|
|
12437
12526
|
queue.submit({
|
|
12438
12527
|
id: id2,
|
|
12439
12528
|
dir,
|
|
@@ -12460,7 +12549,7 @@ function deckTools(opts) {
|
|
|
12460
12549
|
};
|
|
12461
12550
|
}
|
|
12462
12551
|
function dirOf(work2, id2) {
|
|
12463
|
-
return
|
|
12552
|
+
return join15(work2, id2);
|
|
12464
12553
|
}
|
|
12465
12554
|
function waitFor(queue, id2, seconds) {
|
|
12466
12555
|
const now = queue.view(id2);
|
|
@@ -12492,9 +12581,9 @@ function report(view, dir) {
|
|
|
12492
12581
|
// (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
|
|
12493
12582
|
// is the shape of the CLI's output directory and not the server's, and an
|
|
12494
12583
|
// agent told to read it would have found nothing there.
|
|
12495
|
-
storyboard_path:
|
|
12584
|
+
storyboard_path: join15(dir, "storyboard.json"),
|
|
12496
12585
|
...done && view.result ? {
|
|
12497
|
-
deck_path:
|
|
12586
|
+
deck_path: join15(dir, "deck"),
|
|
12498
12587
|
slides: view.result.slides,
|
|
12499
12588
|
duration_seconds: view.result.duration,
|
|
12500
12589
|
warnings: view.result.warnings
|
|
@@ -12503,7 +12592,7 @@ function report(view, dir) {
|
|
|
12503
12592
|
};
|
|
12504
12593
|
}
|
|
12505
12594
|
function defaultWork() {
|
|
12506
|
-
return
|
|
12595
|
+
return join15(tmpdir4(), "decksmith-mcp");
|
|
12507
12596
|
}
|
|
12508
12597
|
|
|
12509
12598
|
// src/mcp/main.ts
|