@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/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.
|
|
@@ -911,8 +1087,7 @@ var DESTINATIONS = {
|
|
|
911
1087
|
"video-16x9": [],
|
|
912
1088
|
"short-9x16": [
|
|
913
1089
|
{ name: "YouTube Shorts", maxSeconds: 180 },
|
|
914
|
-
{ name: "Instagram Reels", maxSeconds: 180 }
|
|
915
|
-
{ name: "Facebook Reels", maxSeconds: 90 }
|
|
1090
|
+
{ name: "Instagram Reels", maxSeconds: 180 }
|
|
916
1091
|
],
|
|
917
1092
|
"post-1x1": [{ name: "X (standard account)", maxSeconds: 140 }]
|
|
918
1093
|
};
|
|
@@ -1223,54 +1398,6 @@ function clock(seconds) {
|
|
|
1223
1398
|
return m === 0 ? `${whole}s` : `${m}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1224
1399
|
}
|
|
1225
1400
|
|
|
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
1401
|
// src/emit/kit.ts
|
|
1275
1402
|
var PAD_X = 110;
|
|
1276
1403
|
var PAD_Y = 84;
|
|
@@ -1289,7 +1416,12 @@ function contentW(format) {
|
|
|
1289
1416
|
return refWidth(format) - 2 * PAD_X;
|
|
1290
1417
|
}
|
|
1291
1418
|
function contentH(format) {
|
|
1292
|
-
return refHeight(format) - 2 * PAD_Y;
|
|
1419
|
+
return refHeight(format) - 2 * PAD_Y - reserveRef(format);
|
|
1420
|
+
}
|
|
1421
|
+
function reserveRef(format) {
|
|
1422
|
+
const reserve = format.captionReserve ?? 0;
|
|
1423
|
+
if (reserve <= 0) return 0;
|
|
1424
|
+
return Math.ceil(reserve * refHeight(format) / format.height);
|
|
1293
1425
|
}
|
|
1294
1426
|
function raw(js2) {
|
|
1295
1427
|
return { __raw: js2 };
|
|
@@ -1788,8 +1920,8 @@ function charUnits(c, tabular, cjkRun, hangul) {
|
|
|
1788
1920
|
}
|
|
1789
1921
|
const measured = ADVANCE[c];
|
|
1790
1922
|
if (measured !== void 0) return [measured, true];
|
|
1791
|
-
const
|
|
1792
|
-
for (const [lo, hi, units] of BLOCK_ADVANCE) if (
|
|
1923
|
+
const cp3 = c.codePointAt(0) ?? 0;
|
|
1924
|
+
for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp3 >= lo && cp3 <= hi) return [units, false];
|
|
1793
1925
|
return [UNMEASURED, true];
|
|
1794
1926
|
}
|
|
1795
1927
|
function weightFactor(weight) {
|
|
@@ -2050,11 +2182,16 @@ function baseCss(theme, format) {
|
|
|
2050
2182
|
gate still catches anything taller than the canvas; it just now
|
|
2051
2183
|
overflows symmetrically instead of only downwards. */
|
|
2052
2184
|
.scene { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
2053
|
-
padding: ${
|
|
2185
|
+
padding: ${scenePadding(format)}; display: flex; flex-direction: column;
|
|
2054
2186
|
justify-content: center; }
|
|
2055
2187
|
${referenceSpaceCss(format)}
|
|
2056
2188
|
${AMBIENT_KEYFRAMES}`;
|
|
2057
2189
|
}
|
|
2190
|
+
function scenePadding(format) {
|
|
2191
|
+
const reserve = reserveRef(format);
|
|
2192
|
+
const base = `${PAD_Y}px ${PAD_X}px`;
|
|
2193
|
+
return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
|
|
2194
|
+
}
|
|
2058
2195
|
function referenceSpaceCss(format) {
|
|
2059
2196
|
const zoom = zoomOf(format);
|
|
2060
2197
|
if (zoom === 1) return "";
|
|
@@ -6385,6 +6522,10 @@ function planTiming(input) {
|
|
|
6385
6522
|
lang: storyboard.lang,
|
|
6386
6523
|
audioDir: segments2.length > 0 ? narration?.dir ?? "" : "",
|
|
6387
6524
|
voice: segments2.length > 0 ? narration?.voice ?? "" : "",
|
|
6525
|
+
// The format the deck was LAID OUT under, which is the only honest source:
|
|
6526
|
+
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
6527
|
+
// disagree with the box the archetypes actually drew into.
|
|
6528
|
+
captionReserve: format.captionReserve ?? 0,
|
|
6388
6529
|
scenes,
|
|
6389
6530
|
segments: segments2
|
|
6390
6531
|
};
|
|
@@ -6677,8 +6818,8 @@ function textOf(node) {
|
|
|
6677
6818
|
|
|
6678
6819
|
// src/source/harvest.ts
|
|
6679
6820
|
import { createHash as createHash5 } from "node:crypto";
|
|
6680
|
-
import { copyFile, mkdir as
|
|
6681
|
-
import { basename as basename2, join as
|
|
6821
|
+
import { copyFile, mkdir as mkdir5, rm as rm2, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
|
|
6822
|
+
import { basename as basename2, join as join5, resolve as resolve2 } from "node:path";
|
|
6682
6823
|
|
|
6683
6824
|
// src/net/fetch.ts
|
|
6684
6825
|
import { lookup as resolveHost } from "node:dns/promises";
|
|
@@ -6688,8 +6829,8 @@ import { isIP } from "node:net";
|
|
|
6688
6829
|
import { brotliDecompressSync, gunzipSync } from "node:zlib";
|
|
6689
6830
|
|
|
6690
6831
|
// src/version.ts
|
|
6691
|
-
import { createRequire } from "node:module";
|
|
6692
|
-
var VERSION =
|
|
6832
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
6833
|
+
var VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
6693
6834
|
|
|
6694
6835
|
// src/net/fetch.ts
|
|
6695
6836
|
var BLOCKED_V4 = [
|
|
@@ -6884,8 +7025,8 @@ async function send(url, target, headers, signal, opts) {
|
|
|
6884
7025
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6885
7026
|
};
|
|
6886
7027
|
try {
|
|
6887
|
-
return await new Promise((
|
|
6888
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
7028
|
+
return await new Promise((resolve8, reject) => {
|
|
7029
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve8);
|
|
6889
7030
|
req.on("error", reject);
|
|
6890
7031
|
req.end();
|
|
6891
7032
|
});
|
|
@@ -6968,10 +7109,10 @@ function message(err) {
|
|
|
6968
7109
|
}
|
|
6969
7110
|
|
|
6970
7111
|
// src/render/capture.ts
|
|
6971
|
-
import { mkdir as
|
|
6972
|
-
import { createRequire as
|
|
7112
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
7113
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
6973
7114
|
import { homedir } from "node:os";
|
|
6974
|
-
import { join as
|
|
7115
|
+
import { join as join3 } from "node:path";
|
|
6975
7116
|
import { pathToFileURL } from "node:url";
|
|
6976
7117
|
async function resolveChrome(need = "open the deck with") {
|
|
6977
7118
|
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
@@ -6982,9 +7123,9 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
6982
7123
|
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
6983
7124
|
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
6984
7125
|
};
|
|
6985
|
-
const hf = await pick(
|
|
7126
|
+
const hf = await pick(join3(homedir(), ".cache", "hyperframes", "chrome"));
|
|
6986
7127
|
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
6987
|
-
const pup = await pick(process.env.PUPPETEER_CACHE_DIR ||
|
|
7128
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer"));
|
|
6988
7129
|
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
6989
7130
|
throw new Error(
|
|
6990
7131
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
@@ -6994,15 +7135,15 @@ async function chromePath(need = "open the deck with") {
|
|
|
6994
7135
|
return (await resolveChrome(need)).path;
|
|
6995
7136
|
}
|
|
6996
7137
|
function runtimePath() {
|
|
6997
|
-
return
|
|
7138
|
+
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
6998
7139
|
}
|
|
6999
7140
|
function renderSeek(t2) {
|
|
7000
7141
|
const player = window.__player;
|
|
7001
7142
|
player.renderSeek(t2, { suppressEvents: true });
|
|
7002
7143
|
}
|
|
7003
7144
|
async function openDeck(dir, opts = {}) {
|
|
7004
|
-
const index =
|
|
7005
|
-
const html = await
|
|
7145
|
+
const index = join3(dir, "index.html");
|
|
7146
|
+
const html = await readFile4(index, "utf8").catch(() => null);
|
|
7006
7147
|
if (html === null) throw new Error(`no index.html in ${dir}`);
|
|
7007
7148
|
const width = Number(/data-width="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
7008
7149
|
const height = Number(/data-height="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
@@ -7080,13 +7221,13 @@ async function openDeck(dir, opts = {}) {
|
|
|
7080
7221
|
|
|
7081
7222
|
// src/source/assets.ts
|
|
7082
7223
|
import { createHash as createHash4 } from "node:crypto";
|
|
7083
|
-
import { mkdir as
|
|
7084
|
-
import { extname as extname2, join as
|
|
7224
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
7225
|
+
import { extname as extname2, join as join4 } from "node:path";
|
|
7085
7226
|
import { z as z2 } from "zod";
|
|
7086
7227
|
async function fetchFigures(source, dir, warnings) {
|
|
7087
7228
|
const drops = warnings ?? [];
|
|
7088
|
-
await
|
|
7089
|
-
const cached2 = await
|
|
7229
|
+
await mkdir4(dir, { recursive: true });
|
|
7230
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
7090
7231
|
const figures = [];
|
|
7091
7232
|
for (const figure of source.figures) {
|
|
7092
7233
|
if (figure.kind === "clip") {
|
|
@@ -7111,11 +7252,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
7111
7252
|
async function localize(figure, dir, cached2) {
|
|
7112
7253
|
const stem = assetStem(figure.id, figure.src);
|
|
7113
7254
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7114
|
-
const bytes = hit ? await
|
|
7255
|
+
const bytes = hit ? await readFile5(join4(dir, hit)) : await load(figure.src);
|
|
7115
7256
|
const size2 = imageSize(bytes);
|
|
7116
7257
|
if (hit) return { ...figure, src: hit, ...size2 };
|
|
7117
7258
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
7118
|
-
await
|
|
7259
|
+
await writeFile4(join4(dir, src), bytes);
|
|
7119
7260
|
cached2.push(src);
|
|
7120
7261
|
return { ...figure, src, ...size2 };
|
|
7121
7262
|
}
|
|
@@ -7140,7 +7281,7 @@ function assetExt(bytes, src) {
|
|
|
7140
7281
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
7141
7282
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
7142
7283
|
async function load(src) {
|
|
7143
|
-
if (!/^https?:/i.test(src)) return
|
|
7284
|
+
if (!/^https?:/i.test(src)) return readFile5(src);
|
|
7144
7285
|
const got = await fetchGuarded(src, {
|
|
7145
7286
|
maxBytes: FIGURE_MAX_BYTES,
|
|
7146
7287
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -7676,7 +7817,7 @@ function readContentRegion() {
|
|
|
7676
7817
|
}
|
|
7677
7818
|
|
|
7678
7819
|
// src/source/transcode.ts
|
|
7679
|
-
import { readFile as
|
|
7820
|
+
import { readFile as readFile6, rm } from "node:fs/promises";
|
|
7680
7821
|
import { basename } from "node:path";
|
|
7681
7822
|
|
|
7682
7823
|
// src/render/ffmpeg.ts
|
|
@@ -7702,7 +7843,7 @@ ${tail3}`);
|
|
|
7702
7843
|
}
|
|
7703
7844
|
}
|
|
7704
7845
|
function runLive(file, args) {
|
|
7705
|
-
return new Promise((
|
|
7846
|
+
return new Promise((resolve8, reject) => {
|
|
7706
7847
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7707
7848
|
child.on("error", (err) => {
|
|
7708
7849
|
reject(
|
|
@@ -7710,7 +7851,7 @@ function runLive(file, args) {
|
|
|
7710
7851
|
);
|
|
7711
7852
|
});
|
|
7712
7853
|
child.on("close", (code, signal) => {
|
|
7713
|
-
if (code === 0)
|
|
7854
|
+
if (code === 0) resolve8();
|
|
7714
7855
|
else if (signal) {
|
|
7715
7856
|
reject(
|
|
7716
7857
|
new Error(
|
|
@@ -7837,11 +7978,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
7837
7978
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
7838
7979
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
7839
7980
|
// three-line band covers the bottom of the slide.
|
|
7840
|
-
|
|
7981
|
+
//
|
|
7982
|
+
// THE RATIOS ARE NOT HERE. They are in src/types.ts beside `bandReserve`,
|
|
7983
|
+
// because `src/emit` has to reserve exactly the strip this draws into and
|
|
7984
|
+
// may not import this file. A local copy of 0.037 is how the two drift back
|
|
7985
|
+
// apart, which is the bug this whole path exists to close.
|
|
7986
|
+
fontSize: captionFontSize(width),
|
|
7841
7987
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
7842
7988
|
// player draws across the bottom eighth of the frame.
|
|
7843
|
-
marginV: Math.round(height *
|
|
7844
|
-
marginX: Math.round(width *
|
|
7989
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
7990
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
7845
7991
|
};
|
|
7846
7992
|
}
|
|
7847
7993
|
|
|
@@ -7907,7 +8053,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7907
8053
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7908
8054
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7909
8055
|
const name = basename(input);
|
|
7910
|
-
const source = videoSize(await
|
|
8056
|
+
const source = videoSize(await readFile6(input));
|
|
7911
8057
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7912
8058
|
const kept = (why3) => ({
|
|
7913
8059
|
path: input,
|
|
@@ -7939,7 +8085,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7939
8085
|
}
|
|
7940
8086
|
let measured;
|
|
7941
8087
|
try {
|
|
7942
|
-
measured = videoSize(await
|
|
8088
|
+
measured = videoSize(await readFile6(out));
|
|
7943
8089
|
} catch (err) {
|
|
7944
8090
|
await rm(out, { force: true });
|
|
7945
8091
|
return kept(
|
|
@@ -7975,8 +8121,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7975
8121
|
async function harvest(url, dir, opts = {}) {
|
|
7976
8122
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7977
8123
|
const warnings = [];
|
|
7978
|
-
const assets =
|
|
7979
|
-
await
|
|
8124
|
+
const assets = resolve2(dir);
|
|
8125
|
+
await mkdir5(assets, { recursive: true });
|
|
7980
8126
|
const page = await fetchGuarded(url, {
|
|
7981
8127
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
7982
8128
|
timeoutMs,
|
|
@@ -8358,8 +8504,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8358
8504
|
`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`
|
|
8359
8505
|
);
|
|
8360
8506
|
}
|
|
8361
|
-
const path2 =
|
|
8362
|
-
await
|
|
8507
|
+
const path2 = join5(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8508
|
+
await writeFile5(path2, bytes);
|
|
8363
8509
|
assets.push(path2);
|
|
8364
8510
|
got = { path: path2, width: size2.width, height: size2.height };
|
|
8365
8511
|
} catch (err) {
|
|
@@ -8371,7 +8517,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8371
8517
|
};
|
|
8372
8518
|
const shrink = async (url, path2, measured, what) => {
|
|
8373
8519
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8374
|
-
const out2 =
|
|
8520
|
+
const out2 = join5(dir, assetName(url, ".vp9.webm"));
|
|
8375
8521
|
let small;
|
|
8376
8522
|
try {
|
|
8377
8523
|
small = await transcode(path2, out2, {
|
|
@@ -8413,8 +8559,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8413
8559
|
try {
|
|
8414
8560
|
const bytes = await bytesOf(url);
|
|
8415
8561
|
const measured = videoSize(bytes);
|
|
8416
|
-
const path2 =
|
|
8417
|
-
await
|
|
8562
|
+
const path2 = join5(dir, assetName(url, `.${measured.container}`));
|
|
8563
|
+
await writeFile5(path2, bytes);
|
|
8418
8564
|
return await shrink(url, path2, measured, what);
|
|
8419
8565
|
} catch (err) {
|
|
8420
8566
|
warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
|
|
@@ -8503,7 +8649,7 @@ function unheld(policy) {
|
|
|
8503
8649
|
return "the file could not be held";
|
|
8504
8650
|
}
|
|
8505
8651
|
async function sizeOf(path2) {
|
|
8506
|
-
return await
|
|
8652
|
+
return await stat2(path2).then(
|
|
8507
8653
|
(s) => s.size,
|
|
8508
8654
|
() => 0
|
|
8509
8655
|
);
|
|
@@ -8711,8 +8857,8 @@ function sane(seconds) {
|
|
|
8711
8857
|
}
|
|
8712
8858
|
async function attachClips(source, clips, dir) {
|
|
8713
8859
|
if (clips.length === 0) return source;
|
|
8714
|
-
const assets =
|
|
8715
|
-
await
|
|
8860
|
+
const assets = resolve2(dir);
|
|
8861
|
+
await mkdir5(assets, { recursive: true });
|
|
8716
8862
|
const figures = [...source.figures];
|
|
8717
8863
|
for (const clip3 of clips) {
|
|
8718
8864
|
const src = clip3.file ? await adopt(clip3.file, assets) : void 0;
|
|
@@ -8743,7 +8889,7 @@ async function attachClips(source, clips, dir) {
|
|
|
8743
8889
|
}
|
|
8744
8890
|
async function adopt(file, dir) {
|
|
8745
8891
|
const name = basename2(file);
|
|
8746
|
-
await copyFile(file,
|
|
8892
|
+
await copyFile(file, join5(dir, name));
|
|
8747
8893
|
return name;
|
|
8748
8894
|
}
|
|
8749
8895
|
function freeId(figures) {
|
|
@@ -8820,9 +8966,9 @@ ${fence}`;
|
|
|
8820
8966
|
|
|
8821
8967
|
// src/plan/codex.ts
|
|
8822
8968
|
import { spawn as spawn2 } from "node:child_process";
|
|
8823
|
-
import { mkdtemp, readFile as
|
|
8969
|
+
import { mkdtemp, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
|
|
8824
8970
|
import { tmpdir } from "node:os";
|
|
8825
|
-
import { join as
|
|
8971
|
+
import { join as join6 } from "node:path";
|
|
8826
8972
|
import { z as z3 } from "zod";
|
|
8827
8973
|
|
|
8828
8974
|
// src/plan/arc.ts
|
|
@@ -9941,11 +10087,11 @@ function schemaFor(prefs) {
|
|
|
9941
10087
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9942
10088
|
async function codexPlanner(source, opts = {}) {
|
|
9943
10089
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9944
|
-
const dir = await mkdtemp(
|
|
10090
|
+
const dir = await mkdtemp(join6(tmpdir(), "decksmith-plan-"));
|
|
9945
10091
|
try {
|
|
9946
|
-
const schemaPath =
|
|
9947
|
-
const outPath =
|
|
9948
|
-
await
|
|
10092
|
+
const schemaPath = join6(dir, "storyboard.schema.json");
|
|
10093
|
+
const outPath = join6(dir, "storyboard.json");
|
|
10094
|
+
await writeFile6(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9949
10095
|
await (opts.run ?? runCodex)({
|
|
9950
10096
|
prompt: buildPrompt(source, prefs),
|
|
9951
10097
|
schemaPath,
|
|
@@ -9953,7 +10099,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
9953
10099
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
9954
10100
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
9955
10101
|
});
|
|
9956
|
-
const raw2 = await
|
|
10102
|
+
const raw2 = await readFile7(outPath, "utf8").catch(() => "");
|
|
9957
10103
|
if (raw2.trim() === "") {
|
|
9958
10104
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
9959
10105
|
}
|
|
@@ -10026,7 +10172,7 @@ function codexCommand(args) {
|
|
|
10026
10172
|
}
|
|
10027
10173
|
function runCodex(args) {
|
|
10028
10174
|
const { argv, env } = codexCommand(args);
|
|
10029
|
-
return new Promise((
|
|
10175
|
+
return new Promise((resolve8, reject) => {
|
|
10030
10176
|
const child = spawn2("codex", argv, {
|
|
10031
10177
|
stdio: ["pipe", "ignore", "pipe"],
|
|
10032
10178
|
...env === void 0 ? {} : { env }
|
|
@@ -10049,7 +10195,7 @@ function runCodex(args) {
|
|
|
10049
10195
|
});
|
|
10050
10196
|
child.on("close", (code) => {
|
|
10051
10197
|
clearTimeout(timer);
|
|
10052
|
-
if (code === 0) return
|
|
10198
|
+
if (code === 0) return resolve8();
|
|
10053
10199
|
reject(
|
|
10054
10200
|
new Error(`codex exec exited ${code}.
|
|
10055
10201
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -10061,14 +10207,14 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
10061
10207
|
|
|
10062
10208
|
// src/images/illustrate.ts
|
|
10063
10209
|
import { createHash as createHash7 } from "node:crypto";
|
|
10064
|
-
import { mkdir as
|
|
10065
|
-
import { join as
|
|
10210
|
+
import { mkdir as mkdir6, readFile as readFile9, rename, rm as rm5, writeFile as writeFile8 } from "node:fs/promises";
|
|
10211
|
+
import { join as join8 } from "node:path";
|
|
10066
10212
|
|
|
10067
10213
|
// src/images/providers.ts
|
|
10068
10214
|
import { createHash as createHash6 } from "node:crypto";
|
|
10069
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
10215
|
+
import { mkdtemp as mkdtemp2, readFile as readFile8, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
10070
10216
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
10071
|
-
import { join as
|
|
10217
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
10072
10218
|
import { z as z4 } from "zod";
|
|
10073
10219
|
var SIZE = {
|
|
10074
10220
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -10200,11 +10346,11 @@ function codexImages(opts = {}) {
|
|
|
10200
10346
|
async check() {
|
|
10201
10347
|
},
|
|
10202
10348
|
async generate(req) {
|
|
10203
|
-
const dir = await mkdtemp2(
|
|
10349
|
+
const dir = await mkdtemp2(join7(tmpdir2(), "decksmith-image-"));
|
|
10204
10350
|
try {
|
|
10205
|
-
const schemaPath =
|
|
10206
|
-
const outPath =
|
|
10207
|
-
await
|
|
10351
|
+
const schemaPath = join7(dir, "answer.schema.json");
|
|
10352
|
+
const outPath = join7(dir, "answer.json");
|
|
10353
|
+
await writeFile7(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
10208
10354
|
await run4({
|
|
10209
10355
|
prompt: codexPrompt(req),
|
|
10210
10356
|
schemaPath,
|
|
@@ -10213,7 +10359,7 @@ function codexImages(opts = {}) {
|
|
|
10213
10359
|
cwd: dir,
|
|
10214
10360
|
sandbox: "workspace-write"
|
|
10215
10361
|
});
|
|
10216
|
-
const raw2 = await
|
|
10362
|
+
const raw2 = await readFile8(outPath, "utf8").catch(() => "");
|
|
10217
10363
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
10218
10364
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
10219
10365
|
if (!answer.data.ok) {
|
|
@@ -10221,10 +10367,10 @@ function codexImages(opts = {}) {
|
|
|
10221
10367
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
10222
10368
|
);
|
|
10223
10369
|
}
|
|
10224
|
-
const candidates2 = [
|
|
10225
|
-
if (answer.data.file) candidates2.push(
|
|
10370
|
+
const candidates2 = [join7(dir, "picture.png")];
|
|
10371
|
+
if (answer.data.file) candidates2.push(resolve3(dir, answer.data.file));
|
|
10226
10372
|
for (const path2 of candidates2) {
|
|
10227
|
-
const bytes = await
|
|
10373
|
+
const bytes = await readFile8(path2).catch(() => null);
|
|
10228
10374
|
if (bytes) return raster(bytes, "codex");
|
|
10229
10375
|
}
|
|
10230
10376
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -10356,7 +10502,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
10356
10502
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
10357
10503
|
if (tool !== last) chain.push(tool);
|
|
10358
10504
|
const dropped = /* @__PURE__ */ new Set();
|
|
10359
|
-
if (pending.length > 0) await
|
|
10505
|
+
if (pending.length > 0) await mkdir6(opts.assetsDir, { recursive: true });
|
|
10360
10506
|
for (const [i, slot] of pending.entries()) {
|
|
10361
10507
|
let rungs = chain;
|
|
10362
10508
|
if (i >= images.max) {
|
|
@@ -10453,16 +10599,16 @@ function cacheKey(providerId, req) {
|
|
|
10453
10599
|
async function draw(provider, req, name, dir) {
|
|
10454
10600
|
for (const ext of Object.values(EXT2)) {
|
|
10455
10601
|
const src2 = `${name}${ext}`;
|
|
10456
|
-
const bytes = await
|
|
10602
|
+
const bytes = await readFile9(join8(dir, src2)).catch(() => null);
|
|
10457
10603
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
10458
10604
|
}
|
|
10459
10605
|
await provider.check();
|
|
10460
10606
|
const img = await provider.generate(req);
|
|
10461
10607
|
const src = `${name}${EXT2[img.mime]}`;
|
|
10462
|
-
const tmp =
|
|
10608
|
+
const tmp = join8(dir, `.${name}.tmp`);
|
|
10463
10609
|
try {
|
|
10464
|
-
await
|
|
10465
|
-
await rename(tmp,
|
|
10610
|
+
await writeFile8(tmp, img.bytes);
|
|
10611
|
+
await rename(tmp, join8(dir, src));
|
|
10466
10612
|
} finally {
|
|
10467
10613
|
await rm5(tmp, { force: true });
|
|
10468
10614
|
}
|
|
@@ -10475,9 +10621,9 @@ function sizeOf2(bytes, ext) {
|
|
|
10475
10621
|
// src/narrate/tts.ts
|
|
10476
10622
|
import { spawn as spawn3 } from "node:child_process";
|
|
10477
10623
|
import { createHash as createHash8 } from "node:crypto";
|
|
10478
|
-
import { mkdir as
|
|
10624
|
+
import { mkdir as mkdir7, readFile as readFile10, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
|
|
10479
10625
|
import { homedir as homedir2 } from "node:os";
|
|
10480
|
-
import { join as
|
|
10626
|
+
import { join as join9 } from "node:path";
|
|
10481
10627
|
var MISSING = [
|
|
10482
10628
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
10483
10629
|
"",
|
|
@@ -10494,8 +10640,8 @@ function candidates() {
|
|
|
10494
10640
|
["edge-tts"],
|
|
10495
10641
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
10496
10642
|
// default, and both are where this actually lands in practice.
|
|
10497
|
-
[
|
|
10498
|
-
[
|
|
10643
|
+
[join9(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10644
|
+
[join9(home, ".local", "bin", "edge-tts")],
|
|
10499
10645
|
// Last resort: the module is installed even though its console script is
|
|
10500
10646
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
10501
10647
|
["python3", "-m", "edge_tts"]
|
|
@@ -10520,7 +10666,7 @@ async function find2(can) {
|
|
|
10520
10666
|
throw new Error(MISSING);
|
|
10521
10667
|
}
|
|
10522
10668
|
function runArgv(cmd, args) {
|
|
10523
|
-
return new Promise((
|
|
10669
|
+
return new Promise((resolve8) => {
|
|
10524
10670
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10525
10671
|
let stdout = "";
|
|
10526
10672
|
let stderr = "";
|
|
@@ -10530,8 +10676,8 @@ function runArgv(cmd, args) {
|
|
|
10530
10676
|
child.stderr.on("data", (b) => {
|
|
10531
10677
|
stderr += b.toString();
|
|
10532
10678
|
});
|
|
10533
|
-
child.on("error", (e) =>
|
|
10534
|
-
child.on("close", (code) =>
|
|
10679
|
+
child.on("error", (e) => resolve8({ code: -1, stderr: String(e), stdout }));
|
|
10680
|
+
child.on("close", (code) => resolve8({ code: code ?? -1, stderr, stdout }));
|
|
10535
10681
|
});
|
|
10536
10682
|
}
|
|
10537
10683
|
var edgeTts = {
|
|
@@ -10598,7 +10744,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
10598
10744
|
async speak(req) {
|
|
10599
10745
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
10600
10746
|
await runner.speak({ ...req, subs });
|
|
10601
|
-
const raw2 = await
|
|
10747
|
+
const raw2 = await readFile10(subs, "utf8").catch(() => "");
|
|
10602
10748
|
await rm6(subs, { force: true });
|
|
10603
10749
|
const cues = parseCues(raw2);
|
|
10604
10750
|
const measured = await runner.measure(req.audio);
|
|
@@ -10628,11 +10774,11 @@ async function synthesize(text2, opts) {
|
|
|
10628
10774
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10629
10775
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10630
10776
|
const file = `${key}.mp3`;
|
|
10631
|
-
const audio =
|
|
10632
|
-
const sidecar =
|
|
10777
|
+
const audio = join9(opts.dir, file);
|
|
10778
|
+
const sidecar = join9(opts.dir, `${key}.json`);
|
|
10633
10779
|
const cached2 = await readSidecar(sidecar);
|
|
10634
10780
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10635
|
-
await
|
|
10781
|
+
await mkdir7(opts.dir, { recursive: true });
|
|
10636
10782
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10637
10783
|
const { cues, seconds } = spoken;
|
|
10638
10784
|
const clamped = cues.map((c) => ({
|
|
@@ -10641,13 +10787,13 @@ async function synthesize(text2, opts) {
|
|
|
10641
10787
|
text: c.text
|
|
10642
10788
|
}));
|
|
10643
10789
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
10644
|
-
await
|
|
10790
|
+
await writeFile9(sidecar, `${JSON.stringify(body, null, 2)}
|
|
10645
10791
|
`);
|
|
10646
10792
|
return { audio, file, seconds, cues: clamped };
|
|
10647
10793
|
}
|
|
10648
10794
|
async function readSidecar(path2) {
|
|
10649
10795
|
try {
|
|
10650
|
-
const parsed = JSON.parse(await
|
|
10796
|
+
const parsed = JSON.parse(await readFile10(path2, "utf8"));
|
|
10651
10797
|
const s = parsed;
|
|
10652
10798
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
10653
10799
|
} catch {
|
|
@@ -10770,8 +10916,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
10770
10916
|
}
|
|
10771
10917
|
|
|
10772
10918
|
// src/verify/index.ts
|
|
10773
|
-
import { readdir as
|
|
10774
|
-
import { join as
|
|
10919
|
+
import { readdir as readdir4, readFile as readFile14 } from "node:fs/promises";
|
|
10920
|
+
import { join as join13, relative, sep } from "node:path";
|
|
10775
10921
|
|
|
10776
10922
|
// src/verify/budget.ts
|
|
10777
10923
|
function readCanvas(html) {
|
|
@@ -10787,13 +10933,13 @@ function readCanvas(html) {
|
|
|
10787
10933
|
if (width === void 0 || height === void 0 || seconds === void 0) return void 0;
|
|
10788
10934
|
return { width, height, seconds };
|
|
10789
10935
|
}
|
|
10790
|
-
function profilesFor(width, height) {
|
|
10791
|
-
return Object.values(
|
|
10936
|
+
function profilesFor(width, height, formats = FORMATS) {
|
|
10937
|
+
return Object.values(formats).filter((f) => f.width === width && f.height === height);
|
|
10792
10938
|
}
|
|
10793
|
-
function scanBudget(html, storyboard, kept) {
|
|
10939
|
+
function scanBudget(html, storyboard, kept, formats = FORMATS) {
|
|
10794
10940
|
const canvas = readCanvas(html);
|
|
10795
10941
|
if (!canvas) return [];
|
|
10796
|
-
const profiles = profilesFor(canvas.width, canvas.height);
|
|
10942
|
+
const profiles = profilesFor(canvas.width, canvas.height, formats);
|
|
10797
10943
|
if (profiles.length === 0) {
|
|
10798
10944
|
return [
|
|
10799
10945
|
{
|
|
@@ -10878,8 +11024,8 @@ function clock2(seconds) {
|
|
|
10878
11024
|
|
|
10879
11025
|
// src/verify/check.ts
|
|
10880
11026
|
import { execFile as execFile2 } from "node:child_process";
|
|
10881
|
-
import { readFile as
|
|
10882
|
-
import { join as
|
|
11027
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
11028
|
+
import { join as join10 } from "node:path";
|
|
10883
11029
|
import { promisify as promisify2 } from "node:util";
|
|
10884
11030
|
var run2 = promisify2(execFile2);
|
|
10885
11031
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -10909,7 +11055,7 @@ async function check(dir, opts = {}) {
|
|
|
10909
11055
|
async function readComposition(dir) {
|
|
10910
11056
|
let html;
|
|
10911
11057
|
try {
|
|
10912
|
-
html = await
|
|
11058
|
+
html = await readFile11(join10(dir, "index.html"), "utf8");
|
|
10913
11059
|
} catch {
|
|
10914
11060
|
return { transit: [], duration: 0 };
|
|
10915
11061
|
}
|
|
@@ -11060,8 +11206,8 @@ function tail(s) {
|
|
|
11060
11206
|
}
|
|
11061
11207
|
|
|
11062
11208
|
// src/verify/fidelity.ts
|
|
11063
|
-
import { readFile as
|
|
11064
|
-
import { join as
|
|
11209
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
11210
|
+
import { join as join11 } from "node:path";
|
|
11065
11211
|
|
|
11066
11212
|
// src/verify/typefloor.ts
|
|
11067
11213
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11317,6 +11463,16 @@ var INK_FLOOR = 15e-4;
|
|
|
11317
11463
|
var INK_DELTA = 12;
|
|
11318
11464
|
var FALLBACK_BAND_TOP = 0.22;
|
|
11319
11465
|
var CAPTION = ".headline, .eyebrow";
|
|
11466
|
+
var RESERVE_INK_TOLERANCE = 1e-4;
|
|
11467
|
+
function readReserve(timingText) {
|
|
11468
|
+
if (!timingText) return 0;
|
|
11469
|
+
try {
|
|
11470
|
+
const reserve = JSON.parse(timingText).captionReserve;
|
|
11471
|
+
return typeof reserve === "number" && reserve > 0 ? reserve : 0;
|
|
11472
|
+
} catch {
|
|
11473
|
+
return 0;
|
|
11474
|
+
}
|
|
11475
|
+
}
|
|
11320
11476
|
async function decodePng(png) {
|
|
11321
11477
|
const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
|
|
11322
11478
|
if (buf.length < 8 || buf.readUInt32BE(0) !== 2303741511) throw new Error("not a PNG");
|
|
@@ -11483,6 +11639,25 @@ function gradeFidelity(rows, floor = INK_FLOOR) {
|
|
|
11483
11639
|
};
|
|
11484
11640
|
});
|
|
11485
11641
|
}
|
|
11642
|
+
function gradeReserve(rows, tolerance = RESERVE_INK_TOLERANCE) {
|
|
11643
|
+
const dirty = /* @__PURE__ */ new Map();
|
|
11644
|
+
for (const row of rows) {
|
|
11645
|
+
if (row.reserveInk === void 0 || row.reserveInk <= tolerance) continue;
|
|
11646
|
+
const seen = dirty.get(row.sid);
|
|
11647
|
+
if (seen) seen.push(row);
|
|
11648
|
+
else dirty.set(row.sid, [row]);
|
|
11649
|
+
}
|
|
11650
|
+
const pct = (v) => `${(100 * v).toFixed(3)}%`;
|
|
11651
|
+
return [...dirty].map(([sid, stops]) => {
|
|
11652
|
+
const worst = stops.reduce((a, b) => (a.reserveInk ?? 0) >= (b.reserveInk ?? 0) ? a : b);
|
|
11653
|
+
return {
|
|
11654
|
+
severity: "error",
|
|
11655
|
+
gate: "fidelity",
|
|
11656
|
+
rule: "ink_in_caption_reserve",
|
|
11657
|
+
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(", ")}.`
|
|
11658
|
+
};
|
|
11659
|
+
});
|
|
11660
|
+
}
|
|
11486
11661
|
function captionBottom(sid, selector, fallbackPx) {
|
|
11487
11662
|
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
11488
11663
|
let bottom = 0;
|
|
@@ -11507,11 +11682,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
11507
11682
|
],
|
|
11508
11683
|
elapsedMs: Date.now() - started
|
|
11509
11684
|
});
|
|
11510
|
-
const
|
|
11511
|
-
|
|
11512
|
-
await readFile11(join10(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11513
|
-
);
|
|
11685
|
+
const timingText = await readFile12(join11(dir, TIMING_FILE), "utf8").catch(() => null);
|
|
11686
|
+
const stops = opts.stops ?? readStops(timingText, await readFile12(join11(dir, DECK_PAGE), "utf8").catch(() => null));
|
|
11514
11687
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
11688
|
+
const reserve = opts.captionReserve ?? readReserve(timingText);
|
|
11515
11689
|
let deck = null;
|
|
11516
11690
|
try {
|
|
11517
11691
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
@@ -11540,7 +11714,13 @@ async function fidelity(dir, opts = {}) {
|
|
|
11540
11714
|
measured.push({
|
|
11541
11715
|
...stop,
|
|
11542
11716
|
ink: inkBelow(frame, bandTopPx),
|
|
11543
|
-
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
11717
|
+
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3,
|
|
11718
|
+
// THE SAME FRAME, a second strip. Free: it is one more pass over pixels
|
|
11719
|
+
// that are already decoded, so the gate that stops the caption
|
|
11720
|
+
// collision regressing costs no extra capture, no extra seek and no
|
|
11721
|
+
// extra browser. That matters — a gate with its own capture is a gate
|
|
11722
|
+
// someone turns off.
|
|
11723
|
+
...reserve > 0 ? { reserveInk: inkBelow(frame, height - reserve) } : {}
|
|
11544
11724
|
});
|
|
11545
11725
|
}
|
|
11546
11726
|
for (const mid of midpoints(stops)) {
|
|
@@ -11551,6 +11731,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
11551
11731
|
stops: measured,
|
|
11552
11732
|
findings: [
|
|
11553
11733
|
...gradeFidelity(measured, floor),
|
|
11734
|
+
...gradeReserve(measured),
|
|
11554
11735
|
...gradeOverprint(collided),
|
|
11555
11736
|
...gradeApparent(apparent)
|
|
11556
11737
|
],
|
|
@@ -11567,9 +11748,9 @@ async function fidelity(dir, opts = {}) {
|
|
|
11567
11748
|
// src/verify/drift.ts
|
|
11568
11749
|
import { execFile as execFile3 } from "node:child_process";
|
|
11569
11750
|
import { createHash as createHash9 } from "node:crypto";
|
|
11570
|
-
import { mkdtemp as mkdtemp3, readdir as
|
|
11751
|
+
import { mkdtemp as mkdtemp3, readdir as readdir3, readFile as readFile13, rm as rm7 } from "node:fs/promises";
|
|
11571
11752
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
11572
|
-
import { join as
|
|
11753
|
+
import { join as join12 } from "node:path";
|
|
11573
11754
|
import { promisify as promisify3 } from "node:util";
|
|
11574
11755
|
var run3 = promisify3(execFile3);
|
|
11575
11756
|
var FLOOR_DB = 40;
|
|
@@ -11608,9 +11789,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
11608
11789
|
async function drift(dir, opts = {}) {
|
|
11609
11790
|
const mode = opts.mode ?? "psnr";
|
|
11610
11791
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
11611
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
11612
|
-
const a =
|
|
11613
|
-
const b =
|
|
11792
|
+
const work = opts.workDir ?? await mkdtemp3(join12(tmpdir3(), "decksmith-drift-"));
|
|
11793
|
+
const a = join12(work, "a");
|
|
11794
|
+
const b = join12(work, "b");
|
|
11614
11795
|
const report = await compare(dir, a, b, mode, floorDb, opts);
|
|
11615
11796
|
const keep = opts.keep || !report.passed;
|
|
11616
11797
|
if (keep) report.kept = { a, b };
|
|
@@ -11665,11 +11846,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
11665
11846
|
const differing = [];
|
|
11666
11847
|
const ha = [];
|
|
11667
11848
|
for (const [i, name] of fa.entries()) {
|
|
11668
|
-
const one = await sha(
|
|
11849
|
+
const one = await sha(join12(a, name));
|
|
11669
11850
|
ha.push(one);
|
|
11670
|
-
if (one !== await sha(
|
|
11851
|
+
if (one !== await sha(join12(b, name))) differing.push(i + 1);
|
|
11671
11852
|
}
|
|
11672
|
-
const html = await
|
|
11853
|
+
const html = await readFile13(join12(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
11673
11854
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
11674
11855
|
let worst;
|
|
11675
11856
|
if (differing.length > 0) {
|
|
@@ -11780,11 +11961,11 @@ async function render(dir, out, opts) {
|
|
|
11780
11961
|
}
|
|
11781
11962
|
}
|
|
11782
11963
|
async function frames(dir) {
|
|
11783
|
-
const names = await
|
|
11964
|
+
const names = await readdir3(dir).catch(() => []);
|
|
11784
11965
|
return names.filter((n3) => n3.endsWith(".png")).sort();
|
|
11785
11966
|
}
|
|
11786
11967
|
async function sha(file) {
|
|
11787
|
-
return createHash9("sha256").update(await
|
|
11968
|
+
return createHash9("sha256").update(await readFile13(file)).digest("hex");
|
|
11788
11969
|
}
|
|
11789
11970
|
var STAT = /^n:(\d+)\b.*\bpsnr_avg:(inf|-?[\d.]+)/;
|
|
11790
11971
|
function framePattern(name) {
|
|
@@ -11817,11 +11998,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
11817
11998
|
"-start_number",
|
|
11818
11999
|
String(seq.start),
|
|
11819
12000
|
"-i",
|
|
11820
|
-
|
|
12001
|
+
join12(a, seq.pattern),
|
|
11821
12002
|
"-start_number",
|
|
11822
12003
|
String(seq.start),
|
|
11823
12004
|
"-i",
|
|
11824
|
-
|
|
12005
|
+
join12(b, seq.pattern),
|
|
11825
12006
|
"-lavfi",
|
|
11826
12007
|
"psnr=stats_file=-",
|
|
11827
12008
|
"-f",
|
|
@@ -11896,7 +12077,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
11896
12077
|
const html = await readCompositions(dir);
|
|
11897
12078
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
11898
12079
|
const narration = scanNarration(
|
|
11899
|
-
await
|
|
12080
|
+
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
11900
12081
|
await listFiles(dir)
|
|
11901
12082
|
);
|
|
11902
12083
|
const budget2 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -11933,8 +12114,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
11933
12114
|
}
|
|
11934
12115
|
async function declaredStops(dir) {
|
|
11935
12116
|
return readStops(
|
|
11936
|
-
await
|
|
11937
|
-
await
|
|
12117
|
+
await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12118
|
+
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11938
12119
|
);
|
|
11939
12120
|
}
|
|
11940
12121
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -11974,7 +12155,7 @@ function scanNarration(page, files) {
|
|
|
11974
12155
|
];
|
|
11975
12156
|
}
|
|
11976
12157
|
async function readTiming(dir) {
|
|
11977
|
-
const raw2 = await
|
|
12158
|
+
const raw2 = await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
11978
12159
|
if (!raw2) return void 0;
|
|
11979
12160
|
try {
|
|
11980
12161
|
const parsed = JSON.parse(raw2);
|
|
@@ -11984,9 +12165,9 @@ async function readTiming(dir) {
|
|
|
11984
12165
|
}
|
|
11985
12166
|
}
|
|
11986
12167
|
async function listFiles(dir) {
|
|
11987
|
-
const entries = await
|
|
12168
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
11988
12169
|
return new Set(
|
|
11989
|
-
entries.filter((e) => e.isFile()).map((e) => relative(dir,
|
|
12170
|
+
entries.filter((e) => e.isFile()).map((e) => relative(dir, join13(e.parentPath, e.name)).split(sep).join("/"))
|
|
11990
12171
|
);
|
|
11991
12172
|
}
|
|
11992
12173
|
var INSTEAD = {
|
|
@@ -12242,22 +12423,22 @@ function scanDeterminism(html, file) {
|
|
|
12242
12423
|
return findings;
|
|
12243
12424
|
}
|
|
12244
12425
|
async function readCompositions(dir) {
|
|
12245
|
-
const entries = await
|
|
12426
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12246
12427
|
const files = entries.filter(
|
|
12247
12428
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12248
|
-
).map((e) =>
|
|
12429
|
+
).map((e) => join13(e.parentPath, e.name));
|
|
12249
12430
|
return Promise.all(
|
|
12250
|
-
files.map(async (f) => [relative(dir, f), await
|
|
12431
|
+
files.map(async (f) => [relative(dir, f), await readFile14(f, "utf8")])
|
|
12251
12432
|
);
|
|
12252
12433
|
}
|
|
12253
12434
|
|
|
12254
12435
|
// src/render/render.ts
|
|
12255
|
-
import { mkdir as
|
|
12256
|
-
import { basename as basename3, dirname, join as
|
|
12436
|
+
import { mkdir as mkdir9, readFile as readFile15, rename as rename2, rm as rm8, writeFile as writeFile11 } from "node:fs/promises";
|
|
12437
|
+
import { basename as basename3, dirname as dirname2, join as join15, resolve as resolve4 } from "node:path";
|
|
12257
12438
|
|
|
12258
12439
|
// src/render/captions.ts
|
|
12259
|
-
import { mkdir as
|
|
12260
|
-
import { join as
|
|
12440
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
|
|
12441
|
+
import { join as join14 } from "node:path";
|
|
12261
12442
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
12262
12443
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
12263
12444
|
function cssString(value) {
|
|
@@ -12348,11 +12529,11 @@ async function captionBlocker() {
|
|
|
12348
12529
|
}
|
|
12349
12530
|
async function renderCaptions(cues, style, deck, work) {
|
|
12350
12531
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
12351
|
-
const fontCss =
|
|
12532
|
+
const fontCss = join14(deck, DECK_FONT_CSS);
|
|
12352
12533
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL2(fontCss).href : null;
|
|
12353
|
-
const page =
|
|
12354
|
-
await
|
|
12355
|
-
await
|
|
12534
|
+
const page = join14(work, "captions.html");
|
|
12535
|
+
await mkdir8(work, { recursive: true });
|
|
12536
|
+
await writeFile10(page, captionPage(cues, style, href));
|
|
12356
12537
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
12357
12538
|
const browser = await puppeteer.launch({
|
|
12358
12539
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -12372,7 +12553,7 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
12372
12553
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
12373
12554
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
12374
12555
|
await tab.screenshot({
|
|
12375
|
-
path:
|
|
12556
|
+
path: join14(work, name),
|
|
12376
12557
|
type: "png",
|
|
12377
12558
|
omitBackground: true,
|
|
12378
12559
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -12413,9 +12594,9 @@ function subtitlePlan(mode) {
|
|
|
12413
12594
|
async function render2(opts) {
|
|
12414
12595
|
const log = opts.log ?? (() => {
|
|
12415
12596
|
});
|
|
12416
|
-
const deck =
|
|
12417
|
-
const out =
|
|
12418
|
-
const work =
|
|
12597
|
+
const deck = resolve4(opts.deck);
|
|
12598
|
+
const out = resolve4(opts.out);
|
|
12599
|
+
const work = join15(dirname2(out), `.${basename3(out)}.parts`);
|
|
12419
12600
|
const timing = await readTiming2(deck);
|
|
12420
12601
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
12421
12602
|
const refusal = playbackRefusal(
|
|
@@ -12427,13 +12608,17 @@ async function render2(opts) {
|
|
|
12427
12608
|
}
|
|
12428
12609
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
12429
12610
|
if (plan0.burn) {
|
|
12611
|
+
if (!timing.captionReserve)
|
|
12612
|
+
throw new Error(
|
|
12613
|
+
"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`."
|
|
12614
|
+
);
|
|
12430
12615
|
const blocker = await captionBlocker();
|
|
12431
12616
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
12432
12617
|
}
|
|
12433
12618
|
const burnable = plan0.burn;
|
|
12434
|
-
await
|
|
12619
|
+
await mkdir9(work, { recursive: true });
|
|
12435
12620
|
try {
|
|
12436
|
-
const raw2 = opts.video ?
|
|
12621
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join15(work, "raw.mp4"), opts, log);
|
|
12437
12622
|
const shot = await probe(raw2);
|
|
12438
12623
|
log(
|
|
12439
12624
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -12446,18 +12631,18 @@ async function render2(opts) {
|
|
|
12446
12631
|
);
|
|
12447
12632
|
}
|
|
12448
12633
|
const retimed = await retime(raw2, plan, work, log);
|
|
12449
|
-
const srtPath =
|
|
12634
|
+
const srtPath = join15(dirname2(out), SRT_NAME(out));
|
|
12450
12635
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
12451
12636
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
12452
12637
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
12453
|
-
if (srt) await
|
|
12638
|
+
if (srt) await writeFile11(srtPath, srt);
|
|
12454
12639
|
const burn = burnable && srt.length > 0;
|
|
12455
12640
|
await mux(retimed, timing, plan, deck, out, work, burn ? plan.cues : void 0, shot.fps, log);
|
|
12456
12641
|
if (playback > 1) {
|
|
12457
12642
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
12458
12643
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
12459
12644
|
if (warning) log(`render: ${warning}`);
|
|
12460
|
-
const fast =
|
|
12645
|
+
const fast = join15(work, `fast.${basename3(out)}`);
|
|
12461
12646
|
const muxed = await probe(out);
|
|
12462
12647
|
await runTool(
|
|
12463
12648
|
"ffmpeg",
|
|
@@ -12487,8 +12672,8 @@ async function render2(opts) {
|
|
|
12487
12672
|
}
|
|
12488
12673
|
}
|
|
12489
12674
|
async function readTiming2(deck) {
|
|
12490
|
-
const path2 =
|
|
12491
|
-
const text2 = await
|
|
12675
|
+
const path2 = join15(deck, TIMING_FILE);
|
|
12676
|
+
const text2 = await readFile15(path2, "utf8").catch(() => {
|
|
12492
12677
|
throw new Error(
|
|
12493
12678
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
12494
12679
|
);
|
|
@@ -12527,20 +12712,20 @@ async function retime(raw2, plan, work, log) {
|
|
|
12527
12712
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
12528
12713
|
const list = [];
|
|
12529
12714
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
12530
|
-
const file =
|
|
12715
|
+
const file = join15(work, `p${String(i).padStart(4, "0")}.ts`);
|
|
12531
12716
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
12532
12717
|
list.push(file);
|
|
12533
12718
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
12534
12719
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
12535
12720
|
}
|
|
12536
12721
|
}
|
|
12537
|
-
const listFile =
|
|
12538
|
-
await
|
|
12722
|
+
const listFile = join15(work, "pieces.txt");
|
|
12723
|
+
await writeFile11(
|
|
12539
12724
|
listFile,
|
|
12540
12725
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
12541
12726
|
`
|
|
12542
12727
|
);
|
|
12543
|
-
const joined =
|
|
12728
|
+
const joined = join15(work, "retimed.mp4");
|
|
12544
12729
|
await runTool("ffmpeg", [
|
|
12545
12730
|
"-y",
|
|
12546
12731
|
"-hide_banner",
|
|
@@ -12562,7 +12747,7 @@ async function retime(raw2, plan, work, log) {
|
|
|
12562
12747
|
}
|
|
12563
12748
|
async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
12564
12749
|
const inputs = plan.audio.map((a) => ({
|
|
12565
|
-
file:
|
|
12750
|
+
file: join15(deck, timing.audioDir, a.audio),
|
|
12566
12751
|
delayMs: a.delayMs
|
|
12567
12752
|
}));
|
|
12568
12753
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -12583,8 +12768,8 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
12583
12768
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
12584
12769
|
}
|
|
12585
12770
|
if (graph.length > 0) {
|
|
12586
|
-
const script =
|
|
12587
|
-
await
|
|
12771
|
+
const script = join15(work, "mux.filter");
|
|
12772
|
+
await writeFile11(script, `${graph.join(";\n")}
|
|
12588
12773
|
`);
|
|
12589
12774
|
args.push("-filter_complex_script", script);
|
|
12590
12775
|
}
|
|
@@ -12599,13 +12784,13 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
12599
12784
|
log(
|
|
12600
12785
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
12601
12786
|
);
|
|
12602
|
-
await
|
|
12787
|
+
await mkdir9(dirname2(out), { recursive: true });
|
|
12603
12788
|
await runTool("ffmpeg", args, { cwd: work });
|
|
12604
12789
|
}
|
|
12605
12790
|
|
|
12606
12791
|
// src/pack/pack.ts
|
|
12607
|
-
import { mkdir as
|
|
12608
|
-
import { dirname as
|
|
12792
|
+
import { mkdir as mkdir10, readFile as readFile16, writeFile as writeFile12 } from "node:fs/promises";
|
|
12793
|
+
import { dirname as dirname3, extname as extname3 } from "node:path";
|
|
12609
12794
|
import { unzipSync, zipSync } from "fflate";
|
|
12610
12795
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
12611
12796
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -12644,12 +12829,12 @@ async function writePack(pack2, files, out) {
|
|
|
12644
12829
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
12645
12830
|
}
|
|
12646
12831
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
12647
|
-
await
|
|
12648
|
-
await
|
|
12832
|
+
await mkdir10(dirname3(out), { recursive: true });
|
|
12833
|
+
await writeFile12(out, zip);
|
|
12649
12834
|
return zip.length;
|
|
12650
12835
|
}
|
|
12651
12836
|
async function readPack(path2) {
|
|
12652
|
-
return openPack(new Uint8Array(await
|
|
12837
|
+
return openPack(new Uint8Array(await readFile16(path2)), path2);
|
|
12653
12838
|
}
|
|
12654
12839
|
function openPack(bytes, label = "pack") {
|
|
12655
12840
|
if (bytes.length < 4 || bytes[0] !== 80 || bytes[1] !== 75)
|
|
@@ -12693,8 +12878,8 @@ function why2(error) {
|
|
|
12693
12878
|
}
|
|
12694
12879
|
|
|
12695
12880
|
// src/prefs.ts
|
|
12696
|
-
import { readFile as
|
|
12697
|
-
import { dirname as
|
|
12881
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
12882
|
+
import { dirname as dirname4, join as join16, resolve as resolve5 } from "node:path";
|
|
12698
12883
|
var CONFIG_FILE = "decksmith.config.json";
|
|
12699
12884
|
var PREF_KEYS = Object.keys(prefsSchema.shape);
|
|
12700
12885
|
var NARRATION_KEYS = Object.keys(prefsSchema.shape.narration.unwrap().shape);
|
|
@@ -12711,10 +12896,10 @@ async function loadPrefs(overrides = {}, cwd = process.cwd(), source) {
|
|
|
12711
12896
|
return parsed;
|
|
12712
12897
|
}
|
|
12713
12898
|
async function findConfig(from) {
|
|
12714
|
-
let dir =
|
|
12899
|
+
let dir = resolve5(from);
|
|
12715
12900
|
for (; ; ) {
|
|
12716
|
-
const path2 =
|
|
12717
|
-
const text2 = await
|
|
12901
|
+
const path2 = join16(dir, CONFIG_FILE);
|
|
12902
|
+
const text2 = await readFile17(path2, "utf8").catch(() => void 0);
|
|
12718
12903
|
if (text2 !== void 0) {
|
|
12719
12904
|
try {
|
|
12720
12905
|
return { path: path2, value: JSON.parse(text2) };
|
|
@@ -12722,7 +12907,7 @@ async function findConfig(from) {
|
|
|
12722
12907
|
throw new Error(`${path2} is not valid JSON.`);
|
|
12723
12908
|
}
|
|
12724
12909
|
}
|
|
12725
|
-
const up =
|
|
12910
|
+
const up = dirname4(dir);
|
|
12726
12911
|
if (up === dir) return void 0;
|
|
12727
12912
|
dir = up;
|
|
12728
12913
|
}
|
|
@@ -12768,26 +12953,26 @@ ${issues}`);
|
|
|
12768
12953
|
|
|
12769
12954
|
// src/tmpdir.ts
|
|
12770
12955
|
import { realpathSync } from "node:fs";
|
|
12771
|
-
import { createRequire as
|
|
12956
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
12772
12957
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12773
|
-
import { basename as basename4, dirname as
|
|
12958
|
+
import { basename as basename4, dirname as dirname5, join as join17, resolve as resolve6, sep as sep2 } from "node:path";
|
|
12774
12959
|
var cached;
|
|
12775
12960
|
function real(dir) {
|
|
12776
|
-
let at =
|
|
12961
|
+
let at = resolve6(dir);
|
|
12777
12962
|
const tail3 = [];
|
|
12778
12963
|
for (; ; ) {
|
|
12779
12964
|
try {
|
|
12780
|
-
return
|
|
12965
|
+
return join17(realpathSync(at), ...tail3);
|
|
12781
12966
|
} catch {
|
|
12782
|
-
const up =
|
|
12783
|
-
if (up === at) return
|
|
12967
|
+
const up = dirname5(at);
|
|
12968
|
+
if (up === at) return resolve6(dir);
|
|
12784
12969
|
tail3.unshift(basename4(at));
|
|
12785
12970
|
at = up;
|
|
12786
12971
|
}
|
|
12787
12972
|
}
|
|
12788
12973
|
}
|
|
12789
12974
|
function packageRoot() {
|
|
12790
|
-
cached ??= real(
|
|
12975
|
+
cached ??= real(dirname5(createRequire4(import.meta.url).resolve("../package.json")));
|
|
12791
12976
|
return cached;
|
|
12792
12977
|
}
|
|
12793
12978
|
function insideRoot(dir) {
|
|
@@ -12820,8 +13005,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12820
13005
|
if (!format) throw new Error("no deck-16x9 format");
|
|
12821
13006
|
const step = opts.onStep ?? (() => {
|
|
12822
13007
|
});
|
|
12823
|
-
const out =
|
|
12824
|
-
await
|
|
13008
|
+
const out = resolve7(outDir);
|
|
13009
|
+
await mkdir11(out, { recursive: true });
|
|
12825
13010
|
const speed = opts.speed ?? 1;
|
|
12826
13011
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
12827
13012
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
@@ -12834,47 +13019,48 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12834
13019
|
});
|
|
12835
13020
|
const files = [];
|
|
12836
13021
|
const write = async (name, text2) => {
|
|
12837
|
-
await
|
|
12838
|
-
files.push(
|
|
13022
|
+
await writeFile13(join18(out, name), text2);
|
|
13023
|
+
files.push(join18(out, name));
|
|
12839
13024
|
};
|
|
12840
13025
|
await write("index.html", deck.composition);
|
|
12841
13026
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
13027
|
+
try {
|
|
13028
|
+
const timing = planTiming({
|
|
13029
|
+
storyboard,
|
|
13030
|
+
source,
|
|
13031
|
+
format,
|
|
13032
|
+
composition: deck.composition,
|
|
13033
|
+
// What was drawn, not what the threshold would have kept: a budgeted
|
|
13034
|
+
// format cuts to its own length as well, and the two lists differ.
|
|
13035
|
+
beats: deck.cut.kept,
|
|
13036
|
+
speed,
|
|
13037
|
+
...opts.narration ? { narration: opts.narration } : {},
|
|
13038
|
+
...opts.theme ? { theme: opts.theme } : {}
|
|
13039
|
+
});
|
|
13040
|
+
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
12857
13041
|
`);
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
}
|
|
13042
|
+
step(`build: timing for ${timing.segments.length} narration segment(s)`);
|
|
13043
|
+
} catch (err) {
|
|
13044
|
+
step(
|
|
13045
|
+
`build: cannot place narration on this deck, so no ${TIMING_FILE} was written and \`render\` will refuse. ${err instanceof Error ? err.message : err}`
|
|
13046
|
+
);
|
|
12864
13047
|
}
|
|
12865
13048
|
if (deck.page) {
|
|
12866
13049
|
await write(DECK_PAGE, deck.page);
|
|
12867
|
-
await
|
|
12868
|
-
files.push(
|
|
13050
|
+
await cp2(playerBundle(), join18(out, PLAYER_FILE));
|
|
13051
|
+
files.push(join18(out, PLAYER_FILE));
|
|
12869
13052
|
}
|
|
12870
13053
|
files.push(...await vendorKatex(out));
|
|
12871
|
-
|
|
13054
|
+
files.push(...await vendorScripts(out, deck.composition));
|
|
13055
|
+
if (opts.assetsFrom) {
|
|
13056
|
+
files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
|
|
13057
|
+
}
|
|
12872
13058
|
if (opts.narration && opts.audioFrom) {
|
|
12873
|
-
files.push(...await copyAudio(opts.audioFrom, opts.narration, out));
|
|
13059
|
+
files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
|
|
12874
13060
|
}
|
|
12875
13061
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
12876
13062
|
step(
|
|
12877
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
13063
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join18(out, "index.html")}`
|
|
12878
13064
|
);
|
|
12879
13065
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
12880
13066
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -12890,79 +13076,28 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
12890
13076
|
)}
|
|
12891
13077
|
`;
|
|
12892
13078
|
async function deckRuntime() {
|
|
12893
|
-
const path2 =
|
|
13079
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
12894
13080
|
try {
|
|
12895
|
-
return await
|
|
13081
|
+
return await readFile18(path2, "utf8");
|
|
12896
13082
|
} catch {
|
|
12897
13083
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
12898
13084
|
}
|
|
12899
13085
|
}
|
|
12900
13086
|
function playerBundle() {
|
|
12901
|
-
const require2 =
|
|
13087
|
+
const require2 = createRequire5(import.meta.url);
|
|
12902
13088
|
try {
|
|
12903
|
-
return
|
|
13089
|
+
return join18(dirname6(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12904
13090
|
} catch {
|
|
12905
13091
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12906
13092
|
}
|
|
12907
13093
|
}
|
|
12908
|
-
async function vendorKatex(out) {
|
|
12909
|
-
const require2 = createRequire4(import.meta.url);
|
|
12910
|
-
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12911
|
-
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12912
|
-
const written = [];
|
|
12913
|
-
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12914
|
-
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12915
|
-
if (!file.endsWith(".woff2")) continue;
|
|
12916
|
-
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12917
|
-
written.push(join17(out, "katex/fonts", file));
|
|
12918
|
-
}
|
|
12919
|
-
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12920
|
-
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12921
|
-
return kept ? `src:${kept}` : whole;
|
|
12922
|
-
});
|
|
12923
|
-
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12924
|
-
written.push(join17(out, "katex/katex.min.css"));
|
|
12925
|
-
return written;
|
|
12926
|
-
}
|
|
12927
|
-
async function copyAssets(sourceDir, out) {
|
|
12928
|
-
const from = join17(resolve6(sourceDir), "assets");
|
|
12929
|
-
if (!await stat2(from).catch(() => null)) return [];
|
|
12930
|
-
await cp(from, join17(out, "assets"), { recursive: true });
|
|
12931
|
-
return [join17(out, "assets")];
|
|
12932
|
-
}
|
|
12933
|
-
async function copyAudio(from, narration, out) {
|
|
12934
|
-
const dir = join17(out, narration.dir);
|
|
12935
|
-
await mkdir10(dir, { recursive: true });
|
|
12936
|
-
const names = [
|
|
12937
|
-
...new Set(
|
|
12938
|
-
Object.values(narration.beats).flat().map((s) => s.audio)
|
|
12939
|
-
)
|
|
12940
|
-
].sort();
|
|
12941
|
-
for (const name of names) {
|
|
12942
|
-
await cp(join17(resolve6(from), name), join17(dir, name)).catch(() => {
|
|
12943
|
-
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
12944
|
-
});
|
|
12945
|
-
}
|
|
12946
|
-
return names.map((n3) => join17(dir, n3));
|
|
12947
|
-
}
|
|
12948
|
-
async function refreshFont(storyboard, source, out, step) {
|
|
12949
|
-
try {
|
|
12950
|
-
const bundle = await bundleFont(
|
|
12951
|
-
storyboard.lang,
|
|
12952
|
-
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
12953
|
-
join17(out, "assets", "fonts")
|
|
12954
|
-
);
|
|
12955
|
-
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
12956
|
-
return bundle?.css;
|
|
12957
|
-
} catch (err) {
|
|
12958
|
-
step(
|
|
12959
|
-
`build: could not refresh the font bundle (${err instanceof Error ? err.message : err}); keeping the one from ingest`
|
|
12960
|
-
);
|
|
12961
|
-
return void 0;
|
|
12962
|
-
}
|
|
12963
|
-
}
|
|
12964
13094
|
export {
|
|
12965
13095
|
ARCHETYPE_FAMILY,
|
|
13096
|
+
CAPTION_FONT_RATIO,
|
|
13097
|
+
CAPTION_LINE_RATIO,
|
|
13098
|
+
CAPTION_MARGIN_V_RATIO,
|
|
13099
|
+
CAPTION_MARGIN_X_RATIO,
|
|
13100
|
+
CAPTION_MAX_LINES,
|
|
12966
13101
|
COMFORTABLE_CPS,
|
|
12967
13102
|
CONFIG_FILE,
|
|
12968
13103
|
DECK_PAGE,
|
|
@@ -12992,6 +13127,7 @@ export {
|
|
|
12992
13127
|
assertInsideResolves,
|
|
12993
13128
|
assertRefsResolve,
|
|
12994
13129
|
attachClips,
|
|
13130
|
+
bandReserve,
|
|
12995
13131
|
barCompareParamsSchema,
|
|
12996
13132
|
beatRoleSchema,
|
|
12997
13133
|
beatSchema,
|
|
@@ -13000,6 +13136,7 @@ export {
|
|
|
13000
13136
|
calloutParamsSchema,
|
|
13001
13137
|
canvasProblem,
|
|
13002
13138
|
canvasWarnings,
|
|
13139
|
+
captionFontSize,
|
|
13003
13140
|
check,
|
|
13004
13141
|
claimFigureParamsSchema,
|
|
13005
13142
|
codexPlanner,
|