@jokerized/decksmith 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/cli.js +488 -384
- package/dist/deck-runtime.js +5 -2
- package/dist/index.js +449 -312
- package/dist/mcp.js +392 -303
- package/dist/types/build/files.d.ts +102 -0
- package/dist/types/deck/runtime.d.ts +4 -2
- package/dist/types/emit/kit.d.ts +32 -0
- package/dist/types/index.d.ts +10 -6
- package/dist/types/render/render.d.ts +40 -12
- package/dist/types/render/timing.d.ts +11 -0
- package/dist/types/types.d.ts +87 -0
- package/dist/types/verify/budget.d.ts +10 -2
- package/dist/types/verify/fidelity.d.ts +55 -0
- package/dist/types/verify/index.d.ts +8 -3
- package/package.json +2 -2
package/dist/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.
|
|
@@ -915,8 +1092,7 @@ var DESTINATIONS = {
|
|
|
915
1092
|
"video-16x9": [],
|
|
916
1093
|
"short-9x16": [
|
|
917
1094
|
{ name: "YouTube Shorts", maxSeconds: 180 },
|
|
918
|
-
{ name: "Instagram Reels", maxSeconds: 180 }
|
|
919
|
-
{ name: "Facebook Reels", maxSeconds: 90 }
|
|
1095
|
+
{ name: "Instagram Reels", maxSeconds: 180 }
|
|
920
1096
|
],
|
|
921
1097
|
"post-1x1": [{ name: "X (standard account)", maxSeconds: 140 }]
|
|
922
1098
|
};
|
|
@@ -1224,54 +1400,6 @@ function clock(seconds) {
|
|
|
1224
1400
|
return m === 0 ? `${whole}s` : `${m}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1225
1401
|
}
|
|
1226
1402
|
|
|
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
1403
|
// src/emit/kit.ts
|
|
1276
1404
|
var PAD_X = 110;
|
|
1277
1405
|
var PAD_Y = 84;
|
|
@@ -1290,7 +1418,12 @@ function contentW(format) {
|
|
|
1290
1418
|
return refWidth(format) - 2 * PAD_X;
|
|
1291
1419
|
}
|
|
1292
1420
|
function contentH(format) {
|
|
1293
|
-
return refHeight(format) - 2 * PAD_Y;
|
|
1421
|
+
return refHeight(format) - 2 * PAD_Y - reserveRef(format);
|
|
1422
|
+
}
|
|
1423
|
+
function reserveRef(format) {
|
|
1424
|
+
const reserve = format.captionReserve ?? 0;
|
|
1425
|
+
if (reserve <= 0) return 0;
|
|
1426
|
+
return Math.ceil(reserve * refHeight(format) / format.height);
|
|
1294
1427
|
}
|
|
1295
1428
|
function raw(js2) {
|
|
1296
1429
|
return { __raw: js2 };
|
|
@@ -1789,8 +1922,8 @@ function charUnits(c, tabular, cjkRun, hangul) {
|
|
|
1789
1922
|
}
|
|
1790
1923
|
const measured = ADVANCE[c];
|
|
1791
1924
|
if (measured !== void 0) return [measured, true];
|
|
1792
|
-
const
|
|
1793
|
-
for (const [lo, hi, units] of BLOCK_ADVANCE) if (
|
|
1925
|
+
const cp3 = c.codePointAt(0) ?? 0;
|
|
1926
|
+
for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp3 >= lo && cp3 <= hi) return [units, false];
|
|
1794
1927
|
return [UNMEASURED, true];
|
|
1795
1928
|
}
|
|
1796
1929
|
function weightFactor(weight) {
|
|
@@ -2051,11 +2184,16 @@ function baseCss(theme, format) {
|
|
|
2051
2184
|
gate still catches anything taller than the canvas; it just now
|
|
2052
2185
|
overflows symmetrically instead of only downwards. */
|
|
2053
2186
|
.scene { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
2054
|
-
padding: ${
|
|
2187
|
+
padding: ${scenePadding(format)}; display: flex; flex-direction: column;
|
|
2055
2188
|
justify-content: center; }
|
|
2056
2189
|
${referenceSpaceCss(format)}
|
|
2057
2190
|
${AMBIENT_KEYFRAMES}`;
|
|
2058
2191
|
}
|
|
2192
|
+
function scenePadding(format) {
|
|
2193
|
+
const reserve = reserveRef(format);
|
|
2194
|
+
const base = `${PAD_Y}px ${PAD_X}px`;
|
|
2195
|
+
return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
|
|
2196
|
+
}
|
|
2059
2197
|
function referenceSpaceCss(format) {
|
|
2060
2198
|
const zoom = zoomOf(format);
|
|
2061
2199
|
if (zoom === 1) return "";
|
|
@@ -6189,13 +6327,13 @@ function rnd(n3) {
|
|
|
6189
6327
|
|
|
6190
6328
|
// src/images/illustrate.ts
|
|
6191
6329
|
import { createHash as createHash5 } from "node:crypto";
|
|
6192
|
-
import { mkdir as
|
|
6193
|
-
import { join as
|
|
6330
|
+
import { mkdir as mkdir4, readFile as readFile7, rename, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
|
|
6331
|
+
import { join as join6 } from "node:path";
|
|
6194
6332
|
|
|
6195
6333
|
// src/source/assets.ts
|
|
6196
6334
|
import { createHash as createHash3 } from "node:crypto";
|
|
6197
|
-
import { mkdir as
|
|
6198
|
-
import { extname as extname2, join as
|
|
6335
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
6336
|
+
import { extname as extname2, join as join3 } from "node:path";
|
|
6199
6337
|
import { z as z2 } from "zod";
|
|
6200
6338
|
|
|
6201
6339
|
// src/net/fetch.ts
|
|
@@ -6206,8 +6344,8 @@ import { isIP } from "node:net";
|
|
|
6206
6344
|
import { brotliDecompressSync, gunzipSync } from "node:zlib";
|
|
6207
6345
|
|
|
6208
6346
|
// src/version.ts
|
|
6209
|
-
import { createRequire } from "node:module";
|
|
6210
|
-
var VERSION =
|
|
6347
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
6348
|
+
var VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
6211
6349
|
|
|
6212
6350
|
// src/net/fetch.ts
|
|
6213
6351
|
var BLOCKED_V4 = [
|
|
@@ -6402,8 +6540,8 @@ async function send(url2, target, headers, signal, opts) {
|
|
|
6402
6540
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6403
6541
|
};
|
|
6404
6542
|
try {
|
|
6405
|
-
return await new Promise((
|
|
6406
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
6543
|
+
return await new Promise((resolve8, reject) => {
|
|
6544
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve8);
|
|
6407
6545
|
req.on("error", reject);
|
|
6408
6546
|
req.end();
|
|
6409
6547
|
});
|
|
@@ -6488,8 +6626,8 @@ function message(err) {
|
|
|
6488
6626
|
// src/source/assets.ts
|
|
6489
6627
|
async function fetchFigures(source, dir, warnings) {
|
|
6490
6628
|
const drops = warnings ?? [];
|
|
6491
|
-
await
|
|
6492
|
-
const cached2 = await
|
|
6629
|
+
await mkdir3(dir, { recursive: true });
|
|
6630
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
6493
6631
|
const figures = [];
|
|
6494
6632
|
for (const figure of source.figures) {
|
|
6495
6633
|
if (figure.kind === "clip") {
|
|
@@ -6514,11 +6652,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6514
6652
|
async function localize(figure, dir, cached2) {
|
|
6515
6653
|
const stem = assetStem(figure.id, figure.src);
|
|
6516
6654
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
6517
|
-
const bytes = hit ? await
|
|
6655
|
+
const bytes = hit ? await readFile4(join3(dir, hit)) : await load(figure.src);
|
|
6518
6656
|
const size3 = imageSize(bytes);
|
|
6519
6657
|
if (hit) return { ...figure, src: hit, ...size3 };
|
|
6520
6658
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6521
|
-
await
|
|
6659
|
+
await writeFile3(join3(dir, src), bytes);
|
|
6522
6660
|
cached2.push(src);
|
|
6523
6661
|
return { ...figure, src, ...size3 };
|
|
6524
6662
|
}
|
|
@@ -6543,7 +6681,7 @@ function assetExt(bytes, src) {
|
|
|
6543
6681
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
6544
6682
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
6545
6683
|
async function load(src) {
|
|
6546
|
-
if (!/^https?:/i.test(src)) return
|
|
6684
|
+
if (!/^https?:/i.test(src)) return readFile4(src);
|
|
6547
6685
|
const got = await fetchGuarded(src, {
|
|
6548
6686
|
maxBytes: FIGURE_MAX_BYTES,
|
|
6549
6687
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -6788,16 +6926,16 @@ function cssPixels(value) {
|
|
|
6788
6926
|
|
|
6789
6927
|
// src/images/providers.ts
|
|
6790
6928
|
import { createHash as createHash4 } from "node:crypto";
|
|
6791
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
6929
|
+
import { mkdtemp as mkdtemp2, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
6792
6930
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
6793
|
-
import { join as
|
|
6931
|
+
import { join as join5, resolve as resolve2 } from "node:path";
|
|
6794
6932
|
import { z as z4 } from "zod";
|
|
6795
6933
|
|
|
6796
6934
|
// src/plan/codex.ts
|
|
6797
6935
|
import { spawn } from "node:child_process";
|
|
6798
|
-
import { mkdtemp, readFile as
|
|
6936
|
+
import { mkdtemp, readFile as readFile5, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
6799
6937
|
import { tmpdir } from "node:os";
|
|
6800
|
-
import { join as
|
|
6938
|
+
import { join as join4 } from "node:path";
|
|
6801
6939
|
import { z as z3 } from "zod";
|
|
6802
6940
|
|
|
6803
6941
|
// src/plan/arc.ts
|
|
@@ -7916,11 +8054,11 @@ function schemaFor(prefs) {
|
|
|
7916
8054
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
7917
8055
|
async function codexPlanner(source, opts = {}) {
|
|
7918
8056
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
7919
|
-
const dir = await mkdtemp(
|
|
8057
|
+
const dir = await mkdtemp(join4(tmpdir(), "decksmith-plan-"));
|
|
7920
8058
|
try {
|
|
7921
|
-
const schemaPath =
|
|
7922
|
-
const outPath =
|
|
7923
|
-
await
|
|
8059
|
+
const schemaPath = join4(dir, "storyboard.schema.json");
|
|
8060
|
+
const outPath = join4(dir, "storyboard.json");
|
|
8061
|
+
await writeFile4(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
7924
8062
|
await (opts.run ?? runCodex)({
|
|
7925
8063
|
prompt: buildPrompt(source, prefs),
|
|
7926
8064
|
schemaPath,
|
|
@@ -7928,7 +8066,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
7928
8066
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
7929
8067
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
7930
8068
|
});
|
|
7931
|
-
const raw2 = await
|
|
8069
|
+
const raw2 = await readFile5(outPath, "utf8").catch(() => "");
|
|
7932
8070
|
if (raw2.trim() === "") {
|
|
7933
8071
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
7934
8072
|
}
|
|
@@ -8001,7 +8139,7 @@ function codexCommand(args) {
|
|
|
8001
8139
|
}
|
|
8002
8140
|
function runCodex(args) {
|
|
8003
8141
|
const { argv, env } = codexCommand(args);
|
|
8004
|
-
return new Promise((
|
|
8142
|
+
return new Promise((resolve8, reject) => {
|
|
8005
8143
|
const child = spawn("codex", argv, {
|
|
8006
8144
|
stdio: ["pipe", "ignore", "pipe"],
|
|
8007
8145
|
...env === void 0 ? {} : { env }
|
|
@@ -8024,7 +8162,7 @@ function runCodex(args) {
|
|
|
8024
8162
|
});
|
|
8025
8163
|
child.on("close", (code) => {
|
|
8026
8164
|
clearTimeout(timer);
|
|
8027
|
-
if (code === 0) return
|
|
8165
|
+
if (code === 0) return resolve8();
|
|
8028
8166
|
reject(
|
|
8029
8167
|
new Error(`codex exec exited ${code}.
|
|
8030
8168
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -8165,11 +8303,11 @@ function codexImages(opts = {}) {
|
|
|
8165
8303
|
async check() {
|
|
8166
8304
|
},
|
|
8167
8305
|
async generate(req) {
|
|
8168
|
-
const dir = await mkdtemp2(
|
|
8306
|
+
const dir = await mkdtemp2(join5(tmpdir2(), "decksmith-image-"));
|
|
8169
8307
|
try {
|
|
8170
|
-
const schemaPath =
|
|
8171
|
-
const outPath =
|
|
8172
|
-
await
|
|
8308
|
+
const schemaPath = join5(dir, "answer.schema.json");
|
|
8309
|
+
const outPath = join5(dir, "answer.json");
|
|
8310
|
+
await writeFile5(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
8173
8311
|
await run4({
|
|
8174
8312
|
prompt: codexPrompt(req),
|
|
8175
8313
|
schemaPath,
|
|
@@ -8178,7 +8316,7 @@ function codexImages(opts = {}) {
|
|
|
8178
8316
|
cwd: dir,
|
|
8179
8317
|
sandbox: "workspace-write"
|
|
8180
8318
|
});
|
|
8181
|
-
const raw2 = await
|
|
8319
|
+
const raw2 = await readFile6(outPath, "utf8").catch(() => "");
|
|
8182
8320
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
8183
8321
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
8184
8322
|
if (!answer.data.ok) {
|
|
@@ -8186,10 +8324,10 @@ function codexImages(opts = {}) {
|
|
|
8186
8324
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
8187
8325
|
);
|
|
8188
8326
|
}
|
|
8189
|
-
const candidates2 = [
|
|
8190
|
-
if (answer.data.file) candidates2.push(
|
|
8327
|
+
const candidates2 = [join5(dir, "picture.png")];
|
|
8328
|
+
if (answer.data.file) candidates2.push(resolve2(dir, answer.data.file));
|
|
8191
8329
|
for (const path2 of candidates2) {
|
|
8192
|
-
const bytes = await
|
|
8330
|
+
const bytes = await readFile6(path2).catch(() => null);
|
|
8193
8331
|
if (bytes) return raster(bytes, "codex");
|
|
8194
8332
|
}
|
|
8195
8333
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -8321,7 +8459,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
8321
8459
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
8322
8460
|
if (tool !== last) chain.push(tool);
|
|
8323
8461
|
const dropped = /* @__PURE__ */ new Set();
|
|
8324
|
-
if (pending.length > 0) await
|
|
8462
|
+
if (pending.length > 0) await mkdir4(opts.assetsDir, { recursive: true });
|
|
8325
8463
|
for (const [i, slot] of pending.entries()) {
|
|
8326
8464
|
let rungs = chain;
|
|
8327
8465
|
if (i >= images.max) {
|
|
@@ -8418,16 +8556,16 @@ function cacheKey(providerId, req) {
|
|
|
8418
8556
|
async function draw(provider, req, name, dir) {
|
|
8419
8557
|
for (const ext of Object.values(EXT2)) {
|
|
8420
8558
|
const src2 = `${name}${ext}`;
|
|
8421
|
-
const bytes = await
|
|
8559
|
+
const bytes = await readFile7(join6(dir, src2)).catch(() => null);
|
|
8422
8560
|
if (bytes) return { src: src2, ...sizeOf(bytes, ext), cached: true };
|
|
8423
8561
|
}
|
|
8424
8562
|
await provider.check();
|
|
8425
8563
|
const img = await provider.generate(req);
|
|
8426
8564
|
const src = `${name}${EXT2[img.mime]}`;
|
|
8427
|
-
const tmp =
|
|
8565
|
+
const tmp = join6(dir, `.${name}.tmp`);
|
|
8428
8566
|
try {
|
|
8429
|
-
await
|
|
8430
|
-
await rename(tmp,
|
|
8567
|
+
await writeFile6(tmp, img.bytes);
|
|
8568
|
+
await rename(tmp, join6(dir, src));
|
|
8431
8569
|
} finally {
|
|
8432
8570
|
await rm3(tmp, { force: true });
|
|
8433
8571
|
}
|
|
@@ -8440,9 +8578,9 @@ function sizeOf(bytes, ext) {
|
|
|
8440
8578
|
// src/narrate/tts.ts
|
|
8441
8579
|
import { spawn as spawn2 } from "node:child_process";
|
|
8442
8580
|
import { createHash as createHash6 } from "node:crypto";
|
|
8443
|
-
import { mkdir as
|
|
8581
|
+
import { mkdir as mkdir5, readFile as readFile8, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
8444
8582
|
import { homedir } from "node:os";
|
|
8445
|
-
import { join as
|
|
8583
|
+
import { join as join7 } from "node:path";
|
|
8446
8584
|
var MISSING = [
|
|
8447
8585
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
8448
8586
|
"",
|
|
@@ -8459,8 +8597,8 @@ function candidates() {
|
|
|
8459
8597
|
["edge-tts"],
|
|
8460
8598
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
8461
8599
|
// default, and both are where this actually lands in practice.
|
|
8462
|
-
[
|
|
8463
|
-
[
|
|
8600
|
+
[join7(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
8601
|
+
[join7(home, ".local", "bin", "edge-tts")],
|
|
8464
8602
|
// Last resort: the module is installed even though its console script is
|
|
8465
8603
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
8466
8604
|
["python3", "-m", "edge_tts"]
|
|
@@ -8485,7 +8623,7 @@ async function find2(can) {
|
|
|
8485
8623
|
throw new Error(MISSING);
|
|
8486
8624
|
}
|
|
8487
8625
|
function runArgv(cmd, args) {
|
|
8488
|
-
return new Promise((
|
|
8626
|
+
return new Promise((resolve8) => {
|
|
8489
8627
|
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8490
8628
|
let stdout = "";
|
|
8491
8629
|
let stderr = "";
|
|
@@ -8495,8 +8633,8 @@ function runArgv(cmd, args) {
|
|
|
8495
8633
|
child.stderr.on("data", (b) => {
|
|
8496
8634
|
stderr += b.toString();
|
|
8497
8635
|
});
|
|
8498
|
-
child.on("error", (e) =>
|
|
8499
|
-
child.on("close", (code) =>
|
|
8636
|
+
child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
|
|
8637
|
+
child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
|
|
8500
8638
|
});
|
|
8501
8639
|
}
|
|
8502
8640
|
var edgeTts = {
|
|
@@ -8563,7 +8701,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
8563
8701
|
async speak(req) {
|
|
8564
8702
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
8565
8703
|
await runner.speak({ ...req, subs });
|
|
8566
|
-
const raw2 = await
|
|
8704
|
+
const raw2 = await readFile8(subs, "utf8").catch(() => "");
|
|
8567
8705
|
await rm4(subs, { force: true });
|
|
8568
8706
|
const cues = parseCues(raw2);
|
|
8569
8707
|
const measured = await runner.measure(req.audio);
|
|
@@ -8581,11 +8719,11 @@ async function synthesize(text2, opts) {
|
|
|
8581
8719
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
8582
8720
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
8583
8721
|
const file = `${key}.mp3`;
|
|
8584
|
-
const audio =
|
|
8585
|
-
const sidecar =
|
|
8722
|
+
const audio = join7(opts.dir, file);
|
|
8723
|
+
const sidecar = join7(opts.dir, `${key}.json`);
|
|
8586
8724
|
const cached2 = await readSidecar(sidecar);
|
|
8587
8725
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
8588
|
-
await
|
|
8726
|
+
await mkdir5(opts.dir, { recursive: true });
|
|
8589
8727
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
8590
8728
|
const { cues, seconds } = spoken;
|
|
8591
8729
|
const clamped = cues.map((c) => ({
|
|
@@ -8594,13 +8732,13 @@ async function synthesize(text2, opts) {
|
|
|
8594
8732
|
text: c.text
|
|
8595
8733
|
}));
|
|
8596
8734
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
8597
|
-
await
|
|
8735
|
+
await writeFile7(sidecar, `${JSON.stringify(body, null, 2)}
|
|
8598
8736
|
`);
|
|
8599
8737
|
return { audio, file, seconds, cues: clamped };
|
|
8600
8738
|
}
|
|
8601
8739
|
async function readSidecar(path2) {
|
|
8602
8740
|
try {
|
|
8603
|
-
const parsed = JSON.parse(await
|
|
8741
|
+
const parsed = JSON.parse(await readFile8(path2, "utf8"));
|
|
8604
8742
|
const s = parsed;
|
|
8605
8743
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
8606
8744
|
} catch {
|
|
@@ -8720,8 +8858,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
8720
8858
|
}
|
|
8721
8859
|
|
|
8722
8860
|
// src/pack/pack.ts
|
|
8723
|
-
import { mkdir as
|
|
8724
|
-
import { dirname, extname as extname3 } from "node:path";
|
|
8861
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
|
|
8862
|
+
import { dirname as dirname2, extname as extname3 } from "node:path";
|
|
8725
8863
|
import { unzipSync, zipSync } from "fflate";
|
|
8726
8864
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
8727
8865
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -8760,12 +8898,12 @@ async function writePack(pack2, files, out) {
|
|
|
8760
8898
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
8761
8899
|
}
|
|
8762
8900
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
8763
|
-
await
|
|
8764
|
-
await
|
|
8901
|
+
await mkdir6(dirname2(out), { recursive: true });
|
|
8902
|
+
await writeFile8(out, zip);
|
|
8765
8903
|
return zip.length;
|
|
8766
8904
|
}
|
|
8767
8905
|
async function readPack(path2) {
|
|
8768
|
-
return openPack(new Uint8Array(await
|
|
8906
|
+
return openPack(new Uint8Array(await readFile9(path2)), path2);
|
|
8769
8907
|
}
|
|
8770
8908
|
function openPack(bytes, label = "pack") {
|
|
8771
8909
|
if (bytes.length < 4 || bytes[0] !== 80 || bytes[1] !== 75)
|
|
@@ -8809,8 +8947,8 @@ function why(error) {
|
|
|
8809
8947
|
}
|
|
8810
8948
|
|
|
8811
8949
|
// src/prefs.ts
|
|
8812
|
-
import { readFile as
|
|
8813
|
-
import { dirname as
|
|
8950
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
8951
|
+
import { dirname as dirname3, join as join8, resolve as resolve3 } from "node:path";
|
|
8814
8952
|
var CONFIG_FILE = "decksmith.config.json";
|
|
8815
8953
|
var PREF_KEYS = Object.keys(prefsSchema.shape);
|
|
8816
8954
|
var NARRATION_KEYS = Object.keys(prefsSchema.shape.narration.unwrap().shape);
|
|
@@ -8856,10 +8994,10 @@ function prefsFromFlags(flags2) {
|
|
|
8856
8994
|
return patch;
|
|
8857
8995
|
}
|
|
8858
8996
|
async function findConfig(from) {
|
|
8859
|
-
let dir =
|
|
8997
|
+
let dir = resolve3(from);
|
|
8860
8998
|
for (; ; ) {
|
|
8861
|
-
const path2 =
|
|
8862
|
-
const text2 = await
|
|
8999
|
+
const path2 = join8(dir, CONFIG_FILE);
|
|
9000
|
+
const text2 = await readFile10(path2, "utf8").catch(() => void 0);
|
|
8863
9001
|
if (text2 !== void 0) {
|
|
8864
9002
|
try {
|
|
8865
9003
|
return { path: path2, value: JSON.parse(text2) };
|
|
@@ -8867,7 +9005,7 @@ async function findConfig(from) {
|
|
|
8867
9005
|
throw new Error(`${path2} is not valid JSON.`);
|
|
8868
9006
|
}
|
|
8869
9007
|
}
|
|
8870
|
-
const up =
|
|
9008
|
+
const up = dirname3(dir);
|
|
8871
9009
|
if (up === dir) return void 0;
|
|
8872
9010
|
dir = up;
|
|
8873
9011
|
}
|
|
@@ -8917,10 +9055,10 @@ function number(flag, value) {
|
|
|
8917
9055
|
}
|
|
8918
9056
|
|
|
8919
9057
|
// src/render/capture.ts
|
|
8920
|
-
import { mkdir as
|
|
8921
|
-
import { createRequire as
|
|
9058
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
|
|
9059
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
8922
9060
|
import { homedir as homedir2 } from "node:os";
|
|
8923
|
-
import { join as
|
|
9061
|
+
import { join as join9 } from "node:path";
|
|
8924
9062
|
import { pathToFileURL } from "node:url";
|
|
8925
9063
|
async function resolveChrome(need = "open the deck with") {
|
|
8926
9064
|
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
@@ -8931,9 +9069,9 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
8931
9069
|
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
8932
9070
|
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
8933
9071
|
};
|
|
8934
|
-
const hf = await pick(
|
|
9072
|
+
const hf = await pick(join9(homedir2(), ".cache", "hyperframes", "chrome"));
|
|
8935
9073
|
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
8936
|
-
const pup = await pick(process.env.PUPPETEER_CACHE_DIR ||
|
|
9074
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join9(homedir2(), ".cache", "puppeteer"));
|
|
8937
9075
|
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
8938
9076
|
throw new Error(
|
|
8939
9077
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
@@ -8943,15 +9081,15 @@ async function chromePath(need = "open the deck with") {
|
|
|
8943
9081
|
return (await resolveChrome(need)).path;
|
|
8944
9082
|
}
|
|
8945
9083
|
function runtimePath() {
|
|
8946
|
-
return
|
|
9084
|
+
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
8947
9085
|
}
|
|
8948
9086
|
function renderSeek(t2) {
|
|
8949
9087
|
const player = window.__player;
|
|
8950
9088
|
player.renderSeek(t2, { suppressEvents: true });
|
|
8951
9089
|
}
|
|
8952
9090
|
async function openDeck(dir, opts = {}) {
|
|
8953
|
-
const index =
|
|
8954
|
-
const html = await
|
|
9091
|
+
const index = join9(dir, "index.html");
|
|
9092
|
+
const html = await readFile11(index, "utf8").catch(() => null);
|
|
8955
9093
|
if (html === null) throw new Error(`no index.html in ${dir}`);
|
|
8956
9094
|
const width = Number(/data-width="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
8957
9095
|
const height = Number(/data-height="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
@@ -9032,7 +9170,7 @@ function frameName(index, t2, total) {
|
|
|
9032
9170
|
}
|
|
9033
9171
|
async function captureFrames(dir, times, outDir, opts = {}) {
|
|
9034
9172
|
if (times.length === 0) return [];
|
|
9035
|
-
await
|
|
9173
|
+
await mkdir7(outDir, { recursive: true });
|
|
9036
9174
|
const deck = await openDeck(dir, opts);
|
|
9037
9175
|
try {
|
|
9038
9176
|
const out = [];
|
|
@@ -9040,8 +9178,8 @@ async function captureFrames(dir, times, outDir, opts = {}) {
|
|
|
9040
9178
|
const t2 = Math.round(raw2 * 1e3) / 1e3;
|
|
9041
9179
|
await deck.seek(t2);
|
|
9042
9180
|
const png = await deck.shoot();
|
|
9043
|
-
const path2 =
|
|
9044
|
-
await
|
|
9181
|
+
const path2 = join9(outDir, frameName(i, t2, times.length));
|
|
9182
|
+
await writeFile9(path2, png);
|
|
9045
9183
|
out.push({ t: t2, path: path2, bytes: png.length });
|
|
9046
9184
|
}
|
|
9047
9185
|
return out;
|
|
@@ -9052,12 +9190,12 @@ async function captureFrames(dir, times, outDir, opts = {}) {
|
|
|
9052
9190
|
}
|
|
9053
9191
|
|
|
9054
9192
|
// src/render/render.ts
|
|
9055
|
-
import { mkdir as
|
|
9056
|
-
import { basename, dirname as
|
|
9193
|
+
import { mkdir as mkdir9, readFile as readFile12, rename as rename2, rm as rm5, writeFile as writeFile11 } from "node:fs/promises";
|
|
9194
|
+
import { basename, dirname as dirname4, join as join11, resolve as resolve4 } from "node:path";
|
|
9057
9195
|
|
|
9058
9196
|
// src/render/captions.ts
|
|
9059
|
-
import { mkdir as
|
|
9060
|
-
import { join as
|
|
9197
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
|
|
9198
|
+
import { join as join10 } from "node:path";
|
|
9061
9199
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
9062
9200
|
|
|
9063
9201
|
// src/deck/subtitles.ts
|
|
@@ -9256,6 +9394,10 @@ function planTiming(input) {
|
|
|
9256
9394
|
lang: storyboard.lang,
|
|
9257
9395
|
audioDir: segments2.length > 0 ? narration?.dir ?? "" : "",
|
|
9258
9396
|
voice: segments2.length > 0 ? narration?.voice ?? "" : "",
|
|
9397
|
+
// The format the deck was LAID OUT under, which is the only honest source:
|
|
9398
|
+
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
9399
|
+
// disagree with the box the archetypes actually drew into.
|
|
9400
|
+
captionReserve: format.captionReserve ?? 0,
|
|
9259
9401
|
scenes,
|
|
9260
9402
|
segments: segments2
|
|
9261
9403
|
};
|
|
@@ -9444,11 +9586,11 @@ async function captionBlocker() {
|
|
|
9444
9586
|
}
|
|
9445
9587
|
async function renderCaptions(cues, style, deck, work) {
|
|
9446
9588
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
9447
|
-
const fontCss =
|
|
9589
|
+
const fontCss = join10(deck, DECK_FONT_CSS);
|
|
9448
9590
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL2(fontCss).href : null;
|
|
9449
|
-
const page =
|
|
9450
|
-
await
|
|
9451
|
-
await
|
|
9591
|
+
const page = join10(work, "captions.html");
|
|
9592
|
+
await mkdir8(work, { recursive: true });
|
|
9593
|
+
await writeFile10(page, captionPage(cues, style, href));
|
|
9452
9594
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
9453
9595
|
const browser = await puppeteer.launch({
|
|
9454
9596
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -9468,7 +9610,7 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
9468
9610
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
9469
9611
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
9470
9612
|
await tab.screenshot({
|
|
9471
|
-
path:
|
|
9613
|
+
path: join10(work, name),
|
|
9472
9614
|
type: "png",
|
|
9473
9615
|
omitBackground: true,
|
|
9474
9616
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -9524,7 +9666,7 @@ ${tail3}`);
|
|
|
9524
9666
|
}
|
|
9525
9667
|
}
|
|
9526
9668
|
function runLive(file, args) {
|
|
9527
|
-
return new Promise((
|
|
9669
|
+
return new Promise((resolve8, reject) => {
|
|
9528
9670
|
const child = spawn3(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
9529
9671
|
child.on("error", (err) => {
|
|
9530
9672
|
reject(
|
|
@@ -9532,7 +9674,7 @@ function runLive(file, args) {
|
|
|
9532
9674
|
);
|
|
9533
9675
|
});
|
|
9534
9676
|
child.on("close", (code, signal) => {
|
|
9535
|
-
if (code === 0)
|
|
9677
|
+
if (code === 0) resolve8();
|
|
9536
9678
|
else if (signal) {
|
|
9537
9679
|
reject(
|
|
9538
9680
|
new Error(
|
|
@@ -9659,11 +9801,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
9659
9801
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
9660
9802
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
9661
9803
|
// three-line band covers the bottom of the slide.
|
|
9662
|
-
|
|
9804
|
+
//
|
|
9805
|
+
// THE RATIOS ARE NOT HERE. They are in src/types.ts beside `bandReserve`,
|
|
9806
|
+
// because `src/emit` has to reserve exactly the strip this draws into and
|
|
9807
|
+
// may not import this file. A local copy of 0.037 is how the two drift back
|
|
9808
|
+
// apart, which is the bug this whole path exists to close.
|
|
9809
|
+
fontSize: captionFontSize(width),
|
|
9663
9810
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
9664
9811
|
// player draws across the bottom eighth of the frame.
|
|
9665
|
-
marginV: Math.round(height *
|
|
9666
|
-
marginX: Math.round(width *
|
|
9812
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
9813
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
9667
9814
|
};
|
|
9668
9815
|
}
|
|
9669
9816
|
|
|
@@ -9675,9 +9822,9 @@ function subtitlePlan(mode) {
|
|
|
9675
9822
|
async function render(opts) {
|
|
9676
9823
|
const log = opts.log ?? (() => {
|
|
9677
9824
|
});
|
|
9678
|
-
const deck =
|
|
9679
|
-
const out =
|
|
9680
|
-
const work =
|
|
9825
|
+
const deck = resolve4(opts.deck);
|
|
9826
|
+
const out = resolve4(opts.out);
|
|
9827
|
+
const work = join11(dirname4(out), `.${basename(out)}.parts`);
|
|
9681
9828
|
const timing = await readTiming(deck);
|
|
9682
9829
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
9683
9830
|
const refusal = playbackRefusal(
|
|
@@ -9689,13 +9836,17 @@ async function render(opts) {
|
|
|
9689
9836
|
}
|
|
9690
9837
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
9691
9838
|
if (plan0.burn) {
|
|
9839
|
+
if (!timing.captionReserve)
|
|
9840
|
+
throw new Error(
|
|
9841
|
+
"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`."
|
|
9842
|
+
);
|
|
9692
9843
|
const blocker = await captionBlocker();
|
|
9693
9844
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
9694
9845
|
}
|
|
9695
9846
|
const burnable = plan0.burn;
|
|
9696
|
-
await
|
|
9847
|
+
await mkdir9(work, { recursive: true });
|
|
9697
9848
|
try {
|
|
9698
|
-
const raw2 = opts.video ?
|
|
9849
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join11(work, "raw.mp4"), opts, log);
|
|
9699
9850
|
const shot = await probe(raw2);
|
|
9700
9851
|
log(
|
|
9701
9852
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -9708,18 +9859,18 @@ async function render(opts) {
|
|
|
9708
9859
|
);
|
|
9709
9860
|
}
|
|
9710
9861
|
const retimed = await retime(raw2, plan, work, log);
|
|
9711
|
-
const srtPath =
|
|
9862
|
+
const srtPath = join11(dirname4(out), SRT_NAME(out));
|
|
9712
9863
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
9713
9864
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
9714
9865
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
9715
|
-
if (srt) await
|
|
9866
|
+
if (srt) await writeFile11(srtPath, srt);
|
|
9716
9867
|
const burn = burnable && srt.length > 0;
|
|
9717
9868
|
await mux(retimed, timing, plan, deck, out, work, burn ? plan.cues : void 0, shot.fps, log);
|
|
9718
9869
|
if (playback > 1) {
|
|
9719
9870
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
9720
9871
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
9721
9872
|
if (warning) log(`render: ${warning}`);
|
|
9722
|
-
const fast =
|
|
9873
|
+
const fast = join11(work, `fast.${basename(out)}`);
|
|
9723
9874
|
const muxed = await probe(out);
|
|
9724
9875
|
await runTool(
|
|
9725
9876
|
"ffmpeg",
|
|
@@ -9749,8 +9900,8 @@ async function render(opts) {
|
|
|
9749
9900
|
}
|
|
9750
9901
|
}
|
|
9751
9902
|
async function readTiming(deck) {
|
|
9752
|
-
const path2 =
|
|
9753
|
-
const text2 = await
|
|
9903
|
+
const path2 = join11(deck, TIMING_FILE);
|
|
9904
|
+
const text2 = await readFile12(path2, "utf8").catch(() => {
|
|
9754
9905
|
throw new Error(
|
|
9755
9906
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
9756
9907
|
);
|
|
@@ -9789,20 +9940,20 @@ async function retime(raw2, plan, work, log) {
|
|
|
9789
9940
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
9790
9941
|
const list = [];
|
|
9791
9942
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
9792
|
-
const file =
|
|
9943
|
+
const file = join11(work, `p${String(i).padStart(4, "0")}.ts`);
|
|
9793
9944
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
9794
9945
|
list.push(file);
|
|
9795
9946
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
9796
9947
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
9797
9948
|
}
|
|
9798
9949
|
}
|
|
9799
|
-
const listFile =
|
|
9800
|
-
await
|
|
9950
|
+
const listFile = join11(work, "pieces.txt");
|
|
9951
|
+
await writeFile11(
|
|
9801
9952
|
listFile,
|
|
9802
9953
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
9803
9954
|
`
|
|
9804
9955
|
);
|
|
9805
|
-
const joined =
|
|
9956
|
+
const joined = join11(work, "retimed.mp4");
|
|
9806
9957
|
await runTool("ffmpeg", [
|
|
9807
9958
|
"-y",
|
|
9808
9959
|
"-hide_banner",
|
|
@@ -9824,7 +9975,7 @@ async function retime(raw2, plan, work, log) {
|
|
|
9824
9975
|
}
|
|
9825
9976
|
async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
9826
9977
|
const inputs = plan.audio.map((a) => ({
|
|
9827
|
-
file:
|
|
9978
|
+
file: join11(deck, timing.audioDir, a.audio),
|
|
9828
9979
|
delayMs: a.delayMs
|
|
9829
9980
|
}));
|
|
9830
9981
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -9845,8 +9996,8 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
9845
9996
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
9846
9997
|
}
|
|
9847
9998
|
if (graph.length > 0) {
|
|
9848
|
-
const script =
|
|
9849
|
-
await
|
|
9999
|
+
const script = join11(work, "mux.filter");
|
|
10000
|
+
await writeFile11(script, `${graph.join(";\n")}
|
|
9850
10001
|
`);
|
|
9851
10002
|
args.push("-filter_complex_script", script);
|
|
9852
10003
|
}
|
|
@@ -9861,14 +10012,14 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
9861
10012
|
log(
|
|
9862
10013
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
9863
10014
|
);
|
|
9864
|
-
await
|
|
10015
|
+
await mkdir9(dirname4(out), { recursive: true });
|
|
9865
10016
|
await runTool("ffmpeg", args, { cwd: work });
|
|
9866
10017
|
}
|
|
9867
10018
|
|
|
9868
10019
|
// src/source/harvest.ts
|
|
9869
10020
|
import { createHash as createHash7 } from "node:crypto";
|
|
9870
|
-
import { copyFile, mkdir as
|
|
9871
|
-
import { basename as basename3, join as
|
|
10021
|
+
import { copyFile, mkdir as mkdir10, rm as rm7, stat as stat2, writeFile as writeFile12 } from "node:fs/promises";
|
|
10022
|
+
import { basename as basename3, join as join12, resolve as resolve5 } from "node:path";
|
|
9872
10023
|
|
|
9873
10024
|
// src/source/readability.ts
|
|
9874
10025
|
var CONTENT_MARKER = "data-ds-content";
|
|
@@ -10163,7 +10314,7 @@ function readContentRegion() {
|
|
|
10163
10314
|
}
|
|
10164
10315
|
|
|
10165
10316
|
// src/source/transcode.ts
|
|
10166
|
-
import { readFile as
|
|
10317
|
+
import { readFile as readFile13, rm as rm6 } from "node:fs/promises";
|
|
10167
10318
|
import { basename as basename2 } from "node:path";
|
|
10168
10319
|
var CLIP_EDGE_PX = 1280;
|
|
10169
10320
|
var MAX_CLIP_SECONDS = 60;
|
|
@@ -10226,7 +10377,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
10226
10377
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
10227
10378
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
10228
10379
|
const name = basename2(input);
|
|
10229
|
-
const source = videoSize(await
|
|
10380
|
+
const source = videoSize(await readFile13(input));
|
|
10230
10381
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
10231
10382
|
const kept = (why3) => ({
|
|
10232
10383
|
path: input,
|
|
@@ -10258,7 +10409,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
10258
10409
|
}
|
|
10259
10410
|
let measured;
|
|
10260
10411
|
try {
|
|
10261
|
-
measured = videoSize(await
|
|
10412
|
+
measured = videoSize(await readFile13(out));
|
|
10262
10413
|
} catch (err) {
|
|
10263
10414
|
await rm6(out, { force: true });
|
|
10264
10415
|
return kept(
|
|
@@ -10294,8 +10445,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
10294
10445
|
async function harvest(url2, dir, opts = {}) {
|
|
10295
10446
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
10296
10447
|
const warnings = [];
|
|
10297
|
-
const assets =
|
|
10298
|
-
await
|
|
10448
|
+
const assets = resolve5(dir);
|
|
10449
|
+
await mkdir10(assets, { recursive: true });
|
|
10299
10450
|
const page = await fetchGuarded(url2, {
|
|
10300
10451
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
10301
10452
|
timeoutMs,
|
|
@@ -10677,8 +10828,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
10677
10828
|
`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`
|
|
10678
10829
|
);
|
|
10679
10830
|
}
|
|
10680
|
-
const path2 =
|
|
10681
|
-
await
|
|
10831
|
+
const path2 = join12(dir, assetName(url2, extFor2(sniffFormat(bytes))));
|
|
10832
|
+
await writeFile12(path2, bytes);
|
|
10682
10833
|
assets.push(path2);
|
|
10683
10834
|
got = { path: path2, width: size3.width, height: size3.height };
|
|
10684
10835
|
} catch (err) {
|
|
@@ -10690,7 +10841,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
10690
10841
|
};
|
|
10691
10842
|
const shrink = async (url2, path2, measured, what) => {
|
|
10692
10843
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
10693
|
-
const out2 =
|
|
10844
|
+
const out2 = join12(dir, assetName(url2, ".vp9.webm"));
|
|
10694
10845
|
let small;
|
|
10695
10846
|
try {
|
|
10696
10847
|
small = await transcode(path2, out2, {
|
|
@@ -10732,8 +10883,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
10732
10883
|
try {
|
|
10733
10884
|
const bytes = await bytesOf(url2);
|
|
10734
10885
|
const measured = videoSize(bytes);
|
|
10735
|
-
const path2 =
|
|
10736
|
-
await
|
|
10886
|
+
const path2 = join12(dir, assetName(url2, `.${measured.container}`));
|
|
10887
|
+
await writeFile12(path2, bytes);
|
|
10737
10888
|
return await shrink(url2, path2, measured, what);
|
|
10738
10889
|
} catch (err) {
|
|
10739
10890
|
warnings.push(`${what} was not downloaded: ${url2} \u2014 ${why2(err)}`);
|
|
@@ -10822,7 +10973,7 @@ function unheld(policy) {
|
|
|
10822
10973
|
return "the file could not be held";
|
|
10823
10974
|
}
|
|
10824
10975
|
async function sizeOf2(path2) {
|
|
10825
|
-
return await
|
|
10976
|
+
return await stat2(path2).then(
|
|
10826
10977
|
(s) => s.size,
|
|
10827
10978
|
() => 0
|
|
10828
10979
|
);
|
|
@@ -11030,8 +11181,8 @@ function sane(seconds) {
|
|
|
11030
11181
|
}
|
|
11031
11182
|
async function attachClips(source, clips, dir) {
|
|
11032
11183
|
if (clips.length === 0) return source;
|
|
11033
|
-
const assets =
|
|
11034
|
-
await
|
|
11184
|
+
const assets = resolve5(dir);
|
|
11185
|
+
await mkdir10(assets, { recursive: true });
|
|
11035
11186
|
const figures = [...source.figures];
|
|
11036
11187
|
for (const clip3 of clips) {
|
|
11037
11188
|
const src = clip3.file ? await adopt(clip3.file, assets) : void 0;
|
|
@@ -11062,7 +11213,7 @@ async function attachClips(source, clips, dir) {
|
|
|
11062
11213
|
}
|
|
11063
11214
|
async function adopt(file, dir) {
|
|
11064
11215
|
const name = basename3(file);
|
|
11065
|
-
await copyFile(file,
|
|
11216
|
+
await copyFile(file, join12(dir, name));
|
|
11066
11217
|
return name;
|
|
11067
11218
|
}
|
|
11068
11219
|
function freeId(figures) {
|
|
@@ -11331,26 +11482,26 @@ function textOf(node) {
|
|
|
11331
11482
|
|
|
11332
11483
|
// src/tmpdir.ts
|
|
11333
11484
|
import { realpathSync } from "node:fs";
|
|
11334
|
-
import { createRequire as
|
|
11485
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
11335
11486
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
11336
|
-
import { basename as basename4, dirname as
|
|
11487
|
+
import { basename as basename4, dirname as dirname5, join as join13, resolve as resolve6, sep } from "node:path";
|
|
11337
11488
|
var cached;
|
|
11338
11489
|
function real(dir) {
|
|
11339
|
-
let at =
|
|
11490
|
+
let at = resolve6(dir);
|
|
11340
11491
|
const tail3 = [];
|
|
11341
11492
|
for (; ; ) {
|
|
11342
11493
|
try {
|
|
11343
|
-
return
|
|
11494
|
+
return join13(realpathSync(at), ...tail3);
|
|
11344
11495
|
} catch {
|
|
11345
|
-
const up =
|
|
11346
|
-
if (up === at) return
|
|
11496
|
+
const up = dirname5(at);
|
|
11497
|
+
if (up === at) return resolve6(dir);
|
|
11347
11498
|
tail3.unshift(basename4(at));
|
|
11348
11499
|
at = up;
|
|
11349
11500
|
}
|
|
11350
11501
|
}
|
|
11351
11502
|
}
|
|
11352
11503
|
function packageRoot() {
|
|
11353
|
-
cached ??= real(
|
|
11504
|
+
cached ??= real(dirname5(createRequire4(import.meta.url).resolve("../package.json")));
|
|
11354
11505
|
return cached;
|
|
11355
11506
|
}
|
|
11356
11507
|
function insideRoot(dir) {
|
|
@@ -11378,8 +11529,8 @@ function guardTmpdir() {
|
|
|
11378
11529
|
}
|
|
11379
11530
|
|
|
11380
11531
|
// src/verify/index.ts
|
|
11381
|
-
import { readdir as
|
|
11382
|
-
import { join as
|
|
11532
|
+
import { readdir as readdir4, readFile as readFile17 } from "node:fs/promises";
|
|
11533
|
+
import { join as join17, relative, sep as sep2 } from "node:path";
|
|
11383
11534
|
|
|
11384
11535
|
// src/verify/budget.ts
|
|
11385
11536
|
function readCanvas(html) {
|
|
@@ -11395,13 +11546,13 @@ function readCanvas(html) {
|
|
|
11395
11546
|
if (width === void 0 || height === void 0 || seconds === void 0) return void 0;
|
|
11396
11547
|
return { width, height, seconds };
|
|
11397
11548
|
}
|
|
11398
|
-
function profilesFor(width, height) {
|
|
11399
|
-
return Object.values(
|
|
11549
|
+
function profilesFor(width, height, formats = FORMATS) {
|
|
11550
|
+
return Object.values(formats).filter((f) => f.width === width && f.height === height);
|
|
11400
11551
|
}
|
|
11401
|
-
function scanBudget(html, storyboard, kept) {
|
|
11552
|
+
function scanBudget(html, storyboard, kept, formats = FORMATS) {
|
|
11402
11553
|
const canvas = readCanvas(html);
|
|
11403
11554
|
if (!canvas) return [];
|
|
11404
|
-
const profiles = profilesFor(canvas.width, canvas.height);
|
|
11555
|
+
const profiles = profilesFor(canvas.width, canvas.height, formats);
|
|
11405
11556
|
if (profiles.length === 0) {
|
|
11406
11557
|
return [
|
|
11407
11558
|
{
|
|
@@ -11486,8 +11637,8 @@ function clock2(seconds) {
|
|
|
11486
11637
|
|
|
11487
11638
|
// src/verify/check.ts
|
|
11488
11639
|
import { execFile as execFile2 } from "node:child_process";
|
|
11489
|
-
import { readFile as
|
|
11490
|
-
import { join as
|
|
11640
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
11641
|
+
import { join as join14 } from "node:path";
|
|
11491
11642
|
import { promisify as promisify2 } from "node:util";
|
|
11492
11643
|
var run2 = promisify2(execFile2);
|
|
11493
11644
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -11517,7 +11668,7 @@ async function check2(dir, opts = {}) {
|
|
|
11517
11668
|
async function readComposition(dir) {
|
|
11518
11669
|
let html;
|
|
11519
11670
|
try {
|
|
11520
|
-
html = await
|
|
11671
|
+
html = await readFile14(join14(dir, "index.html"), "utf8");
|
|
11521
11672
|
} catch {
|
|
11522
11673
|
return { transit: [], duration: 0 };
|
|
11523
11674
|
}
|
|
@@ -11665,8 +11816,8 @@ function tail(s) {
|
|
|
11665
11816
|
}
|
|
11666
11817
|
|
|
11667
11818
|
// src/verify/fidelity.ts
|
|
11668
|
-
import { readFile as
|
|
11669
|
-
import { join as
|
|
11819
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
11820
|
+
import { join as join15 } from "node:path";
|
|
11670
11821
|
|
|
11671
11822
|
// src/verify/typefloor.ts
|
|
11672
11823
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11922,6 +12073,16 @@ var INK_FLOOR = 15e-4;
|
|
|
11922
12073
|
var INK_DELTA = 12;
|
|
11923
12074
|
var FALLBACK_BAND_TOP = 0.22;
|
|
11924
12075
|
var CAPTION = ".headline, .eyebrow";
|
|
12076
|
+
var RESERVE_INK_TOLERANCE = 1e-4;
|
|
12077
|
+
function readReserve(timingText) {
|
|
12078
|
+
if (!timingText) return 0;
|
|
12079
|
+
try {
|
|
12080
|
+
const reserve = JSON.parse(timingText).captionReserve;
|
|
12081
|
+
return typeof reserve === "number" && reserve > 0 ? reserve : 0;
|
|
12082
|
+
} catch {
|
|
12083
|
+
return 0;
|
|
12084
|
+
}
|
|
12085
|
+
}
|
|
11925
12086
|
async function decodePng(png) {
|
|
11926
12087
|
const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
|
|
11927
12088
|
if (buf.length < 8 || buf.readUInt32BE(0) !== 2303741511) throw new Error("not a PNG");
|
|
@@ -12088,6 +12249,25 @@ function gradeFidelity(rows, floor = INK_FLOOR) {
|
|
|
12088
12249
|
};
|
|
12089
12250
|
});
|
|
12090
12251
|
}
|
|
12252
|
+
function gradeReserve(rows, tolerance = RESERVE_INK_TOLERANCE) {
|
|
12253
|
+
const dirty = /* @__PURE__ */ new Map();
|
|
12254
|
+
for (const row of rows) {
|
|
12255
|
+
if (row.reserveInk === void 0 || row.reserveInk <= tolerance) continue;
|
|
12256
|
+
const seen = dirty.get(row.sid);
|
|
12257
|
+
if (seen) seen.push(row);
|
|
12258
|
+
else dirty.set(row.sid, [row]);
|
|
12259
|
+
}
|
|
12260
|
+
const pct = (v) => `${(100 * v).toFixed(3)}%`;
|
|
12261
|
+
return [...dirty].map(([sid, stops]) => {
|
|
12262
|
+
const worst = stops.reduce((a, b) => (a.reserveInk ?? 0) >= (b.reserveInk ?? 0) ? a : b);
|
|
12263
|
+
return {
|
|
12264
|
+
severity: "error",
|
|
12265
|
+
gate: "fidelity",
|
|
12266
|
+
rule: "ink_in_caption_reserve",
|
|
12267
|
+
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(", ")}.`
|
|
12268
|
+
};
|
|
12269
|
+
});
|
|
12270
|
+
}
|
|
12091
12271
|
function captionBottom(sid, selector, fallbackPx) {
|
|
12092
12272
|
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
12093
12273
|
let bottom = 0;
|
|
@@ -12112,11 +12292,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
12112
12292
|
],
|
|
12113
12293
|
elapsedMs: Date.now() - started
|
|
12114
12294
|
});
|
|
12115
|
-
const
|
|
12116
|
-
|
|
12117
|
-
await readFile14(join14(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
12118
|
-
);
|
|
12295
|
+
const timingText = await readFile15(join15(dir, TIMING_FILE), "utf8").catch(() => null);
|
|
12296
|
+
const stops = opts.stops ?? readStops(timingText, await readFile15(join15(dir, DECK_PAGE), "utf8").catch(() => null));
|
|
12119
12297
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
12298
|
+
const reserve = opts.captionReserve ?? readReserve(timingText);
|
|
12120
12299
|
let deck = null;
|
|
12121
12300
|
try {
|
|
12122
12301
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
@@ -12145,7 +12324,13 @@ async function fidelity(dir, opts = {}) {
|
|
|
12145
12324
|
measured.push({
|
|
12146
12325
|
...stop,
|
|
12147
12326
|
ink: inkBelow(frame, bandTopPx),
|
|
12148
|
-
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
12327
|
+
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3,
|
|
12328
|
+
// THE SAME FRAME, a second strip. Free: it is one more pass over pixels
|
|
12329
|
+
// that are already decoded, so the gate that stops the caption
|
|
12330
|
+
// collision regressing costs no extra capture, no extra seek and no
|
|
12331
|
+
// extra browser. That matters — a gate with its own capture is a gate
|
|
12332
|
+
// someone turns off.
|
|
12333
|
+
...reserve > 0 ? { reserveInk: inkBelow(frame, height - reserve) } : {}
|
|
12149
12334
|
});
|
|
12150
12335
|
}
|
|
12151
12336
|
for (const mid of midpoints(stops)) {
|
|
@@ -12156,6 +12341,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
12156
12341
|
stops: measured,
|
|
12157
12342
|
findings: [
|
|
12158
12343
|
...gradeFidelity(measured, floor),
|
|
12344
|
+
...gradeReserve(measured),
|
|
12159
12345
|
...gradeOverprint(collided),
|
|
12160
12346
|
...gradeApparent(apparent)
|
|
12161
12347
|
],
|
|
@@ -12172,9 +12358,9 @@ async function fidelity(dir, opts = {}) {
|
|
|
12172
12358
|
// src/verify/drift.ts
|
|
12173
12359
|
import { execFile as execFile3 } from "node:child_process";
|
|
12174
12360
|
import { createHash as createHash9 } from "node:crypto";
|
|
12175
|
-
import { mkdtemp as mkdtemp3, readdir as
|
|
12361
|
+
import { mkdtemp as mkdtemp3, readdir as readdir3, readFile as readFile16, rm as rm8 } from "node:fs/promises";
|
|
12176
12362
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12177
|
-
import { join as
|
|
12363
|
+
import { join as join16 } from "node:path";
|
|
12178
12364
|
import { promisify as promisify3 } from "node:util";
|
|
12179
12365
|
var run3 = promisify3(execFile3);
|
|
12180
12366
|
var FLOOR_DB = 40;
|
|
@@ -12213,9 +12399,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
12213
12399
|
async function drift(dir, opts = {}) {
|
|
12214
12400
|
const mode = opts.mode ?? "psnr";
|
|
12215
12401
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
12216
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
12217
|
-
const a =
|
|
12218
|
-
const b =
|
|
12402
|
+
const work = opts.workDir ?? await mkdtemp3(join16(tmpdir4(), "decksmith-drift-"));
|
|
12403
|
+
const a = join16(work, "a");
|
|
12404
|
+
const b = join16(work, "b");
|
|
12219
12405
|
const report2 = await compare(dir, a, b, mode, floorDb, opts);
|
|
12220
12406
|
const keep = opts.keep || !report2.passed;
|
|
12221
12407
|
if (keep) report2.kept = { a, b };
|
|
@@ -12270,11 +12456,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
12270
12456
|
const differing = [];
|
|
12271
12457
|
const ha = [];
|
|
12272
12458
|
for (const [i, name] of fa.entries()) {
|
|
12273
|
-
const one = await sha(
|
|
12459
|
+
const one = await sha(join16(a, name));
|
|
12274
12460
|
ha.push(one);
|
|
12275
|
-
if (one !== await sha(
|
|
12461
|
+
if (one !== await sha(join16(b, name))) differing.push(i + 1);
|
|
12276
12462
|
}
|
|
12277
|
-
const html = await
|
|
12463
|
+
const html = await readFile16(join16(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
12278
12464
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
12279
12465
|
let worst;
|
|
12280
12466
|
if (differing.length > 0) {
|
|
@@ -12385,11 +12571,11 @@ async function render2(dir, out, opts) {
|
|
|
12385
12571
|
}
|
|
12386
12572
|
}
|
|
12387
12573
|
async function frames(dir) {
|
|
12388
|
-
const names = await
|
|
12574
|
+
const names = await readdir3(dir).catch(() => []);
|
|
12389
12575
|
return names.filter((n3) => n3.endsWith(".png")).sort();
|
|
12390
12576
|
}
|
|
12391
12577
|
async function sha(file) {
|
|
12392
|
-
return createHash9("sha256").update(await
|
|
12578
|
+
return createHash9("sha256").update(await readFile16(file)).digest("hex");
|
|
12393
12579
|
}
|
|
12394
12580
|
var STAT = /^n:(\d+)\b.*\bpsnr_avg:(inf|-?[\d.]+)/;
|
|
12395
12581
|
function framePattern(name) {
|
|
@@ -12422,11 +12608,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
12422
12608
|
"-start_number",
|
|
12423
12609
|
String(seq.start),
|
|
12424
12610
|
"-i",
|
|
12425
|
-
|
|
12611
|
+
join16(a, seq.pattern),
|
|
12426
12612
|
"-start_number",
|
|
12427
12613
|
String(seq.start),
|
|
12428
12614
|
"-i",
|
|
12429
|
-
|
|
12615
|
+
join16(b, seq.pattern),
|
|
12430
12616
|
"-lavfi",
|
|
12431
12617
|
"psnr=stats_file=-",
|
|
12432
12618
|
"-f",
|
|
@@ -12501,7 +12687,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12501
12687
|
const html = await readCompositions(dir);
|
|
12502
12688
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
12503
12689
|
const narration = scanNarration(
|
|
12504
|
-
await
|
|
12690
|
+
await readFile17(join17(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12505
12691
|
await listFiles(dir)
|
|
12506
12692
|
);
|
|
12507
12693
|
const budget3 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -12538,8 +12724,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12538
12724
|
}
|
|
12539
12725
|
async function declaredStops(dir) {
|
|
12540
12726
|
return readStops(
|
|
12541
|
-
await
|
|
12542
|
-
await
|
|
12727
|
+
await readFile17(join17(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12728
|
+
await readFile17(join17(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
12543
12729
|
);
|
|
12544
12730
|
}
|
|
12545
12731
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -12579,7 +12765,7 @@ function scanNarration(page, files) {
|
|
|
12579
12765
|
];
|
|
12580
12766
|
}
|
|
12581
12767
|
async function readTiming2(dir) {
|
|
12582
|
-
const raw2 = await
|
|
12768
|
+
const raw2 = await readFile17(join17(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
12583
12769
|
if (!raw2) return void 0;
|
|
12584
12770
|
try {
|
|
12585
12771
|
const parsed = JSON.parse(raw2);
|
|
@@ -12589,9 +12775,9 @@ async function readTiming2(dir) {
|
|
|
12589
12775
|
}
|
|
12590
12776
|
}
|
|
12591
12777
|
async function listFiles(dir) {
|
|
12592
|
-
const entries = await
|
|
12778
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12593
12779
|
return new Set(
|
|
12594
|
-
entries.filter((e) => e.isFile()).map((e) => relative(dir,
|
|
12780
|
+
entries.filter((e) => e.isFile()).map((e) => relative(dir, join17(e.parentPath, e.name)).split(sep2).join("/"))
|
|
12595
12781
|
);
|
|
12596
12782
|
}
|
|
12597
12783
|
var INSTEAD = {
|
|
@@ -12847,72 +13033,35 @@ function scanDeterminism(html, file) {
|
|
|
12847
13033
|
return findings;
|
|
12848
13034
|
}
|
|
12849
13035
|
async function readCompositions(dir) {
|
|
12850
|
-
const entries = await
|
|
13036
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12851
13037
|
const files = entries.filter(
|
|
12852
13038
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12853
|
-
).map((e) =>
|
|
13039
|
+
).map((e) => join17(e.parentPath, e.name));
|
|
12854
13040
|
return Promise.all(
|
|
12855
|
-
files.map(async (f) => [relative(dir, f), await
|
|
13041
|
+
files.map(async (f) => [relative(dir, f), await readFile17(f, "utf8")])
|
|
12856
13042
|
);
|
|
12857
13043
|
}
|
|
12858
13044
|
|
|
12859
13045
|
// src/cli.ts
|
|
12860
13046
|
guardTmpdir();
|
|
12861
|
-
var AUDIO_DIR = "audio";
|
|
12862
13047
|
var NARRATION_FILE = "narration.json";
|
|
12863
13048
|
var DEFAULTS = prefsSchema.parse({});
|
|
12864
13049
|
async function deckRuntime() {
|
|
12865
|
-
const path2 =
|
|
13050
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
12866
13051
|
try {
|
|
12867
|
-
return await
|
|
13052
|
+
return await readFile18(path2, "utf8");
|
|
12868
13053
|
} catch {
|
|
12869
13054
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
12870
13055
|
}
|
|
12871
13056
|
}
|
|
12872
13057
|
function playerBundle() {
|
|
12873
|
-
const require2 =
|
|
13058
|
+
const require2 = createRequire5(import.meta.url);
|
|
12874
13059
|
try {
|
|
12875
|
-
return
|
|
13060
|
+
return join18(dirname6(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12876
13061
|
} catch {
|
|
12877
13062
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12878
13063
|
}
|
|
12879
13064
|
}
|
|
12880
|
-
async function vendorKatex(out) {
|
|
12881
|
-
const require2 = createRequire4(import.meta.url);
|
|
12882
|
-
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12883
|
-
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12884
|
-
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12885
|
-
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12886
|
-
if (file.endsWith(".woff2"))
|
|
12887
|
-
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12888
|
-
}
|
|
12889
|
-
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12890
|
-
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12891
|
-
return kept ? `src:${kept}` : whole;
|
|
12892
|
-
});
|
|
12893
|
-
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12894
|
-
}
|
|
12895
|
-
async function vendorScripts(out, composition) {
|
|
12896
|
-
const require2 = createRequire4(import.meta.url);
|
|
12897
|
-
await mkdir10(join17(out, "vendor"), { recursive: true });
|
|
12898
|
-
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
12899
|
-
for (const [pkg, rel, name] of [
|
|
12900
|
-
["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
|
|
12901
|
-
["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
|
|
12902
|
-
["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
|
|
12903
|
-
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
12904
|
-
]) {
|
|
12905
|
-
if (!wanted(name)) continue;
|
|
12906
|
-
const from = join17(dirname5(require2.resolve(pkg)), rel);
|
|
12907
|
-
await cp(from, join17(out, "vendor", name));
|
|
12908
|
-
}
|
|
12909
|
-
if (wanted("ds-morph.js")) {
|
|
12910
|
-
await cp(
|
|
12911
|
-
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
12912
|
-
join17(out, "vendor", "ds-morph.js")
|
|
12913
|
-
);
|
|
12914
|
-
}
|
|
12915
|
-
}
|
|
12916
13065
|
var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
12917
13066
|
{
|
|
12918
13067
|
$schema: "https://hyperframes.heygen.com/schema/hyperframes.json",
|
|
@@ -13014,13 +13163,13 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
13014
13163
|
"--max-clip-seconds <n>",
|
|
13015
13164
|
"URL only: seconds of each clip kept, the rest trimmed (maxClipSeconds; default 60)"
|
|
13016
13165
|
).option("--no-transcode", "URL only: ship each clip as the page served it, unshrunk").action(async (input, o) => {
|
|
13017
|
-
const assets =
|
|
13018
|
-
const scratch = url(input) ? await mkdtemp4(
|
|
13166
|
+
const assets = join18(dirname6(resolve7(o.out)), "assets");
|
|
13167
|
+
const scratch = url(input) ? await mkdtemp4(join18(tmpdir5(), "decksmith-harvest-")) : void 0;
|
|
13019
13168
|
try {
|
|
13020
13169
|
let md;
|
|
13021
13170
|
let clips = [];
|
|
13022
13171
|
if (scratch === void 0) {
|
|
13023
|
-
md = await
|
|
13172
|
+
md = await readFile18(resolve7(input), "utf8").catch(() => {
|
|
13024
13173
|
throw new Error(`Cannot read ${input}.`);
|
|
13025
13174
|
});
|
|
13026
13175
|
} else {
|
|
@@ -13041,7 +13190,7 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
13041
13190
|
const dropped = [];
|
|
13042
13191
|
const source = await fetchFigures(withClips, assets, dropped);
|
|
13043
13192
|
for (const why3 of dropped) step(`ingest: ${why3}`);
|
|
13044
|
-
const bundle = await bundleFont(source.lang, glyphs(source),
|
|
13193
|
+
const bundle = await bundleFont(source.lang, glyphs(source), join18(assets, "fonts"));
|
|
13045
13194
|
if (bundle) step(`ingest: bundled ${bundle.family} for ${source.lang}`);
|
|
13046
13195
|
await writeJson(o.out, source);
|
|
13047
13196
|
step(`ingest: wrote ${o.out}`);
|
|
@@ -13102,7 +13251,7 @@ imageFlags(
|
|
|
13102
13251
|
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")
|
|
13103
13252
|
).action(async (sbPath, o) => {
|
|
13104
13253
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
13105
|
-
const sourcePath =
|
|
13254
|
+
const sourcePath = resolve7(String(o.source));
|
|
13106
13255
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
13107
13256
|
assertRefsResolve(storyboard, source, { pending: "allow" });
|
|
13108
13257
|
const prefs = await loadPrefs(prefsFromFlags({ ...flags(o), images: true }));
|
|
@@ -13116,7 +13265,7 @@ imageFlags(
|
|
|
13116
13265
|
);
|
|
13117
13266
|
const drawn = await illustrate(storyboard, source, {
|
|
13118
13267
|
prefs,
|
|
13119
|
-
assetsDir:
|
|
13268
|
+
assetsDir: join18(dirname6(sourcePath), "assets"),
|
|
13120
13269
|
onStep: step
|
|
13121
13270
|
});
|
|
13122
13271
|
for (const p of drawn.illustrated) {
|
|
@@ -13141,34 +13290,44 @@ voiceFlags(
|
|
|
13141
13290
|
const format = pickFormat(String(o.format), o.width, o.height);
|
|
13142
13291
|
const chosen = await loadPrefs(prefsFromFlags(flags(o)));
|
|
13143
13292
|
const prefs = stated(chosen, "lang") ? chosen : { ...chosen, lang: storyboard.lang };
|
|
13144
|
-
const dir =
|
|
13293
|
+
const dir = resolve7(String(o.out));
|
|
13145
13294
|
const speaking = storyboard.beats.filter((b) => b.narration?.trim()).length;
|
|
13146
13295
|
if (speaking === 0) {
|
|
13147
13296
|
throw new Error(`No beat in ${sbPath} has a "narration" field, so there is nothing to speak.`);
|
|
13148
13297
|
}
|
|
13149
13298
|
step(`narrate: ${speaking} of ${storyboard.beats.length} beats have narration`);
|
|
13150
13299
|
const narration = await narrate(storyboard, source, prefs, { dir, format });
|
|
13151
|
-
await writeJson(
|
|
13300
|
+
await writeJson(join18(dir, NARRATION_FILE), narration);
|
|
13152
13301
|
const segments2 = Object.values(narration.beats).flat();
|
|
13153
13302
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
13154
13303
|
step(
|
|
13155
|
-
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${
|
|
13304
|
+
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${join18(dir, NARRATION_FILE)}`
|
|
13156
13305
|
);
|
|
13157
13306
|
});
|
|
13158
13307
|
lookFlags(
|
|
13159
13308
|
sizeFlags(
|
|
13160
|
-
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(
|
|
13309
|
+
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
|
+
"--reserve-captions",
|
|
13311
|
+
"keep the bottom of the frame clear for a burned caption \u2014 required by `render --subtitles burn`"
|
|
13312
|
+
).option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser")
|
|
13161
13313
|
)
|
|
13162
13314
|
).action(
|
|
13163
13315
|
async (sbPath, o) => {
|
|
13164
13316
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
13165
13317
|
const source = await readValidated(o.source, sourceSchema, "source");
|
|
13166
13318
|
assertRefsResolve(storyboard, source);
|
|
13167
|
-
const format =
|
|
13319
|
+
const format = withCaptionReserve(
|
|
13320
|
+
withMinWeight(pickFormat(o.format, o.width, o.height), o.minWeight),
|
|
13321
|
+
o.reserveCaptions === true
|
|
13322
|
+
);
|
|
13323
|
+
if (format.captionReserve)
|
|
13324
|
+
step(
|
|
13325
|
+
`build: reserving ${format.captionReserve}px at the bottom for burned captions \u2014 ${(100 * format.captionReserve / format.height).toFixed(1)}% of the frame`
|
|
13326
|
+
);
|
|
13168
13327
|
const prefs = await loadPrefs(prefsFromFlags(flags(o)), process.cwd(), source);
|
|
13169
13328
|
const theme = stated(prefs, "theme") ?? storyboard.theme;
|
|
13170
|
-
const out =
|
|
13171
|
-
await
|
|
13329
|
+
const out = resolve7(o.out);
|
|
13330
|
+
await mkdir11(out, { recursive: true });
|
|
13172
13331
|
const found = await findNarration(sbPath, o.narration);
|
|
13173
13332
|
const narration = found ? await loadNarration(found) : void 0;
|
|
13174
13333
|
if (narration) {
|
|
@@ -13181,7 +13340,7 @@ lookFlags(
|
|
|
13181
13340
|
const paced = durationPlan(prefs, storyboard.beats.length);
|
|
13182
13341
|
for (const f of scanBeatCount(storyboard, prefs)) step(`build: ${f.message}`);
|
|
13183
13342
|
for (const w of paced.warnings) step(`build: ${w}`);
|
|
13184
|
-
const fontCss = await refreshFont(storyboard, source, out);
|
|
13343
|
+
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
13185
13344
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
13186
13345
|
theme,
|
|
13187
13346
|
...fontCss ? { fontCss } : {},
|
|
@@ -13198,8 +13357,8 @@ lookFlags(
|
|
|
13198
13357
|
// place anyone learns it is not.
|
|
13199
13358
|
onBeatWarning: (id2, warning) => step(`build: kept ${id2} \u2014 ${warning}`)
|
|
13200
13359
|
});
|
|
13201
|
-
await
|
|
13202
|
-
await
|
|
13360
|
+
await writeFile13(join18(out, "index.html"), deck.composition);
|
|
13361
|
+
await writeFile13(join18(out, "hyperframes.json"), HYPERFRAMES_JSON);
|
|
13203
13362
|
await writeTiming(out, {
|
|
13204
13363
|
storyboard,
|
|
13205
13364
|
source,
|
|
@@ -13215,36 +13374,36 @@ lookFlags(
|
|
|
13215
13374
|
...narration ? { narration } : {}
|
|
13216
13375
|
});
|
|
13217
13376
|
if (deck.page) {
|
|
13218
|
-
await
|
|
13219
|
-
await
|
|
13377
|
+
await writeFile13(join18(out, DECK_PAGE), deck.page);
|
|
13378
|
+
await cp2(playerBundle(), join18(out, PLAYER_FILE));
|
|
13220
13379
|
}
|
|
13221
13380
|
await vendorKatex(out);
|
|
13222
13381
|
await vendorScripts(out, deck.composition);
|
|
13223
|
-
await copyAssets(
|
|
13224
|
-
if (found && narration) await copyAudio(
|
|
13382
|
+
await copyAssets(dirname6(resolve7(o.source)), out, source.figures, step);
|
|
13383
|
+
if (found && narration) await copyAudio(dirname6(found), narration, out, step);
|
|
13225
13384
|
const look = [theme, paced.speed === 1 ? "" : `${paced.speed}\xD7 speed`].filter(Boolean).join(", ");
|
|
13226
13385
|
const { cut } = deck;
|
|
13227
13386
|
const floor = cut.dropped.filter((d) => d.rule === "below_min_weight").length;
|
|
13228
13387
|
const of = cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
13229
13388
|
step(
|
|
13230
|
-
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${
|
|
13389
|
+
`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")}`
|
|
13231
13390
|
);
|
|
13232
13391
|
for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
|
|
13233
13392
|
step(`build: ${f.message}`);
|
|
13234
13393
|
reportCut(cut);
|
|
13235
|
-
if (deck.page) step(`build: navigable deck \u2192 ${
|
|
13394
|
+
if (deck.page) step(`build: navigable deck \u2192 ${join18(out, DECK_PAGE)}`);
|
|
13236
13395
|
await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
|
|
13237
13396
|
}
|
|
13238
13397
|
);
|
|
13239
13398
|
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) => {
|
|
13240
|
-
await gate(
|
|
13399
|
+
await gate(resolve7(dir), o.snapshots, void 0, void 0, o.fidelity !== false);
|
|
13241
13400
|
});
|
|
13242
13401
|
program.command("frames").description("Write PNGs of a built deck through the capture path the renderer uses.").argument("<dir>", "a built deck directory").option(
|
|
13243
13402
|
"--at <seconds...>",
|
|
13244
13403
|
"absolute times to photograph; defaults to every hold the deck declares"
|
|
13245
13404
|
).option("--out <dir>", "where to write the PNGs (default: <dir>/frames)").action(async (dir, o) => {
|
|
13246
|
-
const deck =
|
|
13247
|
-
const out =
|
|
13405
|
+
const deck = resolve7(dir);
|
|
13406
|
+
const out = resolve7(o.out ?? join18(deck, "frames"));
|
|
13248
13407
|
let times;
|
|
13249
13408
|
if (o.at?.length) {
|
|
13250
13409
|
times = o.at.map((raw2) => {
|
|
@@ -13254,8 +13413,8 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
13254
13413
|
});
|
|
13255
13414
|
} else {
|
|
13256
13415
|
times = readStops(
|
|
13257
|
-
await
|
|
13258
|
-
await
|
|
13416
|
+
await readFile18(join18(deck, TIMING_FILE), "utf8").catch(() => null),
|
|
13417
|
+
await readFile18(join18(deck, DECK_PAGE), "utf8").catch(() => null)
|
|
13259
13418
|
).map((s) => s.t);
|
|
13260
13419
|
if (times.length === 0)
|
|
13261
13420
|
throw new Error(
|
|
@@ -13269,7 +13428,7 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
13269
13428
|
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(
|
|
13270
13429
|
async (dir, o) => {
|
|
13271
13430
|
step("drift: rendering twice, several minutes a render");
|
|
13272
|
-
const verdict = await drift(
|
|
13431
|
+
const verdict = await drift(resolve7(dir), {
|
|
13273
13432
|
mode: o.identical ? "identical" : "psnr",
|
|
13274
13433
|
floorDb: Number(o.floor),
|
|
13275
13434
|
...o.keep ? { keep: true } : {},
|
|
@@ -13296,8 +13455,8 @@ program.command("render").description("Render a built deck to a finished video:
|
|
|
13296
13455
|
throw new Error(`Unknown --subtitles "${o.subtitles}". Use sidecar, burn or none.`);
|
|
13297
13456
|
}
|
|
13298
13457
|
const result = await render({
|
|
13299
|
-
deck:
|
|
13300
|
-
out:
|
|
13458
|
+
deck: resolve7(dir),
|
|
13459
|
+
out: resolve7(o.out),
|
|
13301
13460
|
subtitles: o.subtitles,
|
|
13302
13461
|
protocolTimeoutMs: Number(o.protocolTimeout),
|
|
13303
13462
|
log: step,
|
|
@@ -13325,7 +13484,7 @@ voiceFlags(
|
|
|
13325
13484
|
)
|
|
13326
13485
|
).action(async (sbPath, o) => {
|
|
13327
13486
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
13328
|
-
const sourcePath =
|
|
13487
|
+
const sourcePath = resolve7(String(o.source));
|
|
13329
13488
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
13330
13489
|
assertRefsResolve(storyboard, source);
|
|
13331
13490
|
if (o.bake && o.link) throw new Error("Choose --bake or --link, not both.");
|
|
@@ -13340,7 +13499,7 @@ voiceFlags(
|
|
|
13340
13499
|
const prefer = o.link ? "link" : "bake";
|
|
13341
13500
|
const plan = await planMedia(figureAssets(source, sourcePath, prefer));
|
|
13342
13501
|
const files = { ...plan.files };
|
|
13343
|
-
if (found && narration) Object.assign(files, await audioFiles(
|
|
13502
|
+
if (found && narration) Object.assign(files, await audioFiles(dirname6(found), narration));
|
|
13344
13503
|
const pack2 = {
|
|
13345
13504
|
version: PACK_VERSION,
|
|
13346
13505
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13355,7 +13514,7 @@ voiceFlags(
|
|
|
13355
13514
|
...narration ? { narration } : {},
|
|
13356
13515
|
media: plan.media
|
|
13357
13516
|
};
|
|
13358
|
-
const out =
|
|
13517
|
+
const out = resolve7(String(o.out));
|
|
13359
13518
|
const bytes = await writePack(pack2, files, out);
|
|
13360
13519
|
if (plan.demoted.length) {
|
|
13361
13520
|
step(`pack: kept as links, not bakeable \u2014 ${plan.demoted.join(", ")}`);
|
|
@@ -13369,19 +13528,19 @@ voiceFlags(
|
|
|
13369
13528
|
step(`pack: ${size2(bytes)} \u2192 ${out}`);
|
|
13370
13529
|
});
|
|
13371
13530
|
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) => {
|
|
13372
|
-
const { pack: pack2, files } = await readPack(
|
|
13373
|
-
const out =
|
|
13374
|
-
await writeJson(
|
|
13375
|
-
await writeJson(
|
|
13376
|
-
await writeJson(
|
|
13377
|
-
if (pack2.narration) await writeJson(
|
|
13531
|
+
const { pack: pack2, files } = await readPack(resolve7(file));
|
|
13532
|
+
const out = resolve7(o.out);
|
|
13533
|
+
await writeJson(join18(out, "source.json"), pack2.source);
|
|
13534
|
+
await writeJson(join18(out, "storyboard.json"), pack2.storyboard);
|
|
13535
|
+
await writeJson(join18(out, "decksmith.config.json"), pack2.prefs);
|
|
13536
|
+
if (pack2.narration) await writeJson(join18(out, AUDIO_DIR, NARRATION_FILE), pack2.narration);
|
|
13378
13537
|
const figures = new Map(pack2.source.figures.map((f) => [f.id, f.src]));
|
|
13379
13538
|
for (const [path2, bytes] of Object.entries(files)) {
|
|
13380
13539
|
const media = pack2.media.find((m) => m.path === path2);
|
|
13381
13540
|
const src = media && figures.get(media.id);
|
|
13382
|
-
const to = src ?
|
|
13383
|
-
await
|
|
13384
|
-
await
|
|
13541
|
+
const to = src ? join18("assets", src) : path2;
|
|
13542
|
+
await mkdir11(dirname6(join18(out, to)), { recursive: true });
|
|
13543
|
+
await writeFile13(join18(out, to), bytes);
|
|
13385
13544
|
}
|
|
13386
13545
|
const linked = pack2.media.filter((m) => m.policy !== "bake");
|
|
13387
13546
|
step(`unpack: "${pack2.title}", ${pack2.storyboard.beats.length} beats \u2192 ${out}`);
|
|
@@ -13391,7 +13550,7 @@ program.command("unpack").description("Open a .deck back into the files build re
|
|
|
13391
13550
|
);
|
|
13392
13551
|
}
|
|
13393
13552
|
step(
|
|
13394
|
-
`unpack: decksmith build ${
|
|
13553
|
+
`unpack: decksmith build ${join18(relative2(process.cwd(), out) || ".", "storyboard.json")} --source ${join18(relative2(process.cwd(), out) || ".", "source.json")} -o deck`
|
|
13395
13554
|
);
|
|
13396
13555
|
});
|
|
13397
13556
|
program.parseAsync(process.argv).catch((err) => {
|
|
@@ -13412,7 +13571,7 @@ async function gate(dir, snapshots = false, storyboard, kept, fidelity2 = true,
|
|
|
13412
13571
|
);
|
|
13413
13572
|
const verdict = await verify(dir, { snapshots, fidelity: fidelity2 }, storyboard, kept, source);
|
|
13414
13573
|
process.stdout.write(report(verdict));
|
|
13415
|
-
if (snapshots) step(`verify: snapshots in ${
|
|
13574
|
+
if (snapshots) step(`verify: snapshots in ${join18(dir, "snapshots")}`);
|
|
13416
13575
|
if (!verdict.passed) process.exitCode = 1;
|
|
13417
13576
|
}
|
|
13418
13577
|
function report(v) {
|
|
@@ -13429,13 +13588,13 @@ function report(v) {
|
|
|
13429
13588
|
async function findNarration(sbPath, flag) {
|
|
13430
13589
|
if (flag === false) return void 0;
|
|
13431
13590
|
if (typeof flag === "string") {
|
|
13432
|
-
const path2 =
|
|
13433
|
-
if (!await
|
|
13591
|
+
const path2 = resolve7(flag);
|
|
13592
|
+
if (!await stat3(path2).catch(() => null)) throw new Error(`Cannot read narration ${flag}.`);
|
|
13434
13593
|
return path2;
|
|
13435
13594
|
}
|
|
13436
|
-
const beside =
|
|
13437
|
-
for (const candidate of [
|
|
13438
|
-
if (await
|
|
13595
|
+
const beside = dirname6(resolve7(sbPath));
|
|
13596
|
+
for (const candidate of [join18(beside, AUDIO_DIR, NARRATION_FILE), join18(beside, NARRATION_FILE)]) {
|
|
13597
|
+
if (await stat3(candidate).catch(() => null)) {
|
|
13439
13598
|
step(`narration: using ${candidate}`);
|
|
13440
13599
|
return candidate;
|
|
13441
13600
|
}
|
|
@@ -13446,28 +13605,10 @@ async function loadNarration(path2) {
|
|
|
13446
13605
|
const narration = await readValidated(path2, narrationSchema, "narration");
|
|
13447
13606
|
return { voice: narration.voice, dir: AUDIO_DIR, beats: narration.beats };
|
|
13448
13607
|
}
|
|
13449
|
-
function audioNames(narration) {
|
|
13450
|
-
return [
|
|
13451
|
-
...new Set(
|
|
13452
|
-
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
13453
|
-
)
|
|
13454
|
-
].sort();
|
|
13455
|
-
}
|
|
13456
|
-
async function copyAudio(from, narration, out) {
|
|
13457
|
-
const dir = join17(out, AUDIO_DIR);
|
|
13458
|
-
await mkdir10(dir, { recursive: true });
|
|
13459
|
-
const names = audioNames(narration);
|
|
13460
|
-
for (const name of names) {
|
|
13461
|
-
await cp(join17(from, name), join17(dir, name)).catch(() => {
|
|
13462
|
-
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13463
|
-
});
|
|
13464
|
-
}
|
|
13465
|
-
step(`narration: ${names.length} audio file(s) \u2192 ${dir}`);
|
|
13466
|
-
}
|
|
13467
13608
|
async function audioFiles(from, narration) {
|
|
13468
13609
|
const files = {};
|
|
13469
13610
|
for (const name of audioNames(narration)) {
|
|
13470
|
-
const bytes = await
|
|
13611
|
+
const bytes = await readFile18(join18(from, name)).catch(() => {
|
|
13471
13612
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13472
13613
|
});
|
|
13473
13614
|
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(bytes);
|
|
@@ -13489,9 +13630,9 @@ function wrapScript(text2, width) {
|
|
|
13489
13630
|
async function writeTiming(out, input) {
|
|
13490
13631
|
try {
|
|
13491
13632
|
const timing = planTiming(input);
|
|
13492
|
-
await writeJson(
|
|
13633
|
+
await writeJson(join18(out, TIMING_FILE), timing);
|
|
13493
13634
|
step(
|
|
13494
|
-
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${
|
|
13635
|
+
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${join18(out, TIMING_FILE)}`
|
|
13495
13636
|
);
|
|
13496
13637
|
} catch (err) {
|
|
13497
13638
|
step(
|
|
@@ -13500,10 +13641,10 @@ async function writeTiming(out, input) {
|
|
|
13500
13641
|
}
|
|
13501
13642
|
}
|
|
13502
13643
|
function figureAssets(source, sourcePath, prefer) {
|
|
13503
|
-
const assets =
|
|
13644
|
+
const assets = join18(dirname6(sourcePath), "assets");
|
|
13504
13645
|
return source.figures.map((f) => ({
|
|
13505
13646
|
id: f.id,
|
|
13506
|
-
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src :
|
|
13647
|
+
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src : join18(assets, f.src),
|
|
13507
13648
|
prefer
|
|
13508
13649
|
}));
|
|
13509
13650
|
}
|
|
@@ -13521,6 +13662,10 @@ function pickFormat(id2, width, height) {
|
|
|
13521
13662
|
for (const warning of canvasWarnings(format.width, format.height)) step(`format: ${warning}`);
|
|
13522
13663
|
return format;
|
|
13523
13664
|
}
|
|
13665
|
+
function withCaptionReserve(format, on) {
|
|
13666
|
+
if (!on) return format;
|
|
13667
|
+
return { ...format, captionReserve: bandReserve(format.width, format.height) };
|
|
13668
|
+
}
|
|
13524
13669
|
function sizeFlags(cmd) {
|
|
13525
13670
|
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})`);
|
|
13526
13671
|
}
|
|
@@ -13531,52 +13676,11 @@ function withMinWeight(format, raw2) {
|
|
|
13531
13676
|
throw new Error(`--min-weight takes a number from 0 to 1, not "${raw2}".`);
|
|
13532
13677
|
return { ...format, minWeight };
|
|
13533
13678
|
}
|
|
13534
|
-
async function copyAssets(sourceDir, out, figures) {
|
|
13535
|
-
const from = join17(sourceDir, "assets");
|
|
13536
|
-
if (!await stat2(from).catch(() => null)) {
|
|
13537
|
-
step(`build: no assets/ beside source.json, skipping`);
|
|
13538
|
-
return;
|
|
13539
|
-
}
|
|
13540
|
-
const wanted = new Set(
|
|
13541
|
-
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
13542
|
-
);
|
|
13543
|
-
await mkdir10(join17(out, "assets"), { recursive: true });
|
|
13544
|
-
let copied = 0;
|
|
13545
|
-
for (const name of wanted) {
|
|
13546
|
-
const src = resolve6(join17(from, name));
|
|
13547
|
-
if (!src.startsWith(`${resolve6(from)}/`)) continue;
|
|
13548
|
-
if (!await stat2(src).catch(() => null)) continue;
|
|
13549
|
-
await mkdir10(dirname5(join17(out, "assets", name)), { recursive: true });
|
|
13550
|
-
await cp(src, join17(out, "assets", name));
|
|
13551
|
-
copied++;
|
|
13552
|
-
}
|
|
13553
|
-
const fonts = join17(from, "fonts");
|
|
13554
|
-
if (await stat2(fonts).catch(() => null)) {
|
|
13555
|
-
await cp(fonts, join17(out, "assets", "fonts"), { recursive: true });
|
|
13556
|
-
}
|
|
13557
|
-
step(`build: copied ${copied} referenced figure(s)`);
|
|
13558
|
-
}
|
|
13559
|
-
async function refreshFont(storyboard, source, out) {
|
|
13560
|
-
try {
|
|
13561
|
-
const bundle = await bundleFont(
|
|
13562
|
-
storyboard.lang,
|
|
13563
|
-
glyphs(source) + glyphs(storyboard),
|
|
13564
|
-
join17(out, "assets", "fonts")
|
|
13565
|
-
);
|
|
13566
|
-
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
13567
|
-
return bundle?.css;
|
|
13568
|
-
} catch (err) {
|
|
13569
|
-
step(
|
|
13570
|
-
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
13571
|
-
);
|
|
13572
|
-
return void 0;
|
|
13573
|
-
}
|
|
13574
|
-
}
|
|
13575
13679
|
function glyphs(value) {
|
|
13576
13680
|
return JSON.stringify(value);
|
|
13577
13681
|
}
|
|
13578
13682
|
async function readValidated(file, schema, label) {
|
|
13579
|
-
const text2 = await
|
|
13683
|
+
const text2 = await readFile18(resolve7(file), "utf8").catch(() => {
|
|
13580
13684
|
throw new Error(`Cannot read ${label} file ${file}.`);
|
|
13581
13685
|
});
|
|
13582
13686
|
let json;
|
|
@@ -13594,9 +13698,9 @@ ${issues}`);
|
|
|
13594
13698
|
return parsed.data;
|
|
13595
13699
|
}
|
|
13596
13700
|
async function writeJson(file, value) {
|
|
13597
|
-
const path2 =
|
|
13598
|
-
await
|
|
13599
|
-
await
|
|
13701
|
+
const path2 = resolve7(file);
|
|
13702
|
+
await mkdir11(dirname6(path2), { recursive: true });
|
|
13703
|
+
await writeFile13(path2, `${JSON.stringify(value, null, 2)}
|
|
13600
13704
|
`);
|
|
13601
13705
|
}
|
|
13602
13706
|
function size2(bytes) {
|