@jokerized/decksmith 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/cli.js +529 -388
- package/dist/deck-runtime.js +5 -2
- package/dist/index.js +490 -316
- package/dist/mcp.js +409 -307
- package/dist/types/build/files.d.ts +102 -0
- package/dist/types/deck/runtime.d.ts +4 -2
- package/dist/types/emit/kit.d.ts +32 -0
- package/dist/types/index.d.ts +10 -6
- package/dist/types/render/capture.d.ts +27 -1
- package/dist/types/render/render.d.ts +40 -12
- package/dist/types/render/timing.d.ts +11 -0
- package/dist/types/types.d.ts +64 -0
- package/dist/types/verify/fidelity.d.ts +55 -0
- package/dist/types/verify/index.d.ts +8 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,176 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { cp
|
|
3
|
-
import { createRequire as
|
|
4
|
-
import { dirname as
|
|
2
|
+
import { cp as cp2, mkdir as mkdir11, readFile as readFile18, writeFile as writeFile13 } from "node:fs/promises";
|
|
3
|
+
import { createRequire as createRequire5 } from "node:module";
|
|
4
|
+
import { dirname as dirname6, join as join18, resolve as resolve7 } from "node:path";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
|
+
|
|
7
|
+
// src/build/files.ts
|
|
8
|
+
import { cp, mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
5
11
|
import { fileURLToPath } from "node:url";
|
|
6
12
|
|
|
7
|
-
// src/
|
|
13
|
+
// src/source/fonts.ts
|
|
8
14
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
function familyFor(lang) {
|
|
18
|
+
const tag = lang.toLowerCase();
|
|
19
|
+
if (tag.startsWith("ko")) return "Noto Sans KR";
|
|
20
|
+
if (tag.startsWith("ja")) return "Noto Sans JP";
|
|
21
|
+
if (tag.startsWith("zh")) return /hant|-tw|-hk|-mo/.test(tag) ? "Noto Sans TC" : "Noto Sans SC";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
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";
|
|
25
|
+
async function bundleFont(lang, glyphs, dir) {
|
|
26
|
+
const family = familyFor(lang);
|
|
27
|
+
if (!family) return null;
|
|
28
|
+
const text2 = [...new Set(glyphs)].filter((c) => c > " ").sort().join("");
|
|
29
|
+
const stamp = `/* decksmith ${createHash("sha256").update(`${family}
|
|
30
|
+
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
31
|
+
await mkdir(dir, { recursive: true });
|
|
32
|
+
const cssPath = join(dir, "fonts.css");
|
|
33
|
+
const cached2 = await readFile(cssPath, "utf8").catch(() => "");
|
|
34
|
+
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
35
|
+
const res = await fetch(
|
|
36
|
+
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
37
|
+
{ headers: { "User-Agent": UA } }
|
|
38
|
+
);
|
|
39
|
+
if (!res.ok) throw new Error(`google fonts: HTTP ${res.status}`);
|
|
40
|
+
let css = await res.text();
|
|
41
|
+
const remote = [...new Set([...css.matchAll(/url\((https:[^)]+)\)/g)].map((m) => m[1] ?? ""))];
|
|
42
|
+
const slug = family.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
43
|
+
const files = [];
|
|
44
|
+
for (const [i, url] of remote.entries()) {
|
|
45
|
+
const font = await fetch(url);
|
|
46
|
+
if (!font.ok) throw new Error(`${url}: HTTP ${font.status}`);
|
|
47
|
+
const name = `${slug}-${i}.woff2`;
|
|
48
|
+
await writeFile(join(dir, name), Buffer.from(await font.arrayBuffer()));
|
|
49
|
+
css = css.replaceAll(url, name);
|
|
50
|
+
files.push(name);
|
|
51
|
+
}
|
|
52
|
+
css = `${stamp}
|
|
53
|
+
${css}`;
|
|
54
|
+
await writeFile(cssPath, css);
|
|
55
|
+
return { family, css, files };
|
|
56
|
+
}
|
|
57
|
+
function localNames(css) {
|
|
58
|
+
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/build/files.ts
|
|
62
|
+
function audioNames(narration) {
|
|
63
|
+
return [
|
|
64
|
+
...new Set(
|
|
65
|
+
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
66
|
+
)
|
|
67
|
+
].sort();
|
|
68
|
+
}
|
|
69
|
+
async function vendorKatex(out) {
|
|
70
|
+
const require2 = createRequire(import.meta.url);
|
|
71
|
+
const dist = join2(dirname(require2.resolve("katex/package.json")), "dist");
|
|
72
|
+
const css = await readFile2(join2(dist, "katex.min.css"), "utf8");
|
|
73
|
+
const written = [];
|
|
74
|
+
await mkdir2(join2(out, "katex/fonts"), { recursive: true });
|
|
75
|
+
for (const file of await readdir(join2(dist, "fonts"))) {
|
|
76
|
+
if (!file.endsWith(".woff2")) continue;
|
|
77
|
+
await cp(join2(dist, "fonts", file), join2(out, "katex/fonts", file));
|
|
78
|
+
written.push(join2(out, "katex/fonts", file));
|
|
79
|
+
}
|
|
80
|
+
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
81
|
+
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
82
|
+
return kept ? `src:${kept}` : whole;
|
|
83
|
+
});
|
|
84
|
+
await writeFile2(join2(out, "katex/katex.min.css"), woff2Only);
|
|
85
|
+
written.push(join2(out, "katex/katex.min.css"));
|
|
86
|
+
return written;
|
|
87
|
+
}
|
|
88
|
+
async function vendorScripts(out, composition) {
|
|
89
|
+
const require2 = createRequire(import.meta.url);
|
|
90
|
+
await mkdir2(join2(out, "vendor"), { recursive: true });
|
|
91
|
+
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
92
|
+
const written = [];
|
|
93
|
+
for (const [pkg, rel, name] of [
|
|
94
|
+
["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
|
|
95
|
+
["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
|
|
96
|
+
["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
|
|
97
|
+
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
98
|
+
]) {
|
|
99
|
+
if (!wanted(name)) continue;
|
|
100
|
+
const from = join2(dirname(require2.resolve(pkg)), rel);
|
|
101
|
+
await cp(from, join2(out, "vendor", name));
|
|
102
|
+
written.push(join2(out, "vendor", name));
|
|
103
|
+
}
|
|
104
|
+
if (wanted("ds-morph.js")) {
|
|
105
|
+
await cp(
|
|
106
|
+
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
107
|
+
join2(out, "vendor", "ds-morph.js")
|
|
108
|
+
);
|
|
109
|
+
written.push(join2(out, "vendor", "ds-morph.js"));
|
|
110
|
+
}
|
|
111
|
+
return written;
|
|
112
|
+
}
|
|
113
|
+
async function copyAssets(sourceDir, out, figures, step) {
|
|
114
|
+
const from = join2(resolve(sourceDir), "assets");
|
|
115
|
+
if (!await stat(from).catch(() => null)) {
|
|
116
|
+
step(`build: no assets/ beside source.json, skipping`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const wanted = new Set(
|
|
120
|
+
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
121
|
+
);
|
|
122
|
+
await mkdir2(join2(out, "assets"), { recursive: true });
|
|
123
|
+
const written = [];
|
|
124
|
+
let copied = 0;
|
|
125
|
+
for (const name of wanted) {
|
|
126
|
+
const src = resolve(join2(from, name));
|
|
127
|
+
if (!src.startsWith(`${from}/`)) continue;
|
|
128
|
+
if (!await stat(src).catch(() => null)) continue;
|
|
129
|
+
await mkdir2(dirname(join2(out, "assets", name)), { recursive: true });
|
|
130
|
+
await cp(src, join2(out, "assets", name));
|
|
131
|
+
written.push(join2(out, "assets", name));
|
|
132
|
+
copied++;
|
|
133
|
+
}
|
|
134
|
+
const fonts = join2(from, "fonts");
|
|
135
|
+
if (await stat(fonts).catch(() => null)) {
|
|
136
|
+
await cp(fonts, join2(out, "assets", "fonts"), { recursive: true });
|
|
137
|
+
written.push(join2(out, "assets", "fonts"));
|
|
138
|
+
}
|
|
139
|
+
step(`build: copied ${copied} referenced figure(s)`);
|
|
140
|
+
return written;
|
|
141
|
+
}
|
|
142
|
+
async function copyAudio(from, narration, out, step) {
|
|
143
|
+
const dir = join2(out, narration.dir);
|
|
144
|
+
await mkdir2(dir, { recursive: true });
|
|
145
|
+
const names = audioNames(narration);
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
await cp(join2(resolve(from), name), join2(dir, name)).catch(() => {
|
|
148
|
+
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
step(`narration: ${names.length} audio file(s) \u2192 ${dir}`);
|
|
152
|
+
return names.map((n3) => join2(dir, n3));
|
|
153
|
+
}
|
|
154
|
+
async function refreshFont(storyboard, source, out, step) {
|
|
155
|
+
try {
|
|
156
|
+
const bundle = await bundleFont(
|
|
157
|
+
storyboard.lang,
|
|
158
|
+
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
159
|
+
join2(out, "assets", "fonts")
|
|
160
|
+
);
|
|
161
|
+
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
162
|
+
return bundle?.css;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
step(
|
|
165
|
+
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
166
|
+
);
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/pack/media.ts
|
|
172
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
173
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
10
174
|
import { extname } from "node:path";
|
|
11
175
|
var PLAYERS = [
|
|
12
176
|
"youtube.com",
|
|
@@ -180,7 +344,7 @@ function mediaSummary(plan) {
|
|
|
180
344
|
}
|
|
181
345
|
function bakedName(id2, url, mime) {
|
|
182
346
|
const slug = id2.toLowerCase().replace(/[^a-z0-9_-]+/g, "-") || "asset";
|
|
183
|
-
const hash =
|
|
347
|
+
const hash = createHash2("sha256").update(url).digest("hex").slice(0, 8);
|
|
184
348
|
return `${slug}-${hash}${extOf(url) || extFor(mime)}`;
|
|
185
349
|
}
|
|
186
350
|
function extOf(url) {
|
|
@@ -235,7 +399,7 @@ async function fetchAsset(url) {
|
|
|
235
399
|
mime: res.headers.get("content-type") ?? void 0
|
|
236
400
|
};
|
|
237
401
|
}
|
|
238
|
-
return { bytes: new Uint8Array(await
|
|
402
|
+
return { bytes: new Uint8Array(await readFile3(url)) };
|
|
239
403
|
}
|
|
240
404
|
|
|
241
405
|
// src/types.ts
|
|
@@ -904,6 +1068,18 @@ var packSchema = z.object({
|
|
|
904
1068
|
narration: narrationSchema.optional(),
|
|
905
1069
|
media: z.array(mediaSchema).default([])
|
|
906
1070
|
});
|
|
1071
|
+
var CAPTION_FONT_RATIO = 0.037;
|
|
1072
|
+
var CAPTION_MARGIN_V_RATIO = 0.09;
|
|
1073
|
+
var CAPTION_MARGIN_X_RATIO = 0.04;
|
|
1074
|
+
var CAPTION_LINE_RATIO = 1.6;
|
|
1075
|
+
var CAPTION_MAX_LINES = 2;
|
|
1076
|
+
function captionFontSize(width) {
|
|
1077
|
+
return Math.round(width * CAPTION_FONT_RATIO);
|
|
1078
|
+
}
|
|
1079
|
+
function bandReserve(width, height) {
|
|
1080
|
+
const line2 = Math.round(captionFontSize(width) * CAPTION_LINE_RATIO);
|
|
1081
|
+
return Math.round(height * CAPTION_MARGIN_V_RATIO) + CAPTION_MAX_LINES * line2;
|
|
1082
|
+
}
|
|
907
1083
|
var DESTINATIONS = {
|
|
908
1084
|
// A presented deck is paced by the person clicking through it, and long-form
|
|
909
1085
|
// video has no ceiling worth encoding. An empty list is "unbudgeted", stated.
|
|
@@ -1223,54 +1399,6 @@ function clock(seconds) {
|
|
|
1223
1399
|
return m === 0 ? `${whole}s` : `${m}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1224
1400
|
}
|
|
1225
1401
|
|
|
1226
|
-
// src/source/fonts.ts
|
|
1227
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1228
|
-
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
1229
|
-
import { join } from "node:path";
|
|
1230
|
-
function familyFor(lang) {
|
|
1231
|
-
const tag = lang.toLowerCase();
|
|
1232
|
-
if (tag.startsWith("ko")) return "Noto Sans KR";
|
|
1233
|
-
if (tag.startsWith("ja")) return "Noto Sans JP";
|
|
1234
|
-
if (tag.startsWith("zh")) return /hant|-tw|-hk|-mo/.test(tag) ? "Noto Sans TC" : "Noto Sans SC";
|
|
1235
|
-
return null;
|
|
1236
|
-
}
|
|
1237
|
-
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";
|
|
1238
|
-
async function bundleFont(lang, glyphs, dir) {
|
|
1239
|
-
const family = familyFor(lang);
|
|
1240
|
-
if (!family) return null;
|
|
1241
|
-
const text2 = [...new Set(glyphs)].filter((c) => c > " ").sort().join("");
|
|
1242
|
-
const stamp = `/* decksmith ${createHash2("sha256").update(`${family}
|
|
1243
|
-
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1244
|
-
await mkdir(dir, { recursive: true });
|
|
1245
|
-
const cssPath = join(dir, "fonts.css");
|
|
1246
|
-
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1247
|
-
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1248
|
-
const res = await fetch(
|
|
1249
|
-
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
1250
|
-
{ headers: { "User-Agent": UA } }
|
|
1251
|
-
);
|
|
1252
|
-
if (!res.ok) throw new Error(`google fonts: HTTP ${res.status}`);
|
|
1253
|
-
let css = await res.text();
|
|
1254
|
-
const remote = [...new Set([...css.matchAll(/url\((https:[^)]+)\)/g)].map((m) => m[1] ?? ""))];
|
|
1255
|
-
const slug = family.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1256
|
-
const files = [];
|
|
1257
|
-
for (const [i, url] of remote.entries()) {
|
|
1258
|
-
const font = await fetch(url);
|
|
1259
|
-
if (!font.ok) throw new Error(`${url}: HTTP ${font.status}`);
|
|
1260
|
-
const name = `${slug}-${i}.woff2`;
|
|
1261
|
-
await writeFile(join(dir, name), Buffer.from(await font.arrayBuffer()));
|
|
1262
|
-
css = css.replaceAll(url, name);
|
|
1263
|
-
files.push(name);
|
|
1264
|
-
}
|
|
1265
|
-
css = `${stamp}
|
|
1266
|
-
${css}`;
|
|
1267
|
-
await writeFile(cssPath, css);
|
|
1268
|
-
return { family, css, files };
|
|
1269
|
-
}
|
|
1270
|
-
function localNames(css) {
|
|
1271
|
-
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
1402
|
// src/emit/kit.ts
|
|
1275
1403
|
var PAD_X = 110;
|
|
1276
1404
|
var PAD_Y = 84;
|
|
@@ -1289,7 +1417,12 @@ function contentW(format) {
|
|
|
1289
1417
|
return refWidth(format) - 2 * PAD_X;
|
|
1290
1418
|
}
|
|
1291
1419
|
function contentH(format) {
|
|
1292
|
-
return refHeight(format) - 2 * PAD_Y;
|
|
1420
|
+
return refHeight(format) - 2 * PAD_Y - reserveRef(format);
|
|
1421
|
+
}
|
|
1422
|
+
function reserveRef(format) {
|
|
1423
|
+
const reserve = format.captionReserve ?? 0;
|
|
1424
|
+
if (reserve <= 0) return 0;
|
|
1425
|
+
return Math.ceil(reserve * refHeight(format) / format.height);
|
|
1293
1426
|
}
|
|
1294
1427
|
function raw(js2) {
|
|
1295
1428
|
return { __raw: js2 };
|
|
@@ -1788,8 +1921,8 @@ function charUnits(c, tabular, cjkRun, hangul) {
|
|
|
1788
1921
|
}
|
|
1789
1922
|
const measured = ADVANCE[c];
|
|
1790
1923
|
if (measured !== void 0) return [measured, true];
|
|
1791
|
-
const
|
|
1792
|
-
for (const [lo, hi, units] of BLOCK_ADVANCE) if (
|
|
1924
|
+
const cp3 = c.codePointAt(0) ?? 0;
|
|
1925
|
+
for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp3 >= lo && cp3 <= hi) return [units, false];
|
|
1793
1926
|
return [UNMEASURED, true];
|
|
1794
1927
|
}
|
|
1795
1928
|
function weightFactor(weight) {
|
|
@@ -2050,11 +2183,16 @@ function baseCss(theme, format) {
|
|
|
2050
2183
|
gate still catches anything taller than the canvas; it just now
|
|
2051
2184
|
overflows symmetrically instead of only downwards. */
|
|
2052
2185
|
.scene { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
2053
|
-
padding: ${
|
|
2186
|
+
padding: ${scenePadding(format)}; display: flex; flex-direction: column;
|
|
2054
2187
|
justify-content: center; }
|
|
2055
2188
|
${referenceSpaceCss(format)}
|
|
2056
2189
|
${AMBIENT_KEYFRAMES}`;
|
|
2057
2190
|
}
|
|
2191
|
+
function scenePadding(format) {
|
|
2192
|
+
const reserve = reserveRef(format);
|
|
2193
|
+
const base = `${PAD_Y}px ${PAD_X}px`;
|
|
2194
|
+
return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
|
|
2195
|
+
}
|
|
2058
2196
|
function referenceSpaceCss(format) {
|
|
2059
2197
|
const zoom = zoomOf(format);
|
|
2060
2198
|
if (zoom === 1) return "";
|
|
@@ -6385,6 +6523,10 @@ function planTiming(input) {
|
|
|
6385
6523
|
lang: storyboard.lang,
|
|
6386
6524
|
audioDir: segments2.length > 0 ? narration?.dir ?? "" : "",
|
|
6387
6525
|
voice: segments2.length > 0 ? narration?.voice ?? "" : "",
|
|
6526
|
+
// The format the deck was LAID OUT under, which is the only honest source:
|
|
6527
|
+
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
6528
|
+
// disagree with the box the archetypes actually drew into.
|
|
6529
|
+
captionReserve: format.captionReserve ?? 0,
|
|
6388
6530
|
scenes,
|
|
6389
6531
|
segments: segments2
|
|
6390
6532
|
};
|
|
@@ -6677,8 +6819,8 @@ function textOf(node) {
|
|
|
6677
6819
|
|
|
6678
6820
|
// src/source/harvest.ts
|
|
6679
6821
|
import { createHash as createHash5 } from "node:crypto";
|
|
6680
|
-
import { copyFile, mkdir as
|
|
6681
|
-
import { basename as basename2, join as
|
|
6822
|
+
import { copyFile, mkdir as mkdir5, rm as rm2, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
|
|
6823
|
+
import { basename as basename2, join as join5, resolve as resolve2 } from "node:path";
|
|
6682
6824
|
|
|
6683
6825
|
// src/net/fetch.ts
|
|
6684
6826
|
import { lookup as resolveHost } from "node:dns/promises";
|
|
@@ -6688,8 +6830,8 @@ import { isIP } from "node:net";
|
|
|
6688
6830
|
import { brotliDecompressSync, gunzipSync } from "node:zlib";
|
|
6689
6831
|
|
|
6690
6832
|
// src/version.ts
|
|
6691
|
-
import { createRequire } from "node:module";
|
|
6692
|
-
var VERSION =
|
|
6833
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
6834
|
+
var VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
6693
6835
|
|
|
6694
6836
|
// src/net/fetch.ts
|
|
6695
6837
|
var BLOCKED_V4 = [
|
|
@@ -6884,8 +7026,8 @@ async function send(url, target, headers, signal, opts) {
|
|
|
6884
7026
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6885
7027
|
};
|
|
6886
7028
|
try {
|
|
6887
|
-
return await new Promise((
|
|
6888
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
7029
|
+
return await new Promise((resolve8, reject) => {
|
|
7030
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve8);
|
|
6889
7031
|
req.on("error", reject);
|
|
6890
7032
|
req.end();
|
|
6891
7033
|
});
|
|
@@ -6968,45 +7110,65 @@ function message(err) {
|
|
|
6968
7110
|
}
|
|
6969
7111
|
|
|
6970
7112
|
// src/render/capture.ts
|
|
6971
|
-
import { mkdir as
|
|
6972
|
-
import { createRequire as
|
|
7113
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
7114
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
6973
7115
|
import { homedir } from "node:os";
|
|
6974
|
-
import { join as
|
|
7116
|
+
import { join as join3 } from "node:path";
|
|
6975
7117
|
import { pathToFileURL } from "node:url";
|
|
6976
|
-
async function
|
|
6977
|
-
const
|
|
6978
|
-
if (
|
|
7118
|
+
async function resolveChrome(need = "open the deck with") {
|
|
7119
|
+
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
7120
|
+
if (env) return { path: env, source: "env" };
|
|
6979
7121
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6980
|
-
const
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
7122
|
+
const pick = async (cacheDir) => {
|
|
7123
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
7124
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
7125
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
7126
|
+
};
|
|
7127
|
+
const hf = await pick(join3(homedir(), ".cache", "hyperframes", "chrome"));
|
|
7128
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
7129
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer"));
|
|
7130
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
6984
7131
|
throw new Error(
|
|
6985
7132
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
6986
7133
|
);
|
|
6987
7134
|
}
|
|
7135
|
+
async function chromePath(need = "open the deck with") {
|
|
7136
|
+
return (await resolveChrome(need)).path;
|
|
7137
|
+
}
|
|
6988
7138
|
function runtimePath() {
|
|
6989
|
-
return
|
|
7139
|
+
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
6990
7140
|
}
|
|
6991
7141
|
function renderSeek(t2) {
|
|
6992
7142
|
const player = window.__player;
|
|
6993
7143
|
player.renderSeek(t2, { suppressEvents: true });
|
|
6994
7144
|
}
|
|
6995
7145
|
async function openDeck(dir, opts = {}) {
|
|
6996
|
-
const index =
|
|
6997
|
-
const html = await
|
|
7146
|
+
const index = join3(dir, "index.html");
|
|
7147
|
+
const html = await readFile4(index, "utf8").catch(() => null);
|
|
6998
7148
|
if (html === null) throw new Error(`no index.html in ${dir}`);
|
|
6999
7149
|
const width = Number(/data-width="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
7000
7150
|
const height = Number(/data-height="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
7001
7151
|
if (!width || !height) throw new Error("index.html declares no canvas size");
|
|
7002
7152
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
7003
7153
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
7154
|
+
const chrome2 = await resolveChrome();
|
|
7004
7155
|
const browser = await puppeteer.launch({
|
|
7005
|
-
executablePath:
|
|
7156
|
+
executablePath: chrome2.path,
|
|
7006
7157
|
headless: true,
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7158
|
+
args: [
|
|
7159
|
+
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
7160
|
+
// renderer's.
|
|
7161
|
+
"--force-device-scale-factor=1",
|
|
7162
|
+
"--hide-scrollbars",
|
|
7163
|
+
// WITHOUT THESE A WebGL CANVAS GETS NO CONTEXT AND SCREENSHOTS EMPTY. The
|
|
7164
|
+
// renderer passes its own GL arguments; this path passed none, so it drew
|
|
7165
|
+
// a blank rectangle and reported success. SwiftShader rather than the host
|
|
7166
|
+
// GPU on purpose: it is the one backend available on every machine, and a
|
|
7167
|
+
// gate that quietly follows the host's hardware is measuring the host.
|
|
7168
|
+
"--use-gl=angle",
|
|
7169
|
+
"--use-angle=swiftshader",
|
|
7170
|
+
"--enable-unsafe-swiftshader"
|
|
7171
|
+
]
|
|
7010
7172
|
});
|
|
7011
7173
|
try {
|
|
7012
7174
|
const page = await browser.newPage();
|
|
@@ -7022,6 +7184,18 @@ async function openDeck(dir, opts = {}) {
|
|
|
7022
7184
|
{ timeout }
|
|
7023
7185
|
);
|
|
7024
7186
|
await page.evaluate(() => document.fonts.ready);
|
|
7187
|
+
const canvas = await page.evaluate(() => {
|
|
7188
|
+
const el = document.querySelector("canvas");
|
|
7189
|
+
if (!el) return { present: false, gl: true };
|
|
7190
|
+
const probe2 = document.createElement("canvas");
|
|
7191
|
+
const gl = !!(probe2.getContext("webgl2") ?? probe2.getContext("webgl"));
|
|
7192
|
+
return { present: true, gl };
|
|
7193
|
+
});
|
|
7194
|
+
if (canvas.present && !canvas.gl) {
|
|
7195
|
+
throw new Error(
|
|
7196
|
+
`this deck draws on a <canvas> and ${chrome2.path} cannot create a WebGL context, so the capture would be of an empty rectangle. Chrome came from ${chrome2.source}; the renderer uses hyperframes' own shell. Set HYPERFRAMES_BROWSER_PATH to the binary \`hyperframes render\` uses so the gate and the render are the same browser.`
|
|
7197
|
+
);
|
|
7198
|
+
}
|
|
7025
7199
|
const cdp = await page.createCDPSession();
|
|
7026
7200
|
return {
|
|
7027
7201
|
width,
|
|
@@ -7048,13 +7222,13 @@ async function openDeck(dir, opts = {}) {
|
|
|
7048
7222
|
|
|
7049
7223
|
// src/source/assets.ts
|
|
7050
7224
|
import { createHash as createHash4 } from "node:crypto";
|
|
7051
|
-
import { mkdir as
|
|
7052
|
-
import { extname as extname2, join as
|
|
7225
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
7226
|
+
import { extname as extname2, join as join4 } from "node:path";
|
|
7053
7227
|
import { z as z2 } from "zod";
|
|
7054
7228
|
async function fetchFigures(source, dir, warnings) {
|
|
7055
7229
|
const drops = warnings ?? [];
|
|
7056
|
-
await
|
|
7057
|
-
const cached2 = await
|
|
7230
|
+
await mkdir4(dir, { recursive: true });
|
|
7231
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
7058
7232
|
const figures = [];
|
|
7059
7233
|
for (const figure of source.figures) {
|
|
7060
7234
|
if (figure.kind === "clip") {
|
|
@@ -7079,11 +7253,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
7079
7253
|
async function localize(figure, dir, cached2) {
|
|
7080
7254
|
const stem = assetStem(figure.id, figure.src);
|
|
7081
7255
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7082
|
-
const bytes = hit ? await
|
|
7256
|
+
const bytes = hit ? await readFile5(join4(dir, hit)) : await load(figure.src);
|
|
7083
7257
|
const size2 = imageSize(bytes);
|
|
7084
7258
|
if (hit) return { ...figure, src: hit, ...size2 };
|
|
7085
7259
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
7086
|
-
await
|
|
7260
|
+
await writeFile4(join4(dir, src), bytes);
|
|
7087
7261
|
cached2.push(src);
|
|
7088
7262
|
return { ...figure, src, ...size2 };
|
|
7089
7263
|
}
|
|
@@ -7108,7 +7282,7 @@ function assetExt(bytes, src) {
|
|
|
7108
7282
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
7109
7283
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
7110
7284
|
async function load(src) {
|
|
7111
|
-
if (!/^https?:/i.test(src)) return
|
|
7285
|
+
if (!/^https?:/i.test(src)) return readFile5(src);
|
|
7112
7286
|
const got = await fetchGuarded(src, {
|
|
7113
7287
|
maxBytes: FIGURE_MAX_BYTES,
|
|
7114
7288
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -7644,7 +7818,7 @@ function readContentRegion() {
|
|
|
7644
7818
|
}
|
|
7645
7819
|
|
|
7646
7820
|
// src/source/transcode.ts
|
|
7647
|
-
import { readFile as
|
|
7821
|
+
import { readFile as readFile6, rm } from "node:fs/promises";
|
|
7648
7822
|
import { basename } from "node:path";
|
|
7649
7823
|
|
|
7650
7824
|
// src/render/ffmpeg.ts
|
|
@@ -7670,7 +7844,7 @@ ${tail3}`);
|
|
|
7670
7844
|
}
|
|
7671
7845
|
}
|
|
7672
7846
|
function runLive(file, args) {
|
|
7673
|
-
return new Promise((
|
|
7847
|
+
return new Promise((resolve8, reject) => {
|
|
7674
7848
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7675
7849
|
child.on("error", (err) => {
|
|
7676
7850
|
reject(
|
|
@@ -7678,7 +7852,7 @@ function runLive(file, args) {
|
|
|
7678
7852
|
);
|
|
7679
7853
|
});
|
|
7680
7854
|
child.on("close", (code, signal) => {
|
|
7681
|
-
if (code === 0)
|
|
7855
|
+
if (code === 0) resolve8();
|
|
7682
7856
|
else if (signal) {
|
|
7683
7857
|
reject(
|
|
7684
7858
|
new Error(
|
|
@@ -7805,11 +7979,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
7805
7979
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
7806
7980
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
7807
7981
|
// three-line band covers the bottom of the slide.
|
|
7808
|
-
|
|
7982
|
+
//
|
|
7983
|
+
// THE RATIOS ARE NOT HERE. They are in src/types.ts beside `bandReserve`,
|
|
7984
|
+
// because `src/emit` has to reserve exactly the strip this draws into and
|
|
7985
|
+
// may not import this file. A local copy of 0.037 is how the two drift back
|
|
7986
|
+
// apart, which is the bug this whole path exists to close.
|
|
7987
|
+
fontSize: captionFontSize(width),
|
|
7809
7988
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
7810
7989
|
// player draws across the bottom eighth of the frame.
|
|
7811
|
-
marginV: Math.round(height *
|
|
7812
|
-
marginX: Math.round(width *
|
|
7990
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
7991
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
7813
7992
|
};
|
|
7814
7993
|
}
|
|
7815
7994
|
|
|
@@ -7875,7 +8054,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7875
8054
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7876
8055
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7877
8056
|
const name = basename(input);
|
|
7878
|
-
const source = videoSize(await
|
|
8057
|
+
const source = videoSize(await readFile6(input));
|
|
7879
8058
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7880
8059
|
const kept = (why3) => ({
|
|
7881
8060
|
path: input,
|
|
@@ -7907,7 +8086,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7907
8086
|
}
|
|
7908
8087
|
let measured;
|
|
7909
8088
|
try {
|
|
7910
|
-
measured = videoSize(await
|
|
8089
|
+
measured = videoSize(await readFile6(out));
|
|
7911
8090
|
} catch (err) {
|
|
7912
8091
|
await rm(out, { force: true });
|
|
7913
8092
|
return kept(
|
|
@@ -7943,8 +8122,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7943
8122
|
async function harvest(url, dir, opts = {}) {
|
|
7944
8123
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7945
8124
|
const warnings = [];
|
|
7946
|
-
const assets =
|
|
7947
|
-
await
|
|
8125
|
+
const assets = resolve2(dir);
|
|
8126
|
+
await mkdir5(assets, { recursive: true });
|
|
7948
8127
|
const page = await fetchGuarded(url, {
|
|
7949
8128
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
7950
8129
|
timeoutMs,
|
|
@@ -8005,7 +8184,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
8005
8184
|
"--disable-background-networking",
|
|
8006
8185
|
"--disable-extensions",
|
|
8007
8186
|
"--no-default-browser-check",
|
|
8008
|
-
"--no-first-run"
|
|
8187
|
+
"--no-first-run",
|
|
8188
|
+
// The floor under interception — see the note above. Not a hardening
|
|
8189
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
8190
|
+
// happens to be that run.
|
|
8191
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
8009
8192
|
]
|
|
8010
8193
|
});
|
|
8011
8194
|
try {
|
|
@@ -8322,8 +8505,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8322
8505
|
`it is ${size2.width}x${size2.height}, under ${MIN_FIGURE_PX}px \u2014 spacers, icons and tracking pixels look like this, and a slide cannot use one`
|
|
8323
8506
|
);
|
|
8324
8507
|
}
|
|
8325
|
-
const path2 =
|
|
8326
|
-
await
|
|
8508
|
+
const path2 = join5(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8509
|
+
await writeFile5(path2, bytes);
|
|
8327
8510
|
assets.push(path2);
|
|
8328
8511
|
got = { path: path2, width: size2.width, height: size2.height };
|
|
8329
8512
|
} catch (err) {
|
|
@@ -8335,7 +8518,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8335
8518
|
};
|
|
8336
8519
|
const shrink = async (url, path2, measured, what) => {
|
|
8337
8520
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8338
|
-
const out2 =
|
|
8521
|
+
const out2 = join5(dir, assetName(url, ".vp9.webm"));
|
|
8339
8522
|
let small;
|
|
8340
8523
|
try {
|
|
8341
8524
|
small = await transcode(path2, out2, {
|
|
@@ -8377,8 +8560,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8377
8560
|
try {
|
|
8378
8561
|
const bytes = await bytesOf(url);
|
|
8379
8562
|
const measured = videoSize(bytes);
|
|
8380
|
-
const path2 =
|
|
8381
|
-
await
|
|
8563
|
+
const path2 = join5(dir, assetName(url, `.${measured.container}`));
|
|
8564
|
+
await writeFile5(path2, bytes);
|
|
8382
8565
|
return await shrink(url, path2, measured, what);
|
|
8383
8566
|
} catch (err) {
|
|
8384
8567
|
warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
|
|
@@ -8467,7 +8650,7 @@ function unheld(policy) {
|
|
|
8467
8650
|
return "the file could not be held";
|
|
8468
8651
|
}
|
|
8469
8652
|
async function sizeOf(path2) {
|
|
8470
|
-
return await
|
|
8653
|
+
return await stat2(path2).then(
|
|
8471
8654
|
(s) => s.size,
|
|
8472
8655
|
() => 0
|
|
8473
8656
|
);
|
|
@@ -8675,8 +8858,8 @@ function sane(seconds) {
|
|
|
8675
8858
|
}
|
|
8676
8859
|
async function attachClips(source, clips, dir) {
|
|
8677
8860
|
if (clips.length === 0) return source;
|
|
8678
|
-
const assets =
|
|
8679
|
-
await
|
|
8861
|
+
const assets = resolve2(dir);
|
|
8862
|
+
await mkdir5(assets, { recursive: true });
|
|
8680
8863
|
const figures = [...source.figures];
|
|
8681
8864
|
for (const clip3 of clips) {
|
|
8682
8865
|
const src = clip3.file ? await adopt(clip3.file, assets) : void 0;
|
|
@@ -8707,7 +8890,7 @@ async function attachClips(source, clips, dir) {
|
|
|
8707
8890
|
}
|
|
8708
8891
|
async function adopt(file, dir) {
|
|
8709
8892
|
const name = basename2(file);
|
|
8710
|
-
await copyFile(file,
|
|
8893
|
+
await copyFile(file, join5(dir, name));
|
|
8711
8894
|
return name;
|
|
8712
8895
|
}
|
|
8713
8896
|
function freeId(figures) {
|
|
@@ -8784,9 +8967,9 @@ ${fence}`;
|
|
|
8784
8967
|
|
|
8785
8968
|
// src/plan/codex.ts
|
|
8786
8969
|
import { spawn as spawn2 } from "node:child_process";
|
|
8787
|
-
import { mkdtemp, readFile as
|
|
8970
|
+
import { mkdtemp, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
|
|
8788
8971
|
import { tmpdir } from "node:os";
|
|
8789
|
-
import { join as
|
|
8972
|
+
import { join as join6 } from "node:path";
|
|
8790
8973
|
import { z as z3 } from "zod";
|
|
8791
8974
|
|
|
8792
8975
|
// src/plan/arc.ts
|
|
@@ -9905,11 +10088,11 @@ function schemaFor(prefs) {
|
|
|
9905
10088
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9906
10089
|
async function codexPlanner(source, opts = {}) {
|
|
9907
10090
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9908
|
-
const dir = await mkdtemp(
|
|
10091
|
+
const dir = await mkdtemp(join6(tmpdir(), "decksmith-plan-"));
|
|
9909
10092
|
try {
|
|
9910
|
-
const schemaPath =
|
|
9911
|
-
const outPath =
|
|
9912
|
-
await
|
|
10093
|
+
const schemaPath = join6(dir, "storyboard.schema.json");
|
|
10094
|
+
const outPath = join6(dir, "storyboard.json");
|
|
10095
|
+
await writeFile6(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9913
10096
|
await (opts.run ?? runCodex)({
|
|
9914
10097
|
prompt: buildPrompt(source, prefs),
|
|
9915
10098
|
schemaPath,
|
|
@@ -9917,7 +10100,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
9917
10100
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
9918
10101
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
9919
10102
|
});
|
|
9920
|
-
const raw2 = await
|
|
10103
|
+
const raw2 = await readFile7(outPath, "utf8").catch(() => "");
|
|
9921
10104
|
if (raw2.trim() === "") {
|
|
9922
10105
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
9923
10106
|
}
|
|
@@ -9990,7 +10173,7 @@ function codexCommand(args) {
|
|
|
9990
10173
|
}
|
|
9991
10174
|
function runCodex(args) {
|
|
9992
10175
|
const { argv, env } = codexCommand(args);
|
|
9993
|
-
return new Promise((
|
|
10176
|
+
return new Promise((resolve8, reject) => {
|
|
9994
10177
|
const child = spawn2("codex", argv, {
|
|
9995
10178
|
stdio: ["pipe", "ignore", "pipe"],
|
|
9996
10179
|
...env === void 0 ? {} : { env }
|
|
@@ -10013,7 +10196,7 @@ function runCodex(args) {
|
|
|
10013
10196
|
});
|
|
10014
10197
|
child.on("close", (code) => {
|
|
10015
10198
|
clearTimeout(timer);
|
|
10016
|
-
if (code === 0) return
|
|
10199
|
+
if (code === 0) return resolve8();
|
|
10017
10200
|
reject(
|
|
10018
10201
|
new Error(`codex exec exited ${code}.
|
|
10019
10202
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -10025,14 +10208,14 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
10025
10208
|
|
|
10026
10209
|
// src/images/illustrate.ts
|
|
10027
10210
|
import { createHash as createHash7 } from "node:crypto";
|
|
10028
|
-
import { mkdir as
|
|
10029
|
-
import { join as
|
|
10211
|
+
import { mkdir as mkdir6, readFile as readFile9, rename, rm as rm5, writeFile as writeFile8 } from "node:fs/promises";
|
|
10212
|
+
import { join as join8 } from "node:path";
|
|
10030
10213
|
|
|
10031
10214
|
// src/images/providers.ts
|
|
10032
10215
|
import { createHash as createHash6 } from "node:crypto";
|
|
10033
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
10216
|
+
import { mkdtemp as mkdtemp2, readFile as readFile8, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
10034
10217
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
10035
|
-
import { join as
|
|
10218
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
10036
10219
|
import { z as z4 } from "zod";
|
|
10037
10220
|
var SIZE = {
|
|
10038
10221
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -10164,11 +10347,11 @@ function codexImages(opts = {}) {
|
|
|
10164
10347
|
async check() {
|
|
10165
10348
|
},
|
|
10166
10349
|
async generate(req) {
|
|
10167
|
-
const dir = await mkdtemp2(
|
|
10350
|
+
const dir = await mkdtemp2(join7(tmpdir2(), "decksmith-image-"));
|
|
10168
10351
|
try {
|
|
10169
|
-
const schemaPath =
|
|
10170
|
-
const outPath =
|
|
10171
|
-
await
|
|
10352
|
+
const schemaPath = join7(dir, "answer.schema.json");
|
|
10353
|
+
const outPath = join7(dir, "answer.json");
|
|
10354
|
+
await writeFile7(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
10172
10355
|
await run4({
|
|
10173
10356
|
prompt: codexPrompt(req),
|
|
10174
10357
|
schemaPath,
|
|
@@ -10177,7 +10360,7 @@ function codexImages(opts = {}) {
|
|
|
10177
10360
|
cwd: dir,
|
|
10178
10361
|
sandbox: "workspace-write"
|
|
10179
10362
|
});
|
|
10180
|
-
const raw2 = await
|
|
10363
|
+
const raw2 = await readFile8(outPath, "utf8").catch(() => "");
|
|
10181
10364
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
10182
10365
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
10183
10366
|
if (!answer.data.ok) {
|
|
@@ -10185,10 +10368,10 @@ function codexImages(opts = {}) {
|
|
|
10185
10368
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
10186
10369
|
);
|
|
10187
10370
|
}
|
|
10188
|
-
const candidates2 = [
|
|
10189
|
-
if (answer.data.file) candidates2.push(
|
|
10371
|
+
const candidates2 = [join7(dir, "picture.png")];
|
|
10372
|
+
if (answer.data.file) candidates2.push(resolve3(dir, answer.data.file));
|
|
10190
10373
|
for (const path2 of candidates2) {
|
|
10191
|
-
const bytes = await
|
|
10374
|
+
const bytes = await readFile8(path2).catch(() => null);
|
|
10192
10375
|
if (bytes) return raster(bytes, "codex");
|
|
10193
10376
|
}
|
|
10194
10377
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -10320,7 +10503,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
10320
10503
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
10321
10504
|
if (tool !== last) chain.push(tool);
|
|
10322
10505
|
const dropped = /* @__PURE__ */ new Set();
|
|
10323
|
-
if (pending.length > 0) await
|
|
10506
|
+
if (pending.length > 0) await mkdir6(opts.assetsDir, { recursive: true });
|
|
10324
10507
|
for (const [i, slot] of pending.entries()) {
|
|
10325
10508
|
let rungs = chain;
|
|
10326
10509
|
if (i >= images.max) {
|
|
@@ -10417,16 +10600,16 @@ function cacheKey(providerId, req) {
|
|
|
10417
10600
|
async function draw(provider, req, name, dir) {
|
|
10418
10601
|
for (const ext of Object.values(EXT2)) {
|
|
10419
10602
|
const src2 = `${name}${ext}`;
|
|
10420
|
-
const bytes = await
|
|
10603
|
+
const bytes = await readFile9(join8(dir, src2)).catch(() => null);
|
|
10421
10604
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
10422
10605
|
}
|
|
10423
10606
|
await provider.check();
|
|
10424
10607
|
const img = await provider.generate(req);
|
|
10425
10608
|
const src = `${name}${EXT2[img.mime]}`;
|
|
10426
|
-
const tmp =
|
|
10609
|
+
const tmp = join8(dir, `.${name}.tmp`);
|
|
10427
10610
|
try {
|
|
10428
|
-
await
|
|
10429
|
-
await rename(tmp,
|
|
10611
|
+
await writeFile8(tmp, img.bytes);
|
|
10612
|
+
await rename(tmp, join8(dir, src));
|
|
10430
10613
|
} finally {
|
|
10431
10614
|
await rm5(tmp, { force: true });
|
|
10432
10615
|
}
|
|
@@ -10439,9 +10622,9 @@ function sizeOf2(bytes, ext) {
|
|
|
10439
10622
|
// src/narrate/tts.ts
|
|
10440
10623
|
import { spawn as spawn3 } from "node:child_process";
|
|
10441
10624
|
import { createHash as createHash8 } from "node:crypto";
|
|
10442
|
-
import { mkdir as
|
|
10625
|
+
import { mkdir as mkdir7, readFile as readFile10, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
|
|
10443
10626
|
import { homedir as homedir2 } from "node:os";
|
|
10444
|
-
import { join as
|
|
10627
|
+
import { join as join9 } from "node:path";
|
|
10445
10628
|
var MISSING = [
|
|
10446
10629
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
10447
10630
|
"",
|
|
@@ -10458,8 +10641,8 @@ function candidates() {
|
|
|
10458
10641
|
["edge-tts"],
|
|
10459
10642
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
10460
10643
|
// default, and both are where this actually lands in practice.
|
|
10461
|
-
[
|
|
10462
|
-
[
|
|
10644
|
+
[join9(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10645
|
+
[join9(home, ".local", "bin", "edge-tts")],
|
|
10463
10646
|
// Last resort: the module is installed even though its console script is
|
|
10464
10647
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
10465
10648
|
["python3", "-m", "edge_tts"]
|
|
@@ -10484,7 +10667,7 @@ async function find2(can) {
|
|
|
10484
10667
|
throw new Error(MISSING);
|
|
10485
10668
|
}
|
|
10486
10669
|
function runArgv(cmd, args) {
|
|
10487
|
-
return new Promise((
|
|
10670
|
+
return new Promise((resolve8) => {
|
|
10488
10671
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10489
10672
|
let stdout = "";
|
|
10490
10673
|
let stderr = "";
|
|
@@ -10494,8 +10677,8 @@ function runArgv(cmd, args) {
|
|
|
10494
10677
|
child.stderr.on("data", (b) => {
|
|
10495
10678
|
stderr += b.toString();
|
|
10496
10679
|
});
|
|
10497
|
-
child.on("error", (e) =>
|
|
10498
|
-
child.on("close", (code) =>
|
|
10680
|
+
child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
|
|
10681
|
+
child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
|
|
10499
10682
|
});
|
|
10500
10683
|
}
|
|
10501
10684
|
var edgeTts = {
|
|
@@ -10562,7 +10745,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
10562
10745
|
async speak(req) {
|
|
10563
10746
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
10564
10747
|
await runner.speak({ ...req, subs });
|
|
10565
|
-
const raw2 = await
|
|
10748
|
+
const raw2 = await readFile10(subs, "utf8").catch(() => "");
|
|
10566
10749
|
await rm6(subs, { force: true });
|
|
10567
10750
|
const cues = parseCues(raw2);
|
|
10568
10751
|
const measured = await runner.measure(req.audio);
|
|
@@ -10592,11 +10775,11 @@ async function synthesize(text2, opts) {
|
|
|
10592
10775
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10593
10776
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10594
10777
|
const file = `${key}.mp3`;
|
|
10595
|
-
const audio =
|
|
10596
|
-
const sidecar =
|
|
10778
|
+
const audio = join9(opts.dir, file);
|
|
10779
|
+
const sidecar = join9(opts.dir, `${key}.json`);
|
|
10597
10780
|
const cached2 = await readSidecar(sidecar);
|
|
10598
10781
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10599
|
-
await
|
|
10782
|
+
await mkdir7(opts.dir, { recursive: true });
|
|
10600
10783
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10601
10784
|
const { cues, seconds } = spoken;
|
|
10602
10785
|
const clamped = cues.map((c) => ({
|
|
@@ -10605,13 +10788,13 @@ async function synthesize(text2, opts) {
|
|
|
10605
10788
|
text: c.text
|
|
10606
10789
|
}));
|
|
10607
10790
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
10608
|
-
await
|
|
10791
|
+
await writeFile9(sidecar, `${JSON.stringify(body, null, 2)}
|
|
10609
10792
|
`);
|
|
10610
10793
|
return { audio, file, seconds, cues: clamped };
|
|
10611
10794
|
}
|
|
10612
10795
|
async function readSidecar(path2) {
|
|
10613
10796
|
try {
|
|
10614
|
-
const parsed = JSON.parse(await
|
|
10797
|
+
const parsed = JSON.parse(await readFile10(path2, "utf8"));
|
|
10615
10798
|
const s = parsed;
|
|
10616
10799
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
10617
10800
|
} catch {
|
|
@@ -10734,8 +10917,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
10734
10917
|
}
|
|
10735
10918
|
|
|
10736
10919
|
// src/verify/index.ts
|
|
10737
|
-
import { readdir as
|
|
10738
|
-
import { join as
|
|
10920
|
+
import { readdir as readdir4, readFile as readFile14 } from "node:fs/promises";
|
|
10921
|
+
import { join as join13, relative, sep } from "node:path";
|
|
10739
10922
|
|
|
10740
10923
|
// src/verify/budget.ts
|
|
10741
10924
|
function readCanvas(html) {
|
|
@@ -10842,8 +11025,8 @@ function clock2(seconds) {
|
|
|
10842
11025
|
|
|
10843
11026
|
// src/verify/check.ts
|
|
10844
11027
|
import { execFile as execFile2 } from "node:child_process";
|
|
10845
|
-
import { readFile as
|
|
10846
|
-
import { join as
|
|
11028
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
11029
|
+
import { join as join10 } from "node:path";
|
|
10847
11030
|
import { promisify as promisify2 } from "node:util";
|
|
10848
11031
|
var run2 = promisify2(execFile2);
|
|
10849
11032
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -10873,7 +11056,7 @@ async function check(dir, opts = {}) {
|
|
|
10873
11056
|
async function readComposition(dir) {
|
|
10874
11057
|
let html;
|
|
10875
11058
|
try {
|
|
10876
|
-
html = await
|
|
11059
|
+
html = await readFile11(join10(dir, "index.html"), "utf8");
|
|
10877
11060
|
} catch {
|
|
10878
11061
|
return { transit: [], duration: 0 };
|
|
10879
11062
|
}
|
|
@@ -11024,8 +11207,8 @@ function tail(s) {
|
|
|
11024
11207
|
}
|
|
11025
11208
|
|
|
11026
11209
|
// src/verify/fidelity.ts
|
|
11027
|
-
import { readFile as
|
|
11028
|
-
import { join as
|
|
11210
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
11211
|
+
import { join as join11 } from "node:path";
|
|
11029
11212
|
|
|
11030
11213
|
// src/verify/typefloor.ts
|
|
11031
11214
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11281,6 +11464,16 @@ var INK_FLOOR = 15e-4;
|
|
|
11281
11464
|
var INK_DELTA = 12;
|
|
11282
11465
|
var FALLBACK_BAND_TOP = 0.22;
|
|
11283
11466
|
var CAPTION = ".headline, .eyebrow";
|
|
11467
|
+
var RESERVE_INK_TOLERANCE = 1e-4;
|
|
11468
|
+
function readReserve(timingText) {
|
|
11469
|
+
if (!timingText) return 0;
|
|
11470
|
+
try {
|
|
11471
|
+
const reserve = JSON.parse(timingText).captionReserve;
|
|
11472
|
+
return typeof reserve === "number" && reserve > 0 ? reserve : 0;
|
|
11473
|
+
} catch {
|
|
11474
|
+
return 0;
|
|
11475
|
+
}
|
|
11476
|
+
}
|
|
11284
11477
|
async function decodePng(png) {
|
|
11285
11478
|
const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
|
|
11286
11479
|
if (buf.length < 8 || buf.readUInt32BE(0) !== 2303741511) throw new Error("not a PNG");
|
|
@@ -11447,6 +11640,25 @@ function gradeFidelity(rows, floor = INK_FLOOR) {
|
|
|
11447
11640
|
};
|
|
11448
11641
|
});
|
|
11449
11642
|
}
|
|
11643
|
+
function gradeReserve(rows, tolerance = RESERVE_INK_TOLERANCE) {
|
|
11644
|
+
const dirty = /* @__PURE__ */ new Map();
|
|
11645
|
+
for (const row of rows) {
|
|
11646
|
+
if (row.reserveInk === void 0 || row.reserveInk <= tolerance) continue;
|
|
11647
|
+
const seen = dirty.get(row.sid);
|
|
11648
|
+
if (seen) seen.push(row);
|
|
11649
|
+
else dirty.set(row.sid, [row]);
|
|
11650
|
+
}
|
|
11651
|
+
const pct = (v) => `${(100 * v).toFixed(3)}%`;
|
|
11652
|
+
return [...dirty].map(([sid, stops]) => {
|
|
11653
|
+
const worst = stops.reduce((a, b) => (a.reserveInk ?? 0) >= (b.reserveInk ?? 0) ? a : b);
|
|
11654
|
+
return {
|
|
11655
|
+
severity: "error",
|
|
11656
|
+
gate: "fidelity",
|
|
11657
|
+
rule: "ink_in_caption_reserve",
|
|
11658
|
+
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(", ")}.`
|
|
11659
|
+
};
|
|
11660
|
+
});
|
|
11661
|
+
}
|
|
11450
11662
|
function captionBottom(sid, selector, fallbackPx) {
|
|
11451
11663
|
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
11452
11664
|
let bottom = 0;
|
|
@@ -11471,11 +11683,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
11471
11683
|
],
|
|
11472
11684
|
elapsedMs: Date.now() - started
|
|
11473
11685
|
});
|
|
11474
|
-
const
|
|
11475
|
-
|
|
11476
|
-
await readFile11(join10(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11477
|
-
);
|
|
11686
|
+
const timingText = await readFile12(join11(dir, TIMING_FILE), "utf8").catch(() => null);
|
|
11687
|
+
const stops = opts.stops ?? readStops(timingText, await readFile12(join11(dir, DECK_PAGE), "utf8").catch(() => null));
|
|
11478
11688
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
11689
|
+
const reserve = opts.captionReserve ?? readReserve(timingText);
|
|
11479
11690
|
let deck = null;
|
|
11480
11691
|
try {
|
|
11481
11692
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
@@ -11504,7 +11715,13 @@ async function fidelity(dir, opts = {}) {
|
|
|
11504
11715
|
measured.push({
|
|
11505
11716
|
...stop,
|
|
11506
11717
|
ink: inkBelow(frame, bandTopPx),
|
|
11507
|
-
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
11718
|
+
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3,
|
|
11719
|
+
// THE SAME FRAME, a second strip. Free: it is one more pass over pixels
|
|
11720
|
+
// that are already decoded, so the gate that stops the caption
|
|
11721
|
+
// collision regressing costs no extra capture, no extra seek and no
|
|
11722
|
+
// extra browser. That matters — a gate with its own capture is a gate
|
|
11723
|
+
// someone turns off.
|
|
11724
|
+
...reserve > 0 ? { reserveInk: inkBelow(frame, height - reserve) } : {}
|
|
11508
11725
|
});
|
|
11509
11726
|
}
|
|
11510
11727
|
for (const mid of midpoints(stops)) {
|
|
@@ -11515,6 +11732,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
11515
11732
|
stops: measured,
|
|
11516
11733
|
findings: [
|
|
11517
11734
|
...gradeFidelity(measured, floor),
|
|
11735
|
+
...gradeReserve(measured),
|
|
11518
11736
|
...gradeOverprint(collided),
|
|
11519
11737
|
...gradeApparent(apparent)
|
|
11520
11738
|
],
|
|
@@ -11531,9 +11749,9 @@ async function fidelity(dir, opts = {}) {
|
|
|
11531
11749
|
// src/verify/drift.ts
|
|
11532
11750
|
import { execFile as execFile3 } from "node:child_process";
|
|
11533
11751
|
import { createHash as createHash9 } from "node:crypto";
|
|
11534
|
-
import { mkdtemp as mkdtemp3, readdir as
|
|
11752
|
+
import { mkdtemp as mkdtemp3, readdir as readdir3, readFile as readFile13, rm as rm7 } from "node:fs/promises";
|
|
11535
11753
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
11536
|
-
import { join as
|
|
11754
|
+
import { join as join12 } from "node:path";
|
|
11537
11755
|
import { promisify as promisify3 } from "node:util";
|
|
11538
11756
|
var run3 = promisify3(execFile3);
|
|
11539
11757
|
var FLOOR_DB = 40;
|
|
@@ -11572,9 +11790,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
11572
11790
|
async function drift(dir, opts = {}) {
|
|
11573
11791
|
const mode = opts.mode ?? "psnr";
|
|
11574
11792
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
11575
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
11576
|
-
const a =
|
|
11577
|
-
const b =
|
|
11793
|
+
const work = opts.workDir ?? await mkdtemp3(join12(tmpdir3(), "decksmith-drift-"));
|
|
11794
|
+
const a = join12(work, "a");
|
|
11795
|
+
const b = join12(work, "b");
|
|
11578
11796
|
const report = await compare(dir, a, b, mode, floorDb, opts);
|
|
11579
11797
|
const keep = opts.keep || !report.passed;
|
|
11580
11798
|
if (keep) report.kept = { a, b };
|
|
@@ -11629,11 +11847,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
11629
11847
|
const differing = [];
|
|
11630
11848
|
const ha = [];
|
|
11631
11849
|
for (const [i, name] of fa.entries()) {
|
|
11632
|
-
const one = await sha(
|
|
11850
|
+
const one = await sha(join12(a, name));
|
|
11633
11851
|
ha.push(one);
|
|
11634
|
-
if (one !== await sha(
|
|
11852
|
+
if (one !== await sha(join12(b, name))) differing.push(i + 1);
|
|
11635
11853
|
}
|
|
11636
|
-
const html = await
|
|
11854
|
+
const html = await readFile13(join12(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
11637
11855
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
11638
11856
|
let worst;
|
|
11639
11857
|
if (differing.length > 0) {
|
|
@@ -11744,11 +11962,11 @@ async function render(dir, out, opts) {
|
|
|
11744
11962
|
}
|
|
11745
11963
|
}
|
|
11746
11964
|
async function frames(dir) {
|
|
11747
|
-
const names = await
|
|
11965
|
+
const names = await readdir3(dir).catch(() => []);
|
|
11748
11966
|
return names.filter((n3) => n3.endsWith(".png")).sort();
|
|
11749
11967
|
}
|
|
11750
11968
|
async function sha(file) {
|
|
11751
|
-
return createHash9("sha256").update(await
|
|
11969
|
+
return createHash9("sha256").update(await readFile13(file)).digest("hex");
|
|
11752
11970
|
}
|
|
11753
11971
|
var STAT = /^n:(\d+)\b.*\bpsnr_avg:(inf|-?[\d.]+)/;
|
|
11754
11972
|
function framePattern(name) {
|
|
@@ -11781,11 +11999,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
11781
11999
|
"-start_number",
|
|
11782
12000
|
String(seq.start),
|
|
11783
12001
|
"-i",
|
|
11784
|
-
|
|
12002
|
+
join12(a, seq.pattern),
|
|
11785
12003
|
"-start_number",
|
|
11786
12004
|
String(seq.start),
|
|
11787
12005
|
"-i",
|
|
11788
|
-
|
|
12006
|
+
join12(b, seq.pattern),
|
|
11789
12007
|
"-lavfi",
|
|
11790
12008
|
"psnr=stats_file=-",
|
|
11791
12009
|
"-f",
|
|
@@ -11860,7 +12078,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
11860
12078
|
const html = await readCompositions(dir);
|
|
11861
12079
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
11862
12080
|
const narration = scanNarration(
|
|
11863
|
-
await
|
|
12081
|
+
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
11864
12082
|
await listFiles(dir)
|
|
11865
12083
|
);
|
|
11866
12084
|
const budget2 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -11897,8 +12115,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
11897
12115
|
}
|
|
11898
12116
|
async function declaredStops(dir) {
|
|
11899
12117
|
return readStops(
|
|
11900
|
-
await
|
|
11901
|
-
await
|
|
12118
|
+
await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12119
|
+
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11902
12120
|
);
|
|
11903
12121
|
}
|
|
11904
12122
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -11938,7 +12156,7 @@ function scanNarration(page, files) {
|
|
|
11938
12156
|
];
|
|
11939
12157
|
}
|
|
11940
12158
|
async function readTiming(dir) {
|
|
11941
|
-
const raw2 = await
|
|
12159
|
+
const raw2 = await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
11942
12160
|
if (!raw2) return void 0;
|
|
11943
12161
|
try {
|
|
11944
12162
|
const parsed = JSON.parse(raw2);
|
|
@@ -11948,9 +12166,9 @@ async function readTiming(dir) {
|
|
|
11948
12166
|
}
|
|
11949
12167
|
}
|
|
11950
12168
|
async function listFiles(dir) {
|
|
11951
|
-
const entries = await
|
|
12169
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
11952
12170
|
return new Set(
|
|
11953
|
-
entries.filter((e) => e.isFile()).map((e) => relative(dir,
|
|
12171
|
+
entries.filter((e) => e.isFile()).map((e) => relative(dir, join13(e.parentPath, e.name)).split(sep).join("/"))
|
|
11954
12172
|
);
|
|
11955
12173
|
}
|
|
11956
12174
|
var INSTEAD = {
|
|
@@ -12206,22 +12424,22 @@ function scanDeterminism(html, file) {
|
|
|
12206
12424
|
return findings;
|
|
12207
12425
|
}
|
|
12208
12426
|
async function readCompositions(dir) {
|
|
12209
|
-
const entries = await
|
|
12427
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12210
12428
|
const files = entries.filter(
|
|
12211
12429
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12212
|
-
).map((e) =>
|
|
12430
|
+
).map((e) => join13(e.parentPath, e.name));
|
|
12213
12431
|
return Promise.all(
|
|
12214
|
-
files.map(async (f) => [relative(dir, f), await
|
|
12432
|
+
files.map(async (f) => [relative(dir, f), await readFile14(f, "utf8")])
|
|
12215
12433
|
);
|
|
12216
12434
|
}
|
|
12217
12435
|
|
|
12218
12436
|
// src/render/render.ts
|
|
12219
|
-
import { mkdir as
|
|
12220
|
-
import { basename as basename3, dirname, join as
|
|
12437
|
+
import { mkdir as mkdir9, readFile as readFile15, rename as rename2, rm as rm8, writeFile as writeFile11 } from "node:fs/promises";
|
|
12438
|
+
import { basename as basename3, dirname as dirname2, join as join15, resolve as resolve4 } from "node:path";
|
|
12221
12439
|
|
|
12222
12440
|
// src/render/captions.ts
|
|
12223
|
-
import { mkdir as
|
|
12224
|
-
import { join as
|
|
12441
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
|
|
12442
|
+
import { join as join14 } from "node:path";
|
|
12225
12443
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
12226
12444
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
12227
12445
|
function cssString(value) {
|
|
@@ -12312,11 +12530,11 @@ async function captionBlocker() {
|
|
|
12312
12530
|
}
|
|
12313
12531
|
async function renderCaptions(cues, style, deck, work) {
|
|
12314
12532
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
12315
|
-
const fontCss =
|
|
12533
|
+
const fontCss = join14(deck, DECK_FONT_CSS);
|
|
12316
12534
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL2(fontCss).href : null;
|
|
12317
|
-
const page =
|
|
12318
|
-
await
|
|
12319
|
-
await
|
|
12535
|
+
const page = join14(work, "captions.html");
|
|
12536
|
+
await mkdir8(work, { recursive: true });
|
|
12537
|
+
await writeFile10(page, captionPage(cues, style, href));
|
|
12320
12538
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
12321
12539
|
const browser = await puppeteer.launch({
|
|
12322
12540
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -12336,7 +12554,7 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
12336
12554
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
12337
12555
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
12338
12556
|
await tab.screenshot({
|
|
12339
|
-
path:
|
|
12557
|
+
path: join14(work, name),
|
|
12340
12558
|
type: "png",
|
|
12341
12559
|
omitBackground: true,
|
|
12342
12560
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -12377,9 +12595,9 @@ function subtitlePlan(mode) {
|
|
|
12377
12595
|
async function render2(opts) {
|
|
12378
12596
|
const log = opts.log ?? (() => {
|
|
12379
12597
|
});
|
|
12380
|
-
const deck =
|
|
12381
|
-
const out =
|
|
12382
|
-
const work =
|
|
12598
|
+
const deck = resolve4(opts.deck);
|
|
12599
|
+
const out = resolve4(opts.out);
|
|
12600
|
+
const work = join15(dirname2(out), `.${basename3(out)}.parts`);
|
|
12383
12601
|
const timing = await readTiming2(deck);
|
|
12384
12602
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
12385
12603
|
const refusal = playbackRefusal(
|
|
@@ -12391,13 +12609,17 @@ async function render2(opts) {
|
|
|
12391
12609
|
}
|
|
12392
12610
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
12393
12611
|
if (plan0.burn) {
|
|
12612
|
+
if (!timing.captionReserve)
|
|
12613
|
+
throw new Error(
|
|
12614
|
+
"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`."
|
|
12615
|
+
);
|
|
12394
12616
|
const blocker = await captionBlocker();
|
|
12395
12617
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
12396
12618
|
}
|
|
12397
12619
|
const burnable = plan0.burn;
|
|
12398
|
-
await
|
|
12620
|
+
await mkdir9(work, { recursive: true });
|
|
12399
12621
|
try {
|
|
12400
|
-
const raw2 = opts.video ?
|
|
12622
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join15(work, "raw.mp4"), opts, log);
|
|
12401
12623
|
const shot = await probe(raw2);
|
|
12402
12624
|
log(
|
|
12403
12625
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -12410,18 +12632,18 @@ async function render2(opts) {
|
|
|
12410
12632
|
);
|
|
12411
12633
|
}
|
|
12412
12634
|
const retimed = await retime(raw2, plan, work, log);
|
|
12413
|
-
const srtPath =
|
|
12635
|
+
const srtPath = join15(dirname2(out), SRT_NAME(out));
|
|
12414
12636
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
12415
12637
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
12416
12638
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
12417
|
-
if (srt) await
|
|
12639
|
+
if (srt) await writeFile11(srtPath, srt);
|
|
12418
12640
|
const burn = burnable && srt.length > 0;
|
|
12419
12641
|
await mux(retimed, timing, plan, deck, out, work, burn ? plan.cues : void 0, shot.fps, log);
|
|
12420
12642
|
if (playback > 1) {
|
|
12421
12643
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
12422
12644
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
12423
12645
|
if (warning) log(`render: ${warning}`);
|
|
12424
|
-
const fast =
|
|
12646
|
+
const fast = join15(work, `fast.${basename3(out)}`);
|
|
12425
12647
|
const muxed = await probe(out);
|
|
12426
12648
|
await runTool(
|
|
12427
12649
|
"ffmpeg",
|
|
@@ -12451,8 +12673,8 @@ async function render2(opts) {
|
|
|
12451
12673
|
}
|
|
12452
12674
|
}
|
|
12453
12675
|
async function readTiming2(deck) {
|
|
12454
|
-
const path2 =
|
|
12455
|
-
const text2 = await
|
|
12676
|
+
const path2 = join15(deck, TIMING_FILE);
|
|
12677
|
+
const text2 = await readFile15(path2, "utf8").catch(() => {
|
|
12456
12678
|
throw new Error(
|
|
12457
12679
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
12458
12680
|
);
|
|
@@ -12491,20 +12713,20 @@ async function retime(raw2, plan, work, log) {
|
|
|
12491
12713
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
12492
12714
|
const list = [];
|
|
12493
12715
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
12494
|
-
const file =
|
|
12716
|
+
const file = join15(work, `p${String(i).padStart(4, "0")}.ts`);
|
|
12495
12717
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
12496
12718
|
list.push(file);
|
|
12497
12719
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
12498
12720
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
12499
12721
|
}
|
|
12500
12722
|
}
|
|
12501
|
-
const listFile =
|
|
12502
|
-
await
|
|
12723
|
+
const listFile = join15(work, "pieces.txt");
|
|
12724
|
+
await writeFile11(
|
|
12503
12725
|
listFile,
|
|
12504
12726
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
12505
12727
|
`
|
|
12506
12728
|
);
|
|
12507
|
-
const joined =
|
|
12729
|
+
const joined = join15(work, "retimed.mp4");
|
|
12508
12730
|
await runTool("ffmpeg", [
|
|
12509
12731
|
"-y",
|
|
12510
12732
|
"-hide_banner",
|
|
@@ -12526,7 +12748,7 @@ async function retime(raw2, plan, work, log) {
|
|
|
12526
12748
|
}
|
|
12527
12749
|
async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
12528
12750
|
const inputs = plan.audio.map((a) => ({
|
|
12529
|
-
file:
|
|
12751
|
+
file: join15(deck, timing.audioDir, a.audio),
|
|
12530
12752
|
delayMs: a.delayMs
|
|
12531
12753
|
}));
|
|
12532
12754
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -12547,8 +12769,8 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
12547
12769
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
12548
12770
|
}
|
|
12549
12771
|
if (graph.length > 0) {
|
|
12550
|
-
const script =
|
|
12551
|
-
await
|
|
12772
|
+
const script = join15(work, "mux.filter");
|
|
12773
|
+
await writeFile11(script, `${graph.join(";\n")}
|
|
12552
12774
|
`);
|
|
12553
12775
|
args.push("-filter_complex_script", script);
|
|
12554
12776
|
}
|
|
@@ -12563,13 +12785,13 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
12563
12785
|
log(
|
|
12564
12786
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
12565
12787
|
);
|
|
12566
|
-
await
|
|
12788
|
+
await mkdir9(dirname2(out), { recursive: true });
|
|
12567
12789
|
await runTool("ffmpeg", args, { cwd: work });
|
|
12568
12790
|
}
|
|
12569
12791
|
|
|
12570
12792
|
// src/pack/pack.ts
|
|
12571
|
-
import { mkdir as
|
|
12572
|
-
import { dirname as
|
|
12793
|
+
import { mkdir as mkdir10, readFile as readFile16, writeFile as writeFile12 } from "node:fs/promises";
|
|
12794
|
+
import { dirname as dirname3, extname as extname3 } from "node:path";
|
|
12573
12795
|
import { unzipSync, zipSync } from "fflate";
|
|
12574
12796
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
12575
12797
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -12608,12 +12830,12 @@ async function writePack(pack2, files, out) {
|
|
|
12608
12830
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
12609
12831
|
}
|
|
12610
12832
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
12611
|
-
await
|
|
12612
|
-
await
|
|
12833
|
+
await mkdir10(dirname3(out), { recursive: true });
|
|
12834
|
+
await writeFile12(out, zip);
|
|
12613
12835
|
return zip.length;
|
|
12614
12836
|
}
|
|
12615
12837
|
async function readPack(path2) {
|
|
12616
|
-
return openPack(new Uint8Array(await
|
|
12838
|
+
return openPack(new Uint8Array(await readFile16(path2)), path2);
|
|
12617
12839
|
}
|
|
12618
12840
|
function openPack(bytes, label = "pack") {
|
|
12619
12841
|
if (bytes.length < 4 || bytes[0] !== 80 || bytes[1] !== 75)
|
|
@@ -12657,8 +12879,8 @@ function why2(error) {
|
|
|
12657
12879
|
}
|
|
12658
12880
|
|
|
12659
12881
|
// src/prefs.ts
|
|
12660
|
-
import { readFile as
|
|
12661
|
-
import { dirname as
|
|
12882
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
12883
|
+
import { dirname as dirname4, join as join16, resolve as resolve5 } from "node:path";
|
|
12662
12884
|
var CONFIG_FILE = "decksmith.config.json";
|
|
12663
12885
|
var PREF_KEYS = Object.keys(prefsSchema.shape);
|
|
12664
12886
|
var NARRATION_KEYS = Object.keys(prefsSchema.shape.narration.unwrap().shape);
|
|
@@ -12675,10 +12897,10 @@ async function loadPrefs(overrides = {}, cwd = process.cwd(), source) {
|
|
|
12675
12897
|
return parsed;
|
|
12676
12898
|
}
|
|
12677
12899
|
async function findConfig(from) {
|
|
12678
|
-
let dir =
|
|
12900
|
+
let dir = resolve5(from);
|
|
12679
12901
|
for (; ; ) {
|
|
12680
|
-
const path2 =
|
|
12681
|
-
const text2 = await
|
|
12902
|
+
const path2 = join16(dir, CONFIG_FILE);
|
|
12903
|
+
const text2 = await readFile17(path2, "utf8").catch(() => void 0);
|
|
12682
12904
|
if (text2 !== void 0) {
|
|
12683
12905
|
try {
|
|
12684
12906
|
return { path: path2, value: JSON.parse(text2) };
|
|
@@ -12686,7 +12908,7 @@ async function findConfig(from) {
|
|
|
12686
12908
|
throw new Error(`${path2} is not valid JSON.`);
|
|
12687
12909
|
}
|
|
12688
12910
|
}
|
|
12689
|
-
const up =
|
|
12911
|
+
const up = dirname4(dir);
|
|
12690
12912
|
if (up === dir) return void 0;
|
|
12691
12913
|
dir = up;
|
|
12692
12914
|
}
|
|
@@ -12732,26 +12954,26 @@ ${issues}`);
|
|
|
12732
12954
|
|
|
12733
12955
|
// src/tmpdir.ts
|
|
12734
12956
|
import { realpathSync } from "node:fs";
|
|
12735
|
-
import { createRequire as
|
|
12957
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
12736
12958
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12737
|
-
import { basename as basename4, dirname as
|
|
12959
|
+
import { basename as basename4, dirname as dirname5, join as join17, resolve as resolve6, sep as sep2 } from "node:path";
|
|
12738
12960
|
var cached;
|
|
12739
12961
|
function real(dir) {
|
|
12740
|
-
let at =
|
|
12962
|
+
let at = resolve6(dir);
|
|
12741
12963
|
const tail3 = [];
|
|
12742
12964
|
for (; ; ) {
|
|
12743
12965
|
try {
|
|
12744
|
-
return
|
|
12966
|
+
return join17(realpathSync(at), ...tail3);
|
|
12745
12967
|
} catch {
|
|
12746
|
-
const up =
|
|
12747
|
-
if (up === at) return
|
|
12968
|
+
const up = dirname5(at);
|
|
12969
|
+
if (up === at) return resolve6(dir);
|
|
12748
12970
|
tail3.unshift(basename4(at));
|
|
12749
12971
|
at = up;
|
|
12750
12972
|
}
|
|
12751
12973
|
}
|
|
12752
12974
|
}
|
|
12753
12975
|
function packageRoot() {
|
|
12754
|
-
cached ??= real(
|
|
12976
|
+
cached ??= real(dirname5(createRequire4(import.meta.url).resolve("../package.json")));
|
|
12755
12977
|
return cached;
|
|
12756
12978
|
}
|
|
12757
12979
|
function insideRoot(dir) {
|
|
@@ -12784,8 +13006,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12784
13006
|
if (!format) throw new Error("no deck-16x9 format");
|
|
12785
13007
|
const step = opts.onStep ?? (() => {
|
|
12786
13008
|
});
|
|
12787
|
-
const out =
|
|
12788
|
-
await
|
|
13009
|
+
const out = resolve7(outDir);
|
|
13010
|
+
await mkdir11(out, { recursive: true });
|
|
12789
13011
|
const speed = opts.speed ?? 1;
|
|
12790
13012
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
12791
13013
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
@@ -12798,47 +13020,48 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12798
13020
|
});
|
|
12799
13021
|
const files = [];
|
|
12800
13022
|
const write = async (name, text2) => {
|
|
12801
|
-
await
|
|
12802
|
-
files.push(
|
|
13023
|
+
await writeFile13(join18(out, name), text2);
|
|
13024
|
+
files.push(join18(out, name));
|
|
12803
13025
|
};
|
|
12804
13026
|
await write("index.html", deck.composition);
|
|
12805
13027
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
13028
|
+
try {
|
|
13029
|
+
const timing = planTiming({
|
|
13030
|
+
storyboard,
|
|
13031
|
+
source,
|
|
13032
|
+
format,
|
|
13033
|
+
composition: deck.composition,
|
|
13034
|
+
// What was drawn, not what the threshold would have kept: a budgeted
|
|
13035
|
+
// format cuts to its own length as well, and the two lists differ.
|
|
13036
|
+
beats: deck.cut.kept,
|
|
13037
|
+
speed,
|
|
13038
|
+
...opts.narration ? { narration: opts.narration } : {},
|
|
13039
|
+
...opts.theme ? { theme: opts.theme } : {}
|
|
13040
|
+
});
|
|
13041
|
+
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
12821
13042
|
`);
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
}
|
|
13043
|
+
step(`build: timing for ${timing.segments.length} narration segment(s)`);
|
|
13044
|
+
} catch (err) {
|
|
13045
|
+
step(
|
|
13046
|
+
`build: cannot place narration on this deck, so no ${TIMING_FILE} was written and \`render\` will refuse. ${err instanceof Error ? err.message : err}`
|
|
13047
|
+
);
|
|
12828
13048
|
}
|
|
12829
13049
|
if (deck.page) {
|
|
12830
13050
|
await write(DECK_PAGE, deck.page);
|
|
12831
|
-
await
|
|
12832
|
-
files.push(
|
|
13051
|
+
await cp2(playerBundle(), join18(out, PLAYER_FILE));
|
|
13052
|
+
files.push(join18(out, PLAYER_FILE));
|
|
12833
13053
|
}
|
|
12834
13054
|
files.push(...await vendorKatex(out));
|
|
12835
|
-
|
|
13055
|
+
files.push(...await vendorScripts(out, deck.composition));
|
|
13056
|
+
if (opts.assetsFrom) {
|
|
13057
|
+
files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
|
|
13058
|
+
}
|
|
12836
13059
|
if (opts.narration && opts.audioFrom) {
|
|
12837
|
-
files.push(...await copyAudio(opts.audioFrom, opts.narration, out));
|
|
13060
|
+
files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
|
|
12838
13061
|
}
|
|
12839
13062
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
12840
13063
|
step(
|
|
12841
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
13064
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join18(out, "index.html")}`
|
|
12842
13065
|
);
|
|
12843
13066
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
12844
13067
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -12854,79 +13077,28 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
12854
13077
|
)}
|
|
12855
13078
|
`;
|
|
12856
13079
|
async function deckRuntime() {
|
|
12857
|
-
const path2 =
|
|
13080
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
12858
13081
|
try {
|
|
12859
|
-
return await
|
|
13082
|
+
return await readFile18(path2, "utf8");
|
|
12860
13083
|
} catch {
|
|
12861
13084
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
12862
13085
|
}
|
|
12863
13086
|
}
|
|
12864
13087
|
function playerBundle() {
|
|
12865
|
-
const require2 =
|
|
13088
|
+
const require2 = createRequire5(import.meta.url);
|
|
12866
13089
|
try {
|
|
12867
|
-
return
|
|
13090
|
+
return join18(dirname6(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12868
13091
|
} catch {
|
|
12869
13092
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12870
13093
|
}
|
|
12871
13094
|
}
|
|
12872
|
-
async function vendorKatex(out) {
|
|
12873
|
-
const require2 = createRequire4(import.meta.url);
|
|
12874
|
-
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12875
|
-
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12876
|
-
const written = [];
|
|
12877
|
-
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12878
|
-
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12879
|
-
if (!file.endsWith(".woff2")) continue;
|
|
12880
|
-
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12881
|
-
written.push(join17(out, "katex/fonts", file));
|
|
12882
|
-
}
|
|
12883
|
-
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12884
|
-
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12885
|
-
return kept ? `src:${kept}` : whole;
|
|
12886
|
-
});
|
|
12887
|
-
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12888
|
-
written.push(join17(out, "katex/katex.min.css"));
|
|
12889
|
-
return written;
|
|
12890
|
-
}
|
|
12891
|
-
async function copyAssets(sourceDir, out) {
|
|
12892
|
-
const from = join17(resolve6(sourceDir), "assets");
|
|
12893
|
-
if (!await stat2(from).catch(() => null)) return [];
|
|
12894
|
-
await cp(from, join17(out, "assets"), { recursive: true });
|
|
12895
|
-
return [join17(out, "assets")];
|
|
12896
|
-
}
|
|
12897
|
-
async function copyAudio(from, narration, out) {
|
|
12898
|
-
const dir = join17(out, narration.dir);
|
|
12899
|
-
await mkdir10(dir, { recursive: true });
|
|
12900
|
-
const names = [
|
|
12901
|
-
...new Set(
|
|
12902
|
-
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
12903
|
-
)
|
|
12904
|
-
].sort();
|
|
12905
|
-
for (const name of names) {
|
|
12906
|
-
await cp(join17(resolve6(from), name), join17(dir, name)).catch(() => {
|
|
12907
|
-
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
12908
|
-
});
|
|
12909
|
-
}
|
|
12910
|
-
return names.map((n3) => join17(dir, n3));
|
|
12911
|
-
}
|
|
12912
|
-
async function refreshFont(storyboard, source, out, step) {
|
|
12913
|
-
try {
|
|
12914
|
-
const bundle = await bundleFont(
|
|
12915
|
-
storyboard.lang,
|
|
12916
|
-
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
12917
|
-
join17(out, "assets", "fonts")
|
|
12918
|
-
);
|
|
12919
|
-
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
12920
|
-
return bundle?.css;
|
|
12921
|
-
} catch (err) {
|
|
12922
|
-
step(
|
|
12923
|
-
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
12924
|
-
);
|
|
12925
|
-
return void 0;
|
|
12926
|
-
}
|
|
12927
|
-
}
|
|
12928
13095
|
export {
|
|
12929
13096
|
ARCHETYPE_FAMILY,
|
|
13097
|
+
CAPTION_FONT_RATIO,
|
|
13098
|
+
CAPTION_LINE_RATIO,
|
|
13099
|
+
CAPTION_MARGIN_V_RATIO,
|
|
13100
|
+
CAPTION_MARGIN_X_RATIO,
|
|
13101
|
+
CAPTION_MAX_LINES,
|
|
12930
13102
|
COMFORTABLE_CPS,
|
|
12931
13103
|
CONFIG_FILE,
|
|
12932
13104
|
DECK_PAGE,
|
|
@@ -12956,6 +13128,7 @@ export {
|
|
|
12956
13128
|
assertInsideResolves,
|
|
12957
13129
|
assertRefsResolve,
|
|
12958
13130
|
attachClips,
|
|
13131
|
+
bandReserve,
|
|
12959
13132
|
barCompareParamsSchema,
|
|
12960
13133
|
beatRoleSchema,
|
|
12961
13134
|
beatSchema,
|
|
@@ -12964,6 +13137,7 @@ export {
|
|
|
12964
13137
|
calloutParamsSchema,
|
|
12965
13138
|
canvasProblem,
|
|
12966
13139
|
canvasWarnings,
|
|
13140
|
+
captionFontSize,
|
|
12967
13141
|
check,
|
|
12968
13142
|
claimFigureParamsSchema,
|
|
12969
13143
|
codexPlanner,
|