@jokerized/decksmith 0.4.1 → 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 +483 -378
- package/dist/deck-runtime.js +5 -2
- package/dist/index.js +444 -306
- package/dist/mcp.js +391 -301
- 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 +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,10 +7110,10 @@ 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
7118
|
async function resolveChrome(need = "open the deck with") {
|
|
6977
7119
|
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
@@ -6982,9 +7124,9 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
6982
7124
|
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
6983
7125
|
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
6984
7126
|
};
|
|
6985
|
-
const hf = await pick(
|
|
7127
|
+
const hf = await pick(join3(homedir(), ".cache", "hyperframes", "chrome"));
|
|
6986
7128
|
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
6987
|
-
const pup = await pick(process.env.PUPPETEER_CACHE_DIR ||
|
|
7129
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join3(homedir(), ".cache", "puppeteer"));
|
|
6988
7130
|
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
6989
7131
|
throw new Error(
|
|
6990
7132
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
@@ -6994,15 +7136,15 @@ async function chromePath(need = "open the deck with") {
|
|
|
6994
7136
|
return (await resolveChrome(need)).path;
|
|
6995
7137
|
}
|
|
6996
7138
|
function runtimePath() {
|
|
6997
|
-
return
|
|
7139
|
+
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
6998
7140
|
}
|
|
6999
7141
|
function renderSeek(t2) {
|
|
7000
7142
|
const player = window.__player;
|
|
7001
7143
|
player.renderSeek(t2, { suppressEvents: true });
|
|
7002
7144
|
}
|
|
7003
7145
|
async function openDeck(dir, opts = {}) {
|
|
7004
|
-
const index =
|
|
7005
|
-
const html = await
|
|
7146
|
+
const index = join3(dir, "index.html");
|
|
7147
|
+
const html = await readFile4(index, "utf8").catch(() => null);
|
|
7006
7148
|
if (html === null) throw new Error(`no index.html in ${dir}`);
|
|
7007
7149
|
const width = Number(/data-width="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
7008
7150
|
const height = Number(/data-height="(\d+)"/.exec(html)?.[1] ?? 0);
|
|
@@ -7080,13 +7222,13 @@ async function openDeck(dir, opts = {}) {
|
|
|
7080
7222
|
|
|
7081
7223
|
// src/source/assets.ts
|
|
7082
7224
|
import { createHash as createHash4 } from "node:crypto";
|
|
7083
|
-
import { mkdir as
|
|
7084
|
-
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";
|
|
7085
7227
|
import { z as z2 } from "zod";
|
|
7086
7228
|
async function fetchFigures(source, dir, warnings) {
|
|
7087
7229
|
const drops = warnings ?? [];
|
|
7088
|
-
await
|
|
7089
|
-
const cached2 = await
|
|
7230
|
+
await mkdir4(dir, { recursive: true });
|
|
7231
|
+
const cached2 = await readdir2(dir).catch(() => []);
|
|
7090
7232
|
const figures = [];
|
|
7091
7233
|
for (const figure of source.figures) {
|
|
7092
7234
|
if (figure.kind === "clip") {
|
|
@@ -7111,11 +7253,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
7111
7253
|
async function localize(figure, dir, cached2) {
|
|
7112
7254
|
const stem = assetStem(figure.id, figure.src);
|
|
7113
7255
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
7114
|
-
const bytes = hit ? await
|
|
7256
|
+
const bytes = hit ? await readFile5(join4(dir, hit)) : await load(figure.src);
|
|
7115
7257
|
const size2 = imageSize(bytes);
|
|
7116
7258
|
if (hit) return { ...figure, src: hit, ...size2 };
|
|
7117
7259
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
7118
|
-
await
|
|
7260
|
+
await writeFile4(join4(dir, src), bytes);
|
|
7119
7261
|
cached2.push(src);
|
|
7120
7262
|
return { ...figure, src, ...size2 };
|
|
7121
7263
|
}
|
|
@@ -7140,7 +7282,7 @@ function assetExt(bytes, src) {
|
|
|
7140
7282
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
7141
7283
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
7142
7284
|
async function load(src) {
|
|
7143
|
-
if (!/^https?:/i.test(src)) return
|
|
7285
|
+
if (!/^https?:/i.test(src)) return readFile5(src);
|
|
7144
7286
|
const got = await fetchGuarded(src, {
|
|
7145
7287
|
maxBytes: FIGURE_MAX_BYTES,
|
|
7146
7288
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -7676,7 +7818,7 @@ function readContentRegion() {
|
|
|
7676
7818
|
}
|
|
7677
7819
|
|
|
7678
7820
|
// src/source/transcode.ts
|
|
7679
|
-
import { readFile as
|
|
7821
|
+
import { readFile as readFile6, rm } from "node:fs/promises";
|
|
7680
7822
|
import { basename } from "node:path";
|
|
7681
7823
|
|
|
7682
7824
|
// src/render/ffmpeg.ts
|
|
@@ -7702,7 +7844,7 @@ ${tail3}`);
|
|
|
7702
7844
|
}
|
|
7703
7845
|
}
|
|
7704
7846
|
function runLive(file, args) {
|
|
7705
|
-
return new Promise((
|
|
7847
|
+
return new Promise((resolve8, reject) => {
|
|
7706
7848
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7707
7849
|
child.on("error", (err) => {
|
|
7708
7850
|
reject(
|
|
@@ -7710,7 +7852,7 @@ function runLive(file, args) {
|
|
|
7710
7852
|
);
|
|
7711
7853
|
});
|
|
7712
7854
|
child.on("close", (code, signal) => {
|
|
7713
|
-
if (code === 0)
|
|
7855
|
+
if (code === 0) resolve8();
|
|
7714
7856
|
else if (signal) {
|
|
7715
7857
|
reject(
|
|
7716
7858
|
new Error(
|
|
@@ -7837,11 +7979,16 @@ function burnStyle(width, height, font = "Arial") {
|
|
|
7837
7979
|
// here is 978px. F = 40 leaves 9% of headroom; F = 45 — which is what
|
|
7838
7980
|
// "4% of the width" looked like on paper — overflows to a THIRD line, and a
|
|
7839
7981
|
// three-line band covers the bottom of the slide.
|
|
7840
|
-
|
|
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),
|
|
7841
7988
|
// Clear of the play button, the progress bar and the handle every vertical
|
|
7842
7989
|
// player draws across the bottom eighth of the frame.
|
|
7843
|
-
marginV: Math.round(height *
|
|
7844
|
-
marginX: Math.round(width *
|
|
7990
|
+
marginV: Math.round(height * CAPTION_MARGIN_V_RATIO),
|
|
7991
|
+
marginX: Math.round(width * CAPTION_MARGIN_X_RATIO)
|
|
7845
7992
|
};
|
|
7846
7993
|
}
|
|
7847
7994
|
|
|
@@ -7907,7 +8054,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7907
8054
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
7908
8055
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
7909
8056
|
const name = basename(input);
|
|
7910
|
-
const source = videoSize(await
|
|
8057
|
+
const source = videoSize(await readFile6(input));
|
|
7911
8058
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
7912
8059
|
const kept = (why3) => ({
|
|
7913
8060
|
path: input,
|
|
@@ -7939,7 +8086,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
7939
8086
|
}
|
|
7940
8087
|
let measured;
|
|
7941
8088
|
try {
|
|
7942
|
-
measured = videoSize(await
|
|
8089
|
+
measured = videoSize(await readFile6(out));
|
|
7943
8090
|
} catch (err) {
|
|
7944
8091
|
await rm(out, { force: true });
|
|
7945
8092
|
return kept(
|
|
@@ -7975,8 +8122,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
7975
8122
|
async function harvest(url, dir, opts = {}) {
|
|
7976
8123
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
7977
8124
|
const warnings = [];
|
|
7978
|
-
const assets =
|
|
7979
|
-
await
|
|
8125
|
+
const assets = resolve2(dir);
|
|
8126
|
+
await mkdir5(assets, { recursive: true });
|
|
7980
8127
|
const page = await fetchGuarded(url, {
|
|
7981
8128
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
7982
8129
|
timeoutMs,
|
|
@@ -8358,8 +8505,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8358
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`
|
|
8359
8506
|
);
|
|
8360
8507
|
}
|
|
8361
|
-
const path2 =
|
|
8362
|
-
await
|
|
8508
|
+
const path2 = join5(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
8509
|
+
await writeFile5(path2, bytes);
|
|
8363
8510
|
assets.push(path2);
|
|
8364
8511
|
got = { path: path2, width: size2.width, height: size2.height };
|
|
8365
8512
|
} catch (err) {
|
|
@@ -8371,7 +8518,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8371
8518
|
};
|
|
8372
8519
|
const shrink = async (url, path2, measured, what) => {
|
|
8373
8520
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
8374
|
-
const out2 =
|
|
8521
|
+
const out2 = join5(dir, assetName(url, ".vp9.webm"));
|
|
8375
8522
|
let small;
|
|
8376
8523
|
try {
|
|
8377
8524
|
small = await transcode(path2, out2, {
|
|
@@ -8413,8 +8560,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
8413
8560
|
try {
|
|
8414
8561
|
const bytes = await bytesOf(url);
|
|
8415
8562
|
const measured = videoSize(bytes);
|
|
8416
|
-
const path2 =
|
|
8417
|
-
await
|
|
8563
|
+
const path2 = join5(dir, assetName(url, `.${measured.container}`));
|
|
8564
|
+
await writeFile5(path2, bytes);
|
|
8418
8565
|
return await shrink(url, path2, measured, what);
|
|
8419
8566
|
} catch (err) {
|
|
8420
8567
|
warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
|
|
@@ -8503,7 +8650,7 @@ function unheld(policy) {
|
|
|
8503
8650
|
return "the file could not be held";
|
|
8504
8651
|
}
|
|
8505
8652
|
async function sizeOf(path2) {
|
|
8506
|
-
return await
|
|
8653
|
+
return await stat2(path2).then(
|
|
8507
8654
|
(s) => s.size,
|
|
8508
8655
|
() => 0
|
|
8509
8656
|
);
|
|
@@ -8711,8 +8858,8 @@ function sane(seconds) {
|
|
|
8711
8858
|
}
|
|
8712
8859
|
async function attachClips(source, clips, dir) {
|
|
8713
8860
|
if (clips.length === 0) return source;
|
|
8714
|
-
const assets =
|
|
8715
|
-
await
|
|
8861
|
+
const assets = resolve2(dir);
|
|
8862
|
+
await mkdir5(assets, { recursive: true });
|
|
8716
8863
|
const figures = [...source.figures];
|
|
8717
8864
|
for (const clip3 of clips) {
|
|
8718
8865
|
const src = clip3.file ? await adopt(clip3.file, assets) : void 0;
|
|
@@ -8743,7 +8890,7 @@ async function attachClips(source, clips, dir) {
|
|
|
8743
8890
|
}
|
|
8744
8891
|
async function adopt(file, dir) {
|
|
8745
8892
|
const name = basename2(file);
|
|
8746
|
-
await copyFile(file,
|
|
8893
|
+
await copyFile(file, join5(dir, name));
|
|
8747
8894
|
return name;
|
|
8748
8895
|
}
|
|
8749
8896
|
function freeId(figures) {
|
|
@@ -8820,9 +8967,9 @@ ${fence}`;
|
|
|
8820
8967
|
|
|
8821
8968
|
// src/plan/codex.ts
|
|
8822
8969
|
import { spawn as spawn2 } from "node:child_process";
|
|
8823
|
-
import { mkdtemp, readFile as
|
|
8970
|
+
import { mkdtemp, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
|
|
8824
8971
|
import { tmpdir } from "node:os";
|
|
8825
|
-
import { join as
|
|
8972
|
+
import { join as join6 } from "node:path";
|
|
8826
8973
|
import { z as z3 } from "zod";
|
|
8827
8974
|
|
|
8828
8975
|
// src/plan/arc.ts
|
|
@@ -9941,11 +10088,11 @@ function schemaFor(prefs) {
|
|
|
9941
10088
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
9942
10089
|
async function codexPlanner(source, opts = {}) {
|
|
9943
10090
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
9944
|
-
const dir = await mkdtemp(
|
|
10091
|
+
const dir = await mkdtemp(join6(tmpdir(), "decksmith-plan-"));
|
|
9945
10092
|
try {
|
|
9946
|
-
const schemaPath =
|
|
9947
|
-
const outPath =
|
|
9948
|
-
await
|
|
10093
|
+
const schemaPath = join6(dir, "storyboard.schema.json");
|
|
10094
|
+
const outPath = join6(dir, "storyboard.json");
|
|
10095
|
+
await writeFile6(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
9949
10096
|
await (opts.run ?? runCodex)({
|
|
9950
10097
|
prompt: buildPrompt(source, prefs),
|
|
9951
10098
|
schemaPath,
|
|
@@ -9953,7 +10100,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
9953
10100
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
9954
10101
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
9955
10102
|
});
|
|
9956
|
-
const raw2 = await
|
|
10103
|
+
const raw2 = await readFile7(outPath, "utf8").catch(() => "");
|
|
9957
10104
|
if (raw2.trim() === "") {
|
|
9958
10105
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
9959
10106
|
}
|
|
@@ -10026,7 +10173,7 @@ function codexCommand(args) {
|
|
|
10026
10173
|
}
|
|
10027
10174
|
function runCodex(args) {
|
|
10028
10175
|
const { argv, env } = codexCommand(args);
|
|
10029
|
-
return new Promise((
|
|
10176
|
+
return new Promise((resolve8, reject) => {
|
|
10030
10177
|
const child = spawn2("codex", argv, {
|
|
10031
10178
|
stdio: ["pipe", "ignore", "pipe"],
|
|
10032
10179
|
...env === void 0 ? {} : { env }
|
|
@@ -10049,7 +10196,7 @@ function runCodex(args) {
|
|
|
10049
10196
|
});
|
|
10050
10197
|
child.on("close", (code) => {
|
|
10051
10198
|
clearTimeout(timer);
|
|
10052
|
-
if (code === 0) return
|
|
10199
|
+
if (code === 0) return resolve8();
|
|
10053
10200
|
reject(
|
|
10054
10201
|
new Error(`codex exec exited ${code}.
|
|
10055
10202
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -10061,14 +10208,14 @@ ${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
|
10061
10208
|
|
|
10062
10209
|
// src/images/illustrate.ts
|
|
10063
10210
|
import { createHash as createHash7 } from "node:crypto";
|
|
10064
|
-
import { mkdir as
|
|
10065
|
-
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";
|
|
10066
10213
|
|
|
10067
10214
|
// src/images/providers.ts
|
|
10068
10215
|
import { createHash as createHash6 } from "node:crypto";
|
|
10069
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
10216
|
+
import { mkdtemp as mkdtemp2, readFile as readFile8, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
10070
10217
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
10071
|
-
import { join as
|
|
10218
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
10072
10219
|
import { z as z4 } from "zod";
|
|
10073
10220
|
var SIZE = {
|
|
10074
10221
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -10200,11 +10347,11 @@ function codexImages(opts = {}) {
|
|
|
10200
10347
|
async check() {
|
|
10201
10348
|
},
|
|
10202
10349
|
async generate(req) {
|
|
10203
|
-
const dir = await mkdtemp2(
|
|
10350
|
+
const dir = await mkdtemp2(join7(tmpdir2(), "decksmith-image-"));
|
|
10204
10351
|
try {
|
|
10205
|
-
const schemaPath =
|
|
10206
|
-
const outPath =
|
|
10207
|
-
await
|
|
10352
|
+
const schemaPath = join7(dir, "answer.schema.json");
|
|
10353
|
+
const outPath = join7(dir, "answer.json");
|
|
10354
|
+
await writeFile7(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
10208
10355
|
await run4({
|
|
10209
10356
|
prompt: codexPrompt(req),
|
|
10210
10357
|
schemaPath,
|
|
@@ -10213,7 +10360,7 @@ function codexImages(opts = {}) {
|
|
|
10213
10360
|
cwd: dir,
|
|
10214
10361
|
sandbox: "workspace-write"
|
|
10215
10362
|
});
|
|
10216
|
-
const raw2 = await
|
|
10363
|
+
const raw2 = await readFile8(outPath, "utf8").catch(() => "");
|
|
10217
10364
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
10218
10365
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
10219
10366
|
if (!answer.data.ok) {
|
|
@@ -10221,10 +10368,10 @@ function codexImages(opts = {}) {
|
|
|
10221
10368
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
10222
10369
|
);
|
|
10223
10370
|
}
|
|
10224
|
-
const candidates2 = [
|
|
10225
|
-
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));
|
|
10226
10373
|
for (const path2 of candidates2) {
|
|
10227
|
-
const bytes = await
|
|
10374
|
+
const bytes = await readFile8(path2).catch(() => null);
|
|
10228
10375
|
if (bytes) return raster(bytes, "codex");
|
|
10229
10376
|
}
|
|
10230
10377
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -10356,7 +10503,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
10356
10503
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
10357
10504
|
if (tool !== last) chain.push(tool);
|
|
10358
10505
|
const dropped = /* @__PURE__ */ new Set();
|
|
10359
|
-
if (pending.length > 0) await
|
|
10506
|
+
if (pending.length > 0) await mkdir6(opts.assetsDir, { recursive: true });
|
|
10360
10507
|
for (const [i, slot] of pending.entries()) {
|
|
10361
10508
|
let rungs = chain;
|
|
10362
10509
|
if (i >= images.max) {
|
|
@@ -10453,16 +10600,16 @@ function cacheKey(providerId, req) {
|
|
|
10453
10600
|
async function draw(provider, req, name, dir) {
|
|
10454
10601
|
for (const ext of Object.values(EXT2)) {
|
|
10455
10602
|
const src2 = `${name}${ext}`;
|
|
10456
|
-
const bytes = await
|
|
10603
|
+
const bytes = await readFile9(join8(dir, src2)).catch(() => null);
|
|
10457
10604
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
10458
10605
|
}
|
|
10459
10606
|
await provider.check();
|
|
10460
10607
|
const img = await provider.generate(req);
|
|
10461
10608
|
const src = `${name}${EXT2[img.mime]}`;
|
|
10462
|
-
const tmp =
|
|
10609
|
+
const tmp = join8(dir, `.${name}.tmp`);
|
|
10463
10610
|
try {
|
|
10464
|
-
await
|
|
10465
|
-
await rename(tmp,
|
|
10611
|
+
await writeFile8(tmp, img.bytes);
|
|
10612
|
+
await rename(tmp, join8(dir, src));
|
|
10466
10613
|
} finally {
|
|
10467
10614
|
await rm5(tmp, { force: true });
|
|
10468
10615
|
}
|
|
@@ -10475,9 +10622,9 @@ function sizeOf2(bytes, ext) {
|
|
|
10475
10622
|
// src/narrate/tts.ts
|
|
10476
10623
|
import { spawn as spawn3 } from "node:child_process";
|
|
10477
10624
|
import { createHash as createHash8 } from "node:crypto";
|
|
10478
|
-
import { mkdir as
|
|
10625
|
+
import { mkdir as mkdir7, readFile as readFile10, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
|
|
10479
10626
|
import { homedir as homedir2 } from "node:os";
|
|
10480
|
-
import { join as
|
|
10627
|
+
import { join as join9 } from "node:path";
|
|
10481
10628
|
var MISSING = [
|
|
10482
10629
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
10483
10630
|
"",
|
|
@@ -10494,8 +10641,8 @@ function candidates() {
|
|
|
10494
10641
|
["edge-tts"],
|
|
10495
10642
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
10496
10643
|
// default, and both are where this actually lands in practice.
|
|
10497
|
-
[
|
|
10498
|
-
[
|
|
10644
|
+
[join9(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
10645
|
+
[join9(home, ".local", "bin", "edge-tts")],
|
|
10499
10646
|
// Last resort: the module is installed even though its console script is
|
|
10500
10647
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
10501
10648
|
["python3", "-m", "edge_tts"]
|
|
@@ -10520,7 +10667,7 @@ async function find2(can) {
|
|
|
10520
10667
|
throw new Error(MISSING);
|
|
10521
10668
|
}
|
|
10522
10669
|
function runArgv(cmd, args) {
|
|
10523
|
-
return new Promise((
|
|
10670
|
+
return new Promise((resolve8) => {
|
|
10524
10671
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10525
10672
|
let stdout = "";
|
|
10526
10673
|
let stderr = "";
|
|
@@ -10530,8 +10677,8 @@ function runArgv(cmd, args) {
|
|
|
10530
10677
|
child.stderr.on("data", (b) => {
|
|
10531
10678
|
stderr += b.toString();
|
|
10532
10679
|
});
|
|
10533
|
-
child.on("error", (e) =>
|
|
10534
|
-
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 }));
|
|
10535
10682
|
});
|
|
10536
10683
|
}
|
|
10537
10684
|
var edgeTts = {
|
|
@@ -10598,7 +10745,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
10598
10745
|
async speak(req) {
|
|
10599
10746
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
10600
10747
|
await runner.speak({ ...req, subs });
|
|
10601
|
-
const raw2 = await
|
|
10748
|
+
const raw2 = await readFile10(subs, "utf8").catch(() => "");
|
|
10602
10749
|
await rm6(subs, { force: true });
|
|
10603
10750
|
const cues = parseCues(raw2);
|
|
10604
10751
|
const measured = await runner.measure(req.audio);
|
|
@@ -10628,11 +10775,11 @@ async function synthesize(text2, opts) {
|
|
|
10628
10775
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
10629
10776
|
const key = cacheKey2(text2, opts.voice, rate, pitch);
|
|
10630
10777
|
const file = `${key}.mp3`;
|
|
10631
|
-
const audio =
|
|
10632
|
-
const sidecar =
|
|
10778
|
+
const audio = join9(opts.dir, file);
|
|
10779
|
+
const sidecar = join9(opts.dir, `${key}.json`);
|
|
10633
10780
|
const cached2 = await readSidecar(sidecar);
|
|
10634
10781
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10635
|
-
await
|
|
10782
|
+
await mkdir7(opts.dir, { recursive: true });
|
|
10636
10783
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10637
10784
|
const { cues, seconds } = spoken;
|
|
10638
10785
|
const clamped = cues.map((c) => ({
|
|
@@ -10641,13 +10788,13 @@ async function synthesize(text2, opts) {
|
|
|
10641
10788
|
text: c.text
|
|
10642
10789
|
}));
|
|
10643
10790
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
10644
|
-
await
|
|
10791
|
+
await writeFile9(sidecar, `${JSON.stringify(body, null, 2)}
|
|
10645
10792
|
`);
|
|
10646
10793
|
return { audio, file, seconds, cues: clamped };
|
|
10647
10794
|
}
|
|
10648
10795
|
async function readSidecar(path2) {
|
|
10649
10796
|
try {
|
|
10650
|
-
const parsed = JSON.parse(await
|
|
10797
|
+
const parsed = JSON.parse(await readFile10(path2, "utf8"));
|
|
10651
10798
|
const s = parsed;
|
|
10652
10799
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
10653
10800
|
} catch {
|
|
@@ -10770,8 +10917,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
10770
10917
|
}
|
|
10771
10918
|
|
|
10772
10919
|
// src/verify/index.ts
|
|
10773
|
-
import { readdir as
|
|
10774
|
-
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";
|
|
10775
10922
|
|
|
10776
10923
|
// src/verify/budget.ts
|
|
10777
10924
|
function readCanvas(html) {
|
|
@@ -10878,8 +11025,8 @@ function clock2(seconds) {
|
|
|
10878
11025
|
|
|
10879
11026
|
// src/verify/check.ts
|
|
10880
11027
|
import { execFile as execFile2 } from "node:child_process";
|
|
10881
|
-
import { readFile as
|
|
10882
|
-
import { join as
|
|
11028
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
11029
|
+
import { join as join10 } from "node:path";
|
|
10883
11030
|
import { promisify as promisify2 } from "node:util";
|
|
10884
11031
|
var run2 = promisify2(execFile2);
|
|
10885
11032
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -10909,7 +11056,7 @@ async function check(dir, opts = {}) {
|
|
|
10909
11056
|
async function readComposition(dir) {
|
|
10910
11057
|
let html;
|
|
10911
11058
|
try {
|
|
10912
|
-
html = await
|
|
11059
|
+
html = await readFile11(join10(dir, "index.html"), "utf8");
|
|
10913
11060
|
} catch {
|
|
10914
11061
|
return { transit: [], duration: 0 };
|
|
10915
11062
|
}
|
|
@@ -11060,8 +11207,8 @@ function tail(s) {
|
|
|
11060
11207
|
}
|
|
11061
11208
|
|
|
11062
11209
|
// src/verify/fidelity.ts
|
|
11063
|
-
import { readFile as
|
|
11064
|
-
import { join as
|
|
11210
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
11211
|
+
import { join as join11 } from "node:path";
|
|
11065
11212
|
|
|
11066
11213
|
// src/verify/typefloor.ts
|
|
11067
11214
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11317,6 +11464,16 @@ var INK_FLOOR = 15e-4;
|
|
|
11317
11464
|
var INK_DELTA = 12;
|
|
11318
11465
|
var FALLBACK_BAND_TOP = 0.22;
|
|
11319
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
|
+
}
|
|
11320
11477
|
async function decodePng(png) {
|
|
11321
11478
|
const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
|
|
11322
11479
|
if (buf.length < 8 || buf.readUInt32BE(0) !== 2303741511) throw new Error("not a PNG");
|
|
@@ -11483,6 +11640,25 @@ function gradeFidelity(rows, floor = INK_FLOOR) {
|
|
|
11483
11640
|
};
|
|
11484
11641
|
});
|
|
11485
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
|
+
}
|
|
11486
11662
|
function captionBottom(sid, selector, fallbackPx) {
|
|
11487
11663
|
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
11488
11664
|
let bottom = 0;
|
|
@@ -11507,11 +11683,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
11507
11683
|
],
|
|
11508
11684
|
elapsedMs: Date.now() - started
|
|
11509
11685
|
});
|
|
11510
|
-
const
|
|
11511
|
-
|
|
11512
|
-
await readFile11(join10(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11513
|
-
);
|
|
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));
|
|
11514
11688
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
11689
|
+
const reserve = opts.captionReserve ?? readReserve(timingText);
|
|
11515
11690
|
let deck = null;
|
|
11516
11691
|
try {
|
|
11517
11692
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
@@ -11540,7 +11715,13 @@ async function fidelity(dir, opts = {}) {
|
|
|
11540
11715
|
measured.push({
|
|
11541
11716
|
...stop,
|
|
11542
11717
|
ink: inkBelow(frame, bandTopPx),
|
|
11543
|
-
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) } : {}
|
|
11544
11725
|
});
|
|
11545
11726
|
}
|
|
11546
11727
|
for (const mid of midpoints(stops)) {
|
|
@@ -11551,6 +11732,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
11551
11732
|
stops: measured,
|
|
11552
11733
|
findings: [
|
|
11553
11734
|
...gradeFidelity(measured, floor),
|
|
11735
|
+
...gradeReserve(measured),
|
|
11554
11736
|
...gradeOverprint(collided),
|
|
11555
11737
|
...gradeApparent(apparent)
|
|
11556
11738
|
],
|
|
@@ -11567,9 +11749,9 @@ async function fidelity(dir, opts = {}) {
|
|
|
11567
11749
|
// src/verify/drift.ts
|
|
11568
11750
|
import { execFile as execFile3 } from "node:child_process";
|
|
11569
11751
|
import { createHash as createHash9 } from "node:crypto";
|
|
11570
|
-
import { mkdtemp as mkdtemp3, readdir as
|
|
11752
|
+
import { mkdtemp as mkdtemp3, readdir as readdir3, readFile as readFile13, rm as rm7 } from "node:fs/promises";
|
|
11571
11753
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
11572
|
-
import { join as
|
|
11754
|
+
import { join as join12 } from "node:path";
|
|
11573
11755
|
import { promisify as promisify3 } from "node:util";
|
|
11574
11756
|
var run3 = promisify3(execFile3);
|
|
11575
11757
|
var FLOOR_DB = 40;
|
|
@@ -11608,9 +11790,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
11608
11790
|
async function drift(dir, opts = {}) {
|
|
11609
11791
|
const mode = opts.mode ?? "psnr";
|
|
11610
11792
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
11611
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
11612
|
-
const a =
|
|
11613
|
-
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");
|
|
11614
11796
|
const report = await compare(dir, a, b, mode, floorDb, opts);
|
|
11615
11797
|
const keep = opts.keep || !report.passed;
|
|
11616
11798
|
if (keep) report.kept = { a, b };
|
|
@@ -11665,11 +11847,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
11665
11847
|
const differing = [];
|
|
11666
11848
|
const ha = [];
|
|
11667
11849
|
for (const [i, name] of fa.entries()) {
|
|
11668
|
-
const one = await sha(
|
|
11850
|
+
const one = await sha(join12(a, name));
|
|
11669
11851
|
ha.push(one);
|
|
11670
|
-
if (one !== await sha(
|
|
11852
|
+
if (one !== await sha(join12(b, name))) differing.push(i + 1);
|
|
11671
11853
|
}
|
|
11672
|
-
const html = await
|
|
11854
|
+
const html = await readFile13(join12(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
11673
11855
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
11674
11856
|
let worst;
|
|
11675
11857
|
if (differing.length > 0) {
|
|
@@ -11780,11 +11962,11 @@ async function render(dir, out, opts) {
|
|
|
11780
11962
|
}
|
|
11781
11963
|
}
|
|
11782
11964
|
async function frames(dir) {
|
|
11783
|
-
const names = await
|
|
11965
|
+
const names = await readdir3(dir).catch(() => []);
|
|
11784
11966
|
return names.filter((n3) => n3.endsWith(".png")).sort();
|
|
11785
11967
|
}
|
|
11786
11968
|
async function sha(file) {
|
|
11787
|
-
return createHash9("sha256").update(await
|
|
11969
|
+
return createHash9("sha256").update(await readFile13(file)).digest("hex");
|
|
11788
11970
|
}
|
|
11789
11971
|
var STAT = /^n:(\d+)\b.*\bpsnr_avg:(inf|-?[\d.]+)/;
|
|
11790
11972
|
function framePattern(name) {
|
|
@@ -11817,11 +11999,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
11817
11999
|
"-start_number",
|
|
11818
12000
|
String(seq.start),
|
|
11819
12001
|
"-i",
|
|
11820
|
-
|
|
12002
|
+
join12(a, seq.pattern),
|
|
11821
12003
|
"-start_number",
|
|
11822
12004
|
String(seq.start),
|
|
11823
12005
|
"-i",
|
|
11824
|
-
|
|
12006
|
+
join12(b, seq.pattern),
|
|
11825
12007
|
"-lavfi",
|
|
11826
12008
|
"psnr=stats_file=-",
|
|
11827
12009
|
"-f",
|
|
@@ -11896,7 +12078,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
11896
12078
|
const html = await readCompositions(dir);
|
|
11897
12079
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
11898
12080
|
const narration = scanNarration(
|
|
11899
|
-
await
|
|
12081
|
+
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
11900
12082
|
await listFiles(dir)
|
|
11901
12083
|
);
|
|
11902
12084
|
const budget2 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -11933,8 +12115,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
11933
12115
|
}
|
|
11934
12116
|
async function declaredStops(dir) {
|
|
11935
12117
|
return readStops(
|
|
11936
|
-
await
|
|
11937
|
-
await
|
|
12118
|
+
await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12119
|
+
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11938
12120
|
);
|
|
11939
12121
|
}
|
|
11940
12122
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -11974,7 +12156,7 @@ function scanNarration(page, files) {
|
|
|
11974
12156
|
];
|
|
11975
12157
|
}
|
|
11976
12158
|
async function readTiming(dir) {
|
|
11977
|
-
const raw2 = await
|
|
12159
|
+
const raw2 = await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
11978
12160
|
if (!raw2) return void 0;
|
|
11979
12161
|
try {
|
|
11980
12162
|
const parsed = JSON.parse(raw2);
|
|
@@ -11984,9 +12166,9 @@ async function readTiming(dir) {
|
|
|
11984
12166
|
}
|
|
11985
12167
|
}
|
|
11986
12168
|
async function listFiles(dir) {
|
|
11987
|
-
const entries = await
|
|
12169
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
11988
12170
|
return new Set(
|
|
11989
|
-
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("/"))
|
|
11990
12172
|
);
|
|
11991
12173
|
}
|
|
11992
12174
|
var INSTEAD = {
|
|
@@ -12242,22 +12424,22 @@ function scanDeterminism(html, file) {
|
|
|
12242
12424
|
return findings;
|
|
12243
12425
|
}
|
|
12244
12426
|
async function readCompositions(dir) {
|
|
12245
|
-
const entries = await
|
|
12427
|
+
const entries = await readdir4(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12246
12428
|
const files = entries.filter(
|
|
12247
12429
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12248
|
-
).map((e) =>
|
|
12430
|
+
).map((e) => join13(e.parentPath, e.name));
|
|
12249
12431
|
return Promise.all(
|
|
12250
|
-
files.map(async (f) => [relative(dir, f), await
|
|
12432
|
+
files.map(async (f) => [relative(dir, f), await readFile14(f, "utf8")])
|
|
12251
12433
|
);
|
|
12252
12434
|
}
|
|
12253
12435
|
|
|
12254
12436
|
// src/render/render.ts
|
|
12255
|
-
import { mkdir as
|
|
12256
|
-
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";
|
|
12257
12439
|
|
|
12258
12440
|
// src/render/captions.ts
|
|
12259
|
-
import { mkdir as
|
|
12260
|
-
import { join as
|
|
12441
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
|
|
12442
|
+
import { join as join14 } from "node:path";
|
|
12261
12443
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
12262
12444
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
12263
12445
|
function cssString(value) {
|
|
@@ -12348,11 +12530,11 @@ async function captionBlocker() {
|
|
|
12348
12530
|
}
|
|
12349
12531
|
async function renderCaptions(cues, style, deck, work) {
|
|
12350
12532
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
12351
|
-
const fontCss =
|
|
12533
|
+
const fontCss = join14(deck, DECK_FONT_CSS);
|
|
12352
12534
|
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
|
|
12535
|
+
const page = join14(work, "captions.html");
|
|
12536
|
+
await mkdir8(work, { recursive: true });
|
|
12537
|
+
await writeFile10(page, captionPage(cues, style, href));
|
|
12356
12538
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
12357
12539
|
const browser = await puppeteer.launch({
|
|
12358
12540
|
executablePath: await chromePath(CAPTION_NEED),
|
|
@@ -12372,7 +12554,7 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
12372
12554
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
12373
12555
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
12374
12556
|
await tab.screenshot({
|
|
12375
|
-
path:
|
|
12557
|
+
path: join14(work, name),
|
|
12376
12558
|
type: "png",
|
|
12377
12559
|
omitBackground: true,
|
|
12378
12560
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -12413,9 +12595,9 @@ function subtitlePlan(mode) {
|
|
|
12413
12595
|
async function render2(opts) {
|
|
12414
12596
|
const log = opts.log ?? (() => {
|
|
12415
12597
|
});
|
|
12416
|
-
const deck =
|
|
12417
|
-
const out =
|
|
12418
|
-
const work =
|
|
12598
|
+
const deck = resolve4(opts.deck);
|
|
12599
|
+
const out = resolve4(opts.out);
|
|
12600
|
+
const work = join15(dirname2(out), `.${basename3(out)}.parts`);
|
|
12419
12601
|
const timing = await readTiming2(deck);
|
|
12420
12602
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
12421
12603
|
const refusal = playbackRefusal(
|
|
@@ -12427,13 +12609,17 @@ async function render2(opts) {
|
|
|
12427
12609
|
}
|
|
12428
12610
|
const plan0 = subtitlePlan(opts.subtitles ?? "sidecar");
|
|
12429
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
|
+
);
|
|
12430
12616
|
const blocker = await captionBlocker();
|
|
12431
12617
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
12432
12618
|
}
|
|
12433
12619
|
const burnable = plan0.burn;
|
|
12434
|
-
await
|
|
12620
|
+
await mkdir9(work, { recursive: true });
|
|
12435
12621
|
try {
|
|
12436
|
-
const raw2 = opts.video ?
|
|
12622
|
+
const raw2 = opts.video ? resolve4(opts.video) : await capture(deck, join15(work, "raw.mp4"), opts, log);
|
|
12437
12623
|
const shot = await probe(raw2);
|
|
12438
12624
|
log(
|
|
12439
12625
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -12446,18 +12632,18 @@ async function render2(opts) {
|
|
|
12446
12632
|
);
|
|
12447
12633
|
}
|
|
12448
12634
|
const retimed = await retime(raw2, plan, work, log);
|
|
12449
|
-
const srtPath =
|
|
12635
|
+
const srtPath = join15(dirname2(out), SRT_NAME(out));
|
|
12450
12636
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
12451
12637
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
12452
12638
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
12453
|
-
if (srt) await
|
|
12639
|
+
if (srt) await writeFile11(srtPath, srt);
|
|
12454
12640
|
const burn = burnable && srt.length > 0;
|
|
12455
12641
|
await mux(retimed, timing, plan, deck, out, work, burn ? plan.cues : void 0, shot.fps, log);
|
|
12456
12642
|
if (playback > 1) {
|
|
12457
12643
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
12458
12644
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
12459
12645
|
if (warning) log(`render: ${warning}`);
|
|
12460
|
-
const fast =
|
|
12646
|
+
const fast = join15(work, `fast.${basename3(out)}`);
|
|
12461
12647
|
const muxed = await probe(out);
|
|
12462
12648
|
await runTool(
|
|
12463
12649
|
"ffmpeg",
|
|
@@ -12487,8 +12673,8 @@ async function render2(opts) {
|
|
|
12487
12673
|
}
|
|
12488
12674
|
}
|
|
12489
12675
|
async function readTiming2(deck) {
|
|
12490
|
-
const path2 =
|
|
12491
|
-
const text2 = await
|
|
12676
|
+
const path2 = join15(deck, TIMING_FILE);
|
|
12677
|
+
const text2 = await readFile15(path2, "utf8").catch(() => {
|
|
12492
12678
|
throw new Error(
|
|
12493
12679
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
12494
12680
|
);
|
|
@@ -12527,20 +12713,20 @@ async function retime(raw2, plan, work, log) {
|
|
|
12527
12713
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
12528
12714
|
const list = [];
|
|
12529
12715
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
12530
|
-
const file =
|
|
12716
|
+
const file = join15(work, `p${String(i).padStart(4, "0")}.ts`);
|
|
12531
12717
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
12532
12718
|
list.push(file);
|
|
12533
12719
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
12534
12720
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
12535
12721
|
}
|
|
12536
12722
|
}
|
|
12537
|
-
const listFile =
|
|
12538
|
-
await
|
|
12723
|
+
const listFile = join15(work, "pieces.txt");
|
|
12724
|
+
await writeFile11(
|
|
12539
12725
|
listFile,
|
|
12540
12726
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
12541
12727
|
`
|
|
12542
12728
|
);
|
|
12543
|
-
const joined =
|
|
12729
|
+
const joined = join15(work, "retimed.mp4");
|
|
12544
12730
|
await runTool("ffmpeg", [
|
|
12545
12731
|
"-y",
|
|
12546
12732
|
"-hide_banner",
|
|
@@ -12562,7 +12748,7 @@ async function retime(raw2, plan, work, log) {
|
|
|
12562
12748
|
}
|
|
12563
12749
|
async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
12564
12750
|
const inputs = plan.audio.map((a) => ({
|
|
12565
|
-
file:
|
|
12751
|
+
file: join15(deck, timing.audioDir, a.audio),
|
|
12566
12752
|
delayMs: a.delayMs
|
|
12567
12753
|
}));
|
|
12568
12754
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -12583,8 +12769,8 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
12583
12769
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
12584
12770
|
}
|
|
12585
12771
|
if (graph.length > 0) {
|
|
12586
|
-
const script =
|
|
12587
|
-
await
|
|
12772
|
+
const script = join15(work, "mux.filter");
|
|
12773
|
+
await writeFile11(script, `${graph.join(";\n")}
|
|
12588
12774
|
`);
|
|
12589
12775
|
args.push("-filter_complex_script", script);
|
|
12590
12776
|
}
|
|
@@ -12599,13 +12785,13 @@ async function mux(video, timing, plan, deck, out, work, burnCues, fps, log) {
|
|
|
12599
12785
|
log(
|
|
12600
12786
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
12601
12787
|
);
|
|
12602
|
-
await
|
|
12788
|
+
await mkdir9(dirname2(out), { recursive: true });
|
|
12603
12789
|
await runTool("ffmpeg", args, { cwd: work });
|
|
12604
12790
|
}
|
|
12605
12791
|
|
|
12606
12792
|
// src/pack/pack.ts
|
|
12607
|
-
import { mkdir as
|
|
12608
|
-
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";
|
|
12609
12795
|
import { unzipSync, zipSync } from "fflate";
|
|
12610
12796
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
12611
12797
|
var STORED = /* @__PURE__ */ new Set([
|
|
@@ -12644,12 +12830,12 @@ async function writePack(pack2, files, out) {
|
|
|
12644
12830
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
12645
12831
|
}
|
|
12646
12832
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
12647
|
-
await
|
|
12648
|
-
await
|
|
12833
|
+
await mkdir10(dirname3(out), { recursive: true });
|
|
12834
|
+
await writeFile12(out, zip);
|
|
12649
12835
|
return zip.length;
|
|
12650
12836
|
}
|
|
12651
12837
|
async function readPack(path2) {
|
|
12652
|
-
return openPack(new Uint8Array(await
|
|
12838
|
+
return openPack(new Uint8Array(await readFile16(path2)), path2);
|
|
12653
12839
|
}
|
|
12654
12840
|
function openPack(bytes, label = "pack") {
|
|
12655
12841
|
if (bytes.length < 4 || bytes[0] !== 80 || bytes[1] !== 75)
|
|
@@ -12693,8 +12879,8 @@ function why2(error) {
|
|
|
12693
12879
|
}
|
|
12694
12880
|
|
|
12695
12881
|
// src/prefs.ts
|
|
12696
|
-
import { readFile as
|
|
12697
|
-
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";
|
|
12698
12884
|
var CONFIG_FILE = "decksmith.config.json";
|
|
12699
12885
|
var PREF_KEYS = Object.keys(prefsSchema.shape);
|
|
12700
12886
|
var NARRATION_KEYS = Object.keys(prefsSchema.shape.narration.unwrap().shape);
|
|
@@ -12711,10 +12897,10 @@ async function loadPrefs(overrides = {}, cwd = process.cwd(), source) {
|
|
|
12711
12897
|
return parsed;
|
|
12712
12898
|
}
|
|
12713
12899
|
async function findConfig(from) {
|
|
12714
|
-
let dir =
|
|
12900
|
+
let dir = resolve5(from);
|
|
12715
12901
|
for (; ; ) {
|
|
12716
|
-
const path2 =
|
|
12717
|
-
const text2 = await
|
|
12902
|
+
const path2 = join16(dir, CONFIG_FILE);
|
|
12903
|
+
const text2 = await readFile17(path2, "utf8").catch(() => void 0);
|
|
12718
12904
|
if (text2 !== void 0) {
|
|
12719
12905
|
try {
|
|
12720
12906
|
return { path: path2, value: JSON.parse(text2) };
|
|
@@ -12722,7 +12908,7 @@ async function findConfig(from) {
|
|
|
12722
12908
|
throw new Error(`${path2} is not valid JSON.`);
|
|
12723
12909
|
}
|
|
12724
12910
|
}
|
|
12725
|
-
const up =
|
|
12911
|
+
const up = dirname4(dir);
|
|
12726
12912
|
if (up === dir) return void 0;
|
|
12727
12913
|
dir = up;
|
|
12728
12914
|
}
|
|
@@ -12768,26 +12954,26 @@ ${issues}`);
|
|
|
12768
12954
|
|
|
12769
12955
|
// src/tmpdir.ts
|
|
12770
12956
|
import { realpathSync } from "node:fs";
|
|
12771
|
-
import { createRequire as
|
|
12957
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
12772
12958
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
12773
|
-
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";
|
|
12774
12960
|
var cached;
|
|
12775
12961
|
function real(dir) {
|
|
12776
|
-
let at =
|
|
12962
|
+
let at = resolve6(dir);
|
|
12777
12963
|
const tail3 = [];
|
|
12778
12964
|
for (; ; ) {
|
|
12779
12965
|
try {
|
|
12780
|
-
return
|
|
12966
|
+
return join17(realpathSync(at), ...tail3);
|
|
12781
12967
|
} catch {
|
|
12782
|
-
const up =
|
|
12783
|
-
if (up === at) return
|
|
12968
|
+
const up = dirname5(at);
|
|
12969
|
+
if (up === at) return resolve6(dir);
|
|
12784
12970
|
tail3.unshift(basename4(at));
|
|
12785
12971
|
at = up;
|
|
12786
12972
|
}
|
|
12787
12973
|
}
|
|
12788
12974
|
}
|
|
12789
12975
|
function packageRoot() {
|
|
12790
|
-
cached ??= real(
|
|
12976
|
+
cached ??= real(dirname5(createRequire4(import.meta.url).resolve("../package.json")));
|
|
12791
12977
|
return cached;
|
|
12792
12978
|
}
|
|
12793
12979
|
function insideRoot(dir) {
|
|
@@ -12820,8 +13006,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12820
13006
|
if (!format) throw new Error("no deck-16x9 format");
|
|
12821
13007
|
const step = opts.onStep ?? (() => {
|
|
12822
13008
|
});
|
|
12823
|
-
const out =
|
|
12824
|
-
await
|
|
13009
|
+
const out = resolve7(outDir);
|
|
13010
|
+
await mkdir11(out, { recursive: true });
|
|
12825
13011
|
const speed = opts.speed ?? 1;
|
|
12826
13012
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
12827
13013
|
const deck = emitDeck(storyboard, source, format, await deckRuntime(), {
|
|
@@ -12834,47 +13020,48 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12834
13020
|
});
|
|
12835
13021
|
const files = [];
|
|
12836
13022
|
const write = async (name, text2) => {
|
|
12837
|
-
await
|
|
12838
|
-
files.push(
|
|
13023
|
+
await writeFile13(join18(out, name), text2);
|
|
13024
|
+
files.push(join18(out, name));
|
|
12839
13025
|
};
|
|
12840
13026
|
await write("index.html", deck.composition);
|
|
12841
13027
|
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)}
|
|
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)}
|
|
12857
13042
|
`);
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
}
|
|
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
|
+
);
|
|
12864
13048
|
}
|
|
12865
13049
|
if (deck.page) {
|
|
12866
13050
|
await write(DECK_PAGE, deck.page);
|
|
12867
|
-
await
|
|
12868
|
-
files.push(
|
|
13051
|
+
await cp2(playerBundle(), join18(out, PLAYER_FILE));
|
|
13052
|
+
files.push(join18(out, PLAYER_FILE));
|
|
12869
13053
|
}
|
|
12870
13054
|
files.push(...await vendorKatex(out));
|
|
12871
|
-
|
|
13055
|
+
files.push(...await vendorScripts(out, deck.composition));
|
|
13056
|
+
if (opts.assetsFrom) {
|
|
13057
|
+
files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
|
|
13058
|
+
}
|
|
12872
13059
|
if (opts.narration && opts.audioFrom) {
|
|
12873
|
-
files.push(...await copyAudio(opts.audioFrom, opts.narration, out));
|
|
13060
|
+
files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
|
|
12874
13061
|
}
|
|
12875
13062
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
12876
13063
|
step(
|
|
12877
|
-
`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")}`
|
|
12878
13065
|
);
|
|
12879
13066
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
12880
13067
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -12890,79 +13077,28 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
12890
13077
|
)}
|
|
12891
13078
|
`;
|
|
12892
13079
|
async function deckRuntime() {
|
|
12893
|
-
const path2 =
|
|
13080
|
+
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
12894
13081
|
try {
|
|
12895
|
-
return await
|
|
13082
|
+
return await readFile18(path2, "utf8");
|
|
12896
13083
|
} catch {
|
|
12897
13084
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
12898
13085
|
}
|
|
12899
13086
|
}
|
|
12900
13087
|
function playerBundle() {
|
|
12901
|
-
const require2 =
|
|
13088
|
+
const require2 = createRequire5(import.meta.url);
|
|
12902
13089
|
try {
|
|
12903
|
-
return
|
|
13090
|
+
return join18(dirname6(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12904
13091
|
} catch {
|
|
12905
13092
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12906
13093
|
}
|
|
12907
13094
|
}
|
|
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
13095
|
export {
|
|
12965
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,
|
|
12966
13102
|
COMFORTABLE_CPS,
|
|
12967
13103
|
CONFIG_FILE,
|
|
12968
13104
|
DECK_PAGE,
|
|
@@ -12992,6 +13128,7 @@ export {
|
|
|
12992
13128
|
assertInsideResolves,
|
|
12993
13129
|
assertRefsResolve,
|
|
12994
13130
|
attachClips,
|
|
13131
|
+
bandReserve,
|
|
12995
13132
|
barCompareParamsSchema,
|
|
12996
13133
|
beatRoleSchema,
|
|
12997
13134
|
beatSchema,
|
|
@@ -13000,6 +13137,7 @@ export {
|
|
|
13000
13137
|
calloutParamsSchema,
|
|
13001
13138
|
canvasProblem,
|
|
13002
13139
|
canvasWarnings,
|
|
13140
|
+
captionFontSize,
|
|
13003
13141
|
check,
|
|
13004
13142
|
claimFigureParamsSchema,
|
|
13005
13143
|
codexPlanner,
|