@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/cli.js
CHANGED
|
@@ -1,16 +1,181 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { cp, mkdir as
|
|
5
|
-
import { createRequire as
|
|
4
|
+
import { cp as cp2, mkdir as mkdir11, mkdtemp as mkdtemp4, readFile as readFile18, rm as rm9, stat as stat3, writeFile as writeFile13 } from "node:fs/promises";
|
|
5
|
+
import { createRequire as createRequire5 } from "node:module";
|
|
6
6
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
7
|
-
import { dirname as
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname as dirname6, join as join18, relative as relative2, resolve as resolve7 } from "node:path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
11
|
+
// src/build/files.ts
|
|
12
|
+
import { cp, mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
// src/source/fonts.ts
|
|
12
18
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { readFile } from "node:fs/promises";
|
|
19
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
function familyFor(lang) {
|
|
22
|
+
const tag = lang.toLowerCase();
|
|
23
|
+
if (tag.startsWith("ko")) return "Noto Sans KR";
|
|
24
|
+
if (tag.startsWith("ja")) return "Noto Sans JP";
|
|
25
|
+
if (tag.startsWith("zh")) return /hant|-tw|-hk|-mo/.test(tag) ? "Noto Sans TC" : "Noto Sans SC";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
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";
|
|
29
|
+
async function bundleFont(lang, glyphs2, dir) {
|
|
30
|
+
const family = familyFor(lang);
|
|
31
|
+
if (!family) return null;
|
|
32
|
+
const text2 = [...new Set(glyphs2)].filter((c) => c > " ").sort().join("");
|
|
33
|
+
const stamp = `/* decksmith ${createHash("sha256").update(`${family}
|
|
34
|
+
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
const cssPath = join(dir, "fonts.css");
|
|
37
|
+
const cached2 = await readFile(cssPath, "utf8").catch(() => "");
|
|
38
|
+
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
39
|
+
const res = await fetch(
|
|
40
|
+
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
41
|
+
{ headers: { "User-Agent": UA } }
|
|
42
|
+
);
|
|
43
|
+
if (!res.ok) throw new Error(`google fonts: HTTP ${res.status}`);
|
|
44
|
+
let css = await res.text();
|
|
45
|
+
const remote = [...new Set([...css.matchAll(/url\((https:[^)]+)\)/g)].map((m) => m[1] ?? ""))];
|
|
46
|
+
const slug = family.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
47
|
+
const files = [];
|
|
48
|
+
for (const [i, url2] of remote.entries()) {
|
|
49
|
+
const font = await fetch(url2);
|
|
50
|
+
if (!font.ok) throw new Error(`${url2}: HTTP ${font.status}`);
|
|
51
|
+
const name = `${slug}-${i}.woff2`;
|
|
52
|
+
await writeFile(join(dir, name), Buffer.from(await font.arrayBuffer()));
|
|
53
|
+
css = css.replaceAll(url2, name);
|
|
54
|
+
files.push(name);
|
|
55
|
+
}
|
|
56
|
+
css = `${stamp}
|
|
57
|
+
${css}`;
|
|
58
|
+
await writeFile(cssPath, css);
|
|
59
|
+
return { family, css, files };
|
|
60
|
+
}
|
|
61
|
+
function localNames(css) {
|
|
62
|
+
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/build/files.ts
|
|
66
|
+
var AUDIO_DIR = "audio";
|
|
67
|
+
function audioNames(narration) {
|
|
68
|
+
return [
|
|
69
|
+
...new Set(
|
|
70
|
+
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
71
|
+
)
|
|
72
|
+
].sort();
|
|
73
|
+
}
|
|
74
|
+
async function vendorKatex(out) {
|
|
75
|
+
const require2 = createRequire(import.meta.url);
|
|
76
|
+
const dist = join2(dirname(require2.resolve("katex/package.json")), "dist");
|
|
77
|
+
const css = await readFile2(join2(dist, "katex.min.css"), "utf8");
|
|
78
|
+
const written = [];
|
|
79
|
+
await mkdir2(join2(out, "katex/fonts"), { recursive: true });
|
|
80
|
+
for (const file of await readdir(join2(dist, "fonts"))) {
|
|
81
|
+
if (!file.endsWith(".woff2")) continue;
|
|
82
|
+
await cp(join2(dist, "fonts", file), join2(out, "katex/fonts", file));
|
|
83
|
+
written.push(join2(out, "katex/fonts", file));
|
|
84
|
+
}
|
|
85
|
+
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
86
|
+
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
87
|
+
return kept ? `src:${kept}` : whole;
|
|
88
|
+
});
|
|
89
|
+
await writeFile2(join2(out, "katex/katex.min.css"), woff2Only);
|
|
90
|
+
written.push(join2(out, "katex/katex.min.css"));
|
|
91
|
+
return written;
|
|
92
|
+
}
|
|
93
|
+
async function vendorScripts(out, composition) {
|
|
94
|
+
const require2 = createRequire(import.meta.url);
|
|
95
|
+
await mkdir2(join2(out, "vendor"), { recursive: true });
|
|
96
|
+
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
97
|
+
const written = [];
|
|
98
|
+
for (const [pkg, rel, name] of [
|
|
99
|
+
["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
|
|
100
|
+
["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
|
|
101
|
+
["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
|
|
102
|
+
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
103
|
+
]) {
|
|
104
|
+
if (!wanted(name)) continue;
|
|
105
|
+
const from = join2(dirname(require2.resolve(pkg)), rel);
|
|
106
|
+
await cp(from, join2(out, "vendor", name));
|
|
107
|
+
written.push(join2(out, "vendor", name));
|
|
108
|
+
}
|
|
109
|
+
if (wanted("ds-morph.js")) {
|
|
110
|
+
await cp(
|
|
111
|
+
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
112
|
+
join2(out, "vendor", "ds-morph.js")
|
|
113
|
+
);
|
|
114
|
+
written.push(join2(out, "vendor", "ds-morph.js"));
|
|
115
|
+
}
|
|
116
|
+
return written;
|
|
117
|
+
}
|
|
118
|
+
async function copyAssets(sourceDir, out, figures, step2) {
|
|
119
|
+
const from = join2(resolve(sourceDir), "assets");
|
|
120
|
+
if (!await stat(from).catch(() => null)) {
|
|
121
|
+
step2(`build: no assets/ beside source.json, skipping`);
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const wanted = new Set(
|
|
125
|
+
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
126
|
+
);
|
|
127
|
+
await mkdir2(join2(out, "assets"), { recursive: true });
|
|
128
|
+
const written = [];
|
|
129
|
+
let copied = 0;
|
|
130
|
+
for (const name of wanted) {
|
|
131
|
+
const src = resolve(join2(from, name));
|
|
132
|
+
if (!src.startsWith(`${from}/`)) continue;
|
|
133
|
+
if (!await stat(src).catch(() => null)) continue;
|
|
134
|
+
await mkdir2(dirname(join2(out, "assets", name)), { recursive: true });
|
|
135
|
+
await cp(src, join2(out, "assets", name));
|
|
136
|
+
written.push(join2(out, "assets", name));
|
|
137
|
+
copied++;
|
|
138
|
+
}
|
|
139
|
+
const fonts = join2(from, "fonts");
|
|
140
|
+
if (await stat(fonts).catch(() => null)) {
|
|
141
|
+
await cp(fonts, join2(out, "assets", "fonts"), { recursive: true });
|
|
142
|
+
written.push(join2(out, "assets", "fonts"));
|
|
143
|
+
}
|
|
144
|
+
step2(`build: copied ${copied} referenced figure(s)`);
|
|
145
|
+
return written;
|
|
146
|
+
}
|
|
147
|
+
async function copyAudio(from, narration, out, step2) {
|
|
148
|
+
const dir = join2(out, narration.dir);
|
|
149
|
+
await mkdir2(dir, { recursive: true });
|
|
150
|
+
const names = audioNames(narration);
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
await cp(join2(resolve(from), name), join2(dir, name)).catch(() => {
|
|
153
|
+
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
step2(`narration: ${names.length} audio file(s) \u2192 ${dir}`);
|
|
157
|
+
return names.map((n3) => join2(dir, n3));
|
|
158
|
+
}
|
|
159
|
+
async function refreshFont(storyboard, source, out, step2) {
|
|
160
|
+
try {
|
|
161
|
+
const bundle = await bundleFont(
|
|
162
|
+
storyboard.lang,
|
|
163
|
+
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
164
|
+
join2(out, "assets", "fonts")
|
|
165
|
+
);
|
|
166
|
+
if (bundle) step2(`build: font bundle covers ${bundle.family}`);
|
|
167
|
+
return bundle?.css;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
step2(
|
|
170
|
+
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
171
|
+
);
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/pack/media.ts
|
|
177
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
178
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
14
179
|
import { extname } from "node:path";
|
|
15
180
|
var PLAYERS = [
|
|
16
181
|
"youtube.com",
|
|
@@ -184,7 +349,7 @@ function mediaSummary(plan) {
|
|
|
184
349
|
}
|
|
185
350
|
function bakedName(id2, url2, mime) {
|
|
186
351
|
const slug = id2.toLowerCase().replace(/[^a-z0-9_-]+/g, "-") || "asset";
|
|
187
|
-
const hash =
|
|
352
|
+
const hash = createHash2("sha256").update(url2).digest("hex").slice(0, 8);
|
|
188
353
|
return `${slug}-${hash}${extOf(url2) || extFor(mime)}`;
|
|
189
354
|
}
|
|
190
355
|
function extOf(url2) {
|
|
@@ -239,7 +404,7 @@ async function fetchAsset(url2) {
|
|
|
239
404
|
mime: res.headers.get("content-type") ?? void 0
|
|
240
405
|
};
|
|
241
406
|
}
|
|
242
|
-
return { bytes: new Uint8Array(await
|
|
407
|
+
return { bytes: new Uint8Array(await readFile3(url2)) };
|
|
243
408
|
}
|
|
244
409
|
|
|
245
410
|
// src/types.ts
|
|
@@ -908,6 +1073,18 @@ var packSchema = z.object({
|
|
|
908
1073
|
narration: narrationSchema.optional(),
|
|
909
1074
|
media: z.array(mediaSchema).default([])
|
|
910
1075
|
});
|
|
1076
|
+
var CAPTION_FONT_RATIO = 0.037;
|
|
1077
|
+
var CAPTION_MARGIN_V_RATIO = 0.09;
|
|
1078
|
+
var CAPTION_MARGIN_X_RATIO = 0.04;
|
|
1079
|
+
var CAPTION_LINE_RATIO = 1.6;
|
|
1080
|
+
var CAPTION_MAX_LINES = 2;
|
|
1081
|
+
function captionFontSize(width) {
|
|
1082
|
+
return Math.round(width * CAPTION_FONT_RATIO);
|
|
1083
|
+
}
|
|
1084
|
+
function bandReserve(width, height) {
|
|
1085
|
+
const line2 = Math.round(captionFontSize(width) * CAPTION_LINE_RATIO);
|
|
1086
|
+
return Math.round(height * CAPTION_MARGIN_V_RATIO) + CAPTION_MAX_LINES * line2;
|
|
1087
|
+
}
|
|
911
1088
|
var DESTINATIONS = {
|
|
912
1089
|
// A presented deck is paced by the person clicking through it, and long-form
|
|
913
1090
|
// video has no ceiling worth encoding. An empty list is "unbudgeted", stated.
|
|
@@ -1224,54 +1401,6 @@ function clock(seconds) {
|
|
|
1224
1401
|
return m === 0 ? `${whole}s` : `${m}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1225
1402
|
}
|
|
1226
1403
|
|
|
1227
|
-
// src/source/fonts.ts
|
|
1228
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1229
|
-
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
1230
|
-
import { join } from "node:path";
|
|
1231
|
-
function familyFor(lang) {
|
|
1232
|
-
const tag = lang.toLowerCase();
|
|
1233
|
-
if (tag.startsWith("ko")) return "Noto Sans KR";
|
|
1234
|
-
if (tag.startsWith("ja")) return "Noto Sans JP";
|
|
1235
|
-
if (tag.startsWith("zh")) return /hant|-tw|-hk|-mo/.test(tag) ? "Noto Sans TC" : "Noto Sans SC";
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
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";
|
|
1239
|
-
async function bundleFont(lang, glyphs2, dir) {
|
|
1240
|
-
const family = familyFor(lang);
|
|
1241
|
-
if (!family) return null;
|
|
1242
|
-
const text2 = [...new Set(glyphs2)].filter((c) => c > " ").sort().join("");
|
|
1243
|
-
const stamp = `/* decksmith ${createHash2("sha256").update(`${family}
|
|
1244
|
-
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1245
|
-
await mkdir(dir, { recursive: true });
|
|
1246
|
-
const cssPath = join(dir, "fonts.css");
|
|
1247
|
-
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1248
|
-
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1249
|
-
const res = await fetch(
|
|
1250
|
-
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
1251
|
-
{ headers: { "User-Agent": UA } }
|
|
1252
|
-
);
|
|
1253
|
-
if (!res.ok) throw new Error(`google fonts: HTTP ${res.status}`);
|
|
1254
|
-
let css = await res.text();
|
|
1255
|
-
const remote = [...new Set([...css.matchAll(/url\((https:[^)]+)\)/g)].map((m) => m[1] ?? ""))];
|
|
1256
|
-
const slug = family.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1257
|
-
const files = [];
|
|
1258
|
-
for (const [i, url2] of remote.entries()) {
|
|
1259
|
-
const font = await fetch(url2);
|
|
1260
|
-
if (!font.ok) throw new Error(`${url2}: HTTP ${font.status}`);
|
|
1261
|
-
const name = `${slug}-${i}.woff2`;
|
|
1262
|
-
await writeFile(join(dir, name), Buffer.from(await font.arrayBuffer()));
|
|
1263
|
-
css = css.replaceAll(url2, name);
|
|
1264
|
-
files.push(name);
|
|
1265
|
-
}
|
|
1266
|
-
css = `${stamp}
|
|
1267
|
-
${css}`;
|
|
1268
|
-
await writeFile(cssPath, css);
|
|
1269
|
-
return { family, css, files };
|
|
1270
|
-
}
|
|
1271
|
-
function localNames(css) {
|
|
1272
|
-
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
1404
|
// src/emit/kit.ts
|
|
1276
1405
|
var PAD_X = 110;
|
|
1277
1406
|
var PAD_Y = 84;
|
|
@@ -1290,7 +1419,12 @@ function contentW(format) {
|
|
|
1290
1419
|
return refWidth(format) - 2 * PAD_X;
|
|
1291
1420
|
}
|
|
1292
1421
|
function contentH(format) {
|
|
1293
|
-
return refHeight(format) - 2 * PAD_Y;
|
|
1422
|
+
return refHeight(format) - 2 * PAD_Y - reserveRef(format);
|
|
1423
|
+
}
|
|
1424
|
+
function reserveRef(format) {
|
|
1425
|
+
const reserve = format.captionReserve ?? 0;
|
|
1426
|
+
if (reserve <= 0) return 0;
|
|
1427
|
+
return Math.ceil(reserve * refHeight(format) / format.height);
|
|
1294
1428
|
}
|
|
1295
1429
|
function raw(js2) {
|
|
1296
1430
|
return { __raw: js2 };
|
|
@@ -1789,8 +1923,8 @@ function charUnits(c, tabular, cjkRun, hangul) {
|
|
|
1789
1923
|
}
|
|
1790
1924
|
const measured = ADVANCE[c];
|
|
1791
1925
|
if (measured !== void 0) return [measured, true];
|
|
1792
|
-
const
|
|
1793
|
-
for (const [lo, hi, units] of BLOCK_ADVANCE) if (
|
|
1926
|
+
const cp3 = c.codePointAt(0) ?? 0;
|
|
1927
|
+
for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp3 >= lo && cp3 <= hi) return [units, false];
|
|
1794
1928
|
return [UNMEASURED, true];
|
|
1795
1929
|
}
|
|
1796
1930
|
function weightFactor(weight) {
|
|
@@ -2051,11 +2185,16 @@ function baseCss(theme, format) {
|
|
|
2051
2185
|
gate still catches anything taller than the canvas; it just now
|
|
2052
2186
|
overflows symmetrically instead of only downwards. */
|
|
2053
2187
|
.scene { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
2054
|
-
padding: ${
|
|
2188
|
+
padding: ${scenePadding(format)}; display: flex; flex-direction: column;
|
|
2055
2189
|
justify-content: center; }
|
|
2056
2190
|
${referenceSpaceCss(format)}
|
|
2057
2191
|
${AMBIENT_KEYFRAMES}`;
|
|
2058
2192
|
}
|
|
2193
|
+
function scenePadding(format) {
|
|
2194
|
+
const reserve = reserveRef(format);
|
|
2195
|
+
const base = `${PAD_Y}px ${PAD_X}px`;
|
|
2196
|
+
return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
|
|
2197
|
+
}
|
|
2059
2198
|
function referenceSpaceCss(format) {
|
|
2060
2199
|
const zoom = zoomOf(format);
|
|
2061
2200
|
if (zoom === 1) return "";
|
|
@@ -6189,13 +6328,13 @@ function rnd(n3) {
|
|
|
6189
6328
|
|
|
6190
6329
|
// src/images/illustrate.ts
|
|
6191
6330
|
import { createHash as createHash5 } from "node:crypto";
|
|
6192
|
-
import { mkdir as
|
|
6193
|
-
import { join as
|
|
6331
|
+
import { mkdir as mkdir4, readFile as readFile7, rename, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
|
|
6332
|
+
import { join as join6 } from "node:path";
|
|
6194
6333
|
|
|
6195
6334
|
// src/source/assets.ts
|
|
6196
6335
|
import { createHash as createHash3 } from "node:crypto";
|
|
6197
|
-
import { mkdir as
|
|
6198
|
-
import { extname as extname2, join as
|
|
6336
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
6337
|
+
import { extname as extname2, join as join3 } from "node:path";
|
|
6199
6338
|
import { z as z2 } from "zod";
|
|
6200
6339
|
|
|
6201
6340
|
// src/net/fetch.ts
|
|
@@ -6206,8 +6345,8 @@ import { isIP } from "node:net";
|
|
|
6206
6345
|
import { brotliDecompressSync, gunzipSync } from "node:zlib";
|
|
6207
6346
|
|
|
6208
6347
|
// src/version.ts
|
|
6209
|
-
import { createRequire } from "node:module";
|
|
6210
|
-
var VERSION =
|
|
6348
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
6349
|
+
var VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
6211
6350
|
|
|
6212
6351
|
// src/net/fetch.ts
|
|
6213
6352
|
var BLOCKED_V4 = [
|
|
@@ -6402,8 +6541,8 @@ async function send(url2, target, headers, signal, opts) {
|
|
|
6402
6541
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6403
6542
|
};
|
|
6404
6543
|
try {
|
|
6405
|
-
return await new Promise((
|
|
6406
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
6544
|
+
return await new Promise((resolve8, reject) => {
|
|
6545
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve8);
|
|
6407
6546
|
req.on("error", reject);
|
|
6408
6547
|
req.end();
|
|
6409
6548
|
});
|
|
@@ -6488,8 +6627,8 @@ function message(err) {
|
|
|
6488
6627
|
// src/source/assets.ts
|
|
6489
6628
|
async function fetchFigures(source, dir, warnings) {
|
|
6490
6629
|
const drops = warnings ?? [];
|
|
6491
|
-
await
|
|
6492
|
-
const cached2 = await
|
|
6630
|
+
await mkdir3(dir, { recursive: true });
|
|
6631
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
6493
6632
|
const figures = [];
|
|
6494
6633
|
for (const figure of source.figures) {
|
|
6495
6634
|
if (figure.kind === "clip") {
|
|
@@ -6514,11 +6653,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6514
6653
|
async function localize(figure, dir, cached2) {
|
|
6515
6654
|
const stem = assetStem(figure.id, figure.src);
|
|
6516
6655
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
6517
|
-
const bytes = hit ? await
|
|
6656
|
+
const bytes = hit ? await readFile4(join3(dir, hit)) : await load(figure.src);
|
|
6518
6657
|
const size3 = imageSize(bytes);
|
|
6519
6658
|
if (hit) return { ...figure, src: hit, ...size3 };
|
|
6520
6659
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6521
|
-
await
|
|
6660
|
+
await writeFile3(join3(dir, src), bytes);
|
|
6522
6661
|
cached2.push(src);
|
|
6523
6662
|
return { ...figure, src, ...size3 };
|
|
6524
6663
|
}
|
|
@@ -6543,7 +6682,7 @@ function assetExt(bytes, src) {
|
|
|
6543
6682
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
6544
6683
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
6545
6684
|
async function load(src) {
|
|
6546
|
-
if (!/^https?:/i.test(src)) return
|
|
6685
|
+
if (!/^https?:/i.test(src)) return readFile4(src);
|
|
6547
6686
|
const got = await fetchGuarded(src, {
|
|
6548
6687
|
maxBytes: FIGURE_MAX_BYTES,
|
|
6549
6688
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -6788,16 +6927,16 @@ function cssPixels(value) {
|
|
|
6788
6927
|
|
|
6789
6928
|
// src/images/providers.ts
|
|
6790
6929
|
import { createHash as createHash4 } from "node:crypto";
|
|
6791
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
6930
|
+
import { mkdtemp as mkdtemp2, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
6792
6931
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
6793
|
-
import { join as
|
|
6932
|
+
import { join as join5, resolve as resolve2 } from "node:path";
|
|
6794
6933
|
import { z as z4 } from "zod";
|
|
6795
6934
|
|
|
6796
6935
|
// src/plan/codex.ts
|
|
6797
6936
|
import { spawn } from "node:child_process";
|
|
6798
|
-
import { mkdtemp, readFile as
|
|
6937
|
+
import { mkdtemp, readFile as readFile5, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
6799
6938
|
import { tmpdir } from "node:os";
|
|
6800
|
-
import { join as
|
|
6939
|
+
import { join as join4 } from "node:path";
|
|
6801
6940
|
import { z as z3 } from "zod";
|
|
6802
6941
|
|
|
6803
6942
|
// src/plan/arc.ts
|
|
@@ -7916,11 +8055,11 @@ function schemaFor(prefs) {
|
|
|
7916
8055
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
7917
8056
|
async function codexPlanner(source, opts = {}) {
|
|
7918
8057
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
7919
|
-
const dir = await mkdtemp(
|
|
8058
|
+
const dir = await mkdtemp(join4(tmpdir(), "decksmith-plan-"));
|
|
7920
8059
|
try {
|
|
7921
|
-
const schemaPath =
|
|
7922
|
-
const outPath =
|
|
7923
|
-
await
|
|
8060
|
+
const schemaPath = join4(dir, "storyboard.schema.json");
|
|
8061
|
+
const outPath = join4(dir, "storyboard.json");
|
|
8062
|
+
await writeFile4(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
7924
8063
|
await (opts.run ?? runCodex)({
|
|
7925
8064
|
prompt: buildPrompt(source, prefs),
|
|
7926
8065
|
schemaPath,
|
|
@@ -7928,7 +8067,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
7928
8067
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
7929
8068
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
7930
8069
|
});
|
|
7931
|
-
const raw2 = await
|
|
8070
|
+
const raw2 = await readFile5(outPath, "utf8").catch(() => "");
|
|
7932
8071
|
if (raw2.trim() === "") {
|
|
7933
8072
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
7934
8073
|
}
|
|
@@ -8001,7 +8140,7 @@ function codexCommand(args) {
|
|
|
8001
8140
|
}
|
|
8002
8141
|
function runCodex(args) {
|
|
8003
8142
|
const { argv, env } = codexCommand(args);
|
|
8004
|
-
return new Promise((
|
|
8143
|
+
return new Promise((resolve8, reject) => {
|
|
8005
8144
|
const child = spawn("codex", argv, {
|
|
8006
8145
|
stdio: ["pipe", "ignore", "pipe"],
|
|
8007
8146
|
...env === void 0 ? {} : { env }
|
|
@@ -8024,7 +8163,7 @@ function runCodex(args) {
|
|
|
8024
8163
|
});
|
|
8025
8164
|
child.on("close", (code) => {
|
|
8026
8165
|
clearTimeout(timer);
|
|
8027
|
-
if (code === 0) return
|
|
8166
|
+
if (code === 0) return resolve8();
|
|
8028
8167
|
reject(
|
|
8029
8168
|
new Error(`codex exec exited ${code}.
|
|
8030
8169
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -8165,11 +8304,11 @@ function codexImages(opts = {}) {
|
|
|
8165
8304
|
async check() {
|
|
8166
8305
|
},
|
|
8167
8306
|
async generate(req) {
|
|
8168
|
-
const dir = await mkdtemp2(
|
|
8307
|
+
const dir = await mkdtemp2(join5(tmpdir2(), "decksmith-image-"));
|
|
8169
8308
|
try {
|
|
8170
|
-
const schemaPath =
|
|
8171
|
-
const outPath =
|
|
8172
|
-
await
|
|
8309
|
+
const schemaPath = join5(dir, "answer.schema.json");
|
|
8310
|
+
const outPath = join5(dir, "answer.json");
|
|
8311
|
+
await writeFile5(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
8173
8312
|
await run4({
|
|
8174
8313
|
prompt: codexPrompt(req),
|
|
8175
8314
|
schemaPath,
|
|
@@ -8178,7 +8317,7 @@ function codexImages(opts = {}) {
|
|
|
8178
8317
|
cwd: dir,
|
|
8179
8318
|
sandbox: "workspace-write"
|
|
8180
8319
|
});
|
|
8181
|
-
const raw2 = await
|
|
8320
|
+
const raw2 = await readFile6(outPath, "utf8").catch(() => "");
|
|
8182
8321
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
8183
8322
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
8184
8323
|
if (!answer.data.ok) {
|
|
@@ -8186,10 +8325,10 @@ function codexImages(opts = {}) {
|
|
|
8186
8325
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
8187
8326
|
);
|
|
8188
8327
|
}
|
|
8189
|
-
const candidates2 = [
|
|
8190
|
-
if (answer.data.file) candidates2.push(
|
|
8328
|
+
const candidates2 = [join5(dir, "picture.png")];
|
|
8329
|
+
if (answer.data.file) candidates2.push(resolve2(dir, answer.data.file));
|
|
8191
8330
|
for (const path2 of candidates2) {
|
|
8192
|
-
const bytes = await
|
|
8331
|
+
const bytes = await readFile6(path2).catch(() => null);
|
|
8193
8332
|
if (bytes) return raster(bytes, "codex");
|
|
8194
8333
|
}
|
|
8195
8334
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -8321,7 +8460,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
8321
8460
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
8322
8461
|
if (tool !== last) chain.push(tool);
|
|
8323
8462
|
const dropped = /* @__PURE__ */ new Set();
|
|
8324
|
-
if (pending.length > 0) await
|
|
8463
|
+
if (pending.length > 0) await mkdir4(opts.assetsDir, { recursive: true });
|
|
8325
8464
|
for (const [i, slot] of pending.entries()) {
|
|
8326
8465
|
let rungs = chain;
|
|
8327
8466
|
if (i >= images.max) {
|
|
@@ -8418,16 +8557,16 @@ function cacheKey(providerId, req) {
|
|
|
8418
8557
|
async function draw(provider, req, name, dir) {
|
|
8419
8558
|
for (const ext of Object.values(EXT2)) {
|
|
8420
8559
|
const src2 = `${name}${ext}`;
|
|
8421
|
-
const bytes = await
|
|
8560
|
+
const bytes = await readFile7(join6(dir, src2)).catch(() => null);
|
|
8422
8561
|
if (bytes) return { src: src2, ...sizeOf(bytes, ext), cached: true };
|
|
8423
8562
|
}
|
|
8424
8563
|
await provider.check();
|
|
8425
8564
|
const img = await provider.generate(req);
|
|
8426
8565
|
const src = `${name}${EXT2[img.mime]}`;
|
|
8427
|
-
const tmp =
|
|
8566
|
+
const tmp = join6(dir, `.${name}.tmp`);
|
|
8428
8567
|
try {
|
|
8429
|
-
await
|
|
8430
|
-
await rename(tmp,
|
|
8568
|
+
await writeFile6(tmp, img.bytes);
|
|
8569
|
+
await rename(tmp, join6(dir, src));
|
|
8431
8570
|
} finally {
|
|
8432
8571
|
await rm3(tmp, { force: true });
|
|
8433
8572
|
}
|
|
@@ -8440,9 +8579,9 @@ function sizeOf(bytes, ext) {
|
|
|
8440
8579
|
// src/narrate/tts.ts
|
|
8441
8580
|
import { spawn as spawn2 } from "node:child_process";
|
|
8442
8581
|
import { createHash as createHash6 } from "node:crypto";
|
|
8443
|
-
import { mkdir as
|
|
8582
|
+
import { mkdir as mkdir5, readFile as readFile8, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
8444
8583
|
import { homedir } from "node:os";
|
|
8445
|
-
import { join as
|
|
8584
|
+
import { join as join7 } from "node:path";
|
|
8446
8585
|
var MISSING = [
|
|
8447
8586
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
8448
8587
|
"",
|
|
@@ -8459,8 +8598,8 @@ function candidates() {
|
|
|
8459
8598
|
["edge-tts"],
|
|
8460
8599
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
8461
8600
|
// default, and both are where this actually lands in practice.
|
|
8462
|
-
[
|
|
8463
|
-
[
|
|
8601
|
+
[join7(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
8602
|
+
[join7(home, ".local", "bin", "edge-tts")],
|
|
8464
8603
|
// Last resort: the module is installed even though its console script is
|
|
8465
8604
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
8466
8605
|
["python3", "-m", "edge_tts"]
|
|
@@ -8485,7 +8624,7 @@ async function find2(can) {
|
|
|
8485
8624
|
throw new Error(MISSING);
|
|
8486
8625
|
}
|
|
8487
8626
|
function runArgv(cmd, args) {
|
|
8488
|
-
return new Promise((
|
|
8627
|
+
return new Promise((resolve8) => {
|
|
8489
8628
|
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8490
8629
|
let stdout = "";
|
|
8491
8630
|
let stderr = "";
|
|
@@ -8495,8 +8634,8 @@ function runArgv(cmd, args) {
|
|
|
8495
8634
|
child.stderr.on("data", (b) => {
|
|
8496
8635
|
stderr += b.toString();
|
|
8497
8636
|
});
|
|
8498
|
-
child.on("error", (e) =>
|
|
8499
|
-
child.on("close", (code) =>
|
|
8637
|
+
child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
|
|
8638
|
+
child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
|
|
8500
8639
|
});
|
|
8501
8640
|
}
|
|
8502
8641
|
var edgeTts = {
|
|
@@ -8563,7 +8702,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
8563
8702
|
async speak(req) {
|
|
8564
8703
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
8565
8704
|
await runner.speak({ ...req, subs });
|
|
8566
|
-
const raw2 = await
|
|
8705
|
+
const raw2 = await readFile8(subs, "utf8").catch(() => "");
|
|
8567
8706
|
await rm4(subs, { force: true });
|
|
8568
8707
|
const cues = parseCues(raw2);
|
|
8569
8708
|
const measured = await runner.measure(req.audio);
|
|
@@ -8581,11 +8720,11 @@ async function synthesize(text2, opts) {
|
|
|
8581
8720
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
8582
8721
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
8583
8722
|
const file = `${key}.mp3`;
|
|
8584
|
-
const audio =
|
|
8585
|
-
const sidecar =
|
|
8723
|
+
const audio = join7(opts.dir, file);
|
|
8724
|
+
const sidecar = join7(opts.dir, `${key}.json`);
|
|
8586
8725
|
const cached2 = await readSidecar(sidecar);
|
|
8587
8726
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
8588
|
-
await
|
|
8727
|
+
await mkdir5(opts.dir, { recursive: true });
|
|
8589
8728
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
8590
8729
|
const { cues, seconds } = spoken;
|
|
8591
8730
|
const clamped = cues.map((c) => ({
|
|
@@ -8594,13 +8733,13 @@ async function synthesize(text2, opts) {
|
|
|
8594
8733
|
text: c.text
|
|
8595
8734
|
}));
|
|
8596
8735
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
8597
|
-
await
|
|
8736
|
+
await writeFile7(sidecar, `${JSON.stringify(body, null, 2)}
|
|
8598
8737
|
`);
|
|
8599
8738
|
return { audio, file, seconds, cues: clamped };
|
|
8600
8739
|
}
|
|
8601
8740
|
async function readSidecar(path2) {
|
|
8602
8741
|
try {
|
|
8603
|
-
const parsed = JSON.parse(await
|
|
8742
|
+
const parsed = JSON.parse(await readFile8(path2, "utf8"));
|
|
8604
8743
|
const s = parsed;
|
|
8605
8744
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
8606
8745
|
} catch {
|
|
@@ -8720,8 +8859,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
8720
8859
|
}
|
|
8721
8860
|
|
|
8722
8861
|
// src/pack/pack.ts
|
|
8723
|
-
import { mkdir as
|
|
8724
|
-
import { dirname, extname as extname3 } from "node:path";
|
|
8862
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
|
|
8863
|
+
import { dirname as dirname2, extname as extname3 } from "node:path";
|
|
8725
8864
|
import { unzipSync, zipSync } from "fflate";
|
|
8726
8865
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
8727
8866
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -8760,12 +8899,12 @@ async function writePack(pack2, files, out) {
|
|
|
8760
8899
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
8761
8900
|
}
|
|
8762
8901
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
8763
|
-
await
|
|
8764
|
-
await
|
|
8902
|
+
await mkdir6(dirname2(out), { recursive: true });
|
|
8903
|
+
await writeFile8(out, zip);
|
|
8765
8904
|
return zip.length;
|
|
8766
8905
|
}
|
|
8767
8906
|
async function readPack(path2) {
|
|
8768
|
-
return openPack(new Uint8Array(await
|
|
8907
|
+
return openPack(new Uint8Array(await readFile9(path2)), path2);
|
|
8769
8908
|
}
|
|
8770
8909
|
function openPack(bytes, label = "pack") {
|
|
8771
8910
|
if (bytes.length < 4 || bytes[0] !== 80 || bytes[1] !== 75)
|
|
@@ -8809,8 +8948,8 @@ function why(error) {
|
|
|
8809
8948
|
}
|
|
8810
8949
|
|
|
8811
8950
|
// src/prefs.ts
|
|
8812
|
-
import { readFile as
|
|
8813
|
-
import { dirname as
|
|
8951
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
8952
|
+
import { dirname as dirname3, join as join8, resolve as resolve3 } from "node:path";
|
|
8814
8953
|
var CONFIG_FILE = "decksmith.config.json";
|
|
8815
8954
|
var PREF_KEYS = Object.keys(prefsSchema.shape);
|
|
8816
8955
|
var NARRATION_KEYS = Object.keys(prefsSchema.shape.narration.unwrap().shape);
|
|
@@ -8856,10 +8995,10 @@ function prefsFromFlags(flags2) {
|
|
|
8856
8995
|
return patch;
|
|
8857
8996
|
}
|
|
8858
8997
|
async function findConfig(from) {
|
|
8859
|
-
let dir =
|
|
8998
|
+
let dir = resolve3(from);
|
|
8860
8999
|
for (; ; ) {
|
|
8861
|
-
const path2 =
|
|
8862
|
-
const text2 = await
|
|
9000
|
+
const path2 = join8(dir, CONFIG_FILE);
|
|
9001
|
+
const text2 = await readFile10(path2, "utf8").catch(() => void 0);
|
|
8863
9002
|
if (text2 !== void 0) {
|
|
8864
9003
|
try {
|
|
8865
9004
|
return { path: path2, value: JSON.parse(text2) };
|
|
@@ -8867,7 +9006,7 @@ async function findConfig(from) {
|
|
|
8867
9006
|
throw new Error(`${path2} is not valid JSON.`);
|
|
8868
9007
|
}
|
|
8869
9008
|
}
|
|
8870
|
-
const up =
|
|
9009
|
+
const up = dirname3(dir);
|
|
8871
9010
|
if (up === dir) return void 0;
|
|
8872
9011
|
dir = up;
|
|
8873
9012
|
}
|
|
@@ -8917,45 +9056,65 @@ function number(flag, value) {
|
|
|
8917
9056
|
}
|
|
8918
9057
|
|
|
8919
9058
|
// src/render/capture.ts
|
|
8920
|
-
import { mkdir as
|
|
8921
|
-
import { createRequire as
|
|
9059
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
|
|
9060
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
8922
9061
|
import { homedir as homedir2 } from "node:os";
|
|
8923
|
-
import { join as
|
|
9062
|
+
import { join as join9 } from "node:path";
|
|
8924
9063
|
import { pathToFileURL } from "node:url";
|
|
8925
|
-
async function
|
|
8926
|
-
const
|
|
8927
|
-
if (
|
|
9064
|
+
async function resolveChrome(need = "open the deck with") {
|
|
9065
|
+
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
9066
|
+
if (env) return { path: env, source: "env" };
|
|
8928
9067
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
8929
|
-
const
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
9068
|
+
const pick = async (cacheDir) => {
|
|
9069
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
9070
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
9071
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
9072
|
+
};
|
|
9073
|
+
const hf = await pick(join9(homedir2(), ".cache", "hyperframes", "chrome"));
|
|
9074
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
9075
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join9(homedir2(), ".cache", "puppeteer"));
|
|
9076
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
8933
9077
|
throw new Error(
|
|
8934
9078
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
8935
9079
|
);
|
|
8936
9080
|
}
|
|
9081
|
+
async function chromePath(need = "open the deck with") {
|
|
9082
|
+
return (await resolveChrome(need)).path;
|
|
9083
|
+
}
|
|
8937
9084
|
function runtimePath() {
|
|
8938
|
-
return
|
|
9085
|
+
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
8939
9086
|
}
|
|
8940
9087
|
function renderSeek(t2) {
|
|
8941
9088
|
const player = window.__player;
|
|
8942
9089
|
player.renderSeek(t2, { suppressEvents: true });
|
|
8943
9090
|
}
|
|
8944
9091
|
async function openDeck(dir, opts = {}) {
|
|
8945
|
-
const index =
|
|
8946
|
-
const html = await
|
|
9092
|
+
const index = join9(dir, "index.html");
|
|
9093
|
+
const html = await readFile11(index, "utf8").catch(() => null);
|
|
8947
9094
|
if (html === null) throw new Error(`no index.html in ${dir}`);
|
|
8948
9095
|
const width = Number(/data-width="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
8949
9096
|
const height = Number(/data-height="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
8950
9097
|
if (!width || !height) throw new Error("index.html declares no canvas size");
|
|
8951
9098
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
8952
9099
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
9100
|
+
const chrome2 = await resolveChrome();
|
|
8953
9101
|
const browser = await puppeteer.launch({
|
|
8954
|
-
executablePath:
|
|
9102
|
+
executablePath: chrome2.path,
|
|
8955
9103
|
headless: true,
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
9104
|
+
args: [
|
|
9105
|
+
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
9106
|
+
// renderer's.
|
|
9107
|
+
"--force-device-scale-factor=1",
|
|
9108
|
+
"--hide-scrollbars",
|
|
9109
|
+
// WITHOUT THESE A WebGL CANVAS GETS NO CONTEXT AND SCREENSHOTS EMPTY. The
|
|
9110
|
+
// renderer passes its own GL arguments; this path passed none, so it drew
|
|
9111
|
+
// a blank rectangle and reported success. SwiftShader rather than the host
|
|
9112
|
+
// GPU on purpose: it is the one backend available on every machine, and a
|
|
9113
|
+
// gate that quietly follows the host's hardware is measuring the host.
|
|
9114
|
+
"--use-gl=angle",
|
|
9115
|
+
"--use-angle=swiftshader",
|
|
9116
|
+
"--enable-unsafe-swiftshader"
|
|
9117
|
+
]
|
|
8959
9118
|
});
|
|
8960
9119
|
try {
|
|
8961
9120
|
const page = await browser.newPage();
|
|
@@ -8971,6 +9130,18 @@ async function openDeck(dir, opts = {}) {
|
|
|
8971
9130
|
{ timeout }
|
|
8972
9131
|
);
|
|
8973
9132
|
await page.evaluate(() => document.fonts.ready);
|
|
9133
|
+
const canvas = await page.evaluate(() => {
|
|
9134
|
+
const el = document.querySelector("canvas");
|
|
9135
|
+
if (!el) return { present: false, gl: true };
|
|
9136
|
+
const probe2 = document.createElement("canvas");
|
|
9137
|
+
const gl = !!(probe2.getContext("webgl2") ?? probe2.getContext("webgl"));
|
|
9138
|
+
return { present: true, gl };
|
|
9139
|
+
});
|
|
9140
|
+
if (canvas.present && !canvas.gl) {
|
|
9141
|
+
throw new Error(
|
|
9142
|
+
`this deck draws on a <canvas> and ${chrome2.path} cannot create a WebGL context, so the capture would be of an empty rectangle. Chrome came from ${chrome2.source}; the renderer uses hyperframes' own shell. Set HYPERFRAMES_BROWSER_PATH to the binary \`hyperframes render\` uses so the gate and the render are the same browser.`
|
|
9143
|
+
);
|
|
9144
|
+
}
|
|
8974
9145
|
const cdp = await page.createCDPSession();
|
|
8975
9146
|
return {
|
|
8976
9147
|
width,
|
|
@@ -9000,7 +9171,7 @@ function frameName(index, t2, total) {
|
|
|
9000
9171
|
}
|
|
9001
9172
|
async function captureFrames(dir, times, outDir, opts = {}) {
|
|
9002
9173
|
if (times.length === 0) return [];
|
|
9003
|
-
await
|
|
9174
|
+
await mkdir7(outDir, { recursive: true });
|
|
9004
9175
|
const deck = await openDeck(dir, opts);
|
|
9005
9176
|
try {
|
|
9006
9177
|
const out = [];
|
|
@@ -9008,8 +9179,8 @@ async function captureFrames(dir, times, outDir, opts = {}) {
|
|
|
9008
9179
|
const t2 = Math.round(raw2 * 1e3) / 1e3;
|
|
9009
9180
|
await deck.seek(t2);
|
|
9010
9181
|
const png = await deck.shoot();
|
|
9011
|
-
const path2 =
|
|
9012
|
-
await
|
|
9182
|
+
const path2 = join9(outDir, frameName(i, t2, times.length));
|
|
9183
|
+
await writeFile9(path2, png);
|
|
9013
9184
|
out.push({ t: t2, path: path2, bytes: png.length });
|
|
9014
9185
|
}
|
|
9015
9186
|
return out;
|
|
@@ -9020,12 +9191,12 @@ async function captureFrames(dir, times, outDir, opts = {}) {
|
|
|
9020
9191
|
}
|
|
9021
9192
|
|
|
9022
9193
|
// src/render/render.ts
|
|
9023
|
-
import { mkdir as
|
|
9024
|
-
import { basename, dirname as
|
|
9194
|
+
import { mkdir as mkdir9, readFile as readFile12, rename as rename2, rm as rm5, writeFile as writeFile11 } from "node:fs/promises";
|
|
9195
|
+
import { basename, dirname as dirname4, join as join11, resolve as resolve4 } from "node:path";
|
|
9025
9196
|
|
|
9026
9197
|
// src/render/captions.ts
|
|
9027
|
-
import { mkdir as
|
|
9028
|
-
import { join as
|
|
9198
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
|
|
9199
|
+
import { join as join10 } from "node:path";
|
|
9029
9200
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
9030
9201
|
|
|
9031
9202
|
// src/deck/subtitles.ts
|
|
@@ -9224,6 +9395,10 @@ function planTiming(input) {
|
|
|
9224
9395
|
lang: storyboard.lang,
|
|
9225
9396
|
audioDir: segments2.length > 0 ? narration?.dir ?? "" : "",
|
|
9226
9397
|
voice: segments2.length > 0 ? narration?.voice ?? "" : "",
|
|
9398
|
+
// The format the deck was LAID OUT under, which is the only honest source:
|
|
9399
|
+
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
9400
|
+
// disagree with the box the archetypes actually drew into.
|
|
9401
|
+
captionReserve: format.captionReserve ?? 0,
|
|
9227
9402
|
scenes,
|
|
9228
9403
|
segments: segments2
|
|
9229
9404
|
};
|
|
@@ -9412,11 +9587,11 @@ async function captionBlocker() {
|
|
|
9412
9587
|
}
|
|
9413
9588
|
async function renderCaptions(cues, style, deck, work) {
|
|
9414
9589
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
9415
|
-
const fontCss =
|
|
9590
|
+
const fontCss = join10(deck, DECK_FONT_CSS);
|
|
9416
9591
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL2(fontCss).href : null;
|
|
9417
|
-
const page =
|
|
9418
|
-
await
|
|
9419
|
-
await
|
|
9592
|
+
const page = join10(work, "captions.html");
|
|
9593
|
+
await mkdir8(work, { recursive: true });
|
|
9594
|
+
await writeFile10(page, captionPage(cues, style, href));
|
|
9420
9595
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
9421
9596
|
const browser = await puppeteer.launch({
|
|
9422
9597
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -9436,7 +9611,7 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
9436
9611
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
9437
9612
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
9438
9613
|
await tab.screenshot({
|
|
9439
|
-
path:
|
|
9614
|
+
path: join10(work, name),
|
|
9440
9615
|
type: "png",
|
|
9441
9616
|
omitBackground: true,
|
|
9442
9617
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -9492,7 +9667,7 @@ ${tail3}`);
|
|
|
9492
9667
|
}
|
|
9493
9668
|
}
|
|
9494
9669
|
function runLive(file, args) {
|
|
9495
|
-
return new Promise((
|
|
9670
|
+
return new Promise((resolve8, reject) => {
|
|
9496
9671
|
const child = spawn3(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
9497
9672
|
child.on("error", (err) => {
|
|
9498
9673
|
reject(
|
|
@@ -9500,7 +9675,7 @@ function runLive(file, args) {
|
|
|
9500
9675
|
);
|
|
9501
9676
|
});
|
|
9502
9677
|
child.on("close", (code, signal) => {
|
|
9503
|
-
if (code === 0)
|
|
9678
|
+
if (code === 0) resolve8();
|
|
9504
9679
|
else if (signal) {
|
|
9505
9680
|
reject(
|
|
9506
9681
|
new Error(
|
|
@@ -9627,11 +9802,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
9627
9802
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
9628
9803
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
9629
9804
|
// three-line band covers the bottom of the slide.
|
|
9630
|
-
|
|
9805
|
+
//
|
|
9806
|
+
// THE RATIOS ARE NOT HERE. They are in src/types.ts beside `bandReserve`,
|
|
9807
|
+
// because `src/emit` has to reserve exactly the strip this draws into and
|
|
9808
|
+
// may not import this file. A local copy of 0.037 is how the two drift back
|
|
9809
|
+
// apart, which is the bug this whole path exists to close.
|
|
9810
|
+
fontSize: captionFontSize(width),
|
|
9631
9811
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
9632
9812
|
// player draws across the bottom eighth of the frame.
|
|
9633
|
-
marginV: Math.round(height *
|
|
9634
|
-
marginX: Math.round(width *
|
|
9813
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
9814
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
9635
9815
|
};
|
|
9636
9816
|
}
|
|
9637
9817
|
|
|
@@ -9643,9 +9823,9 @@ function subtitlePlan(mode) {
|
|
|
9643
9823
|
async function render(opts) {
|
|
9644
9824
|
const log = opts.log ?? (() => {
|
|
9645
9825
|
});
|
|
9646
|
-
const deck =
|
|
9647
|
-
const out =
|
|
9648
|
-
const work =
|
|
9826
|
+
const deck = resolve4(opts.deck);
|
|
9827
|
+
const out = resolve4(opts.out);
|
|
9828
|
+
const work = join11(dirname4(out), `.${basename(out)}.parts`);
|
|
9649
9829
|
const timing = await readTiming(deck);
|
|
9650
9830
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
9651
9831
|
const refusal = playbackRefusal(
|
|
@@ -9657,13 +9837,17 @@ async function render(opts) {
|
|
|
9657
9837
|
}
|
|
9658
9838
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
9659
9839
|
if (plan0.burn) {
|
|
9840
|
+
if (!timing.captionReserve)
|
|
9841
|
+
throw new Error(
|
|
9842
|
+
"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`."
|
|
9843
|
+
);
|
|
9660
9844
|
const blocker = await captionBlocker();
|
|
9661
9845
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
9662
9846
|
}
|
|
9663
9847
|
const burnable = plan0.burn;
|
|
9664
|
-
await
|
|
9848
|
+
await mkdir9(work, { recursive: true });
|
|
9665
9849
|
try {
|
|
9666
|
-
const raw2 = opts.video ?
|
|
9850
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join11(work, "raw.mp4"), opts, log);
|
|
9667
9851
|
const shot = await probe(raw2);
|
|
9668
9852
|
log(
|
|
9669
9853
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -9676,18 +9860,18 @@ async function render(opts) {
|
|
|
9676
9860
|
);
|
|
9677
9861
|
}
|
|
9678
9862
|
const retimed = await retime(raw2, plan, work, log);
|
|
9679
|
-
const srtPath =
|
|
9863
|
+
const srtPath = join11(dirname4(out), SRT_NAME(out));
|
|
9680
9864
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
9681
9865
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
9682
9866
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
9683
|
-
if (srt) await
|
|
9867
|
+
if (srt) await writeFile11(srtPath, srt);
|
|
9684
9868
|
const burn = burnable && srt.length > 0;
|
|
9685
9869
|
await mux(retimed, timing, plan, deck, out, work, burn ? plan.cues : void 0, shot.fps, log);
|
|
9686
9870
|
if (playback > 1) {
|
|
9687
9871
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
9688
9872
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
9689
9873
|
if (warning) log(`render: ${warning}`);
|
|
9690
|
-
const fast =
|
|
9874
|
+
const fast = join11(work, `fast.${basename(out)}`);
|
|
9691
9875
|
const muxed = await probe(out);
|
|
9692
9876
|
await runTool(
|
|
9693
9877
|
"ffmpeg",
|
|
@@ -9717,8 +9901,8 @@ async function render(opts) {
|
|
|
9717
9901
|
}
|
|
9718
9902
|
}
|
|
9719
9903
|
async function readTiming(deck) {
|
|
9720
|
-
const path2 =
|
|
9721
|
-
const text2 = await
|
|
9904
|
+
const path2 = join11(deck, TIMING_FILE);
|
|
9905
|
+
const text2 = await readFile12(path2, "utf8").catch(() => {
|
|
9722
9906
|
throw new Error(
|
|
9723
9907
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
9724
9908
|
);
|
|
@@ -9757,20 +9941,20 @@ async function retime(raw2, plan, work, log) {
|
|
|
9757
9941
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
9758
9942
|
const list = [];
|
|
9759
9943
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
9760
|
-
const file =
|
|
9944
|
+
const file = join11(work, `p${String(i).padStart(4, "0")}.ts`);
|
|
9761
9945
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
9762
9946
|
list.push(file);
|
|
9763
9947
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
9764
9948
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
9765
9949
|
}
|
|
9766
9950
|
}
|
|
9767
|
-
const listFile =
|
|
9768
|
-
await
|
|
9951
|
+
const listFile = join11(work, "pieces.txt");
|
|
9952
|
+
await writeFile11(
|
|
9769
9953
|
listFile,
|
|
9770
9954
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
9771
9955
|
`
|
|
9772
9956
|
);
|
|
9773
|
-
const joined =
|
|
9957
|
+
const joined = join11(work, "retimed.mp4");
|
|
9774
9958
|
await runTool("ffmpeg", [
|
|
9775
9959
|
"-y",
|
|
9776
9960
|
"-hide_banner",
|
|
@@ -9792,7 +9976,7 @@ async function retime(raw2, plan, work, log) {
|
|
|
9792
9976
|
}
|
|
9793
9977
|
async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
9794
9978
|
const inputs = plan.audio.map((a) => ({
|
|
9795
|
-
file:
|
|
9979
|
+
file: join11(deck, timing.audioDir, a.audio),
|
|
9796
9980
|
delayMs: a.delayMs
|
|
9797
9981
|
}));
|
|
9798
9982
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -9813,8 +9997,8 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
9813
9997
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
9814
9998
|
}
|
|
9815
9999
|
if (graph.length > 0) {
|
|
9816
|
-
const script =
|
|
9817
|
-
await
|
|
10000
|
+
const script = join11(work, "mux.filter");
|
|
10001
|
+
await writeFile11(script, `${graph.join(";\n")}
|
|
9818
10002
|
`);
|
|
9819
10003
|
args.push("-filter_complex_script", script);
|
|
9820
10004
|
}
|
|
@@ -9829,14 +10013,14 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
9829
10013
|
log(
|
|
9830
10014
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
9831
10015
|
);
|
|
9832
|
-
await
|
|
10016
|
+
await mkdir9(dirname4(out), { recursive: true });
|
|
9833
10017
|
await runTool("ffmpeg", args, { cwd: work });
|
|
9834
10018
|
}
|
|
9835
10019
|
|
|
9836
10020
|
// src/source/harvest.ts
|
|
9837
10021
|
import { createHash as createHash7 } from "node:crypto";
|
|
9838
|
-
import { copyFile, mkdir as
|
|
9839
|
-
import { basename as basename3, join as
|
|
10022
|
+
import { copyFile, mkdir as mkdir10, rm as rm7, stat as stat2, writeFile as writeFile12 } from "node:fs/promises";
|
|
10023
|
+
import { basename as basename3, join as join12, resolve as resolve5 } from "node:path";
|
|
9840
10024
|
|
|
9841
10025
|
// src/source/readability.ts
|
|
9842
10026
|
var CONTENT_MARKER = "data-ds-content";
|
|
@@ -10131,7 +10315,7 @@ function readContentRegion() {
|
|
|
10131
10315
|
}
|
|
10132
10316
|
|
|
10133
10317
|
// src/source/transcode.ts
|
|
10134
|
-
import { readFile as
|
|
10318
|
+
import { readFile as readFile13, rm as rm6 } from "node:fs/promises";
|
|
10135
10319
|
import { basename as basename2 } from "node:path";
|
|
10136
10320
|
var CLIP_EDGE_PX = 1280;
|
|
10137
10321
|
var MAX_CLIP_SECONDS = 60;
|
|
@@ -10194,7 +10378,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
10194
10378
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
10195
10379
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
10196
10380
|
const name = basename2(input);
|
|
10197
|
-
const source = videoSize(await
|
|
10381
|
+
const source = videoSize(await readFile13(input));
|
|
10198
10382
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
10199
10383
|
const kept = (why3) => ({
|
|
10200
10384
|
path: input,
|
|
@@ -10226,7 +10410,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
10226
10410
|
}
|
|
10227
10411
|
let measured;
|
|
10228
10412
|
try {
|
|
10229
|
-
measured = videoSize(await
|
|
10413
|
+
measured = videoSize(await readFile13(out));
|
|
10230
10414
|
} catch (err) {
|
|
10231
10415
|
await rm6(out, { force: true });
|
|
10232
10416
|
return kept(
|
|
@@ -10262,8 +10446,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
10262
10446
|
async function harvest(url2, dir, opts = {}) {
|
|
10263
10447
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
10264
10448
|
const warnings = [];
|
|
10265
|
-
const assets =
|
|
10266
|
-
await
|
|
10449
|
+
const assets = resolve5(dir);
|
|
10450
|
+
await mkdir10(assets, { recursive: true });
|
|
10267
10451
|
const page = await fetchGuarded(url2, {
|
|
10268
10452
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
10269
10453
|
timeoutMs,
|
|
@@ -10324,7 +10508,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
10324
10508
|
"--disable-background-networking",
|
|
10325
10509
|
"--disable-extensions",
|
|
10326
10510
|
"--no-default-browser-check",
|
|
10327
|
-
"--no-first-run"
|
|
10511
|
+
"--no-first-run",
|
|
10512
|
+
// The floor under interception — see the note above. Not a hardening
|
|
10513
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
10514
|
+
// happens to be that run.
|
|
10515
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
10328
10516
|
]
|
|
10329
10517
|
});
|
|
10330
10518
|
try {
|
|
@@ -10641,8 +10829,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
10641
10829
|
`it is ${size3.width}x${size3.height}, under ${MIN_FIGURE_PX}px \u2014 spacers, icons and tracking pixels look like this, and a slide cannot use one`
|
|
10642
10830
|
);
|
|
10643
10831
|
}
|
|
10644
|
-
const path2 =
|
|
10645
|
-
await
|
|
10832
|
+
const path2 = join12(dir, assetName(url2, extFor2(sniffFormat(bytes))));
|
|
10833
|
+
await writeFile12(path2, bytes);
|
|
10646
10834
|
assets.push(path2);
|
|
10647
10835
|
got = { path: path2, width: size3.width, height: size3.height };
|
|
10648
10836
|
} catch (err) {
|
|
@@ -10654,7 +10842,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
10654
10842
|
};
|
|
10655
10843
|
const shrink = async (url2, path2, measured, what) => {
|
|
10656
10844
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
10657
|
-
const out2 =
|
|
10845
|
+
const out2 = join12(dir, assetName(url2, ".vp9.webm"));
|
|
10658
10846
|
let small;
|
|
10659
10847
|
try {
|
|
10660
10848
|
small = await transcode(path2, out2, {
|
|
@@ -10696,8 +10884,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
10696
10884
|
try {
|
|
10697
10885
|
const bytes = await bytesOf(url2);
|
|
10698
10886
|
const measured = videoSize(bytes);
|
|
10699
|
-
const path2 =
|
|
10700
|
-
await
|
|
10887
|
+
const path2 = join12(dir, assetName(url2, `.${measured.container}`));
|
|
10888
|
+
await writeFile12(path2, bytes);
|
|
10701
10889
|
return await shrink(url2, path2, measured, what);
|
|
10702
10890
|
} catch (err) {
|
|
10703
10891
|
warnings.push(`${what} was not downloaded: ${url2} \u2014 ${why2(err)}`);
|
|
@@ -10786,7 +10974,7 @@ function unheld(policy) {
|
|
|
10786
10974
|
return "the file could not be held";
|
|
10787
10975
|
}
|
|
10788
10976
|
async function sizeOf2(path2) {
|
|
10789
|
-
return await
|
|
10977
|
+
return await stat2(path2).then(
|
|
10790
10978
|
(s) => s.size,
|
|
10791
10979
|
() => 0
|
|
10792
10980
|
);
|
|
@@ -10994,8 +11182,8 @@ function sane(seconds) {
|
|
|
10994
11182
|
}
|
|
10995
11183
|
async function attachClips(source, clips, dir) {
|
|
10996
11184
|
if (clips.length === 0) return source;
|
|
10997
|
-
const assets =
|
|
10998
|
-
await
|
|
11185
|
+
const assets = resolve5(dir);
|
|
11186
|
+
await mkdir10(assets, { recursive: true });
|
|
10999
11187
|
const figures = [...source.figures];
|
|
11000
11188
|
for (const clip3 of clips) {
|
|
11001
11189
|
const src = clip3.file ? await adopt(clip3.file, assets) : void 0;
|
|
@@ -11026,7 +11214,7 @@ async function attachClips(source, clips, dir) {
|
|
|
11026
11214
|
}
|
|
11027
11215
|
async function adopt(file, dir) {
|
|
11028
11216
|
const name = basename3(file);
|
|
11029
|
-
await copyFile(file,
|
|
11217
|
+
await copyFile(file, join12(dir, name));
|
|
11030
11218
|
return name;
|
|
11031
11219
|
}
|
|
11032
11220
|
function freeId(figures) {
|
|
@@ -11295,26 +11483,26 @@ function textOf(node) {
|
|
|
11295
11483
|
|
|
11296
11484
|
// src/tmpdir.ts
|
|
11297
11485
|
import { realpathSync } from "node:fs";
|
|
11298
|
-
import { createRequire as
|
|
11486
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
11299
11487
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
11300
|
-
import { basename as basename4, dirname as
|
|
11488
|
+
import { basename as basename4, dirname as dirname5, join as join13, resolve as resolve6, sep } from "node:path";
|
|
11301
11489
|
var cached;
|
|
11302
11490
|
function real(dir) {
|
|
11303
|
-
let at =
|
|
11491
|
+
let at = resolve6(dir);
|
|
11304
11492
|
const tail3 = [];
|
|
11305
11493
|
for (; ; ) {
|
|
11306
11494
|
try {
|
|
11307
|
-
return
|
|
11495
|
+
return join13(realpathSync(at), ...tail3);
|
|
11308
11496
|
} catch {
|
|
11309
|
-
const up =
|
|
11310
|
-
if (up === at) return
|
|
11497
|
+
const up = dirname5(at);
|
|
11498
|
+
if (up === at) return resolve6(dir);
|
|
11311
11499
|
tail3.unshift(basename4(at));
|
|
11312
11500
|
at = up;
|
|
11313
11501
|
}
|
|
11314
11502
|
}
|
|
11315
11503
|
}
|
|
11316
11504
|
function packageRoot() {
|
|
11317
|
-
cached ??= real(
|
|
11505
|
+
cached ??= real(dirname5(createRequire4(import.meta.url).resolve("../package.json")));
|
|
11318
11506
|
return cached;
|
|
11319
11507
|
}
|
|
11320
11508
|
function insideRoot(dir) {
|
|
@@ -11342,8 +11530,8 @@ function guardTmpdir() {
|
|
|
11342
11530
|
}
|
|
11343
11531
|
|
|
11344
11532
|
// src/verify/index.ts
|
|
11345
|
-
import { readdir as
|
|
11346
|
-
import { join as
|
|
11533
|
+
import { readdir as readdir4, readFile as readFile17 } from "node:fs/promises";
|
|
11534
|
+
import { join as join17, relative, sep as sep2 } from "node:path";
|
|
11347
11535
|
|
|
11348
11536
|
// src/verify/budget.ts
|
|
11349
11537
|
function readCanvas(html) {
|
|
@@ -11450,8 +11638,8 @@ function clock2(seconds) {
|
|
|
11450
11638
|
|
|
11451
11639
|
// src/verify/check.ts
|
|
11452
11640
|
import { execFile as execFile2 } from "node:child_process";
|
|
11453
|
-
import { readFile as
|
|
11454
|
-
import { join as
|
|
11641
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
11642
|
+
import { join as join14 } from "node:path";
|
|
11455
11643
|
import { promisify as promisify2 } from "node:util";
|
|
11456
11644
|
var run2 = promisify2(execFile2);
|
|
11457
11645
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -11481,7 +11669,7 @@ async function check2(dir, opts = {}) {
|
|
|
11481
11669
|
async function readComposition(dir) {
|
|
11482
11670
|
let html;
|
|
11483
11671
|
try {
|
|
11484
|
-
html = await
|
|
11672
|
+
html = await readFile14(join14(dir, "index.html"), "utf8");
|
|
11485
11673
|
} catch {
|
|
11486
11674
|
return { transit: [], duration: 0 };
|
|
11487
11675
|
}
|
|
@@ -11629,8 +11817,8 @@ function tail(s) {
|
|
|
11629
11817
|
}
|
|
11630
11818
|
|
|
11631
11819
|
// src/verify/fidelity.ts
|
|
11632
|
-
import { readFile as
|
|
11633
|
-
import { join as
|
|
11820
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
11821
|
+
import { join as join15 } from "node:path";
|
|
11634
11822
|
|
|
11635
11823
|
// src/verify/typefloor.ts
|
|
11636
11824
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11886,6 +12074,16 @@ var INK_FLOOR = 15e-4;
|
|
|
11886
12074
|
var INK_DELTA = 12;
|
|
11887
12075
|
var FALLBACK_BAND_TOP = 0.22;
|
|
11888
12076
|
var CAPTION = ".headline, .eyebrow";
|
|
12077
|
+
var RESERVE_INK_TOLERANCE = 1e-4;
|
|
12078
|
+
function readReserve(timingText) {
|
|
12079
|
+
if (!timingText) return 0;
|
|
12080
|
+
try {
|
|
12081
|
+
const reserve = JSON.parse(timingText).captionReserve;
|
|
12082
|
+
return typeof reserve === "number" && reserve > 0 ? reserve : 0;
|
|
12083
|
+
} catch {
|
|
12084
|
+
return 0;
|
|
12085
|
+
}
|
|
12086
|
+
}
|
|
11889
12087
|
async function decodePng(png) {
|
|
11890
12088
|
const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
|
|
11891
12089
|
if (buf.length < 8 || buf.readUInt32BE(0) !== 2303741511) throw new Error("not a PNG");
|
|
@@ -12052,6 +12250,25 @@ function gradeFidelity(rows, floor = INK_FLOOR) {
|
|
|
12052
12250
|
};
|
|
12053
12251
|
});
|
|
12054
12252
|
}
|
|
12253
|
+
function gradeReserve(rows, tolerance = RESERVE_INK_TOLERANCE) {
|
|
12254
|
+
const dirty = /* @__PURE__ */ new Map();
|
|
12255
|
+
for (const row of rows) {
|
|
12256
|
+
if (row.reserveInk === void 0 || row.reserveInk <= tolerance) continue;
|
|
12257
|
+
const seen = dirty.get(row.sid);
|
|
12258
|
+
if (seen) seen.push(row);
|
|
12259
|
+
else dirty.set(row.sid, [row]);
|
|
12260
|
+
}
|
|
12261
|
+
const pct = (v) => `${(100 * v).toFixed(3)}%`;
|
|
12262
|
+
return [...dirty].map(([sid, stops]) => {
|
|
12263
|
+
const worst = stops.reduce((a, b) => (a.reserveInk ?? 0) >= (b.reserveInk ?? 0) ? a : b);
|
|
12264
|
+
return {
|
|
12265
|
+
severity: "error",
|
|
12266
|
+
gate: "fidelity",
|
|
12267
|
+
rule: "ink_in_caption_reserve",
|
|
12268
|
+
message: `#${sid} draws into the caption reserve ${stops.length === 1 ? "" : `at ${stops.length} stops `}\u2014 ${pct(worst.reserveInk ?? 0)} of the frame is ink inside the band at t=${worst.t}s (tolerance ${pct(tolerance)}). A burned caption would sit on top of it. Times: ${stops.map((s) => `${s.t}s`).join(", ")}.`
|
|
12269
|
+
};
|
|
12270
|
+
});
|
|
12271
|
+
}
|
|
12055
12272
|
function captionBottom(sid, selector, fallbackPx) {
|
|
12056
12273
|
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
12057
12274
|
let bottom = 0;
|
|
@@ -12076,11 +12293,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
12076
12293
|
],
|
|
12077
12294
|
elapsedMs: Date.now() - started
|
|
12078
12295
|
});
|
|
12079
|
-
const
|
|
12080
|
-
|
|
12081
|
-
await readFile14(join14(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
12082
|
-
);
|
|
12296
|
+
const timingText = await readFile15(join15(dir, TIMING_FILE), "utf8").catch(() => null);
|
|
12297
|
+
const stops = opts.stops ?? readStops(timingText, await readFile15(join15(dir, DECK_PAGE), "utf8").catch(() => null));
|
|
12083
12298
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
12299
|
+
const reserve = opts.captionReserve ?? readReserve(timingText);
|
|
12084
12300
|
let deck = null;
|
|
12085
12301
|
try {
|
|
12086
12302
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
@@ -12109,7 +12325,13 @@ async function fidelity(dir, opts = {}) {
|
|
|
12109
12325
|
measured.push({
|
|
12110
12326
|
...stop,
|
|
12111
12327
|
ink: inkBelow(frame, bandTopPx),
|
|
12112
|
-
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
12328
|
+
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3,
|
|
12329
|
+
// THE SAME FRAME, a second strip. Free: it is one more pass over pixels
|
|
12330
|
+
// that are already decoded, so the gate that stops the caption
|
|
12331
|
+
// collision regressing costs no extra capture, no extra seek and no
|
|
12332
|
+
// extra browser. That matters — a gate with its own capture is a gate
|
|
12333
|
+
// someone turns off.
|
|
12334
|
+
...reserve > 0 ? { reserveInk: inkBelow(frame, height - reserve) } : {}
|
|
12113
12335
|
});
|
|
12114
12336
|
}
|
|
12115
12337
|
for (const mid of midpoints(stops)) {
|
|
@@ -12120,6 +12342,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
12120
12342
|
stops: measured,
|
|
12121
12343
|
findings: [
|
|
12122
12344
|
...gradeFidelity(measured, floor),
|
|
12345
|
+
...gradeReserve(measured),
|
|
12123
12346
|
...gradeOverprint(collided),
|
|
12124
12347
|
...gradeApparent(apparent)
|
|
12125
12348
|
],
|
|
@@ -12136,9 +12359,9 @@ async function fidelity(dir, opts = {}) {
|
|
|
12136
12359
|
// src/verify/drift.ts
|
|
12137
12360
|
import { execFile as execFile3 } from "node:child_process";
|
|
12138
12361
|
import { createHash as createHash9 } from "node:crypto";
|
|
12139
|
-
import { mkdtemp as mkdtemp3, readdir as
|
|
12362
|
+
import { mkdtemp as mkdtemp3, readdir as readdir3, readFile as readFile16, rm as rm8 } from "node:fs/promises";
|
|
12140
12363
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12141
|
-
import { join as
|
|
12364
|
+
import { join as join16 } from "node:path";
|
|
12142
12365
|
import { promisify as promisify3 } from "node:util";
|
|
12143
12366
|
var run3 = promisify3(execFile3);
|
|
12144
12367
|
var FLOOR_DB = 40;
|
|
@@ -12177,9 +12400,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
12177
12400
|
async function drift(dir, opts = {}) {
|
|
12178
12401
|
const mode = opts.mode ?? "psnr";
|
|
12179
12402
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
12180
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
12181
|
-
const a =
|
|
12182
|
-
const b =
|
|
12403
|
+
const work = opts.workDir ?? await mkdtemp3(join16(tmpdir4(), "decksmith-drift-"));
|
|
12404
|
+
const a = join16(work, "a");
|
|
12405
|
+
const b = join16(work, "b");
|
|
12183
12406
|
const report2 = await compare(dir, a, b, mode, floorDb, opts);
|
|
12184
12407
|
const keep = opts.keep || !report2.passed;
|
|
12185
12408
|
if (keep) report2.kept = { a, b };
|
|
@@ -12234,11 +12457,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
12234
12457
|
const differing = [];
|
|
12235
12458
|
const ha = [];
|
|
12236
12459
|
for (const [i, name] of fa.entries()) {
|
|
12237
|
-
const one = await sha(
|
|
12460
|
+
const one = await sha(join16(a, name));
|
|
12238
12461
|
ha.push(one);
|
|
12239
|
-
if (one !== await sha(
|
|
12462
|
+
if (one !== await sha(join16(b, name))) differing.push(i + 1);
|
|
12240
12463
|
}
|
|
12241
|
-
const html = await
|
|
12464
|
+
const html = await readFile16(join16(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
12242
12465
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
12243
12466
|
let worst;
|
|
12244
12467
|
if (differing.length > 0) {
|
|
@@ -12349,11 +12572,11 @@ async function render2(dir, out, opts) {
|
|
|
12349
12572
|
}
|
|
12350
12573
|
}
|
|
12351
12574
|
async function frames(dir) {
|
|
12352
|
-
const names = await
|
|
12575
|
+
const names = await readdir3(dir).catch(() => []);
|
|
12353
12576
|
return names.filter((n3) => n3.endsWith(".png")).sort();
|
|
12354
12577
|
}
|
|
12355
12578
|
async function sha(file) {
|
|
12356
|
-
return createHash9("sha256").update(await
|
|
12579
|
+
return createHash9("sha256").update(await readFile16(file)).digest("hex");
|
|
12357
12580
|
}
|
|
12358
12581
|
var STAT = /^n:(\d+)\b.*\bpsnr_avg:(inf|-?[\d.]+)/;
|
|
12359
12582
|
function framePattern(name) {
|
|
@@ -12386,11 +12609,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
12386
12609
|
"-start_number",
|
|
12387
12610
|
String(seq.start),
|
|
12388
12611
|
"-i",
|
|
12389
|
-
|
|
12612
|
+
join16(a, seq.pattern),
|
|
12390
12613
|
"-start_number",
|
|
12391
12614
|
String(seq.start),
|
|
12392
12615
|
"-i",
|
|
12393
|
-
|
|
12616
|
+
join16(b, seq.pattern),
|
|
12394
12617
|
"-lavfi",
|
|
12395
12618
|
"psnr=stats_file=-",
|
|
12396
12619
|
"-f",
|
|
@@ -12465,7 +12688,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12465
12688
|
const html = await readCompositions(dir);
|
|
12466
12689
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
12467
12690
|
const narration = scanNarration(
|
|
12468
|
-
await
|
|
12691
|
+
await readFile17(join17(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12469
12692
|
await listFiles(dir)
|
|
12470
12693
|
);
|
|
12471
12694
|
const budget3 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -12502,8 +12725,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12502
12725
|
}
|
|
12503
12726
|
async function declaredStops(dir) {
|
|
12504
12727
|
return readStops(
|
|
12505
|
-
await
|
|
12506
|
-
await
|
|
12728
|
+
await readFile17(join17(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12729
|
+
await readFile17(join17(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
12507
12730
|
);
|
|
12508
12731
|
}
|
|
12509
12732
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -12543,7 +12766,7 @@ function scanNarration(page, files) {
|
|
|
12543
12766
|
];
|
|
12544
12767
|
}
|
|
12545
12768
|
async function readTiming2(dir) {
|
|
12546
|
-
const raw2 = await
|
|
12769
|
+
const raw2 = await readFile17(join17(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
12547
12770
|
if (!raw2) return void 0;
|
|
12548
12771
|
try {
|
|
12549
12772
|
const parsed = JSON.parse(raw2);
|
|
@@ -12553,9 +12776,9 @@ async function readTiming2(dir) {
|
|
|
12553
12776
|
}
|
|
12554
12777
|
}
|
|
12555
12778
|
async function listFiles(dir) {
|
|
12556
|
-
const entries = await
|
|
12779
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12557
12780
|
return new Set(
|
|
12558
|
-
entries.filter((e) => e.isFile()).map((e) => relative(dir,
|
|
12781
|
+
entries.filter((e) => e.isFile()).map((e) => relative(dir, join17(e.parentPath, e.name)).split(sep2).join("/"))
|
|
12559
12782
|
);
|
|
12560
12783
|
}
|
|
12561
12784
|
var INSTEAD = {
|
|
@@ -12811,72 +13034,35 @@ function scanDeterminism(html, file) {
|
|
|
12811
13034
|
return findings;
|
|
12812
13035
|
}
|
|
12813
13036
|
async function readCompositions(dir) {
|
|
12814
|
-
const entries = await
|
|
13037
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12815
13038
|
const files = entries.filter(
|
|
12816
13039
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12817
|
-
).map((e) =>
|
|
13040
|
+
).map((e) => join17(e.parentPath, e.name));
|
|
12818
13041
|
return Promise.all(
|
|
12819
|
-
files.map(async (f) => [relative(dir, f), await
|
|
13042
|
+
files.map(async (f) => [relative(dir, f), await readFile17(f, "utf8")])
|
|
12820
13043
|
);
|
|
12821
13044
|
}
|
|
12822
13045
|
|
|
12823
13046
|
// src/cli.ts
|
|
12824
13047
|
guardTmpdir();
|
|
12825
|
-
var AUDIO_DIR = "audio";
|
|
12826
13048
|
var NARRATION_FILE = "narration.json";
|
|
12827
13049
|
var DEFAULTS = prefsSchema.parse({});
|
|
12828
13050
|
async function deckRuntime() {
|
|
12829
|
-
const path2 =
|
|
13051
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
12830
13052
|
try {
|
|
12831
|
-
return await
|
|
13053
|
+
return await readFile18(path2, "utf8");
|
|
12832
13054
|
} catch {
|
|
12833
13055
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
12834
13056
|
}
|
|
12835
13057
|
}
|
|
12836
13058
|
function playerBundle() {
|
|
12837
|
-
const require2 =
|
|
13059
|
+
const require2 = createRequire5(import.meta.url);
|
|
12838
13060
|
try {
|
|
12839
|
-
return
|
|
13061
|
+
return join18(dirname6(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12840
13062
|
} catch {
|
|
12841
13063
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12842
13064
|
}
|
|
12843
13065
|
}
|
|
12844
|
-
async function vendorKatex(out) {
|
|
12845
|
-
const require2 = createRequire4(import.meta.url);
|
|
12846
|
-
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12847
|
-
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12848
|
-
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12849
|
-
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12850
|
-
if (file.endsWith(".woff2"))
|
|
12851
|
-
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12852
|
-
}
|
|
12853
|
-
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12854
|
-
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12855
|
-
return kept ? `src:${kept}` : whole;
|
|
12856
|
-
});
|
|
12857
|
-
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12858
|
-
}
|
|
12859
|
-
async function vendorScripts(out, composition) {
|
|
12860
|
-
const require2 = createRequire4(import.meta.url);
|
|
12861
|
-
await mkdir10(join17(out, "vendor"), { recursive: true });
|
|
12862
|
-
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
12863
|
-
for (const [pkg, rel, name] of [
|
|
12864
|
-
["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
|
|
12865
|
-
["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
|
|
12866
|
-
["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
|
|
12867
|
-
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
12868
|
-
]) {
|
|
12869
|
-
if (!wanted(name)) continue;
|
|
12870
|
-
const from = join17(dirname5(require2.resolve(pkg)), rel);
|
|
12871
|
-
await cp(from, join17(out, "vendor", name));
|
|
12872
|
-
}
|
|
12873
|
-
if (wanted("ds-morph.js")) {
|
|
12874
|
-
await cp(
|
|
12875
|
-
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
12876
|
-
join17(out, "vendor", "ds-morph.js")
|
|
12877
|
-
);
|
|
12878
|
-
}
|
|
12879
|
-
}
|
|
12880
13066
|
var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
12881
13067
|
{
|
|
12882
13068
|
$schema: "https://hyperframes.heygen.com/schema/hyperframes.json",
|
|
@@ -12978,13 +13164,13 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
12978
13164
|
"--max-clip-seconds <n>",
|
|
12979
13165
|
"URL only: seconds of each clip kept, the rest trimmed (maxClipSeconds; default 60)"
|
|
12980
13166
|
).option("--no-transcode", "URL only: ship each clip as the page served it, unshrunk").action(async (input, o) => {
|
|
12981
|
-
const assets =
|
|
12982
|
-
const scratch = url(input) ? await mkdtemp4(
|
|
13167
|
+
const assets = join18(dirname6(resolve7(o.out)), "assets");
|
|
13168
|
+
const scratch = url(input) ? await mkdtemp4(join18(tmpdir5(), "decksmith-harvest-")) : void 0;
|
|
12983
13169
|
try {
|
|
12984
13170
|
let md;
|
|
12985
13171
|
let clips = [];
|
|
12986
13172
|
if (scratch === void 0) {
|
|
12987
|
-
md = await
|
|
13173
|
+
md = await readFile18(resolve7(input), "utf8").catch(() => {
|
|
12988
13174
|
throw new Error(`Cannot read ${input}.`);
|
|
12989
13175
|
});
|
|
12990
13176
|
} else {
|
|
@@ -13005,7 +13191,7 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
13005
13191
|
const dropped = [];
|
|
13006
13192
|
const source = await fetchFigures(withClips, assets, dropped);
|
|
13007
13193
|
for (const why3 of dropped) step(`ingest: ${why3}`);
|
|
13008
|
-
const bundle = await bundleFont(source.lang, glyphs(source),
|
|
13194
|
+
const bundle = await bundleFont(source.lang, glyphs(source), join18(assets, "fonts"));
|
|
13009
13195
|
if (bundle) step(`ingest: bundled ${bundle.family} for ${source.lang}`);
|
|
13010
13196
|
await writeJson(o.out, source);
|
|
13011
13197
|
step(`ingest: wrote ${o.out}`);
|
|
@@ -13066,7 +13252,7 @@ imageFlags(
|
|
|
13066
13252
|
program.command("illustrate").description("Draw the pictures the plan asked for, and point the storyboard at them.").argument("<storyboard>", "storyboard.json carrying illustration briefs").requiredOption("--source <file>", "source.json the storyboard was planned from")
|
|
13067
13253
|
).action(async (sbPath, o) => {
|
|
13068
13254
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
13069
|
-
const sourcePath =
|
|
13255
|
+
const sourcePath = resolve7(String(o.source));
|
|
13070
13256
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
13071
13257
|
assertRefsResolve(storyboard, source, { pending: "allow" });
|
|
13072
13258
|
const prefs = await loadPrefs(prefsFromFlags({ ...flags(o), images: true }));
|
|
@@ -13080,7 +13266,7 @@ imageFlags(
|
|
|
13080
13266
|
);
|
|
13081
13267
|
const drawn = await illustrate(storyboard, source, {
|
|
13082
13268
|
prefs,
|
|
13083
|
-
assetsDir:
|
|
13269
|
+
assetsDir: join18(dirname6(sourcePath), "assets"),
|
|
13084
13270
|
onStep: step
|
|
13085
13271
|
});
|
|
13086
13272
|
for (const p of drawn.illustrated) {
|
|
@@ -13105,34 +13291,44 @@ voiceFlags(
|
|
|
13105
13291
|
const format = pickFormat(String(o.format), o.width, o.height);
|
|
13106
13292
|
const chosen = await loadPrefs(prefsFromFlags(flags(o)));
|
|
13107
13293
|
const prefs = stated(chosen, "lang") ? chosen : { ...chosen, lang: storyboard.lang };
|
|
13108
|
-
const dir =
|
|
13294
|
+
const dir = resolve7(String(o.out));
|
|
13109
13295
|
const speaking = storyboard.beats.filter((b) => b.narration?.trim()).length;
|
|
13110
13296
|
if (speaking === 0) {
|
|
13111
13297
|
throw new Error(`No beat in ${sbPath} has a "narration" field, so there is nothing to speak.`);
|
|
13112
13298
|
}
|
|
13113
13299
|
step(`narrate: ${speaking} of ${storyboard.beats.length} beats have narration`);
|
|
13114
13300
|
const narration = await narrate(storyboard, source, prefs, { dir, format });
|
|
13115
|
-
await writeJson(
|
|
13301
|
+
await writeJson(join18(dir, NARRATION_FILE), narration);
|
|
13116
13302
|
const segments2 = Object.values(narration.beats).flat();
|
|
13117
13303
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
13118
13304
|
step(
|
|
13119
|
-
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${
|
|
13305
|
+
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${join18(dir, NARRATION_FILE)}`
|
|
13120
13306
|
);
|
|
13121
13307
|
});
|
|
13122
13308
|
lookFlags(
|
|
13123
13309
|
sizeFlags(
|
|
13124
|
-
lengthFlags(program.command("build")).description("Emit the composition, write its assets, and run the gates.").argument("<storyboard>", "storyboard.json, edited to taste").requiredOption("--source <file>", "source.json the storyboard was planned from").requiredOption("-o, --out <dir>", "directory to write the deck into").option("--format <id>", `output profile: ${Object.keys(FORMATS).join(" | ")}`, "deck-16x9").option("--min-weight <n>", "keep only beats at or above this weight \u2014 see the budget gate").option("--genre <genre>", "general | paper \u2014 report when a paper deck lacks its arc").option("--narration <file>", `${NARRATION_FILE} from \`decksmith narrate\``).option("--no-narration", "ignore narration sitting beside the storyboard").option(
|
|
13310
|
+
lengthFlags(program.command("build")).description("Emit the composition, write its assets, and run the gates.").argument("<storyboard>", "storyboard.json, edited to taste").requiredOption("--source <file>", "source.json the storyboard was planned from").requiredOption("-o, --out <dir>", "directory to write the deck into").option("--format <id>", `output profile: ${Object.keys(FORMATS).join(" | ")}`, "deck-16x9").option("--min-weight <n>", "keep only beats at or above this weight \u2014 see the budget gate").option("--genre <genre>", "general | paper \u2014 report when a paper deck lacks its arc").option("--narration <file>", `${NARRATION_FILE} from \`decksmith narrate\``).option("--no-narration", "ignore narration sitting beside the storyboard").option(
|
|
13311
|
+
"--reserve-captions",
|
|
13312
|
+
"keep the bottom of the frame clear for a burned caption \u2014 required by `render --subtitles burn`"
|
|
13313
|
+
).option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser")
|
|
13125
13314
|
)
|
|
13126
13315
|
).action(
|
|
13127
13316
|
async (sbPath, o) => {
|
|
13128
13317
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
13129
13318
|
const source = await readValidated(o.source, sourceSchema, "source");
|
|
13130
13319
|
assertRefsResolve(storyboard, source);
|
|
13131
|
-
const format =
|
|
13320
|
+
const format = withCaptionReserve(
|
|
13321
|
+
withMinWeight(pickFormat(o.format, o.width, o.height), o.minWeight),
|
|
13322
|
+
o.reserveCaptions === true
|
|
13323
|
+
);
|
|
13324
|
+
if (format.captionReserve)
|
|
13325
|
+
step(
|
|
13326
|
+
`build: reserving ${format.captionReserve}px at the bottom for burned captions \u2014 ${(100 * format.captionReserve / format.height).toFixed(1)}% of the frame`
|
|
13327
|
+
);
|
|
13132
13328
|
const prefs = await loadPrefs(prefsFromFlags(flags(o)), process.cwd(), source);
|
|
13133
13329
|
const theme = stated(prefs, "theme") ?? storyboard.theme;
|
|
13134
|
-
const out =
|
|
13135
|
-
await
|
|
13330
|
+
const out = resolve7(o.out);
|
|
13331
|
+
await mkdir11(out, { recursive: true });
|
|
13136
13332
|
const found = await findNarration(sbPath, o.narration);
|
|
13137
13333
|
const narration = found ? await loadNarration(found) : void 0;
|
|
13138
13334
|
if (narration) {
|
|
@@ -13145,7 +13341,7 @@ lookFlags(
|
|
|
13145
13341
|
const paced = durationPlan(prefs, storyboard.beats.length);
|
|
13146
13342
|
for (const f of scanBeatCount(storyboard, prefs)) step(`build: ${f.message}`);
|
|
13147
13343
|
for (const w of paced.warnings) step(`build: ${w}`);
|
|
13148
|
-
const fontCss = await refreshFont(storyboard, source, out);
|
|
13344
|
+
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
13149
13345
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
13150
13346
|
theme,
|
|
13151
13347
|
...fontCss ? { fontCss } : {},
|
|
@@ -13162,8 +13358,8 @@ lookFlags(
|
|
|
13162
13358
|
// place anyone learns it is not.
|
|
13163
13359
|
onBeatWarning: (id2, warning) => step(`build: kept ${id2} \u2014 ${warning}`)
|
|
13164
13360
|
});
|
|
13165
|
-
await
|
|
13166
|
-
await
|
|
13361
|
+
await writeFile13(join18(out, "index.html"), deck.composition);
|
|
13362
|
+
await writeFile13(join18(out, "hyperframes.json"), HYPERFRAMES_JSON);
|
|
13167
13363
|
await writeTiming(out, {
|
|
13168
13364
|
storyboard,
|
|
13169
13365
|
source,
|
|
@@ -13179,36 +13375,36 @@ lookFlags(
|
|
|
13179
13375
|
...narration ? { narration } : {}
|
|
13180
13376
|
});
|
|
13181
13377
|
if (deck.page) {
|
|
13182
|
-
await
|
|
13183
|
-
await
|
|
13378
|
+
await writeFile13(join18(out, DECK_PAGE), deck.page);
|
|
13379
|
+
await cp2(playerBundle(), join18(out, PLAYER_FILE));
|
|
13184
13380
|
}
|
|
13185
13381
|
await vendorKatex(out);
|
|
13186
13382
|
await vendorScripts(out, deck.composition);
|
|
13187
|
-
await copyAssets(
|
|
13188
|
-
if (found && narration) await copyAudio(
|
|
13383
|
+
await copyAssets(dirname6(resolve7(o.source)), out, source.figures, step);
|
|
13384
|
+
if (found && narration) await copyAudio(dirname6(found), narration, out, step);
|
|
13189
13385
|
const look = [theme, paced.speed === 1 ? "" : `${paced.speed}\xD7 speed`].filter(Boolean).join(", ");
|
|
13190
13386
|
const { cut } = deck;
|
|
13191
13387
|
const floor = cut.dropped.filter((d) => d.rule === "below_min_weight").length;
|
|
13192
13388
|
const of = cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
13193
13389
|
step(
|
|
13194
|
-
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${
|
|
13390
|
+
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${join18(out, "index.html")}`
|
|
13195
13391
|
);
|
|
13196
13392
|
for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
|
|
13197
13393
|
step(`build: ${f.message}`);
|
|
13198
13394
|
reportCut(cut);
|
|
13199
|
-
if (deck.page) step(`build: navigable deck \u2192 ${
|
|
13395
|
+
if (deck.page) step(`build: navigable deck \u2192 ${join18(out, DECK_PAGE)}`);
|
|
13200
13396
|
await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
|
|
13201
13397
|
}
|
|
13202
13398
|
);
|
|
13203
13399
|
program.command("verify").description("Re-run the gates on a built deck.").argument("<dir>", "a built deck directory").option("--snapshots", "also write the contrast-pass PNGs to <dir>/snapshots", false).option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser").action(async (dir, o) => {
|
|
13204
|
-
await gate(
|
|
13400
|
+
await gate(resolve7(dir), o.snapshots, void 0, void 0, o.fidelity !== false);
|
|
13205
13401
|
});
|
|
13206
13402
|
program.command("frames").description("Write PNGs of a built deck through the capture path the renderer uses.").argument("<dir>", "a built deck directory").option(
|
|
13207
13403
|
"--at <seconds...>",
|
|
13208
13404
|
"absolute times to photograph; defaults to every hold the deck declares"
|
|
13209
13405
|
).option("--out <dir>", "where to write the PNGs (default: <dir>/frames)").action(async (dir, o) => {
|
|
13210
|
-
const deck =
|
|
13211
|
-
const out =
|
|
13406
|
+
const deck = resolve7(dir);
|
|
13407
|
+
const out = resolve7(o.out ?? join18(deck, "frames"));
|
|
13212
13408
|
let times;
|
|
13213
13409
|
if (o.at?.length) {
|
|
13214
13410
|
times = o.at.map((raw2) => {
|
|
@@ -13218,8 +13414,8 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
13218
13414
|
});
|
|
13219
13415
|
} else {
|
|
13220
13416
|
times = readStops(
|
|
13221
|
-
await
|
|
13222
|
-
await
|
|
13417
|
+
await readFile18(join18(deck, TIMING_FILE), "utf8").catch(() => null),
|
|
13418
|
+
await readFile18(join18(deck, DECK_PAGE), "utf8").catch(() => null)
|
|
13223
13419
|
).map((s) => s.t);
|
|
13224
13420
|
if (times.length === 0)
|
|
13225
13421
|
throw new Error(
|
|
@@ -13233,7 +13429,7 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
13233
13429
|
program.command("drift").description("Render a built deck twice and compare, frame by frame. Minutes, not seconds.").argument("<dir>", "a built deck directory").option("--identical", "require every frame byte-identical \u2014 image-free decks only", false).option("--floor <dB>", "per-frame PSNR floor", String(FLOOR_DB)).option("--keep", "keep both frame directories even when they agree").option("--workers <n>", "pin both renders to one worker count (default: 1 vs 3)").action(
|
|
13234
13430
|
async (dir, o) => {
|
|
13235
13431
|
step("drift: rendering twice, several minutes a render");
|
|
13236
|
-
const verdict = await drift(
|
|
13432
|
+
const verdict = await drift(resolve7(dir), {
|
|
13237
13433
|
mode: o.identical ? "identical" : "psnr",
|
|
13238
13434
|
floorDb: Number(o.floor),
|
|
13239
13435
|
...o.keep ? { keep: true } : {},
|
|
@@ -13260,8 +13456,8 @@ program.command("render").description("Render a built deck to a finished video:
|
|
|
13260
13456
|
throw new Error(`Unknown --subtitles "${o.subtitles}". Use sidecar, burn or none.`);
|
|
13261
13457
|
}
|
|
13262
13458
|
const result = await render({
|
|
13263
|
-
deck:
|
|
13264
|
-
out:
|
|
13459
|
+
deck: resolve7(dir),
|
|
13460
|
+
out: resolve7(o.out),
|
|
13265
13461
|
subtitles: o.subtitles,
|
|
13266
13462
|
protocolTimeoutMs: Number(o.protocolTimeout),
|
|
13267
13463
|
log: step,
|
|
@@ -13289,7 +13485,7 @@ voiceFlags(
|
|
|
13289
13485
|
)
|
|
13290
13486
|
).action(async (sbPath, o) => {
|
|
13291
13487
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
13292
|
-
const sourcePath =
|
|
13488
|
+
const sourcePath = resolve7(String(o.source));
|
|
13293
13489
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
13294
13490
|
assertRefsResolve(storyboard, source);
|
|
13295
13491
|
if (o.bake && o.link) throw new Error("Choose --bake or --link, not both.");
|
|
@@ -13304,7 +13500,7 @@ voiceFlags(
|
|
|
13304
13500
|
const prefer = o.link ? "link" : "bake";
|
|
13305
13501
|
const plan = await planMedia(figureAssets(source, sourcePath, prefer));
|
|
13306
13502
|
const files = { ...plan.files };
|
|
13307
|
-
if (found && narration) Object.assign(files, await audioFiles(
|
|
13503
|
+
if (found && narration) Object.assign(files, await audioFiles(dirname6(found), narration));
|
|
13308
13504
|
const pack2 = {
|
|
13309
13505
|
version: PACK_VERSION,
|
|
13310
13506
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13319,7 +13515,7 @@ voiceFlags(
|
|
|
13319
13515
|
...narration ? { narration } : {},
|
|
13320
13516
|
media: plan.media
|
|
13321
13517
|
};
|
|
13322
|
-
const out =
|
|
13518
|
+
const out = resolve7(String(o.out));
|
|
13323
13519
|
const bytes = await writePack(pack2, files, out);
|
|
13324
13520
|
if (plan.demoted.length) {
|
|
13325
13521
|
step(`pack: kept as links, not bakeable \u2014 ${plan.demoted.join(", ")}`);
|
|
@@ -13333,19 +13529,19 @@ voiceFlags(
|
|
|
13333
13529
|
step(`pack: ${size2(bytes)} \u2192 ${out}`);
|
|
13334
13530
|
});
|
|
13335
13531
|
program.command("unpack").description("Open a .deck back into the files build reads.").argument("<file>", "a .deck archive").requiredOption("-o, --out <dir>", "directory to open it into").action(async (file, o) => {
|
|
13336
|
-
const { pack: pack2, files } = await readPack(
|
|
13337
|
-
const out =
|
|
13338
|
-
await writeJson(
|
|
13339
|
-
await writeJson(
|
|
13340
|
-
await writeJson(
|
|
13341
|
-
if (pack2.narration) await writeJson(
|
|
13532
|
+
const { pack: pack2, files } = await readPack(resolve7(file));
|
|
13533
|
+
const out = resolve7(o.out);
|
|
13534
|
+
await writeJson(join18(out, "source.json"), pack2.source);
|
|
13535
|
+
await writeJson(join18(out, "storyboard.json"), pack2.storyboard);
|
|
13536
|
+
await writeJson(join18(out, "decksmith.config.json"), pack2.prefs);
|
|
13537
|
+
if (pack2.narration) await writeJson(join18(out, AUDIO_DIR, NARRATION_FILE), pack2.narration);
|
|
13342
13538
|
const figures = new Map(pack2.source.figures.map((f) => [f.id, f.src]));
|
|
13343
13539
|
for (const [path2, bytes] of Object.entries(files)) {
|
|
13344
13540
|
const media = pack2.media.find((m) => m.path === path2);
|
|
13345
13541
|
const src = media && figures.get(media.id);
|
|
13346
|
-
const to = src ?
|
|
13347
|
-
await
|
|
13348
|
-
await
|
|
13542
|
+
const to = src ? join18("assets", src) : path2;
|
|
13543
|
+
await mkdir11(dirname6(join18(out, to)), { recursive: true });
|
|
13544
|
+
await writeFile13(join18(out, to), bytes);
|
|
13349
13545
|
}
|
|
13350
13546
|
const linked = pack2.media.filter((m) => m.policy !== "bake");
|
|
13351
13547
|
step(`unpack: "${pack2.title}", ${pack2.storyboard.beats.length} beats \u2192 ${out}`);
|
|
@@ -13355,7 +13551,7 @@ program.command("unpack").description("Open a .deck back into the files build re
|
|
|
13355
13551
|
);
|
|
13356
13552
|
}
|
|
13357
13553
|
step(
|
|
13358
|
-
`unpack: decksmith build ${
|
|
13554
|
+
`unpack: decksmith build ${join18(relative2(process.cwd(), out) || ".", "storyboard.json")} --source ${join18(relative2(process.cwd(), out) || ".", "source.json")} -o deck`
|
|
13359
13555
|
);
|
|
13360
13556
|
});
|
|
13361
13557
|
program.parseAsync(process.argv).catch((err) => {
|
|
@@ -13376,7 +13572,7 @@ async function gate(dir, snapshots = false, storyboard, kept, fidelity2 = true,
|
|
|
13376
13572
|
);
|
|
13377
13573
|
const verdict = await verify(dir, { snapshots, fidelity: fidelity2 }, storyboard, kept, source);
|
|
13378
13574
|
process.stdout.write(report(verdict));
|
|
13379
|
-
if (snapshots) step(`verify: snapshots in ${
|
|
13575
|
+
if (snapshots) step(`verify: snapshots in ${join18(dir, "snapshots")}`);
|
|
13380
13576
|
if (!verdict.passed) process.exitCode = 1;
|
|
13381
13577
|
}
|
|
13382
13578
|
function report(v) {
|
|
@@ -13393,13 +13589,13 @@ function report(v) {
|
|
|
13393
13589
|
async function findNarration(sbPath, flag) {
|
|
13394
13590
|
if (flag === false) return void 0;
|
|
13395
13591
|
if (typeof flag === "string") {
|
|
13396
|
-
const path2 =
|
|
13397
|
-
if (!await
|
|
13592
|
+
const path2 = resolve7(flag);
|
|
13593
|
+
if (!await stat3(path2).catch(() => null)) throw new Error(`Cannot read narration ${flag}.`);
|
|
13398
13594
|
return path2;
|
|
13399
13595
|
}
|
|
13400
|
-
const beside =
|
|
13401
|
-
for (const candidate of [
|
|
13402
|
-
if (await
|
|
13596
|
+
const beside = dirname6(resolve7(sbPath));
|
|
13597
|
+
for (const candidate of [join18(beside, AUDIO_DIR, NARRATION_FILE), join18(beside, NARRATION_FILE)]) {
|
|
13598
|
+
if (await stat3(candidate).catch(() => null)) {
|
|
13403
13599
|
step(`narration: using ${candidate}`);
|
|
13404
13600
|
return candidate;
|
|
13405
13601
|
}
|
|
@@ -13410,28 +13606,10 @@ async function loadNarration(path2) {
|
|
|
13410
13606
|
const narration = await readValidated(path2, narrationSchema, "narration");
|
|
13411
13607
|
return { voice: narration.voice, dir: AUDIO_DIR, beats: narration.beats };
|
|
13412
13608
|
}
|
|
13413
|
-
function audioNames(narration) {
|
|
13414
|
-
return [
|
|
13415
|
-
...new Set(
|
|
13416
|
-
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
13417
|
-
)
|
|
13418
|
-
].sort();
|
|
13419
|
-
}
|
|
13420
|
-
async function copyAudio(from, narration, out) {
|
|
13421
|
-
const dir = join17(out, AUDIO_DIR);
|
|
13422
|
-
await mkdir10(dir, { recursive: true });
|
|
13423
|
-
const names = audioNames(narration);
|
|
13424
|
-
for (const name of names) {
|
|
13425
|
-
await cp(join17(from, name), join17(dir, name)).catch(() => {
|
|
13426
|
-
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13427
|
-
});
|
|
13428
|
-
}
|
|
13429
|
-
step(`narration: ${names.length} audio file(s) \u2192 ${dir}`);
|
|
13430
|
-
}
|
|
13431
13609
|
async function audioFiles(from, narration) {
|
|
13432
13610
|
const files = {};
|
|
13433
13611
|
for (const name of audioNames(narration)) {
|
|
13434
|
-
const bytes = await
|
|
13612
|
+
const bytes = await readFile18(join18(from, name)).catch(() => {
|
|
13435
13613
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13436
13614
|
});
|
|
13437
13615
|
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(bytes);
|
|
@@ -13453,9 +13631,9 @@ function wrapScript(text2, width) {
|
|
|
13453
13631
|
async function writeTiming(out, input) {
|
|
13454
13632
|
try {
|
|
13455
13633
|
const timing = planTiming(input);
|
|
13456
|
-
await writeJson(
|
|
13634
|
+
await writeJson(join18(out, TIMING_FILE), timing);
|
|
13457
13635
|
step(
|
|
13458
|
-
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${
|
|
13636
|
+
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${join18(out, TIMING_FILE)}`
|
|
13459
13637
|
);
|
|
13460
13638
|
} catch (err) {
|
|
13461
13639
|
step(
|
|
@@ -13464,10 +13642,10 @@ async function writeTiming(out, input) {
|
|
|
13464
13642
|
}
|
|
13465
13643
|
}
|
|
13466
13644
|
function figureAssets(source, sourcePath, prefer) {
|
|
13467
|
-
const assets =
|
|
13645
|
+
const assets = join18(dirname6(sourcePath), "assets");
|
|
13468
13646
|
return source.figures.map((f) => ({
|
|
13469
13647
|
id: f.id,
|
|
13470
|
-
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src :
|
|
13648
|
+
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src : join18(assets, f.src),
|
|
13471
13649
|
prefer
|
|
13472
13650
|
}));
|
|
13473
13651
|
}
|
|
@@ -13485,6 +13663,10 @@ function pickFormat(id2, width, height) {
|
|
|
13485
13663
|
for (const warning of canvasWarnings(format.width, format.height)) step(`format: ${warning}`);
|
|
13486
13664
|
return format;
|
|
13487
13665
|
}
|
|
13666
|
+
function withCaptionReserve(format, on) {
|
|
13667
|
+
if (!on) return format;
|
|
13668
|
+
return { ...format, captionReserve: bandReserve(format.width, format.height) };
|
|
13669
|
+
}
|
|
13488
13670
|
function sizeFlags(cmd) {
|
|
13489
13671
|
return cmd.option("--width <px>", `canvas width in pixels (${MIN_EDGE}\u2013${MAX_EDGE})`).option("--height <px>", `canvas height in pixels (${MIN_EDGE}\u2013${MAX_EDGE})`);
|
|
13490
13672
|
}
|
|
@@ -13495,52 +13677,11 @@ function withMinWeight(format, raw2) {
|
|
|
13495
13677
|
throw new Error(`--min-weight takes a number from 0 to 1, not "${raw2}".`);
|
|
13496
13678
|
return { ...format, minWeight };
|
|
13497
13679
|
}
|
|
13498
|
-
async function copyAssets(sourceDir, out, figures) {
|
|
13499
|
-
const from = join17(sourceDir, "assets");
|
|
13500
|
-
if (!await stat2(from).catch(() => null)) {
|
|
13501
|
-
step(`build: no assets/ beside source.json, skipping`);
|
|
13502
|
-
return;
|
|
13503
|
-
}
|
|
13504
|
-
const wanted = new Set(
|
|
13505
|
-
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
13506
|
-
);
|
|
13507
|
-
await mkdir10(join17(out, "assets"), { recursive: true });
|
|
13508
|
-
let copied = 0;
|
|
13509
|
-
for (const name of wanted) {
|
|
13510
|
-
const src = resolve6(join17(from, name));
|
|
13511
|
-
if (!src.startsWith(`${resolve6(from)}/`)) continue;
|
|
13512
|
-
if (!await stat2(src).catch(() => null)) continue;
|
|
13513
|
-
await mkdir10(dirname5(join17(out, "assets", name)), { recursive: true });
|
|
13514
|
-
await cp(src, join17(out, "assets", name));
|
|
13515
|
-
copied++;
|
|
13516
|
-
}
|
|
13517
|
-
const fonts = join17(from, "fonts");
|
|
13518
|
-
if (await stat2(fonts).catch(() => null)) {
|
|
13519
|
-
await cp(fonts, join17(out, "assets", "fonts"), { recursive: true });
|
|
13520
|
-
}
|
|
13521
|
-
step(`build: copied ${copied} referenced figure(s)`);
|
|
13522
|
-
}
|
|
13523
|
-
async function refreshFont(storyboard, source, out) {
|
|
13524
|
-
try {
|
|
13525
|
-
const bundle = await bundleFont(
|
|
13526
|
-
storyboard.lang,
|
|
13527
|
-
glyphs(source) + glyphs(storyboard),
|
|
13528
|
-
join17(out, "assets", "fonts")
|
|
13529
|
-
);
|
|
13530
|
-
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
13531
|
-
return bundle?.css;
|
|
13532
|
-
} catch (err) {
|
|
13533
|
-
step(
|
|
13534
|
-
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
13535
|
-
);
|
|
13536
|
-
return void 0;
|
|
13537
|
-
}
|
|
13538
|
-
}
|
|
13539
13680
|
function glyphs(value) {
|
|
13540
13681
|
return JSON.stringify(value);
|
|
13541
13682
|
}
|
|
13542
13683
|
async function readValidated(file, schema, label) {
|
|
13543
|
-
const text2 = await
|
|
13684
|
+
const text2 = await readFile18(resolve7(file), "utf8").catch(() => {
|
|
13544
13685
|
throw new Error(`Cannot read ${label} file ${file}.`);
|
|
13545
13686
|
});
|
|
13546
13687
|
let json;
|
|
@@ -13558,9 +13699,9 @@ ${issues}`);
|
|
|
13558
13699
|
return parsed.data;
|
|
13559
13700
|
}
|
|
13560
13701
|
async function writeJson(file, value) {
|
|
13561
|
-
const path2 =
|
|
13562
|
-
await
|
|
13563
|
-
await
|
|
13702
|
+
const path2 = resolve7(file);
|
|
13703
|
+
await mkdir11(dirname6(path2), { recursive: true });
|
|
13704
|
+
await writeFile13(path2, `${JSON.stringify(value, null, 2)}
|
|
13564
13705
|
`);
|
|
13565
13706
|
}
|
|
13566
13707
|
function size2(bytes) {
|