@jokerized/decksmith 0.9.0 → 0.9.2
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 +213 -0
- package/dist/cli.js +20037 -14179
- package/dist/deck-player-element.js +1 -1
- package/dist/deck-player.js +1 -1
- package/dist/index.js +10605 -4957
- package/dist/mcp.js +1558 -319
- package/dist/types/bespoke/art.d.ts +173 -0
- package/dist/types/bespoke/cache.d.ts +69 -0
- package/dist/types/bespoke/callouts.d.ts +76 -0
- package/dist/types/bespoke/contract.d.ts +57 -0
- package/dist/types/bespoke/inspect.d.ts +69 -0
- package/dist/types/bespoke/pipeline.d.ts +207 -0
- package/dist/types/bespoke/probe.d.ts +34 -0
- package/dist/types/bespoke/prompt.d.ts +182 -0
- package/dist/types/bespoke/references.d.ts +73 -0
- package/dist/types/bespoke/repair.d.ts +121 -0
- package/dist/types/bespoke/scene.d.ts +97 -0
- package/dist/types/bespoke/select.d.ts +52 -0
- package/dist/types/bespoke/sheet.d.ts +26 -0
- package/dist/types/bespoke/shots.d.ts +128 -0
- package/dist/types/bespoke/untangle.d.ts +5 -0
- package/dist/types/emit/archetypes/index.d.ts +0 -7
- package/dist/types/emit/composition.d.ts +37 -2
- package/dist/types/emit/kit.d.ts +17 -0
- package/dist/types/emit/themes/pick.d.ts +6 -3
- package/dist/types/index.d.ts +17 -0
- package/dist/types/plan/codex.d.ts +36 -0
- package/dist/types/prefs.d.ts +10 -1
- package/dist/types/render/capture.d.ts +6 -0
- package/dist/types/render/timing.d.ts +3 -0
- package/dist/types/types.d.ts +71 -0
- package/dist/types/verify/check.d.ts +1 -1
- package/dist/types/verify/index.d.ts +22 -0
- package/dist/types/verify/scenes.d.ts +325 -0
- package/package.json +2 -1
package/dist/mcp.js
CHANGED
|
@@ -58,22 +58,43 @@ var VERSION = createRequire2(import.meta.url)("../package.json").version;
|
|
|
58
58
|
|
|
59
59
|
// src/mcp/tools.ts
|
|
60
60
|
import { randomBytes } from "node:crypto";
|
|
61
|
-
import { mkdir as
|
|
61
|
+
import { mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile15, rm as rm8 } from "node:fs/promises";
|
|
62
62
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
63
|
-
import { basename as basename5, isAbsolute as isAbsolute2, join as
|
|
63
|
+
import { basename as basename5, isAbsolute as isAbsolute2, join as join16, resolve as resolve9, sep as sep4 } from "node:path";
|
|
64
64
|
import { zipSync as zipSync2 } from "fflate";
|
|
65
65
|
import { z as z5 } from "zod";
|
|
66
66
|
|
|
67
67
|
// src/index.ts
|
|
68
|
-
import { cp as cp2, mkdir as
|
|
68
|
+
import { cp as cp2, mkdir as mkdir11, readFile as readFile13, writeFile as writeFile13 } from "node:fs/promises";
|
|
69
69
|
import { createRequire as createRequire4 } from "node:module";
|
|
70
|
-
import { dirname as dirname5, join as
|
|
70
|
+
import { dirname as dirname5, join as join14, resolve as resolve7 } from "node:path";
|
|
71
71
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
72
72
|
|
|
73
|
+
// src/bespoke/art.ts
|
|
74
|
+
import { copyFile, mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
75
|
+
import { join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
76
|
+
var MAX_ART_BYTES = 12 * 1024 * 1024;
|
|
77
|
+
async function copyArt(entries, out) {
|
|
78
|
+
const written = [];
|
|
79
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
if (!e.art || seen.has(e.art.name)) continue;
|
|
82
|
+
seen.add(e.art.name);
|
|
83
|
+
const dir = join2(out, "assets", "bespoke");
|
|
84
|
+
await mkdir(dir, { recursive: true });
|
|
85
|
+
await copyFile(e.art.file, join2(dir, e.art.name));
|
|
86
|
+
written.push(join2(dir, e.art.name));
|
|
87
|
+
}
|
|
88
|
+
return written;
|
|
89
|
+
}
|
|
90
|
+
function artHref(art) {
|
|
91
|
+
return `assets/bespoke/${art.name}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
// src/build/files.ts
|
|
74
|
-
import { cp, mkdir as
|
|
95
|
+
import { cp, mkdir as mkdir3, readdir, readFile as readFile3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
75
96
|
import { createRequire as createRequire3 } from "node:module";
|
|
76
|
-
import { dirname as dirname2, join as
|
|
97
|
+
import { dirname as dirname2, join as join4, resolve as resolve3 } from "node:path";
|
|
77
98
|
import { fileURLToPath } from "node:url";
|
|
78
99
|
|
|
79
100
|
// src/emit/faces.ts
|
|
@@ -1065,11 +1086,11 @@ function reshape(target, to, at, seconds, first) {
|
|
|
1065
1086
|
sec(at)
|
|
1066
1087
|
);
|
|
1067
1088
|
}
|
|
1068
|
-
function travel(target,
|
|
1089
|
+
function travel(target, route2, at, seconds) {
|
|
1069
1090
|
if (!(seconds > 0)) throw new Error(`travel ${target}: ${seconds}s is no time to travel in`);
|
|
1070
1091
|
const legs = [];
|
|
1071
1092
|
let prev;
|
|
1072
|
-
for (const p of
|
|
1093
|
+
for (const p of route2) {
|
|
1073
1094
|
const q = { x: nv(p.x), y: nv(p.y) };
|
|
1074
1095
|
if (prev === void 0) {
|
|
1075
1096
|
prev = q;
|
|
@@ -1610,8 +1631,8 @@ function fitBoxes(req) {
|
|
|
1610
1631
|
|
|
1611
1632
|
// src/source/fonts.ts
|
|
1612
1633
|
import { createHash } from "node:crypto";
|
|
1613
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1614
|
-
import { join as
|
|
1634
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1635
|
+
import { join as join3 } from "node:path";
|
|
1615
1636
|
function serifFamilyFor(lang) {
|
|
1616
1637
|
return familyFor(lang)?.replace("Noto Sans", "Noto Serif") ?? null;
|
|
1617
1638
|
}
|
|
@@ -1631,9 +1652,9 @@ async function bundleFont(lang, glyphs, dir, opts = {}) {
|
|
|
1631
1652
|
const text2 = [...new Set(glyphs)].filter((c) => c > " ").sort().join("");
|
|
1632
1653
|
const stamp = `/* decksmith ${createHash("sha256").update(`${families.join("+")}
|
|
1633
1654
|
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1634
|
-
await
|
|
1635
|
-
const cssPath =
|
|
1636
|
-
const cached2 = await
|
|
1655
|
+
await mkdir2(dir, { recursive: true });
|
|
1656
|
+
const cssPath = join3(dir, "fonts.css");
|
|
1657
|
+
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1637
1658
|
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1638
1659
|
const res = await fetch(
|
|
1639
1660
|
`https://fonts.googleapis.com/css2?${families.map((f) => `family=${f.replaceAll(" ", "+")}:wght@400;500;700`).join("&")}&text=${encodeURIComponent(text2)}&display=block`,
|
|
@@ -1648,13 +1669,13 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1648
1669
|
const font = await fetch(url);
|
|
1649
1670
|
if (!font.ok) throw new Error(`${url}: HTTP ${font.status}`);
|
|
1650
1671
|
const name = `${slug}-${i}.woff2`;
|
|
1651
|
-
await
|
|
1672
|
+
await writeFile2(join3(dir, name), Buffer.from(await font.arrayBuffer()));
|
|
1652
1673
|
css = css.replaceAll(url, name);
|
|
1653
1674
|
files.push(name);
|
|
1654
1675
|
}
|
|
1655
1676
|
css = `${stamp}
|
|
1656
1677
|
${css}`;
|
|
1657
|
-
await
|
|
1678
|
+
await writeFile2(cssPath, css);
|
|
1658
1679
|
return { family, css, files };
|
|
1659
1680
|
}
|
|
1660
1681
|
function localNames(css) {
|
|
@@ -1960,9 +1981,9 @@ function scenePadding(format) {
|
|
|
1960
1981
|
return reserve > 0 ? `${base} ${PAD_Y + reserve}px` : base;
|
|
1961
1982
|
}
|
|
1962
1983
|
function referenceSpaceCss(format) {
|
|
1963
|
-
const
|
|
1964
|
-
if (
|
|
1965
|
-
return ` .scene { width: ${refWidth(format)}px; height: ${refHeight(format)}px; zoom: ${
|
|
1984
|
+
const zoom2 = zoomOf(format);
|
|
1985
|
+
if (zoom2 === 1) return "";
|
|
1986
|
+
return ` .scene { width: ${refWidth(format)}px; height: ${refHeight(format)}px; zoom: ${zoom2}; }
|
|
1966
1987
|
.scene:has(.scene) { width: 100%; height: 100%; padding: 0; zoom: 1; }`;
|
|
1967
1988
|
}
|
|
1968
1989
|
function pace(scene, speed) {
|
|
@@ -2017,26 +2038,26 @@ function audioNames(narration) {
|
|
|
2017
2038
|
}
|
|
2018
2039
|
async function vendorKatex(out) {
|
|
2019
2040
|
const require2 = createRequire3(import.meta.url);
|
|
2020
|
-
const dist =
|
|
2021
|
-
const css = await
|
|
2041
|
+
const dist = join4(dirname2(require2.resolve("katex/package.json")), "dist");
|
|
2042
|
+
const css = await readFile3(join4(dist, "katex.min.css"), "utf8");
|
|
2022
2043
|
const written = [];
|
|
2023
|
-
await
|
|
2024
|
-
for (const file of await readdir(
|
|
2044
|
+
await mkdir3(join4(out, "katex/fonts"), { recursive: true });
|
|
2045
|
+
for (const file of await readdir(join4(dist, "fonts"))) {
|
|
2025
2046
|
if (!file.endsWith(".woff2")) continue;
|
|
2026
|
-
await cp(
|
|
2027
|
-
written.push(
|
|
2047
|
+
await cp(join4(dist, "fonts", file), join4(out, "katex/fonts", file));
|
|
2048
|
+
written.push(join4(out, "katex/fonts", file));
|
|
2028
2049
|
}
|
|
2029
2050
|
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
2030
2051
|
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
2031
2052
|
return kept ? `src:${kept}` : whole;
|
|
2032
2053
|
});
|
|
2033
|
-
await
|
|
2034
|
-
written.push(
|
|
2054
|
+
await writeFile3(join4(out, "katex/katex.min.css"), woff2Only);
|
|
2055
|
+
written.push(join4(out, "katex/katex.min.css"));
|
|
2035
2056
|
return written;
|
|
2036
2057
|
}
|
|
2037
2058
|
async function vendorScripts(out, composition) {
|
|
2038
2059
|
const require2 = createRequire3(import.meta.url);
|
|
2039
|
-
await
|
|
2060
|
+
await mkdir3(join4(out, "vendor"), { recursive: true });
|
|
2040
2061
|
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
2041
2062
|
const written = [];
|
|
2042
2063
|
for (const [pkg, rel, name] of [
|
|
@@ -2046,62 +2067,62 @@ async function vendorScripts(out, composition) {
|
|
|
2046
2067
|
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
2047
2068
|
]) {
|
|
2048
2069
|
if (!wanted(name)) continue;
|
|
2049
|
-
const from =
|
|
2050
|
-
await cp(from,
|
|
2051
|
-
written.push(
|
|
2070
|
+
const from = join4(dirname2(require2.resolve(pkg)), rel);
|
|
2071
|
+
await cp(from, join4(out, "vendor", name));
|
|
2072
|
+
written.push(join4(out, "vendor", name));
|
|
2052
2073
|
}
|
|
2053
2074
|
if (wanted("ds-morph.js")) {
|
|
2054
2075
|
await cp(
|
|
2055
2076
|
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
2056
|
-
|
|
2077
|
+
join4(out, "vendor", "ds-morph.js")
|
|
2057
2078
|
);
|
|
2058
|
-
written.push(
|
|
2079
|
+
written.push(join4(out, "vendor", "ds-morph.js"));
|
|
2059
2080
|
}
|
|
2060
2081
|
return written;
|
|
2061
2082
|
}
|
|
2062
2083
|
async function copyAssets(sourceDir, out, figures, step) {
|
|
2063
|
-
const from =
|
|
2064
|
-
if (!await
|
|
2084
|
+
const from = join4(resolve3(sourceDir), "assets");
|
|
2085
|
+
if (!await stat2(from).catch(() => null)) {
|
|
2065
2086
|
step(`build: no assets/ beside source.json, skipping`);
|
|
2066
2087
|
return [];
|
|
2067
2088
|
}
|
|
2068
2089
|
const wanted = new Set(
|
|
2069
2090
|
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
2070
2091
|
);
|
|
2071
|
-
await
|
|
2092
|
+
await mkdir3(join4(out, "assets"), { recursive: true });
|
|
2072
2093
|
const written = [];
|
|
2073
2094
|
let copied = 0;
|
|
2074
2095
|
for (const name of wanted) {
|
|
2075
|
-
const src =
|
|
2096
|
+
const src = resolve3(join4(from, name));
|
|
2076
2097
|
if (!src.startsWith(`${from}/`)) continue;
|
|
2077
|
-
if (!await
|
|
2078
|
-
await
|
|
2079
|
-
await cp(src,
|
|
2080
|
-
written.push(
|
|
2098
|
+
if (!await stat2(src).catch(() => null)) continue;
|
|
2099
|
+
await mkdir3(dirname2(join4(out, "assets", name)), { recursive: true });
|
|
2100
|
+
await cp(src, join4(out, "assets", name));
|
|
2101
|
+
written.push(join4(out, "assets", name));
|
|
2081
2102
|
copied++;
|
|
2082
2103
|
}
|
|
2083
|
-
const fonts =
|
|
2084
|
-
if (await
|
|
2085
|
-
await cp(fonts,
|
|
2086
|
-
written.push(
|
|
2104
|
+
const fonts = join4(from, "fonts");
|
|
2105
|
+
if (await stat2(fonts).catch(() => null)) {
|
|
2106
|
+
await cp(fonts, join4(out, "assets", "fonts"), { recursive: true });
|
|
2107
|
+
written.push(join4(out, "assets", "fonts"));
|
|
2087
2108
|
}
|
|
2088
2109
|
step(`build: copied ${copied} referenced figure(s)`);
|
|
2089
2110
|
return written;
|
|
2090
2111
|
}
|
|
2091
2112
|
async function copyAudio(from, narration, out, step) {
|
|
2092
|
-
const dir =
|
|
2093
|
-
await
|
|
2113
|
+
const dir = join4(out, narration.dir);
|
|
2114
|
+
await mkdir3(dir, { recursive: true });
|
|
2094
2115
|
const names = audioNames(narration);
|
|
2095
2116
|
for (const name of names) {
|
|
2096
|
-
await cp(
|
|
2117
|
+
await cp(join4(resolve3(from), name), join4(dir, name)).catch(() => {
|
|
2097
2118
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
2098
2119
|
});
|
|
2099
2120
|
}
|
|
2100
2121
|
step(`narration: ${names.length} audio file(s) \u2192 ${dir}`);
|
|
2101
|
-
return names.map((n3) =>
|
|
2122
|
+
return names.map((n3) => join4(dir, n3));
|
|
2102
2123
|
}
|
|
2103
2124
|
async function refreshFont(storyboard, source, out, step, theme) {
|
|
2104
|
-
const dir =
|
|
2125
|
+
const dir = join4(out, "assets", "fonts");
|
|
2105
2126
|
const look = deckLook(storyboard, theme).theme;
|
|
2106
2127
|
const type = typeOf(faceOf(look.fontStack));
|
|
2107
2128
|
const serif = /"Noto Serif /.test(`${look.fontStack} ${look.displayStack ?? ""}`);
|
|
@@ -2147,10 +2168,10 @@ async function vendorFace(face, dir) {
|
|
|
2147
2168
|
var PACK_DISPLAY = "font-display: block;\n ascent-override: 96.875%;\n descent-override: 24.1211%;\n line-gap-override: 0%";
|
|
2148
2169
|
async function vendorFontsource(pkgName, family, dir) {
|
|
2149
2170
|
const pkg = dirname2(createRequire3(import.meta.url).resolve(pkgName));
|
|
2150
|
-
const css = await
|
|
2151
|
-
await
|
|
2171
|
+
const css = await readFile3(join4(pkg, "index.css"), "utf8");
|
|
2172
|
+
await mkdir3(dir, { recursive: true });
|
|
2152
2173
|
for (const [, name] of css.matchAll(/url\(\.\/files\/([^)]+)\)/g)) {
|
|
2153
|
-
await cp(
|
|
2174
|
+
await cp(join4(pkg, "files", name ?? ""), join4(dir, name ?? ""));
|
|
2154
2175
|
}
|
|
2155
2176
|
const declared = `${family} Variable`;
|
|
2156
2177
|
const ours = css.replaceAll(`font-family: '${declared}'`, `font-family: '${family}'`).replaceAll("font-display: swap", family === "Inter" ? "font-display: block" : PACK_DISPLAY).replace(/url\(\.\/files\/([^)]+)\) format\('woff2-variations'\)/g, "url($1) format('woff2')");
|
|
@@ -2160,43 +2181,6 @@ async function vendorFontsource(pkgName, family, dir) {
|
|
|
2160
2181
|
return ours;
|
|
2161
2182
|
}
|
|
2162
2183
|
|
|
2163
|
-
// src/deck/playback.ts
|
|
2164
|
-
var PLAYER_META = "decksmith-player";
|
|
2165
|
-
var PLAYER_MARKER = `<meta name="${PLAYER_META}" content="2" />`;
|
|
2166
|
-
function markV2(page) {
|
|
2167
|
-
const existing = new RegExp(`<meta name="${PLAYER_META}"[^>]*>`);
|
|
2168
|
-
if (existing.test(page)) return page.replace(existing, PLAYER_MARKER);
|
|
2169
|
-
const head = /<head[^>]*>/i.exec(page);
|
|
2170
|
-
if (!head) throw new Error("not a deck page: it has no <head> to mark");
|
|
2171
|
-
const at = head.index + head[0].length;
|
|
2172
|
-
return `${page.slice(0, at)}
|
|
2173
|
-
${PLAYER_MARKER}${page.slice(at)}`;
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
// src/emit/fit.ts
|
|
2177
|
-
function fitOf(ink2, region) {
|
|
2178
|
-
const r = (n3) => Math.round(n3 * 1e3) / 1e3;
|
|
2179
|
-
return { fill: region > 0 ? r(ink2 / region) : 0, region: r(region), ink: r(ink2) };
|
|
2180
|
-
}
|
|
2181
|
-
var GROWTH = 1.6;
|
|
2182
|
-
function growToFit(height, budget2, lo = 1, hi = GROWTH) {
|
|
2183
|
-
if (height(hi) <= budget2) return hi;
|
|
2184
|
-
if (height(lo) > budget2) return lo;
|
|
2185
|
-
let a = lo;
|
|
2186
|
-
let b = hi;
|
|
2187
|
-
for (let i = 0; i < 24; i++) {
|
|
2188
|
-
const mid = (a + b) / 2;
|
|
2189
|
-
if (height(mid) <= budget2) a = mid;
|
|
2190
|
-
else b = mid;
|
|
2191
|
-
}
|
|
2192
|
-
return Math.floor(a * 128) / 128;
|
|
2193
|
-
}
|
|
2194
|
-
var MEASURE_SLACK = 0.92;
|
|
2195
|
-
function isV2(ctx) {
|
|
2196
|
-
return ctx.design === "v2";
|
|
2197
|
-
}
|
|
2198
|
-
var FIT_FILE = "fit.json";
|
|
2199
|
-
|
|
2200
2184
|
// src/emit/archetypes/title.ts
|
|
2201
2185
|
function isPortrait(format) {
|
|
2202
2186
|
return format.height > format.width;
|
|
@@ -2396,6 +2380,452 @@ var title = (beat, ctx) => {
|
|
|
2396
2380
|
};
|
|
2397
2381
|
};
|
|
2398
2382
|
|
|
2383
|
+
// src/bespoke/callouts.ts
|
|
2384
|
+
var LABEL_PX = 56;
|
|
2385
|
+
var LABEL_MIN_PX = 44;
|
|
2386
|
+
var ZONE_H = Math.round(LABEL_PX * 1.7);
|
|
2387
|
+
var GAP = 12;
|
|
2388
|
+
var ZONE_W = 560;
|
|
2389
|
+
var EDGE = 8;
|
|
2390
|
+
var DOT_IN = 14;
|
|
2391
|
+
function calloutZones(subjects, W) {
|
|
2392
|
+
const cx = subjects.map((s) => s.x + s.w / 2);
|
|
2393
|
+
const order = cx.map((_, i) => i).sort((a, b) => cx[a] - cx[b]);
|
|
2394
|
+
const zones = [];
|
|
2395
|
+
order.forEach((i, rank) => {
|
|
2396
|
+
const s = subjects[i];
|
|
2397
|
+
const c = cx[i];
|
|
2398
|
+
const left = rank > 0 ? (c - cx[order[rank - 1]]) / 2 - GAP / 2 : c - EDGE;
|
|
2399
|
+
const right = rank < order.length - 1 ? (cx[order[rank + 1]] - c) / 2 - GAP / 2 : W - EDGE - c;
|
|
2400
|
+
const half = Math.max(80, Math.min(ZONE_W / 2, left, right));
|
|
2401
|
+
const x0 = Math.max(EDGE, Math.min(W - EDGE - 2 * half, c - half));
|
|
2402
|
+
const above = s.y - GAP - ZONE_H;
|
|
2403
|
+
const side = above >= EDGE ? "above" : "top";
|
|
2404
|
+
const y = side === "above" ? above : Math.max(EDGE, s.y + GAP);
|
|
2405
|
+
const dotY = side === "above" ? Math.min(s.y + s.h - 10, s.y + DOT_IN) : Math.min(s.y + s.h - 10, y + ZONE_H + DOT_IN);
|
|
2406
|
+
zones.push({
|
|
2407
|
+
subject: i + 1,
|
|
2408
|
+
side,
|
|
2409
|
+
x: Math.round(x0),
|
|
2410
|
+
y: Math.round(y),
|
|
2411
|
+
w: Math.round(2 * half),
|
|
2412
|
+
h: ZONE_H,
|
|
2413
|
+
dot: { x: Math.round(c), y: Math.round(dotY) }
|
|
2414
|
+
});
|
|
2415
|
+
});
|
|
2416
|
+
return zones.sort((a, b) => a.subject - b.subject);
|
|
2417
|
+
}
|
|
2418
|
+
function halves(text2) {
|
|
2419
|
+
const t2 = text2.trim();
|
|
2420
|
+
const spaces = [...t2.matchAll(/ /g)].map((m) => m.index ?? 0);
|
|
2421
|
+
if (spaces.length) {
|
|
2422
|
+
const mid = t2.length / 2;
|
|
2423
|
+
const at = spaces.reduce((a, b) => Math.abs(b - mid) < Math.abs(a - mid) ? b : a);
|
|
2424
|
+
return [t2.slice(0, at), t2.slice(at + 1)];
|
|
2425
|
+
}
|
|
2426
|
+
const chars = [...t2];
|
|
2427
|
+
if (chars.length < 4 || chars.every((c) => (c.codePointAt(0) ?? 0) < 128)) return void 0;
|
|
2428
|
+
const k = Math.ceil(chars.length / 2);
|
|
2429
|
+
return [chars.slice(0, k).join(""), chars.slice(k).join("")];
|
|
2430
|
+
}
|
|
2431
|
+
var plateH = (n3, fs) => Math.round(n3 === 1 ? fs * 1.6 : fs * (1.25 * n3 + 0.5));
|
|
2432
|
+
function fitLabel(text2, zone, theme, boxH = Infinity) {
|
|
2433
|
+
const face = faceOf(theme.fontStack);
|
|
2434
|
+
const w = (s, fs) => textWidth(s, fs, 700, 0, false, face) + 0.8 * fs;
|
|
2435
|
+
for (let fs = LABEL_PX; fs >= LABEL_MIN_PX; fs -= 2)
|
|
2436
|
+
if (w(text2, fs) <= zone.w) return { fs, lines: [text2] };
|
|
2437
|
+
const two = halves(text2);
|
|
2438
|
+
if (!two) return void 0;
|
|
2439
|
+
for (let fs = LABEL_PX; fs >= LABEL_MIN_PX; fs -= 2) {
|
|
2440
|
+
if (Math.max(w(two[0], fs), w(two[1], fs)) > zone.w) continue;
|
|
2441
|
+
const h = plateH(2, fs);
|
|
2442
|
+
const ok = zone.side === "above" ? zone.y + zone.h - h >= EDGE : zone.y + h <= boxH - EDGE;
|
|
2443
|
+
if (ok) return { fs, lines: two };
|
|
2444
|
+
}
|
|
2445
|
+
return void 0;
|
|
2446
|
+
}
|
|
2447
|
+
function withZone(s, z7) {
|
|
2448
|
+
if (!z7) return s;
|
|
2449
|
+
const x0 = Math.min(s.x, z7.x);
|
|
2450
|
+
const y0 = Math.min(s.y, z7.y);
|
|
2451
|
+
const x1 = Math.max(s.x + s.w, z7.x + z7.w);
|
|
2452
|
+
const y1 = Math.max(s.y + s.h, z7.y + z7.h);
|
|
2453
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
2454
|
+
}
|
|
2455
|
+
var esc2 = (s) => s.replace(
|
|
2456
|
+
/[&<>"]/g,
|
|
2457
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]
|
|
2458
|
+
);
|
|
2459
|
+
function place(label, zone, theme, boxH) {
|
|
2460
|
+
const fit = fitLabel(label.text, zone, theme, boxH);
|
|
2461
|
+
if (!fit) return void 0;
|
|
2462
|
+
const { fs, lines } = fit;
|
|
2463
|
+
const face = faceOf(theme.fontStack);
|
|
2464
|
+
const pw = Math.ceil(
|
|
2465
|
+
Math.max(...lines.map((l) => textWidth(l, fs, 700, 0, false, face))) + 0.8 * fs
|
|
2466
|
+
);
|
|
2467
|
+
const ph = plateH(lines.length, fs);
|
|
2468
|
+
const cx = Math.max(zone.x + pw / 2, Math.min(zone.x + zone.w - pw / 2, zone.dot.x));
|
|
2469
|
+
const py = lines.length === 1 ? zone.y + (zone.h - ph) / 2 : zone.side === "above" ? zone.y + zone.h - ph : zone.y;
|
|
2470
|
+
const plate2 = { x: Math.round(cx - pw / 2), y: Math.round(py), w: pw, h: ph };
|
|
2471
|
+
const lx = Math.max(plate2.x + 24, Math.min(plate2.x + plate2.w - 24, zone.dot.x));
|
|
2472
|
+
const dotY = Math.max(zone.dot.y, plate2.y + plate2.h + 22);
|
|
2473
|
+
return {
|
|
2474
|
+
k: label.subject,
|
|
2475
|
+
fs,
|
|
2476
|
+
lines,
|
|
2477
|
+
plate: plate2,
|
|
2478
|
+
text: { x: Math.round(cx), y: Math.round(py + ph / 2) },
|
|
2479
|
+
lead: { x1: lx, y1: plate2.y + plate2.h, x2: zone.dot.x, y2: dotY - 10 },
|
|
2480
|
+
dot: { x: zone.dot.x, y: dotY }
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
function entrances(moves, subjects) {
|
|
2484
|
+
const out = /* @__PURE__ */ new Map();
|
|
2485
|
+
for (const m of moves)
|
|
2486
|
+
if (m.subject > 0 && !out.has(m.subject)) out.set(m.subject, m.t + 0.6 * m.dur);
|
|
2487
|
+
const reveal = moves.find((m, i) => m.subject === 0 && i === moves.length - 1);
|
|
2488
|
+
for (let k = 1; k <= subjects; k++) if (!out.has(k)) out.set(k, reveal ? reveal.t + 0.4 : 0.5);
|
|
2489
|
+
return out;
|
|
2490
|
+
}
|
|
2491
|
+
function calloutLayer(sid, labels, zones, moves, theme, W, H) {
|
|
2492
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2493
|
+
const placed = [];
|
|
2494
|
+
for (const l of labels) {
|
|
2495
|
+
const z7 = zones.find((x) => x.subject === l.subject);
|
|
2496
|
+
if (!z7 || seen.has(l.subject)) continue;
|
|
2497
|
+
const p = place(l, z7, theme, H);
|
|
2498
|
+
if (!p) continue;
|
|
2499
|
+
seen.add(l.subject);
|
|
2500
|
+
placed.push({ ...p, k: l.subject });
|
|
2501
|
+
}
|
|
2502
|
+
if (!placed.length) return { markup: "", script: "" };
|
|
2503
|
+
const tone2 = [theme.tones.a, theme.tones.b, theme.tones.c, theme.tones.d];
|
|
2504
|
+
const markup = `<svg class="ds-callouts" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="position:absolute;left:0;top:0;overflow:visible;pointer-events:none">${placed.map((p) => {
|
|
2505
|
+
const lab = labels.find((l) => l.subject === p.k);
|
|
2506
|
+
const len = Math.round(Math.hypot(p.lead.x2 - p.lead.x1, p.lead.y2 - p.lead.y1));
|
|
2507
|
+
return `<g id="${sid}-callout${p.k}" data-subject="${p.k}"><line id="${sid}-callout${p.k}-lead" x1="${p.lead.x1}" y1="${p.lead.y1}" x2="${p.lead.x2}" y2="${p.lead.y2}" stroke="${theme.fg}" stroke-width="5" stroke-linecap="round" stroke-dasharray="${len}" stroke-dashoffset="0"/><circle id="${sid}-callout${p.k}-dot" cx="${p.dot.x}" cy="${p.dot.y}" r="11" fill="${theme.fg}" stroke="${theme.bg}" stroke-width="4"/><g id="${sid}-callout${p.k}-tag"><rect x="${p.plate.x}" y="${p.plate.y}" width="${p.plate.w}" height="${p.plate.h}" rx="${Math.round(p.fs * 0.4)}" fill="${theme.panel}" stroke="${tone2[(p.k - 1) % 4]}" stroke-width="5"/><text x="${p.text.x}" y="${p.text.y}" font-size="${p.fs}" font-weight="700" fill="${theme.fg}" text-anchor="middle" dominant-baseline="middle">${p.lines.length === 1 ? esc2(lab.text) : p.lines.map(
|
|
2508
|
+
(l, i) => `<tspan x="${p.text.x}" dy="${i === 0 ? -0.625 * p.fs : 1.25 * p.fs}">${esc2(l)}</tspan>`
|
|
2509
|
+
).join("")}</text></g></g>`;
|
|
2510
|
+
}).join("")}</svg>`;
|
|
2511
|
+
const at = entrances(moves, Math.max(...placed.map((p) => p.k)));
|
|
2512
|
+
const r2 = (n3) => Math.round(n3 * 100) / 100;
|
|
2513
|
+
const lines = [
|
|
2514
|
+
"// The shell's labels (src/bespoke/callouts.ts): each enters as the camera arrives on its subject."
|
|
2515
|
+
];
|
|
2516
|
+
for (const p of placed) {
|
|
2517
|
+
const g = JSON.stringify(`#${sid}-callout${p.k}`);
|
|
2518
|
+
const tag = JSON.stringify(`#${sid}-callout${p.k}-tag`);
|
|
2519
|
+
const lead = JSON.stringify(`#${sid}-callout${p.k}-lead`);
|
|
2520
|
+
const dot = JSON.stringify(`#${sid}-callout${p.k}-dot`);
|
|
2521
|
+
const len = Math.round(Math.hypot(p.lead.x2 - p.lead.x1, p.lead.y2 - p.lead.y1));
|
|
2522
|
+
const t2 = r2(at.get(p.k) ?? 0.5);
|
|
2523
|
+
lines.push(
|
|
2524
|
+
`gsap.set(${g}, { opacity: 0 });`,
|
|
2525
|
+
`gsap.set(${tag}, { y: 14 });`,
|
|
2526
|
+
`gsap.set(${lead}, { strokeDashoffset: ${len} });`,
|
|
2527
|
+
`gsap.set(${dot}, { scale: 0, transformOrigin: "50% 50%" });`,
|
|
2528
|
+
`tl.fromTo(${g}, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: "power2.out", immediateRender: false }, ${t2});`,
|
|
2529
|
+
`tl.fromTo(${tag}, { y: 14 }, { y: 0, duration: 0.45, ease: "power3.out", immediateRender: false }, ${t2});`,
|
|
2530
|
+
`tl.fromTo(${lead}, { strokeDashoffset: ${len} }, { strokeDashoffset: 0, duration: 0.4, ease: "power2.out", immediateRender: false }, ${r2(t2 + 0.1)});`,
|
|
2531
|
+
`tl.fromTo(${dot}, { scale: 0, transformOrigin: "50% 50%" }, { scale: 1, duration: 0.3, ease: "back.out(2)", immediateRender: false }, ${r2(t2 + 0.4)});`
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
return { markup, script: lines.join("\n") };
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/bespoke/contract.ts
|
|
2538
|
+
import { parse } from "acorn";
|
|
2539
|
+
var SID_TOKEN = "SCENEID";
|
|
2540
|
+
function instantiate(f, sid) {
|
|
2541
|
+
const swap = (s) => s.split(SID_TOKEN).join(sid);
|
|
2542
|
+
return {
|
|
2543
|
+
markup: swap(f.markup),
|
|
2544
|
+
css: swap(f.css),
|
|
2545
|
+
script: swap(f.script),
|
|
2546
|
+
...f.shots ? { shots: f.shots } : {},
|
|
2547
|
+
...f.labels ? { labels: f.labels } : {}
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
var SCOPED = new RegExp(`^#${SID_TOKEN}(?![A-Za-z0-9_])`);
|
|
2551
|
+
var LOCAL_URL = new RegExp(`url\\(\\s*['"]?#${SID_TOKEN}-[\\w-]+['"]?\\s*\\)`, "g");
|
|
2552
|
+
|
|
2553
|
+
// src/bespoke/shots.ts
|
|
2554
|
+
var CAMERA_MAX_SCALE = 2.2;
|
|
2555
|
+
var CLOSE_MIN = 1.6;
|
|
2556
|
+
var MOVE = 1.1;
|
|
2557
|
+
var REVEAL = 1.3;
|
|
2558
|
+
var MIN_HOLD = 1.6;
|
|
2559
|
+
var ESTABLISH = 1.6;
|
|
2560
|
+
var CREEP = 1.03;
|
|
2561
|
+
var CREEP_GAP = 0.05;
|
|
2562
|
+
var ART_BAND = 120;
|
|
2563
|
+
function artPlacement(box) {
|
|
2564
|
+
return { x: 0, y: ART_BAND, w: box.width, h: Math.max(1, box.height - ART_BAND) };
|
|
2565
|
+
}
|
|
2566
|
+
function subjectsInBox(units, art, box) {
|
|
2567
|
+
const at = artPlacement(box);
|
|
2568
|
+
const k = Math.max(at.w / art.width, at.h / art.height);
|
|
2569
|
+
const ox = at.x + (at.w - art.width * k) / 2;
|
|
2570
|
+
const oy = at.y + (at.h - art.height * k) / 2;
|
|
2571
|
+
const out = [];
|
|
2572
|
+
for (const [ux, uy, uw, uh] of units) {
|
|
2573
|
+
const x0 = ox + ux * art.width * k;
|
|
2574
|
+
const y0 = oy + uy * art.height * k;
|
|
2575
|
+
const x1 = x0 + uw * art.width * k;
|
|
2576
|
+
const y1 = y0 + uh * art.height * k;
|
|
2577
|
+
const cx0 = Math.max(at.x, x0);
|
|
2578
|
+
const cy0 = Math.max(at.y, y0);
|
|
2579
|
+
const cx1 = Math.min(at.x + at.w, x1);
|
|
2580
|
+
const cy1 = Math.min(at.y + at.h, y1);
|
|
2581
|
+
if (cx1 <= cx0 || cy1 <= cy0) continue;
|
|
2582
|
+
if ((cx1 - cx0) * (cy1 - cy0) < 0.5 * (x1 - x0) * (y1 - y0)) continue;
|
|
2583
|
+
out.push({
|
|
2584
|
+
x: Math.round(cx0),
|
|
2585
|
+
y: Math.round(cy0),
|
|
2586
|
+
w: Math.round(cx1 - cx0),
|
|
2587
|
+
h: Math.round(cy1 - cy0)
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
return out;
|
|
2591
|
+
}
|
|
2592
|
+
function frameOn(b, W, H) {
|
|
2593
|
+
const padX = Math.max(40, 0.15 * b.w);
|
|
2594
|
+
const padY = Math.max(40, 0.15 * b.h);
|
|
2595
|
+
const fit = Math.min(W / (b.w + 2 * padX), H / (b.h + 2 * padY));
|
|
2596
|
+
const s = Math.min(CAMERA_MAX_SCALE, Math.max(CLOSE_MIN, fit));
|
|
2597
|
+
const cx = b.x + b.w / 2;
|
|
2598
|
+
const cy = fit < CLOSE_MIN ? b.y - 12 + H / (2 * s) : b.y + b.h / 2;
|
|
2599
|
+
const clamp3 = (v, lo) => Math.min(0, Math.max(lo, v));
|
|
2600
|
+
const r = (v) => Math.round(v * 1e3) / 1e3;
|
|
2601
|
+
return {
|
|
2602
|
+
s: r(s),
|
|
2603
|
+
x: Math.round(clamp3(W / 2 - cx * s, W - W * s)),
|
|
2604
|
+
y: Math.round(clamp3(H / 2 - cy * s, H - H * s))
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
function defaultShots(cues, subjects) {
|
|
2608
|
+
if (subjects === 0 || cues === 0) return [];
|
|
2609
|
+
const out = [];
|
|
2610
|
+
if (cues <= 2) {
|
|
2611
|
+
out.push({ cue: 1, at: 0.5, subject: 1 });
|
|
2612
|
+
if (subjects > 1 && cues === 2) out.push({ cue: 2, at: 0, subject: 2 });
|
|
2613
|
+
return out;
|
|
2614
|
+
}
|
|
2615
|
+
for (let c = 2; c < cues; c++) out.push({ cue: c, at: 0, subject: (c - 2) % subjects + 1 });
|
|
2616
|
+
return out;
|
|
2617
|
+
}
|
|
2618
|
+
function compileShots(shots, cues, duration, subjects, W, H) {
|
|
2619
|
+
const n3 = cues.length;
|
|
2620
|
+
if (n3 === 0 || subjects.length === 0) return [];
|
|
2621
|
+
const valid = shots.filter(
|
|
2622
|
+
(s) => Number.isInteger(s.cue) && s.cue >= 1 && s.cue <= n3 && Number.isInteger(s.subject) && s.subject >= 0 && s.subject <= subjects.length && Number.isFinite(s.at)
|
|
2623
|
+
);
|
|
2624
|
+
const list = valid.some((s) => s.subject > 0) ? valid : defaultShots(n3, subjects.length);
|
|
2625
|
+
const first = cues[0];
|
|
2626
|
+
const last = cues[n3 - 1];
|
|
2627
|
+
const latest = Math.max(0, duration - 0.6 - REVEAL);
|
|
2628
|
+
const timed = list.map((s) => {
|
|
2629
|
+
const c = cues[s.cue - 1];
|
|
2630
|
+
const share2 = s.at > 1 ? s.at / Math.max(0.1, c.t1 - c.t0) : s.at;
|
|
2631
|
+
const at2 = Math.min(0.85, Math.max(0, share2));
|
|
2632
|
+
return { t: c.t0 + at2 * (c.t1 - c.t0), subject: s.subject };
|
|
2633
|
+
}).sort((a, b) => a.t - b.t);
|
|
2634
|
+
const earliest = Math.min(first.t0 + ESTABLISH, Math.max(first.t0, first.t1 - MOVE));
|
|
2635
|
+
const r2 = (v) => Math.round(v * 100) / 100;
|
|
2636
|
+
const moves = [];
|
|
2637
|
+
let at = earliest;
|
|
2638
|
+
let current = 0;
|
|
2639
|
+
for (const s of timed) {
|
|
2640
|
+
const t2 = Math.max(s.t, at);
|
|
2641
|
+
if (t2 > latest - MIN_HOLD) break;
|
|
2642
|
+
if (s.subject === current) continue;
|
|
2643
|
+
const f = s.subject === 0 ? { s: 1, x: 0, y: 0 } : frameOn(subjects[s.subject - 1], W, H);
|
|
2644
|
+
moves.push({ t: r2(t2), dur: MOVE, ...f, ease: "power3.inOut", subject: s.subject });
|
|
2645
|
+
current = s.subject;
|
|
2646
|
+
at = t2 + Math.max(MIN_HOLD, MOVE + 0.5);
|
|
2647
|
+
}
|
|
2648
|
+
if (current !== 0) {
|
|
2649
|
+
const t2 = Math.min(latest, Math.max(last.t0, at));
|
|
2650
|
+
moves.push({ t: r2(t2), dur: REVEAL, s: 1, x: 0, y: 0, ease: "power2.inOut", subject: 0 });
|
|
2651
|
+
}
|
|
2652
|
+
return moves;
|
|
2653
|
+
}
|
|
2654
|
+
function cameraScript(sid, moves, W, H) {
|
|
2655
|
+
const cam = JSON.stringify(`#${sid}-cam`);
|
|
2656
|
+
const lines = [
|
|
2657
|
+
"// The shell's camera (src/bespoke/shots.ts): establishing, push in, reveal.",
|
|
2658
|
+
`gsap.set(${cam}, { scale: 1, x: 0, y: 0, transformOrigin: "0 0" });`
|
|
2659
|
+
];
|
|
2660
|
+
const v = (f) => `scale: ${f.s}, x: ${f.x}, y: ${f.y}`;
|
|
2661
|
+
const crept = (f) => ({
|
|
2662
|
+
s: Math.round(f.s * CREEP * 1e3) / 1e3,
|
|
2663
|
+
x: Math.round(W / 2 - (W / 2 - f.x) * CREEP),
|
|
2664
|
+
y: Math.round(H / 2 - (H / 2 - f.y) * CREEP)
|
|
2665
|
+
});
|
|
2666
|
+
let from = { s: 1, x: 0, y: 0 };
|
|
2667
|
+
let free2 = 0;
|
|
2668
|
+
for (const m of moves) {
|
|
2669
|
+
const start = Math.ceil((free2 + (free2 > 0 ? CREEP_GAP : 0)) * 100) / 100;
|
|
2670
|
+
const hold = Math.floor((m.t - CREEP_GAP - start) * 100) / 100;
|
|
2671
|
+
if (hold > 0.6) {
|
|
2672
|
+
const to = crept(from);
|
|
2673
|
+
lines.push(
|
|
2674
|
+
`tl.fromTo(${cam}, { ${v(from)} }, { ${v(to)}, duration: ${hold}, ease: "sine.inOut", immediateRender: false }, ${start});`
|
|
2675
|
+
);
|
|
2676
|
+
from = to;
|
|
2677
|
+
}
|
|
2678
|
+
lines.push(
|
|
2679
|
+
`tl.fromTo(${cam}, { ${v(from)} }, { ${v(m)}, duration: ${m.dur}, ease: "${m.ease}", immediateRender: false }, ${m.t});`
|
|
2680
|
+
);
|
|
2681
|
+
from = { s: m.s, x: m.x, y: m.y };
|
|
2682
|
+
free2 = m.t + m.dur;
|
|
2683
|
+
}
|
|
2684
|
+
return lines.join("\n");
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// src/bespoke/scene.ts
|
|
2688
|
+
var BODY_TOP = 34;
|
|
2689
|
+
function bespokeRegion(beat, ctx) {
|
|
2690
|
+
const p = beat.params;
|
|
2691
|
+
const face = faceOf(ctx.theme.fontStack);
|
|
2692
|
+
return {
|
|
2693
|
+
width: contentW(ctx.format),
|
|
2694
|
+
height: Math.round(bodyBudget(ctx.format, p.eyebrow, p.headline, 0, BODY_TOP, 320, face))
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function usesCamera(script) {
|
|
2698
|
+
return /#[\w]+-cam(?![\w-])/.test(script);
|
|
2699
|
+
}
|
|
2700
|
+
function withArt(markup, art, box) {
|
|
2701
|
+
if (!art) return markup;
|
|
2702
|
+
const href = (s) => s.replace(
|
|
2703
|
+
/<image\b([^>]*?)\sdata-art\s*=\s*(["']?)1\2/gi,
|
|
2704
|
+
(m) => `${m} href="${artHref(art)}"`
|
|
2705
|
+
);
|
|
2706
|
+
if (!box) return href(markup);
|
|
2707
|
+
return href(
|
|
2708
|
+
markup.replace(/<image\b([^>]*?)(\/?)>/gi, (m, attrs2, close) => {
|
|
2709
|
+
if (!/\sdata-art\s*=\s*(["']?)1\1/i.test(` ${attrs2}`)) return m;
|
|
2710
|
+
const kept = attrs2.replace(
|
|
2711
|
+
/\s(x|y|width|height|preserveAspectRatio)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
2712
|
+
""
|
|
2713
|
+
);
|
|
2714
|
+
const at = artPlacement(box);
|
|
2715
|
+
return `<image${kept} x="${at.x}" y="${at.y}" width="${at.w}" height="${at.h}" preserveAspectRatio="xMidYMid slice"${close}>`;
|
|
2716
|
+
})
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
function subjectLayer(subjects, width, height) {
|
|
2720
|
+
if (!subjects.length) return "";
|
|
2721
|
+
return `<svg class="ds-subjects" aria-hidden="true" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" style="position:absolute;left:0;top:0;overflow:visible;pointer-events:none">${subjects.map(
|
|
2722
|
+
(b, i) => `<rect data-ds-subject="${i + 1}" x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" fill="none" visibility="hidden"/>`
|
|
2723
|
+
).join("")}</svg>`;
|
|
2724
|
+
}
|
|
2725
|
+
function staging(entry, box) {
|
|
2726
|
+
const art = entry.art;
|
|
2727
|
+
if (!art?.subjects?.length || !entry.stage) return { subjects: [], zones: [], moves: [] };
|
|
2728
|
+
const subjects = subjectsInBox(art.subjects, art, box);
|
|
2729
|
+
const zones = calloutZones(subjects, box.width);
|
|
2730
|
+
const labelled = new Set((entry.fragment.labels ?? []).map((l) => l.subject));
|
|
2731
|
+
const targets = subjects.map(
|
|
2732
|
+
(s, i) => labelled.has(i + 1) ? withZone(
|
|
2733
|
+
s,
|
|
2734
|
+
zones.find((z7) => z7.subject === i + 1)
|
|
2735
|
+
) : s
|
|
2736
|
+
);
|
|
2737
|
+
const moves = compileShots(
|
|
2738
|
+
entry.fragment.shots ?? [],
|
|
2739
|
+
entry.stage.cues,
|
|
2740
|
+
entry.stage.duration,
|
|
2741
|
+
targets,
|
|
2742
|
+
box.width,
|
|
2743
|
+
box.height
|
|
2744
|
+
);
|
|
2745
|
+
return { subjects, zones, moves };
|
|
2746
|
+
}
|
|
2747
|
+
var ART_FEATHER = "-webkit-mask-image:linear-gradient(to right,transparent,#000 6%,#000 94%,transparent),linear-gradient(to bottom,transparent,#000 10%,#000 90%,transparent);-webkit-mask-composite:source-in;mask-composite:intersect";
|
|
2748
|
+
function usesMorph(script) {
|
|
2749
|
+
return /\bmorphSVG\b/.test(script);
|
|
2750
|
+
}
|
|
2751
|
+
function bespokeScene(beat, ctx, entry) {
|
|
2752
|
+
const p = beat.params;
|
|
2753
|
+
const { sid, theme } = ctx;
|
|
2754
|
+
const face = faceOf(theme.fontStack);
|
|
2755
|
+
const f = instantiate(entry.fragment, sid);
|
|
2756
|
+
const { width, height } = bespokeRegion(beat, ctx);
|
|
2757
|
+
const { subjects, zones, moves } = staging(entry, { width, height });
|
|
2758
|
+
const shot = moves.length ? cameraScript(sid, moves, width, height) : "";
|
|
2759
|
+
const callouts = subjects.length ? calloutLayer(sid, f.labels ?? [], zones, moves, theme, width, height) : { markup: "", script: "" };
|
|
2760
|
+
const script = [shot, callouts.script, f.script].filter(Boolean).join("\n");
|
|
2761
|
+
const camera = usesCamera(script) || /\bviewBox\b/.test(script);
|
|
2762
|
+
return {
|
|
2763
|
+
html: `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format), face)}
|
|
2764
|
+
<div class="ds-bespoke" id="${sid}-g"${camera ? " data-ds-clip" : ""}>
|
|
2765
|
+
<div class="ds-cam" id="${sid}-cam">
|
|
2766
|
+
${withArt(f.markup, entry.art, subjects.length ? { width, height } : void 0)}
|
|
2767
|
+
${subjectLayer(subjects, width, height)}
|
|
2768
|
+
${callouts.markup}
|
|
2769
|
+
</div>
|
|
2770
|
+
</div>`,
|
|
2771
|
+
tl: chromeIn(sid, p.eyebrow !== void 0),
|
|
2772
|
+
script,
|
|
2773
|
+
holds: entry.holds,
|
|
2774
|
+
// Vendored only when a scene morphs, so no other deck carries its bytes.
|
|
2775
|
+
...usesMorph(f.script) ? { plugins: ["morphSVG"] } : {},
|
|
2776
|
+
css: [
|
|
2777
|
+
chromeCss(theme),
|
|
2778
|
+
`#${sid}-g{position:relative;flex:none;width:${width}px;height:${height}px;margin-top:${BODY_TOP}px;color:${theme.fg}${camera ? ";overflow:hidden" : ""}}`,
|
|
2779
|
+
`#${sid}-cam{position:absolute;left:0;top:0;width:${width}px;height:${height}px;transform-origin:0 0}`,
|
|
2780
|
+
// The illustration's edges, feathered by the shell unless the scene masks
|
|
2781
|
+
// it itself: its flat ground meets a pack ground that is often a
|
|
2782
|
+
// gradient, and an unmasked picture showed as a lighter rectangle.
|
|
2783
|
+
// A picture whose copy is feathered already (alpha baked in by the pass)
|
|
2784
|
+
// needs none: MEASURED 2026-10-09, this CSS mask made every screenshot of
|
|
2785
|
+
// its scene ~0.7s slower (13.5s against 4.1s for one scene's 13 frames).
|
|
2786
|
+
...entry.art && !entry.art.feathered ? [`#${sid}-g image[data-art]:not([mask]){${ART_FEATHER}}`] : [],
|
|
2787
|
+
f.css
|
|
2788
|
+
].join("\n")
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// src/deck/playback.ts
|
|
2793
|
+
var PLAYER_META = "decksmith-player";
|
|
2794
|
+
var PLAYER_MARKER = `<meta name="${PLAYER_META}" content="2" />`;
|
|
2795
|
+
function markV2(page) {
|
|
2796
|
+
const existing = new RegExp(`<meta name="${PLAYER_META}"[^>]*>`);
|
|
2797
|
+
if (existing.test(page)) return page.replace(existing, PLAYER_MARKER);
|
|
2798
|
+
const head = /<head[^>]*>/i.exec(page);
|
|
2799
|
+
if (!head) throw new Error("not a deck page: it has no <head> to mark");
|
|
2800
|
+
const at = head.index + head[0].length;
|
|
2801
|
+
return `${page.slice(0, at)}
|
|
2802
|
+
${PLAYER_MARKER}${page.slice(at)}`;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/emit/fit.ts
|
|
2806
|
+
function fitOf(ink2, region) {
|
|
2807
|
+
const r = (n3) => Math.round(n3 * 1e3) / 1e3;
|
|
2808
|
+
return { fill: region > 0 ? r(ink2 / region) : 0, region: r(region), ink: r(ink2) };
|
|
2809
|
+
}
|
|
2810
|
+
var GROWTH = 1.6;
|
|
2811
|
+
function growToFit(height, budget2, lo = 1, hi = GROWTH) {
|
|
2812
|
+
if (height(hi) <= budget2) return hi;
|
|
2813
|
+
if (height(lo) > budget2) return lo;
|
|
2814
|
+
let a = lo;
|
|
2815
|
+
let b = hi;
|
|
2816
|
+
for (let i = 0; i < 24; i++) {
|
|
2817
|
+
const mid = (a + b) / 2;
|
|
2818
|
+
if (height(mid) <= budget2) a = mid;
|
|
2819
|
+
else b = mid;
|
|
2820
|
+
}
|
|
2821
|
+
return Math.floor(a * 128) / 128;
|
|
2822
|
+
}
|
|
2823
|
+
var MEASURE_SLACK = 0.92;
|
|
2824
|
+
function isV2(ctx) {
|
|
2825
|
+
return ctx.design === "v2";
|
|
2826
|
+
}
|
|
2827
|
+
var FIT_FILE = "fit.json";
|
|
2828
|
+
|
|
2399
2829
|
// src/emit/archetypes/annotated-figure.ts
|
|
2400
2830
|
var STAGE_GAP = 30;
|
|
2401
2831
|
var CAP_GAP = 26;
|
|
@@ -2403,7 +2833,7 @@ var CAP_LH = 1.35;
|
|
|
2403
2833
|
var CAP_LINES = 2;
|
|
2404
2834
|
var PLATE = 15;
|
|
2405
2835
|
var LEAD = 18;
|
|
2406
|
-
var
|
|
2836
|
+
var EDGE2 = 10;
|
|
2407
2837
|
var LAB = MIN_FONT;
|
|
2408
2838
|
var LAB_WEIGHT = 600;
|
|
2409
2839
|
var LAB_LH = 1.3;
|
|
@@ -2469,8 +2899,8 @@ function attempt(stageW, notes, fig, stageH, want, tall, face, sides = { l: true
|
|
|
2469
2899
|
const stacks = row ? 1 : Math.max(1, Math.ceil(notes.length / 2));
|
|
2470
2900
|
const bandMin = tall ? stacks * (ASCENT + RULE_LIFT + RULE_W) + (stacks - 1) * LAB_GAP + BAND_GAP : 0;
|
|
2471
2901
|
const asked = Math.min(want, stageW / 3);
|
|
2472
|
-
const margin = (s) => tall || !sides[s] ?
|
|
2473
|
-
const plateMax = tall ? stageW - 2 *
|
|
2902
|
+
const margin = (s) => tall || !sides[s] ? EDGE2 : asked + LEAD + EDGE2;
|
|
2903
|
+
const plateMax = tall ? stageW - 2 * EDGE2 : stageW - margin("l") - margin("r");
|
|
2474
2904
|
const scale = Math.min(
|
|
2475
2905
|
(plateMax - 2 * PLATE) / fig.width,
|
|
2476
2906
|
(stageH - bandMin - 2 * PLATE) / fig.height,
|
|
@@ -2487,7 +2917,7 @@ function attempt(stageW, notes, fig, stageH, want, tall, face, sides = { l: true
|
|
|
2487
2917
|
// band under it, so centring it would open a gap the labels then hang off.
|
|
2488
2918
|
y: tall ? PLATE : (stageH - fig.height * scale) / 2
|
|
2489
2919
|
};
|
|
2490
|
-
const col = row ? band[0]?.w ?? stageW : tall ? (stageW - MID_GAP) / 2 : sides.l ? img.x - PLATE - LEAD -
|
|
2920
|
+
const col = row ? band[0]?.w ?? stageW : tall ? (stageW - MID_GAP) / 2 : sides.l ? img.x - PLATE - LEAD - EDGE2 : stageW - (img.x + img.w) - PLATE - LEAD - EDGE2;
|
|
2491
2921
|
const stackTop = tall ? img.y + img.h + PLATE + BAND_GAP : 0;
|
|
2492
2922
|
const room = stageH - stackTop;
|
|
2493
2923
|
const innerOf = (side) => tall ? side === "l" ? col : stageW - col : side === "l" ? img.x - PLATE - LEAD : img.x + img.w + PLATE + LEAD;
|
|
@@ -3041,7 +3471,7 @@ function asideFor(ctx, input, railW, room) {
|
|
|
3041
3471
|
}
|
|
3042
3472
|
|
|
3043
3473
|
// src/emit/archetypes/bar-compare.ts
|
|
3044
|
-
var
|
|
3474
|
+
var BODY_TOP2 = 34;
|
|
3045
3475
|
var NOTE_H = noteHeight("x", MIN_FONT, 26);
|
|
3046
3476
|
var UNIT_BAND = 58;
|
|
3047
3477
|
var FOOT = 40;
|
|
@@ -3201,7 +3631,7 @@ var barCompare = (beat, ctx) => {
|
|
|
3201
3631
|
const face = faceOf(ctx.theme.fontStack);
|
|
3202
3632
|
const tall = isPortrait(ctx.format);
|
|
3203
3633
|
const unitBand = p.unit ? UNIT_BAND : 0;
|
|
3204
|
-
const avail = F.budget(p.note ? NOTE_H : 0,
|
|
3634
|
+
const avail = F.budget(p.note ? NOTE_H : 0, BODY_TOP2, 0);
|
|
3205
3635
|
const variant = variantOf(ctx, "bar-compare");
|
|
3206
3636
|
if (variant === "columns") return columns2(beat, ctx, F, face, avail);
|
|
3207
3637
|
if (variant === "versus") return versus(beat, ctx, F, face, avail);
|
|
@@ -3210,7 +3640,7 @@ var barCompare = (beat, ctx) => {
|
|
|
3210
3640
|
let L;
|
|
3211
3641
|
if (v2) {
|
|
3212
3642
|
const noteBand = p.note ? Math.max(NOTE_H, noteHeight(p.note, F.noteW * MEASURE_SLACK, 26, face)) : 0;
|
|
3213
|
-
const grownAvail = F.budget(noteBand,
|
|
3643
|
+
const grownAvail = F.budget(noteBand, BODY_TOP2, 0);
|
|
3214
3644
|
try {
|
|
3215
3645
|
L = barLayout({ ...args, avail: grownAvail, caps: GROWN });
|
|
3216
3646
|
} catch {
|
|
@@ -3414,7 +3844,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
3414
3844
|
}
|
|
3415
3845
|
if (p.unit || p.note) holds.push(tailAt + 0.7);
|
|
3416
3846
|
const region = F.budget(0, 0, 0);
|
|
3417
|
-
const fit = v2 ? { fit: fitOf(p.note ? region -
|
|
3847
|
+
const fit = v2 ? { fit: fitOf(p.note ? region - BODY_TOP2 : H, region) } : {};
|
|
3418
3848
|
return {
|
|
3419
3849
|
html,
|
|
3420
3850
|
tl,
|
|
@@ -3423,7 +3853,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
3423
3853
|
fill: Math.min(1, H / avail),
|
|
3424
3854
|
css: [
|
|
3425
3855
|
chromeCss(theme),
|
|
3426
|
-
`.bc-wrap{margin-top:${
|
|
3856
|
+
`.bc-wrap{margin-top:${BODY_TOP2}px}`,
|
|
3427
3857
|
// Scoped to the scene, so a classic scene in the same document — there is
|
|
3428
3858
|
// none today, but the dedup set does not know that — keeps its centring.
|
|
3429
3859
|
...v2 && p.note ? [`#${sid} .bc-wrap{margin-bottom:auto}`] : [],
|
|
@@ -3646,7 +4076,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
3646
4076
|
fill: Math.min(1, H / avail),
|
|
3647
4077
|
css: [
|
|
3648
4078
|
chromeCss(theme),
|
|
3649
|
-
`.bc-wrap{margin-top:${
|
|
4079
|
+
`.bc-wrap{margin-top:${BODY_TOP2}px}`,
|
|
3650
4080
|
`.bc-rail{fill:${theme.panel}}`,
|
|
3651
4081
|
noteCss("bc-note", theme, 26),
|
|
3652
4082
|
ambient(sid, `-bar${focal.i}`, BREATHE),
|
|
@@ -3865,7 +4295,7 @@ ${svg(id(sid, "chart"), W, Math.max(H, avail), body)}
|
|
|
3865
4295
|
fill: Math.min(1, H / avail),
|
|
3866
4296
|
css: [
|
|
3867
4297
|
chromeCss(theme),
|
|
3868
|
-
`.bc-wrap{margin-top:${
|
|
4298
|
+
`.bc-wrap{margin-top:${BODY_TOP2}px}`,
|
|
3869
4299
|
`.bc-rail{fill:${theme.panel}}`,
|
|
3870
4300
|
noteCss("bc-note", theme, 26),
|
|
3871
4301
|
ambient(sid, `-bar${focal.i}`, BREATHE),
|
|
@@ -5004,6 +5434,7 @@ ${slide}`;
|
|
|
5004
5434
|
const holds = [];
|
|
5005
5435
|
const spot = spotlighter(sid, ".term");
|
|
5006
5436
|
const swell = (t2) => raw(`dsSwell.${t2.tone}`);
|
|
5437
|
+
const later = v2 ? { immediateRender: false } : {};
|
|
5007
5438
|
terms.forEach((term, i) => {
|
|
5008
5439
|
const at = first + i * step;
|
|
5009
5440
|
const colour = theme.tones[term.tone];
|
|
@@ -5027,7 +5458,12 @@ ${slide}`;
|
|
|
5027
5458
|
const prev = terms[i - 1];
|
|
5028
5459
|
if (prev) {
|
|
5029
5460
|
tl.push(
|
|
5030
|
-
tween(
|
|
5461
|
+
tween(
|
|
5462
|
+
`#${sid} .t-${prev.tone}`,
|
|
5463
|
+
{ scale: swell(prev) },
|
|
5464
|
+
{ scale: 1, duration: 0.4, ...later },
|
|
5465
|
+
at
|
|
5466
|
+
)
|
|
5031
5467
|
);
|
|
5032
5468
|
}
|
|
5033
5469
|
holds.push(at + 0.6);
|
|
@@ -5036,7 +5472,12 @@ ${slide}`;
|
|
|
5036
5472
|
if (last) {
|
|
5037
5473
|
const at = first + terms.length * step;
|
|
5038
5474
|
tl.push(
|
|
5039
|
-
tween(
|
|
5475
|
+
tween(
|
|
5476
|
+
`#${sid} .t-${last.tone}`,
|
|
5477
|
+
{ scale: swell(last) },
|
|
5478
|
+
{ scale: 1, duration: 0.4, ...later },
|
|
5479
|
+
at
|
|
5480
|
+
),
|
|
5040
5481
|
...spot.restore(at)
|
|
5041
5482
|
);
|
|
5042
5483
|
}
|
|
@@ -5259,7 +5700,7 @@ var equationMorph = (beat, ctx) => {
|
|
|
5259
5700
|
var LABEL = 42;
|
|
5260
5701
|
var LH = 1.25;
|
|
5261
5702
|
var PAD = 16;
|
|
5262
|
-
var
|
|
5703
|
+
var GAP2 = 0.14;
|
|
5263
5704
|
var CELL_MAX2 = 280;
|
|
5264
5705
|
var MARGIN = 6;
|
|
5265
5706
|
var MAX_LINES = 2;
|
|
@@ -5275,8 +5716,8 @@ function noteColumn(note, size, face) {
|
|
|
5275
5716
|
function solveCell(cols, rows, boxW, boxH) {
|
|
5276
5717
|
return Math.min(
|
|
5277
5718
|
CELL_MAX2,
|
|
5278
|
-
(boxW - 2 * MARGIN) / (cols +
|
|
5279
|
-
(boxH - 2 * MARGIN) / (rows +
|
|
5719
|
+
(boxW - 2 * MARGIN) / (cols + GAP2 * (cols - 1)),
|
|
5720
|
+
(boxH - 2 * MARGIN) / (rows + GAP2 * (rows - 1))
|
|
5280
5721
|
);
|
|
5281
5722
|
}
|
|
5282
5723
|
var grid = (beat, ctx) => {
|
|
@@ -5305,7 +5746,7 @@ var grid = (beat, ctx) => {
|
|
|
5305
5746
|
);
|
|
5306
5747
|
const solve3 = (boxW, budget3) => {
|
|
5307
5748
|
const cell = solveCell(p.cols, p.rows, boxW, budget3);
|
|
5308
|
-
const gap = cell *
|
|
5749
|
+
const gap = cell * GAP2;
|
|
5309
5750
|
return {
|
|
5310
5751
|
cell,
|
|
5311
5752
|
gap,
|
|
@@ -5881,10 +6322,10 @@ var lineChart = (beat, ctx) => {
|
|
|
5881
6322
|
}
|
|
5882
6323
|
const walk = cmp ? sec(step * count2) : DRAW_SECONDS;
|
|
5883
6324
|
if (count2 > 1) {
|
|
5884
|
-
const
|
|
6325
|
+
const route2 = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
5885
6326
|
tl.push(
|
|
5886
6327
|
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
5887
|
-
...travel(`#${sid}-ring`,
|
|
6328
|
+
...travel(`#${sid}-ring`, route2, settled, walk),
|
|
5888
6329
|
// And it leaves once the curve is whole: a marker parked on the last
|
|
5889
6330
|
// point for the rest of the beat reads as a defect, not as emphasis.
|
|
5890
6331
|
tween(
|
|
@@ -5984,7 +6425,7 @@ var NOTE_TOP = 16;
|
|
|
5984
6425
|
var PAD_X_EM = 0.45;
|
|
5985
6426
|
var PAD_Y2 = 34;
|
|
5986
6427
|
var MIN_BOX_H = 172;
|
|
5987
|
-
var
|
|
6428
|
+
var GAP3 = 120;
|
|
5988
6429
|
var MIN_GAP = 30;
|
|
5989
6430
|
var MAX_BOX_W = 430;
|
|
5990
6431
|
var MAX_BOX_H = 340;
|
|
@@ -6131,7 +6572,7 @@ function fitAt(stages, k, width, stageW, face, size = LABEL2, noteK = k, split =
|
|
|
6131
6572
|
),
|
|
6132
6573
|
width,
|
|
6133
6574
|
size,
|
|
6134
|
-
gap:
|
|
6575
|
+
gap: GAP3,
|
|
6135
6576
|
minGap: MIN_GAP,
|
|
6136
6577
|
padEm: PAD_X_EM,
|
|
6137
6578
|
weight: 600,
|
|
@@ -6146,7 +6587,7 @@ function solve(stages, width, stageW, face, size = LABEL2) {
|
|
|
6146
6587
|
function pipeLayout(stageW, stages, loop, face = "latin", grow) {
|
|
6147
6588
|
const width = stageW - 2 * M;
|
|
6148
6589
|
if (grow) return grownRow(stageW, stages, loop, face, grow);
|
|
6149
|
-
const capped = Math.min(width, stages.length * MAX_BOX_W + (stages.length - 1) *
|
|
6590
|
+
const capped = Math.min(width, stages.length * MAX_BOX_W + (stages.length - 1) * GAP3);
|
|
6150
6591
|
let fit = solve(stages, capped, stageW, face);
|
|
6151
6592
|
if (!fit.ok && capped < width) fit = solve(stages, width, stageW, face);
|
|
6152
6593
|
const row = measureRow(fit, stages, loop, face, stageW, width);
|
|
@@ -6191,7 +6632,7 @@ function measureRow(fit, stages, loop, face, stageW, width, grow) {
|
|
|
6191
6632
|
}
|
|
6192
6633
|
function grownRow(stageW, stages, loop, face, grow) {
|
|
6193
6634
|
const width = stageW - 2 * M;
|
|
6194
|
-
const capped = Math.min(width, stages.length * GROWN_BOX_W + (stages.length - 1) *
|
|
6635
|
+
const capped = Math.min(width, stages.length * GROWN_BOX_W + (stages.length - 1) * GAP3);
|
|
6195
6636
|
const preferred = LABEL2 * GROWTH;
|
|
6196
6637
|
let best;
|
|
6197
6638
|
for (const span of capped < width ? [capped, width] : [width]) {
|
|
@@ -6342,7 +6783,7 @@ var pipeline = (beat, ctx) => {
|
|
|
6342
6783
|
const selfSpan = loop.self ? Math.min((L.boxes[loop.from]?.w ?? 200) * 0.55, 180) : 0;
|
|
6343
6784
|
const leaveAt = cx(loop.from) + selfSpan / 2;
|
|
6344
6785
|
const landAt = cx(loop.to) - selfSpan / 2;
|
|
6345
|
-
const
|
|
6786
|
+
const route2 = elbow(
|
|
6346
6787
|
sid,
|
|
6347
6788
|
L.vertical ? { x: edge, y: leaveAt } : { x: leaveAt, y: edge },
|
|
6348
6789
|
L.vertical ? { x: edge, y: landAt } : { x: landAt, y: edge },
|
|
@@ -6369,7 +6810,7 @@ var pipeline = (beat, ctx) => {
|
|
|
6369
6810
|
const a0 = Math.max(0, Math.min(cx(loop.to) - 30, mid - half));
|
|
6370
6811
|
sweep = Math.min(span, Math.max(cx(loop.from) + 30, mid + half)) - a0;
|
|
6371
6812
|
const clip3 = L.vertical ? { x: 0, y: a0, w: W, h: sweep } : { x: a0, y: 0, w: sweep, h: L.svgH };
|
|
6372
|
-
loopSvg = `<defs><clipPath id="${id(sid, "loopclip")}">${rect(clip3, { id: id(sid, "sweep") })}</clipPath></defs>` + group([
|
|
6813
|
+
loopSvg = `<defs><clipPath id="${id(sid, "loopclip")}">${rect(clip3, { id: id(sid, "sweep") })}</clipPath></defs>` + group([route2, label], { "clip-path": `url(#${id(sid, "loopclip")})` });
|
|
6373
6814
|
}
|
|
6374
6815
|
const body = [
|
|
6375
6816
|
arrowDefs(sid, loop ? [theme.muted, loopColour] : [theme.muted]),
|
|
@@ -6909,7 +7350,7 @@ function depthCss(sid, pose, part) {
|
|
|
6909
7350
|
}
|
|
6910
7351
|
|
|
6911
7352
|
// src/emit/archetypes/stack.ts
|
|
6912
|
-
var
|
|
7353
|
+
var GAP4 = 44;
|
|
6913
7354
|
var NUM_X = 48;
|
|
6914
7355
|
function numSpine(floor) {
|
|
6915
7356
|
return Math.round(NUM_X * floor * 100 / MIN_FONT) / 100;
|
|
@@ -6922,7 +7363,7 @@ var RISE_MAX = { wide: 180, tall: 230 };
|
|
|
6922
7363
|
var RISE_MIN = 24;
|
|
6923
7364
|
var SY_MAX = { wide: 100, tall: 132 };
|
|
6924
7365
|
var T_MAX = { wide: 18, tall: 24 };
|
|
6925
|
-
var
|
|
7366
|
+
var EDGE3 = 10;
|
|
6926
7367
|
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
6927
7368
|
function stackLayout(p, format, face = "latin", grow = false) {
|
|
6928
7369
|
if (grow) {
|
|
@@ -6989,7 +7430,7 @@ function solve2(p, format, inline2, face, growth = 1) {
|
|
|
6989
7430
|
(l) => inline2 ? Math.max(l.label.length * labelSize, l.note.length * floor) * 1.16 : l.label.length * labelSize * 1.16 + (l.note.length > 0 ? 6 + l.note.length * floor * 1.16 : 0)
|
|
6990
7431
|
)
|
|
6991
7432
|
);
|
|
6992
|
-
const pad = Math.max(
|
|
7433
|
+
const pad = Math.max(EDGE3, blockH / 2);
|
|
6993
7434
|
const chromeH = chromeHeight(p.eyebrow, p.headline, width, face);
|
|
6994
7435
|
const noteH = noteHeight(p.note, noteWidth(format), 28, face);
|
|
6995
7436
|
const free2 = boxH - chromeH - 20 - noteH;
|
|
@@ -6998,7 +7439,7 @@ function solve2(p, format, inline2, face, growth = 1) {
|
|
|
6998
7439
|
const rise = clamp(room, RISE_MIN, riseMax);
|
|
6999
7440
|
const sy = clamp(rise * 0.62, 46, syMax);
|
|
7000
7441
|
const t2 = clamp(rise * 0.14, 10, tMax);
|
|
7001
|
-
const span = width - NUM_W -
|
|
7442
|
+
const span = width - NUM_W - GAP4 - colW;
|
|
7002
7443
|
const sx = clamp(span * 0.2, 110, 220);
|
|
7003
7444
|
const height = 2 * pad + (count2 - 1) * rise + sy + t2;
|
|
7004
7445
|
return {
|
|
@@ -7158,12 +7599,12 @@ var stack = (beat, ctx) => {
|
|
|
7158
7599
|
holds.push(at + 0.62);
|
|
7159
7600
|
});
|
|
7160
7601
|
if (mids.length > 1) {
|
|
7161
|
-
const
|
|
7602
|
+
const route2 = mids.map((mid) => ({ x: 0, y: nv(mid - probeY) }));
|
|
7162
7603
|
const walkFrom = first + 0.3;
|
|
7163
7604
|
const walkTo = first + (mids.length - 1) * step + 0.55;
|
|
7164
7605
|
tl.push(
|
|
7165
7606
|
tween(`#${sid}-probe`, { opacity: 0 }, { opacity: 1, duration: 0.3 }, walkFrom),
|
|
7166
|
-
...travel(`#${sid}-probe`,
|
|
7607
|
+
...travel(`#${sid}-probe`, route2, walkFrom, Math.max(0.4, walkTo - walkFrom))
|
|
7167
7608
|
);
|
|
7168
7609
|
}
|
|
7169
7610
|
if (p.note) {
|
|
@@ -7218,6 +7659,8 @@ var emitters = {
|
|
|
7218
7659
|
callout
|
|
7219
7660
|
};
|
|
7220
7661
|
function emitScene(beat, ctx) {
|
|
7662
|
+
const bespoke = ctx.bespoke?.[beat.id];
|
|
7663
|
+
if (bespoke) return bespokeScene(beat, ctx, bespoke);
|
|
7221
7664
|
return emitters[beat.archetype](beat, ctx);
|
|
7222
7665
|
}
|
|
7223
7666
|
|
|
@@ -7798,7 +8241,15 @@ var beatCore = {
|
|
|
7798
8241
|
/** Salience, 0–1. Decides what survives a 30-second cut. */
|
|
7799
8242
|
weight: z.number().min(0).max(1).default(0.5),
|
|
7800
8243
|
/** Seconds this beat holds in a linear render. Deck mode is human-paced. */
|
|
7801
|
-
seconds: z.number().positive().max(60).default(7)
|
|
8244
|
+
seconds: z.number().positive().max(60).default(7),
|
|
8245
|
+
/**
|
|
8246
|
+
* OPTIONAL, and only read by `build --design v2 --bespoke`: the planner's hint
|
|
8247
|
+
* that this beat explains a mechanism worth a generated scene (`true`), or
|
|
8248
|
+
* must keep its archetype (`false`). A hint, not an order — see
|
|
8249
|
+
* src/bespoke/select.ts. Hidden from the planner's schema unless the plan was
|
|
8250
|
+
* asked for with `--bespoke`, so every other plan is byte-for-byte unchanged.
|
|
8251
|
+
*/
|
|
8252
|
+
bespoke: z.boolean().optional()
|
|
7802
8253
|
};
|
|
7803
8254
|
var beatTail = {
|
|
7804
8255
|
/** What the presenter says over this beat. Written last, knowing the picture. */
|
|
@@ -8046,7 +8497,50 @@ var prefsSchema = z.object({
|
|
|
8046
8497
|
* Zero is legal and means "every picture by the tool".
|
|
8047
8498
|
*/
|
|
8048
8499
|
max: z.int().min(0).default(4)
|
|
8049
|
-
}).default({ enabled: false, provider: "codex", style: "flat vector illustration", max: 4 })
|
|
8500
|
+
}).default({ enabled: false, provider: "codex", style: "flat vector illustration", max: 4 }),
|
|
8501
|
+
/* --- bespoke scenes --- */
|
|
8502
|
+
/**
|
|
8503
|
+
* `--design v2` only: a few beats per deck drawn by a model-written GSAP+SVG
|
|
8504
|
+
* scene instead of their archetype (src/bespoke/). OPTIONAL WITH NO DEFAULT,
|
|
8505
|
+
* unlike the two blocks above, and that is deliberate: a resolved prefs
|
|
8506
|
+
* object travels into `.deck` manifests, and a defaulted block would move the
|
|
8507
|
+
* bytes of every pack built without it.
|
|
8508
|
+
*/
|
|
8509
|
+
bespoke: z.object({
|
|
8510
|
+
enabled: z.boolean().default(false),
|
|
8511
|
+
/** Hard cap on Codex calls for one deck. Each bespoke beat costs two. */
|
|
8512
|
+
maxCalls: z.int().min(0).max(40).default(12),
|
|
8513
|
+
/** Hard cap on the bespoke pass's wall time, seconds. */
|
|
8514
|
+
maxSeconds: z.int().min(60).max(7200).default(1800),
|
|
8515
|
+
/** Passed to `codex exec --model`. Absent: whatever the account is configured for. */
|
|
8516
|
+
model: z.string().optional(),
|
|
8517
|
+
/** The Codex CLI to run. Absent: `codex` on PATH. */
|
|
8518
|
+
cli: z.string().optional(),
|
|
8519
|
+
/** Where generated scenes are cached. Absent: the user cache directory. */
|
|
8520
|
+
cache: z.string().optional(),
|
|
8521
|
+
/**
|
|
8522
|
+
* Codex calls in flight at once. Five: MEASURED 2026-10-08, a deck's
|
|
8523
|
+
* five drafts at three in flight took two waves (252s) where one wave is
|
|
8524
|
+
* ~150s, and the second wave alone put the pass over ten minutes. Still
|
|
8525
|
+
* bounded by `maxCalls` per deck.
|
|
8526
|
+
*/
|
|
8527
|
+
concurrency: z.int().min(1).max(8).default(5),
|
|
8528
|
+
/**
|
|
8529
|
+
* `model_reasoning_effort` for the bespoke calls. MEASURED 2026-10-08, one
|
|
8530
|
+
* draft prompt, tools off: medium 166s and 24k tokens, high 371s and 50k.
|
|
8531
|
+
* The references and the gates carry the look; the extra reasoning bought
|
|
8532
|
+
* time. Absent from a config file means medium; "default" leaves the
|
|
8533
|
+
* account's own setting.
|
|
8534
|
+
*/
|
|
8535
|
+
effort: z.enum(["low", "medium", "high", "default"]).default("medium"),
|
|
8536
|
+
/**
|
|
8537
|
+
* Illustrations per deck: each bespoke beat may first have ONE picture
|
|
8538
|
+
* drawn by the same account's image tool (src/bespoke/art.ts), which its
|
|
8539
|
+
* scene is built around. 0 turns them off. Counted apart from `maxCalls`:
|
|
8540
|
+
* MEASURED 2026-10-09, one picture is 36-49s and 19-25k tokens.
|
|
8541
|
+
*/
|
|
8542
|
+
art: z.int().min(0).max(6).default(6)
|
|
8543
|
+
}).optional()
|
|
8050
8544
|
});
|
|
8051
8545
|
var cueSchema = z.object({
|
|
8052
8546
|
/** Seconds from the start of this segment's audio. */
|
|
@@ -8239,9 +8733,9 @@ function resizeFormat(base, width, height) {
|
|
|
8239
8733
|
// src/narrate/tts.ts
|
|
8240
8734
|
import { spawn } from "node:child_process";
|
|
8241
8735
|
import { createHash as createHash2 } from "node:crypto";
|
|
8242
|
-
import { mkdir as
|
|
8736
|
+
import { mkdir as mkdir4, readFile as readFile4, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
8243
8737
|
import { homedir } from "node:os";
|
|
8244
|
-
import { join as
|
|
8738
|
+
import { join as join5 } from "node:path";
|
|
8245
8739
|
var MISSING = [
|
|
8246
8740
|
"edge-tts is not installed, so narration cannot be synthesised.",
|
|
8247
8741
|
"",
|
|
@@ -8258,8 +8752,8 @@ function candidates2() {
|
|
|
8258
8752
|
["edge-tts"],
|
|
8259
8753
|
// pip --user on macOS and on Linux respectively. Neither is on PATH by
|
|
8260
8754
|
// default, and both are where this actually lands in practice.
|
|
8261
|
-
[
|
|
8262
|
-
[
|
|
8755
|
+
[join5(home, "Library", "Python", "3.9", "bin", "edge-tts")],
|
|
8756
|
+
[join5(home, ".local", "bin", "edge-tts")],
|
|
8263
8757
|
// Last resort: the module is installed even though its console script is
|
|
8264
8758
|
// nowhere findable, which is the normal state of a pip --user install.
|
|
8265
8759
|
["python3", "-m", "edge_tts"]
|
|
@@ -8284,7 +8778,7 @@ async function find(can) {
|
|
|
8284
8778
|
throw new Error(MISSING);
|
|
8285
8779
|
}
|
|
8286
8780
|
function runArgv(cmd, args) {
|
|
8287
|
-
return new Promise((
|
|
8781
|
+
return new Promise((resolve10) => {
|
|
8288
8782
|
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8289
8783
|
let stdout = "";
|
|
8290
8784
|
let stderr = "";
|
|
@@ -8294,8 +8788,8 @@ function runArgv(cmd, args) {
|
|
|
8294
8788
|
child.stderr.on("data", (b) => {
|
|
8295
8789
|
stderr += b.toString();
|
|
8296
8790
|
});
|
|
8297
|
-
child.on("error", (e) =>
|
|
8298
|
-
child.on("close", (code) =>
|
|
8791
|
+
child.on("error", (e) => resolve10({ code: -1, stderr: String(e), stdout }));
|
|
8792
|
+
child.on("close", (code) => resolve10({ code: code ?? -1, stderr, stdout }));
|
|
8299
8793
|
});
|
|
8300
8794
|
}
|
|
8301
8795
|
var edgeTts = {
|
|
@@ -8362,7 +8856,7 @@ function edgeProvider(runner = edgeTts) {
|
|
|
8362
8856
|
async speak(req) {
|
|
8363
8857
|
const subs = `${req.audio.replace(/\.mp3$/, "")}.srt`;
|
|
8364
8858
|
await runner.speak({ ...req, subs });
|
|
8365
|
-
const raw2 = await
|
|
8859
|
+
const raw2 = await readFile4(subs, "utf8").catch(() => "");
|
|
8366
8860
|
await rm(subs, { force: true });
|
|
8367
8861
|
const cues = parseCues(raw2);
|
|
8368
8862
|
const measured = await runner.measure(req.audio);
|
|
@@ -8380,11 +8874,11 @@ async function synthesize(text2, opts) {
|
|
|
8380
8874
|
const provider = opts.provider ?? edgeProvider(opts.runner ?? edgeTts);
|
|
8381
8875
|
const key = cacheKey(text2, opts.voice, rate, pitch);
|
|
8382
8876
|
const file = `${key}.mp3`;
|
|
8383
|
-
const audio =
|
|
8384
|
-
const sidecar =
|
|
8877
|
+
const audio = join5(opts.dir, file);
|
|
8878
|
+
const sidecar = join5(opts.dir, `${key}.json`);
|
|
8385
8879
|
const cached2 = await readSidecar(sidecar);
|
|
8386
8880
|
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
8387
|
-
await
|
|
8881
|
+
await mkdir4(opts.dir, { recursive: true });
|
|
8388
8882
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
8389
8883
|
const { cues, seconds } = spoken;
|
|
8390
8884
|
const clamped = cues.map((c) => ({
|
|
@@ -8393,13 +8887,13 @@ async function synthesize(text2, opts) {
|
|
|
8393
8887
|
text: c.text
|
|
8394
8888
|
}));
|
|
8395
8889
|
const body = { seconds, cues: clamped, text: text2, voice: opts.voice };
|
|
8396
|
-
await
|
|
8890
|
+
await writeFile4(sidecar, `${JSON.stringify(body, null, 2)}
|
|
8397
8891
|
`);
|
|
8398
8892
|
return { audio, file, seconds, cues: clamped };
|
|
8399
8893
|
}
|
|
8400
8894
|
async function readSidecar(path2) {
|
|
8401
8895
|
try {
|
|
8402
|
-
const parsed = JSON.parse(await
|
|
8896
|
+
const parsed = JSON.parse(await readFile4(path2, "utf8"));
|
|
8403
8897
|
const s = parsed;
|
|
8404
8898
|
return typeof s?.seconds === "number" && s.seconds > 0 && Array.isArray(s.cues) ? s : null;
|
|
8405
8899
|
} catch {
|
|
@@ -8576,7 +9070,7 @@ function assertNarrationStaging(narration, staged, format) {
|
|
|
8576
9070
|
|
|
8577
9071
|
// src/pack/media.ts
|
|
8578
9072
|
import { createHash as createHash3 } from "node:crypto";
|
|
8579
|
-
import { readFile as
|
|
9073
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
8580
9074
|
import { extname } from "node:path";
|
|
8581
9075
|
var PLAYERS = [
|
|
8582
9076
|
"youtube.com",
|
|
@@ -8785,7 +9279,7 @@ async function fetchAsset(url) {
|
|
|
8785
9279
|
mime: res.headers.get("content-type") ?? void 0
|
|
8786
9280
|
};
|
|
8787
9281
|
}
|
|
8788
|
-
return { bytes: new Uint8Array(await
|
|
9282
|
+
return { bytes: new Uint8Array(await readFile5(url)) };
|
|
8789
9283
|
}
|
|
8790
9284
|
|
|
8791
9285
|
// src/plan/direct.ts
|
|
@@ -9332,15 +9826,15 @@ function fnv1a2(s) {
|
|
|
9332
9826
|
var RISE_SHARE = 0.25;
|
|
9333
9827
|
function planMotion(seed, beats) {
|
|
9334
9828
|
const n3 = beats.length;
|
|
9335
|
-
const
|
|
9829
|
+
const entrances2 = [];
|
|
9336
9830
|
const riseCap = Math.floor(n3 * RISE_SHARE);
|
|
9337
9831
|
let rises = 0;
|
|
9338
9832
|
for (const [i, beat] of beats.entries()) {
|
|
9339
|
-
const prev =
|
|
9833
|
+
const prev = entrances2[i - 1];
|
|
9340
9834
|
const allowed = ENTRANCES.filter((v) => v !== prev && (v !== "rise" || rises < riseCap));
|
|
9341
9835
|
const pick = allowed[fnv1a2(`${seed}|enter|${beat.id}|${i}`) % allowed.length];
|
|
9342
9836
|
if (pick === "rise") rises++;
|
|
9343
|
-
|
|
9837
|
+
entrances2.push(pick);
|
|
9344
9838
|
}
|
|
9345
9839
|
const seams = [];
|
|
9346
9840
|
for (let i = 0; i + 1 < n3; i++) {
|
|
@@ -9371,7 +9865,7 @@ function planMotion(seed, beats) {
|
|
|
9371
9865
|
const start = fnv1a2(`${seed}|emph|${beat.id}|${i}`) % EMPHASES.length;
|
|
9372
9866
|
return EMPHASES.map((_, k) => EMPHASES[(start + k) % EMPHASES.length]);
|
|
9373
9867
|
});
|
|
9374
|
-
return { entrances, seams, emphases };
|
|
9868
|
+
return { entrances: entrances2, seams, emphases };
|
|
9375
9869
|
}
|
|
9376
9870
|
function ensureSeamKinds(seams, want) {
|
|
9377
9871
|
const kinds = () => new Set(seams.filter((s) => s !== "dive"));
|
|
@@ -9876,8 +10370,9 @@ function layout(storyboard, source, format, options = {}) {
|
|
|
9876
10370
|
};
|
|
9877
10371
|
const segments2 = opts.narration?.beats[beat.id];
|
|
9878
10372
|
const emitted = emitScene(beat, ctx);
|
|
10373
|
+
const bespoke = opts.bespoke?.[beat.id] !== void 0;
|
|
9879
10374
|
const { scene } = stageScene(
|
|
9880
|
-
motion ? restyleEntrance(emitted, sid, motion.entrances[i] ?? "rise") : emitted,
|
|
10375
|
+
motion && !bespoke ? restyleEntrance(emitted, sid, motion.entrances[i] ?? "rise") : emitted,
|
|
9881
10376
|
speed
|
|
9882
10377
|
);
|
|
9883
10378
|
const seconds = beatSeconds(beat.seconds * speed, scene, segments2);
|
|
@@ -9886,7 +10381,7 @@ function layout(storyboard, source, format, options = {}) {
|
|
|
9886
10381
|
const duration = dive ? seconds + diveTail(dive) : seconds;
|
|
9887
10382
|
const start = at;
|
|
9888
10383
|
at += duration;
|
|
9889
|
-
return { beat, sid, scene, segments: segments2, inside, dive, duration, start, seconds };
|
|
10384
|
+
return { beat, sid, scene, segments: segments2, inside, dive, duration, start, seconds, bespoke };
|
|
9890
10385
|
});
|
|
9891
10386
|
let builds = false;
|
|
9892
10387
|
const plugins = /* @__PURE__ */ new Set();
|
|
@@ -9967,11 +10462,17 @@ function layout(storyboard, source, format, options = {}) {
|
|
|
9967
10462
|
} : void 0,
|
|
9968
10463
|
looks,
|
|
9969
10464
|
motion,
|
|
9970
|
-
holdMotion
|
|
10465
|
+
holdMotion,
|
|
10466
|
+
// Whether any scene runs generated code — the one thing that puts a CSP in
|
|
10467
|
+
// the head. Classic and plain v2 decks never set it, so their heads are unchanged.
|
|
10468
|
+
scripted: cuts.some((c) => c.bespoke)
|
|
9971
10469
|
};
|
|
9972
10470
|
}
|
|
9973
10471
|
function design(opts) {
|
|
9974
|
-
return
|
|
10472
|
+
return {
|
|
10473
|
+
...opts.design ? { design: opts.design } : {},
|
|
10474
|
+
...opts.bespoke ? { bespoke: opts.bespoke } : {}
|
|
10475
|
+
};
|
|
9975
10476
|
}
|
|
9976
10477
|
function withMotion(motion, i, cut, scene, accent, speed, holdMotion) {
|
|
9977
10478
|
const { sid } = cut;
|
|
@@ -9983,7 +10484,17 @@ function withMotion(motion, i, cut, scene, accent, speed, holdMotion) {
|
|
|
9983
10484
|
const incoming = Number.isFinite(firstHold) ? seamIn(before, sid, over, firstHold) : [];
|
|
9984
10485
|
if (incoming.length) out = { ...out, tl: [...out.tl, ...incoming] };
|
|
9985
10486
|
}
|
|
9986
|
-
if (cut.segments?.length) {
|
|
10487
|
+
if (cut.segments?.length && cut.bespoke) {
|
|
10488
|
+
const windows = bespokeWindows(out, cut.segments, cut.seconds);
|
|
10489
|
+
if (windows.length) {
|
|
10490
|
+
holdMotion[sid] = windows.map((w) => ({
|
|
10491
|
+
stop: w.stop,
|
|
10492
|
+
at: rnd(cut.start + w.at),
|
|
10493
|
+
from: rnd(cut.start + w.from),
|
|
10494
|
+
to: rnd(cut.start + w.to)
|
|
10495
|
+
}));
|
|
10496
|
+
}
|
|
10497
|
+
} else if (cut.segments?.length) {
|
|
9987
10498
|
const { scene: emphasised, windows } = emphasize(out, sid, {
|
|
9988
10499
|
segments: cut.segments,
|
|
9989
10500
|
starts: spokenStarts(out, cut.segments).starts,
|
|
@@ -10003,6 +10514,23 @@ function withMotion(motion, i, cut, scene, accent, speed, holdMotion) {
|
|
|
10003
10514
|
}
|
|
10004
10515
|
return out;
|
|
10005
10516
|
}
|
|
10517
|
+
function bespokeWindows(scene, segments2, end) {
|
|
10518
|
+
const holds = [...new Set(scene.holds.filter((h) => Number.isFinite(h) && h > 0))].sort(
|
|
10519
|
+
(a, b) => a - b
|
|
10520
|
+
);
|
|
10521
|
+
const { starts } = spokenStarts(scene, segments2);
|
|
10522
|
+
const out = [];
|
|
10523
|
+
segments2.forEach((segment2, i) => {
|
|
10524
|
+
const index = Math.min(segment2.stop, holds.length - 1);
|
|
10525
|
+
const at = holds[index];
|
|
10526
|
+
if (at === void 0) return;
|
|
10527
|
+
const next = holds[index + 1];
|
|
10528
|
+
const to = rnd((next ?? end) - 1e-3);
|
|
10529
|
+
if (to <= at) return;
|
|
10530
|
+
out.push({ stop: segment2.stop, at, from: rnd(starts[i] ?? at), to });
|
|
10531
|
+
});
|
|
10532
|
+
return out;
|
|
10533
|
+
}
|
|
10006
10534
|
function motionIsland(holds) {
|
|
10007
10535
|
const json = JSON.stringify({ version: 1, seams: true, holds }, null, 2).replace(/</g, "\\u003c");
|
|
10008
10536
|
return `
|
|
@@ -10099,7 +10627,8 @@ ${emitIsland(slides)}` : "";
|
|
|
10099
10627
|
return `<!doctype html>
|
|
10100
10628
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
10101
10629
|
<head>
|
|
10102
|
-
<meta charset="UTF-8"
|
|
10630
|
+
<meta charset="UTF-8" />${laid.scripted ? `
|
|
10631
|
+
<meta http-equiv="Content-Security-Policy" content="${BESPOKE_CSP}" />` : ""}
|
|
10103
10632
|
<title>${esc(storyboard.title)}</title>
|
|
10104
10633
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
10105
10634
|
<script src="${GSAP_SRC}"></script>
|
|
@@ -10136,6 +10665,17 @@ ${scenes.join("\n")}
|
|
|
10136
10665
|
</html>
|
|
10137
10666
|
`;
|
|
10138
10667
|
}
|
|
10668
|
+
var BESPOKE_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' data: blob:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
|
|
10669
|
+
function bespokeCall(sid, script) {
|
|
10670
|
+
return `try {
|
|
10671
|
+
(function (tl, root) {
|
|
10672
|
+
"use strict";
|
|
10673
|
+
${closeSafe(script)}
|
|
10674
|
+
})(tl, document.getElementById("${sid}"));
|
|
10675
|
+
} catch (e) {
|
|
10676
|
+
console.error("decksmith bespoke #${sid}: " + (e && e.message));
|
|
10677
|
+
}`;
|
|
10678
|
+
}
|
|
10139
10679
|
function buildingFlag() {
|
|
10140
10680
|
return `
|
|
10141
10681
|
<script>window.__hfTimelinesBuilding = true;</script>`;
|
|
@@ -10234,6 +10774,9 @@ function sceneHtml(sid, start, clip3, label, scene, transit) {
|
|
|
10234
10774
|
`// times relative to its start \u2014 one absolute root timeline yields a`,
|
|
10235
10775
|
`// deck the slideshow controller cannot bind to.`,
|
|
10236
10776
|
`var tl = gsap.timeline({ paused: true });`,
|
|
10777
|
+
// A bespoke scene's generated script, checked before it got here. Before
|
|
10778
|
+
// the shell's tweens so its `gsap.set` baselines are in place first.
|
|
10779
|
+
...scene.script ? [bespokeCall(sid, scene.script)] : [],
|
|
10237
10780
|
// `tweenText` is the ONLY place a tween becomes GSAP source. Everything
|
|
10238
10781
|
// above this line is a typed object the checker can see; below it is text.
|
|
10239
10782
|
...scene.tl.map(tweenText),
|
|
@@ -10288,11 +10831,9 @@ function rnd(n3) {
|
|
|
10288
10831
|
var AFFINITY = {
|
|
10289
10832
|
signal: { quantity: 0.6, frame: 0.3 },
|
|
10290
10833
|
blueprint: { structure: 0.7, formal: 0.2 },
|
|
10291
|
-
atlas: { frame: 0.6, structure: 0.2 }
|
|
10292
|
-
folio: { formal: 0.8, frame: 0.2 },
|
|
10293
|
-
chalk: { quantity: 0.5, structure: 0.4 },
|
|
10294
|
-
journal: { formal: 0.4, frame: 0.4 }
|
|
10834
|
+
atlas: { frame: 0.6, structure: 0.2 }
|
|
10295
10835
|
};
|
|
10836
|
+
var PICKED = Object.keys(PACKS).filter((n3) => PACKS[n3]?.ground === "dark");
|
|
10296
10837
|
function fnv1a3(text2) {
|
|
10297
10838
|
let h = 2166136261;
|
|
10298
10839
|
for (let i = 0; i < text2.length; i++) {
|
|
@@ -10309,7 +10850,7 @@ function mix(beats) {
|
|
|
10309
10850
|
function packWeights(beats) {
|
|
10310
10851
|
const m = mix(beats);
|
|
10311
10852
|
const out = {};
|
|
10312
|
-
for (const name of
|
|
10853
|
+
for (const name of PICKED) {
|
|
10313
10854
|
const a = AFFINITY[name] ?? {};
|
|
10314
10855
|
out[name] = 1 + Object.entries(a).reduce((s, [f, w]) => s + (w ?? 0) * m[f], 0);
|
|
10315
10856
|
}
|
|
@@ -10321,7 +10862,7 @@ function rankPacks(beats, seed) {
|
|
|
10321
10862
|
const u = (fnv1a3(`${seed}\0${name}`) + 0.5) / 2 ** 32;
|
|
10322
10863
|
return -Math.log(u) / (weights[name] ?? 1);
|
|
10323
10864
|
};
|
|
10324
|
-
return
|
|
10865
|
+
return PICKED.map((name) => ({ name, d: draw2(name) })).sort((a, b) => a.d - b.d || (a.name < b.name ? -1 : 1)).map((r) => r.name);
|
|
10325
10866
|
}
|
|
10326
10867
|
function chooseLook(c) {
|
|
10327
10868
|
if (c.stated) return c.stated;
|
|
@@ -10445,8 +10986,15 @@ function readFragments(html) {
|
|
|
10445
10986
|
}
|
|
10446
10987
|
return out;
|
|
10447
10988
|
}
|
|
10448
|
-
function holdsFor(beat, source, format, theme, sid, speed) {
|
|
10449
|
-
const ctx = {
|
|
10989
|
+
function holdsFor(beat, source, format, theme, sid, speed, bespoke) {
|
|
10990
|
+
const ctx = {
|
|
10991
|
+
source,
|
|
10992
|
+
format,
|
|
10993
|
+
theme,
|
|
10994
|
+
sid,
|
|
10995
|
+
start: 0,
|
|
10996
|
+
...bespoke ? { bespoke } : {}
|
|
10997
|
+
};
|
|
10450
10998
|
const { scene, open } = stageScene(emitScene(beat, ctx), speed);
|
|
10451
10999
|
return {
|
|
10452
11000
|
holds: [...new Set(scene.holds.filter((h) => Number.isFinite(h) && h > 0))].sort(
|
|
@@ -10467,7 +11015,7 @@ function assertHoldsAgree(scenes, fragments) {
|
|
|
10467
11015
|
}
|
|
10468
11016
|
}
|
|
10469
11017
|
}
|
|
10470
|
-
function
|
|
11018
|
+
function place2(scenes, spoken) {
|
|
10471
11019
|
const out = [];
|
|
10472
11020
|
for (const scene of scenes) {
|
|
10473
11021
|
const segments2 = [...spoken[scene.id] ?? []].sort((a, b) => a.stop - b.stop);
|
|
@@ -10536,10 +11084,10 @@ function planTiming(input) {
|
|
|
10536
11084
|
const stops = /* @__PURE__ */ new Map();
|
|
10537
11085
|
beats.forEach((beat, i) => {
|
|
10538
11086
|
const scene = scenes[i];
|
|
10539
|
-
const
|
|
10540
|
-
scene.holds =
|
|
10541
|
-
scene.open =
|
|
10542
|
-
stops.set(beat.id, stopCount(
|
|
11087
|
+
const staging2 = holdsFor(beat, source, format, theme, scene.id, speed, input.bespoke);
|
|
11088
|
+
scene.holds = staging2.holds;
|
|
11089
|
+
scene.open = staging2.open;
|
|
11090
|
+
stops.set(beat.id, stopCount(staging2.holds));
|
|
10543
11091
|
const segments3 = narration?.beats[beat.id];
|
|
10544
11092
|
if (segments3?.length) spoken[scene.id] = segments3;
|
|
10545
11093
|
});
|
|
@@ -10551,7 +11099,7 @@ function planTiming(input) {
|
|
|
10551
11099
|
fragments
|
|
10552
11100
|
);
|
|
10553
11101
|
}
|
|
10554
|
-
const segments2 =
|
|
11102
|
+
const segments2 = place2(scenes, spoken);
|
|
10555
11103
|
assertFits(scenes, segments2);
|
|
10556
11104
|
return {
|
|
10557
11105
|
version: 1,
|
|
@@ -10866,8 +11414,8 @@ function textOf(node) {
|
|
|
10866
11414
|
|
|
10867
11415
|
// src/source/harvest.ts
|
|
10868
11416
|
import { createHash as createHash6 } from "node:crypto";
|
|
10869
|
-
import { copyFile, mkdir as
|
|
10870
|
-
import { basename as basename3, join as
|
|
11417
|
+
import { copyFile as copyFile2, mkdir as mkdir6, rm as rm3, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
11418
|
+
import { basename as basename3, join as join8, resolve as resolve4 } from "node:path";
|
|
10871
11419
|
|
|
10872
11420
|
// src/net/fetch.ts
|
|
10873
11421
|
import { lookup as resolveHost } from "node:dns/promises";
|
|
@@ -10929,10 +11477,10 @@ function expandV6(v6) {
|
|
|
10929
11477
|
const head = text2.slice(0, text2.length - quad[1].length);
|
|
10930
11478
|
text2 = `${head}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
10931
11479
|
}
|
|
10932
|
-
const
|
|
10933
|
-
if (
|
|
10934
|
-
const left =
|
|
10935
|
-
const right =
|
|
11480
|
+
const halves2 = text2.split("::");
|
|
11481
|
+
if (halves2.length > 2) return null;
|
|
11482
|
+
const left = halves2[0] ? halves2[0].split(":") : [];
|
|
11483
|
+
const right = halves2.length === 2 ? halves2[1] ? halves2[1].split(":") : [] : null;
|
|
10936
11484
|
const fill = 8 - left.length - (right?.length ?? 0);
|
|
10937
11485
|
if (right !== null && fill < 0) return null;
|
|
10938
11486
|
const parts = right === null ? left : [...left, ...new Array(fill).fill("0"), ...right];
|
|
@@ -11067,8 +11615,8 @@ async function send(url, target, headers, signal2, opts) {
|
|
|
11067
11615
|
...secure && isIP(host2) === 0 ? { servername: host2 } : {}
|
|
11068
11616
|
};
|
|
11069
11617
|
try {
|
|
11070
|
-
return await new Promise((
|
|
11071
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
11618
|
+
return await new Promise((resolve10, reject) => {
|
|
11619
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve10);
|
|
11072
11620
|
req.on("error", reject);
|
|
11073
11621
|
req.end();
|
|
11074
11622
|
});
|
|
@@ -11152,7 +11700,7 @@ function message(err) {
|
|
|
11152
11700
|
|
|
11153
11701
|
// src/render/capture.ts
|
|
11154
11702
|
import { homedir as homedir2 } from "node:os";
|
|
11155
|
-
import { join as
|
|
11703
|
+
import { join as join6 } from "node:path";
|
|
11156
11704
|
async function resolveChrome(need = "open the deck with") {
|
|
11157
11705
|
const env2 = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
11158
11706
|
if (env2) return { path: env2, source: "env" };
|
|
@@ -11162,9 +11710,9 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
11162
11710
|
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
11163
11711
|
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
11164
11712
|
};
|
|
11165
|
-
const hf = await pick(
|
|
11713
|
+
const hf = await pick(join6(homedir2(), ".cache", "hyperframes", "chrome"));
|
|
11166
11714
|
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
11167
|
-
const pup = await pick(process.env.PUPPETEER_CACHE_DIR ||
|
|
11715
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join6(homedir2(), ".cache", "puppeteer"));
|
|
11168
11716
|
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
11169
11717
|
throw new Error(
|
|
11170
11718
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
@@ -11192,12 +11740,12 @@ async function launchOwnPage(launch, args, what) {
|
|
|
11192
11740
|
|
|
11193
11741
|
// src/source/assets.ts
|
|
11194
11742
|
import { createHash as createHash5 } from "node:crypto";
|
|
11195
|
-
import { mkdir as
|
|
11196
|
-
import { extname as extname2, join as
|
|
11743
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
|
|
11744
|
+
import { extname as extname2, join as join7 } from "node:path";
|
|
11197
11745
|
import { z as z2 } from "zod";
|
|
11198
11746
|
async function fetchFigures(source, dir, warnings) {
|
|
11199
11747
|
const drops = warnings ?? [];
|
|
11200
|
-
await
|
|
11748
|
+
await mkdir5(dir, { recursive: true });
|
|
11201
11749
|
const cached2 = await readdir2(dir).catch(() => []);
|
|
11202
11750
|
const figures = [];
|
|
11203
11751
|
for (const figure of source.figures) {
|
|
@@ -11223,11 +11771,11 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
11223
11771
|
async function localize(figure, dir, cached2) {
|
|
11224
11772
|
const stem = assetStem(figure.id, figure.src);
|
|
11225
11773
|
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
11226
|
-
const bytes = hit ? await
|
|
11774
|
+
const bytes = hit ? await readFile6(join7(dir, hit)) : await load(figure.src);
|
|
11227
11775
|
const size = imageSize(bytes);
|
|
11228
11776
|
if (hit) return { ...figure, src: hit, ...size };
|
|
11229
11777
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
11230
|
-
await
|
|
11778
|
+
await writeFile5(join7(dir, src), bytes);
|
|
11231
11779
|
cached2.push(src);
|
|
11232
11780
|
return { ...figure, src, ...size };
|
|
11233
11781
|
}
|
|
@@ -11252,7 +11800,7 @@ function assetExt(bytes, src) {
|
|
|
11252
11800
|
var FIGURE_MAX_BYTES = 32 * 1024 * 1024;
|
|
11253
11801
|
var FIGURE_TIMEOUT_MS = 2e4;
|
|
11254
11802
|
async function load(src) {
|
|
11255
|
-
if (!/^https?:/i.test(src)) return
|
|
11803
|
+
if (!/^https?:/i.test(src)) return readFile6(src);
|
|
11256
11804
|
const got = await fetchGuarded(src, {
|
|
11257
11805
|
maxBytes: FIGURE_MAX_BYTES,
|
|
11258
11806
|
timeoutMs: FIGURE_TIMEOUT_MS
|
|
@@ -11788,7 +12336,7 @@ function readContentRegion() {
|
|
|
11788
12336
|
}
|
|
11789
12337
|
|
|
11790
12338
|
// src/source/transcode.ts
|
|
11791
|
-
import { readFile as
|
|
12339
|
+
import { readFile as readFile7, rm as rm2 } from "node:fs/promises";
|
|
11792
12340
|
import { basename as basename2 } from "node:path";
|
|
11793
12341
|
|
|
11794
12342
|
// src/render/ffmpeg.ts
|
|
@@ -11814,7 +12362,7 @@ ${tail}`);
|
|
|
11814
12362
|
}
|
|
11815
12363
|
}
|
|
11816
12364
|
function runLive(file, args) {
|
|
11817
|
-
return new Promise((
|
|
12365
|
+
return new Promise((resolve10, reject) => {
|
|
11818
12366
|
const child = spawn2(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
11819
12367
|
child.on("error", (err) => {
|
|
11820
12368
|
reject(
|
|
@@ -11822,7 +12370,7 @@ function runLive(file, args) {
|
|
|
11822
12370
|
);
|
|
11823
12371
|
});
|
|
11824
12372
|
child.on("close", (code, signal2) => {
|
|
11825
|
-
if (code === 0)
|
|
12373
|
+
if (code === 0) resolve10();
|
|
11826
12374
|
else if (signal2) {
|
|
11827
12375
|
reject(
|
|
11828
12376
|
new Error(
|
|
@@ -12024,7 +12572,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
12024
12572
|
const maxSeconds = opts.maxSeconds ?? MAX_CLIP_SECONDS;
|
|
12025
12573
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
12026
12574
|
const name = basename2(input);
|
|
12027
|
-
const source = videoSize(await
|
|
12575
|
+
const source = videoSize(await readFile7(input));
|
|
12028
12576
|
const box = fitBox(source.width, source.height, opts.maxEdgePx ?? CLIP_EDGE_PX);
|
|
12029
12577
|
const kept = (why2) => ({
|
|
12030
12578
|
path: input,
|
|
@@ -12056,7 +12604,7 @@ async function transcode(input, out, opts = {}) {
|
|
|
12056
12604
|
}
|
|
12057
12605
|
let measured;
|
|
12058
12606
|
try {
|
|
12059
|
-
measured = videoSize(await
|
|
12607
|
+
measured = videoSize(await readFile7(out));
|
|
12060
12608
|
} catch (err) {
|
|
12061
12609
|
await rm2(out, { force: true });
|
|
12062
12610
|
return kept(
|
|
@@ -12092,8 +12640,8 @@ var HTML_TYPES = /^text\/html$|^application\/xhtml\+xml$/;
|
|
|
12092
12640
|
async function harvest(url, dir, opts = {}) {
|
|
12093
12641
|
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS2;
|
|
12094
12642
|
const warnings = [];
|
|
12095
|
-
const assets =
|
|
12096
|
-
await
|
|
12643
|
+
const assets = resolve4(dir);
|
|
12644
|
+
await mkdir6(assets, { recursive: true });
|
|
12097
12645
|
const page = await fetchGuarded(url, {
|
|
12098
12646
|
maxBytes: opts.maxBytes ?? HTML_MAX_BYTES,
|
|
12099
12647
|
timeoutMs,
|
|
@@ -12483,8 +13031,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
12483
13031
|
`it is ${size.width}x${size.height}, under ${MIN_FIGURE_PX}px \u2014 spacers, icons and tracking pixels look like this, and a slide cannot use one`
|
|
12484
13032
|
);
|
|
12485
13033
|
}
|
|
12486
|
-
const path2 =
|
|
12487
|
-
await
|
|
13034
|
+
const path2 = join8(dir, assetName(url, extFor2(sniffFormat(bytes))));
|
|
13035
|
+
await writeFile6(path2, bytes);
|
|
12488
13036
|
assets.push(path2);
|
|
12489
13037
|
got = { path: path2, width: size.width, height: size.height };
|
|
12490
13038
|
} catch (err) {
|
|
@@ -12496,7 +13044,7 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
12496
13044
|
};
|
|
12497
13045
|
const shrink = async (url, path2, measured, what) => {
|
|
12498
13046
|
if (opts.transcode === false) return { path: path2, ...measured };
|
|
12499
|
-
const out2 =
|
|
13047
|
+
const out2 = join8(dir, assetName(url, ".vp9.webm"));
|
|
12500
13048
|
let small;
|
|
12501
13049
|
try {
|
|
12502
13050
|
small = await transcode(path2, out2, {
|
|
@@ -12538,8 +13086,8 @@ async function localise(seen, base, dir, opts, timeoutMs, warnings) {
|
|
|
12538
13086
|
try {
|
|
12539
13087
|
const bytes = await bytesOf(url);
|
|
12540
13088
|
const measured = videoSize(bytes);
|
|
12541
|
-
const path2 =
|
|
12542
|
-
await
|
|
13089
|
+
const path2 = join8(dir, assetName(url, `.${measured.container}`));
|
|
13090
|
+
await writeFile6(path2, bytes);
|
|
12543
13091
|
return await shrink(url, path2, measured, what);
|
|
12544
13092
|
} catch (err) {
|
|
12545
13093
|
warnings.push(`${what} was not downloaded: ${url} \u2014 ${why(err)}`);
|
|
@@ -12628,7 +13176,7 @@ function unheld(policy) {
|
|
|
12628
13176
|
return "the file could not be held";
|
|
12629
13177
|
}
|
|
12630
13178
|
async function sizeOf(path2) {
|
|
12631
|
-
return await
|
|
13179
|
+
return await stat3(path2).then(
|
|
12632
13180
|
(s) => s.size,
|
|
12633
13181
|
() => 0
|
|
12634
13182
|
);
|
|
@@ -12902,9 +13450,9 @@ ${fence}`;
|
|
|
12902
13450
|
|
|
12903
13451
|
// src/plan/codex.ts
|
|
12904
13452
|
import { spawn as spawn3 } from "node:child_process";
|
|
12905
|
-
import { mkdtemp, readFile as
|
|
13453
|
+
import { mkdtemp, readFile as readFile8, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
12906
13454
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
12907
|
-
import { join as
|
|
13455
|
+
import { join as join9 } from "node:path";
|
|
12908
13456
|
import { z as z3 } from "zod";
|
|
12909
13457
|
|
|
12910
13458
|
// src/plan/arc.ts
|
|
@@ -13408,9 +13956,19 @@ nothing, so it can never dangle.
|
|
|
13408
13956
|
- At most ${images.max} pictures in the whole deck; a split-compare with two
|
|
13409
13957
|
briefs spends two. Past that, find the point's shape and draw it.`;
|
|
13410
13958
|
}
|
|
13959
|
+
var BESPOKE_HINT = `
|
|
13960
|
+
|
|
13961
|
+
BESPOKE SCENES
|
|
13962
|
+
|
|
13963
|
+
Set \`bespoke: true\` on the four to six beats that explain a MECHANISM \u2014 a
|
|
13964
|
+
process that runs, an equation whose terms act, a curve that moves, a
|
|
13965
|
+
comparison that is a distance \u2014 and that the narration talks over for a while.
|
|
13966
|
+
Those beats get a custom animation drawn for them; everything else keeps its
|
|
13967
|
+
archetype. Set \`bespoke: false\` on a beat whose archetype must stay (a real
|
|
13968
|
+
figure the viewer must see). Leave it null otherwise.`;
|
|
13411
13969
|
function systemPrompt(prefs) {
|
|
13412
13970
|
const plan = durationPlan(prefs);
|
|
13413
|
-
return `${rules(cadenceFor(prefs, plan))}${paperArcRequested(prefs) ? paperArc(prefs.slides) : ""}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
13971
|
+
return `${rules(cadenceFor(prefs, plan))}${paperArcRequested(prefs) ? paperArc(prefs.slides) : ""}${prefs.images.enabled ? illustrations(prefs.images) : ""}${prefs.bespoke?.enabled ? BESPOKE_HINT : ""}
|
|
13414
13972
|
|
|
13415
13973
|
PREFERENCES \u2014 chosen by the person who asked for this deck.
|
|
13416
13974
|
${prefs.duration === void 0 ? "" : `
|
|
@@ -13704,7 +14262,10 @@ function stripNulls(node) {
|
|
|
13704
14262
|
}
|
|
13705
14263
|
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
13706
14264
|
function plannerInvisible(prefs) {
|
|
13707
|
-
|
|
14265
|
+
const hidden = new Set(PLANNER_INVISIBLE);
|
|
14266
|
+
if (!paperArcRequested(prefs)) hidden.add("role");
|
|
14267
|
+
if (!prefs.bespoke?.enabled) hidden.add("bespoke");
|
|
14268
|
+
return hidden;
|
|
13708
14269
|
}
|
|
13709
14270
|
function hideFromPlanner(node, hidden) {
|
|
13710
14271
|
if (Array.isArray(node)) return node.map((n3) => hideFromPlanner(n3, hidden));
|
|
@@ -13736,11 +14297,11 @@ function schemaFor(prefs) {
|
|
|
13736
14297
|
var SCHEMA = schemaFor({ genre: "general" });
|
|
13737
14298
|
async function codexPlanner(source, opts = {}) {
|
|
13738
14299
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
13739
|
-
const dir = await mkdtemp(
|
|
14300
|
+
const dir = await mkdtemp(join9(tmpdir2(), "decksmith-plan-"));
|
|
13740
14301
|
try {
|
|
13741
|
-
const schemaPath =
|
|
13742
|
-
const outPath =
|
|
13743
|
-
await
|
|
14302
|
+
const schemaPath = join9(dir, "storyboard.schema.json");
|
|
14303
|
+
const outPath = join9(dir, "storyboard.json");
|
|
14304
|
+
await writeFile7(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
13744
14305
|
await (opts.run ?? runCodex)({
|
|
13745
14306
|
prompt: buildPrompt(source, prefs),
|
|
13746
14307
|
schemaPath,
|
|
@@ -13748,7 +14309,7 @@ async function codexPlanner(source, opts = {}) {
|
|
|
13748
14309
|
...opts.model === void 0 ? {} : { model: opts.model },
|
|
13749
14310
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
13750
14311
|
});
|
|
13751
|
-
const raw2 = await
|
|
14312
|
+
const raw2 = await readFile8(outPath, "utf8").catch(() => "");
|
|
13752
14313
|
if (raw2.trim() === "") {
|
|
13753
14314
|
throw new Error("Codex produced no final message. Re-run, or try a shorter source.");
|
|
13754
14315
|
}
|
|
@@ -13815,18 +14376,24 @@ function codexCommand(args) {
|
|
|
13815
14376
|
"--color",
|
|
13816
14377
|
"never",
|
|
13817
14378
|
...args.model ? ["--model", args.model] : [],
|
|
14379
|
+
...(args.config ?? []).flatMap((c) => ["-c", c]),
|
|
14380
|
+
...(args.images ?? []).flatMap((path2) => ["-i", path2]),
|
|
13818
14381
|
"-"
|
|
13819
14382
|
];
|
|
13820
14383
|
return sandbox === "workspace-write" && args.cwd !== void 0 ? { argv, env: { ...process.env, TMPDIR: args.cwd } } : { argv };
|
|
13821
14384
|
}
|
|
13822
14385
|
function runCodex(args) {
|
|
13823
14386
|
const { argv, env: env2 } = codexCommand(args);
|
|
13824
|
-
return new Promise((
|
|
13825
|
-
const child = spawn3("codex", argv, {
|
|
13826
|
-
stdio: ["pipe", "ignore", "pipe"],
|
|
14387
|
+
return new Promise((resolve10, reject) => {
|
|
14388
|
+
const child = spawn3(args.bin ?? "codex", argv, {
|
|
14389
|
+
stdio: ["pipe", args.onUsage ? "pipe" : "ignore", "pipe"],
|
|
13827
14390
|
...env2 === void 0 ? {} : { env: env2 }
|
|
13828
14391
|
});
|
|
13829
14392
|
let stderr = "";
|
|
14393
|
+
let stdout = "";
|
|
14394
|
+
child.stdout?.on("data", (c) => {
|
|
14395
|
+
stdout = (stdout + c.toString()).slice(-4e3);
|
|
14396
|
+
});
|
|
13830
14397
|
const timer = setTimeout(() => {
|
|
13831
14398
|
child.kill("SIGKILL");
|
|
13832
14399
|
reject(new Error(`Codex did not finish within ${Math.round(args.timeoutMs / 1e3)}s.`));
|
|
@@ -13838,32 +14405,35 @@ function runCodex(args) {
|
|
|
13838
14405
|
clearTimeout(timer);
|
|
13839
14406
|
reject(
|
|
13840
14407
|
err.code === "ENOENT" ? new Error(
|
|
13841
|
-
|
|
14408
|
+
`The "${args.bin ?? "codex"}" CLI is not on PATH. Install it, or sign in with \`codex login\`, then retry.`
|
|
13842
14409
|
) : err
|
|
13843
14410
|
);
|
|
13844
14411
|
});
|
|
13845
14412
|
child.on("close", (code) => {
|
|
13846
14413
|
clearTimeout(timer);
|
|
13847
|
-
|
|
14414
|
+
const used = /tokens used\s*\n?\s*([\d,]+)/i.exec(`${stdout}
|
|
14415
|
+
${stderr}`);
|
|
14416
|
+
if (used?.[1] && args.onUsage) args.onUsage(Number(used[1].replace(/,/g, "")));
|
|
14417
|
+
if (code === 0) return resolve10();
|
|
13848
14418
|
reject(
|
|
13849
14419
|
new Error(`codex exec exited ${code}.
|
|
13850
14420
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
13851
14421
|
);
|
|
13852
14422
|
});
|
|
13853
|
-
child.stdin
|
|
14423
|
+
child.stdin?.end(args.prompt);
|
|
13854
14424
|
});
|
|
13855
14425
|
}
|
|
13856
14426
|
|
|
13857
14427
|
// src/images/illustrate.ts
|
|
13858
14428
|
import { createHash as createHash8 } from "node:crypto";
|
|
13859
|
-
import { mkdir as
|
|
13860
|
-
import { join as
|
|
14429
|
+
import { mkdir as mkdir7, readFile as readFile10, rename as rename2, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
|
|
14430
|
+
import { join as join11 } from "node:path";
|
|
13861
14431
|
|
|
13862
14432
|
// src/images/providers.ts
|
|
13863
14433
|
import { createHash as createHash7 } from "node:crypto";
|
|
13864
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
14434
|
+
import { mkdtemp as mkdtemp2, readFile as readFile9, rm as rm5, writeFile as writeFile8 } from "node:fs/promises";
|
|
13865
14435
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
13866
|
-
import { join as
|
|
14436
|
+
import { join as join10, resolve as resolve5 } from "node:path";
|
|
13867
14437
|
import { z as z4 } from "zod";
|
|
13868
14438
|
var SIZE = {
|
|
13869
14439
|
landscape: { width: 1536, height: 1024 },
|
|
@@ -13995,11 +14565,11 @@ function codexImages(opts = {}) {
|
|
|
13995
14565
|
async check() {
|
|
13996
14566
|
},
|
|
13997
14567
|
async generate(req) {
|
|
13998
|
-
const dir = await mkdtemp2(
|
|
14568
|
+
const dir = await mkdtemp2(join10(tmpdir3(), "decksmith-image-"));
|
|
13999
14569
|
try {
|
|
14000
|
-
const schemaPath =
|
|
14001
|
-
const outPath =
|
|
14002
|
-
await
|
|
14570
|
+
const schemaPath = join10(dir, "answer.schema.json");
|
|
14571
|
+
const outPath = join10(dir, "answer.json");
|
|
14572
|
+
await writeFile8(schemaPath, JSON.stringify(ANSWER_SCHEMA));
|
|
14003
14573
|
await run5({
|
|
14004
14574
|
prompt: codexPrompt(req),
|
|
14005
14575
|
schemaPath,
|
|
@@ -14008,7 +14578,7 @@ function codexImages(opts = {}) {
|
|
|
14008
14578
|
cwd: dir,
|
|
14009
14579
|
sandbox: "workspace-write"
|
|
14010
14580
|
});
|
|
14011
|
-
const raw2 = await
|
|
14581
|
+
const raw2 = await readFile9(outPath, "utf8").catch(() => "");
|
|
14012
14582
|
const answer = answerSchema.safeParse(parseJson(Buffer.from(raw2)));
|
|
14013
14583
|
if (!answer.success) throw new Error("codex could not generate a picture: no final answer");
|
|
14014
14584
|
if (!answer.data.ok) {
|
|
@@ -14016,10 +14586,10 @@ function codexImages(opts = {}) {
|
|
|
14016
14586
|
`codex could not generate a picture: ${answer.data.reason ?? "no reason given"}`
|
|
14017
14587
|
);
|
|
14018
14588
|
}
|
|
14019
|
-
const candidates3 = [
|
|
14020
|
-
if (answer.data.file) candidates3.push(
|
|
14589
|
+
const candidates3 = [join10(dir, "picture.png")];
|
|
14590
|
+
if (answer.data.file) candidates3.push(resolve5(dir, answer.data.file));
|
|
14021
14591
|
for (const path2 of candidates3) {
|
|
14022
|
-
const bytes = await
|
|
14592
|
+
const bytes = await readFile9(path2).catch(() => null);
|
|
14023
14593
|
if (bytes) return raster(bytes, "codex");
|
|
14024
14594
|
}
|
|
14025
14595
|
throw new Error("codex said ok but wrote no picture.png");
|
|
@@ -14151,7 +14721,7 @@ async function illustrate(storyboard, source, opts) {
|
|
|
14151
14721
|
const tool = last?.id === "svg" ? last : toolSvg();
|
|
14152
14722
|
if (tool !== last) chain.push(tool);
|
|
14153
14723
|
const dropped = /* @__PURE__ */ new Set();
|
|
14154
|
-
if (pending.length > 0) await
|
|
14724
|
+
if (pending.length > 0) await mkdir7(opts.assetsDir, { recursive: true });
|
|
14155
14725
|
for (const [i, slot] of pending.entries()) {
|
|
14156
14726
|
let rungs = chain;
|
|
14157
14727
|
if (i >= images.max) {
|
|
@@ -14248,16 +14818,16 @@ function cacheKey2(providerId, req) {
|
|
|
14248
14818
|
async function draw(provider, req, name, dir) {
|
|
14249
14819
|
for (const ext of Object.values(EXT2)) {
|
|
14250
14820
|
const src2 = `${name}${ext}`;
|
|
14251
|
-
const bytes = await
|
|
14821
|
+
const bytes = await readFile10(join11(dir, src2)).catch(() => null);
|
|
14252
14822
|
if (bytes) return { src: src2, ...sizeOf2(bytes, ext), cached: true };
|
|
14253
14823
|
}
|
|
14254
14824
|
await provider.check();
|
|
14255
14825
|
const img = await provider.generate(req);
|
|
14256
14826
|
const src = `${name}${EXT2[img.mime]}`;
|
|
14257
|
-
const tmp =
|
|
14827
|
+
const tmp = join11(dir, `.${name}.tmp`);
|
|
14258
14828
|
try {
|
|
14259
|
-
await
|
|
14260
|
-
await
|
|
14829
|
+
await writeFile9(tmp, img.bytes);
|
|
14830
|
+
await rename2(tmp, join11(dir, src));
|
|
14261
14831
|
} finally {
|
|
14262
14832
|
await rm6(tmp, { force: true });
|
|
14263
14833
|
}
|
|
@@ -14272,6 +14842,353 @@ import { execFile as execFile2 } from "node:child_process";
|
|
|
14272
14842
|
import { promisify as promisify2 } from "node:util";
|
|
14273
14843
|
var run2 = promisify2(execFile2);
|
|
14274
14844
|
|
|
14845
|
+
// src/verify/scenes.ts
|
|
14846
|
+
var TYPE_FLOOR_PX2 = 39.5;
|
|
14847
|
+
var MEASURE = `(sid, wantGeo) => {
|
|
14848
|
+
const root = document.getElementById(sid);
|
|
14849
|
+
if (!root) return { crossings: [], occlusions: [], overlaps: [], small: [], off: [], strays: [] };
|
|
14850
|
+
const W = window.innerWidth, H = window.innerHeight;
|
|
14851
|
+
// A bespoke scene that moves the camera is clipped to its body box: what the
|
|
14852
|
+
// push-in leaves outside it is not drawn, so it is neither off the frame nor
|
|
14853
|
+
// crossing anything.
|
|
14854
|
+
const bodyEl = document.getElementById(sid + "-g");
|
|
14855
|
+
const bodyClip = bodyEl && getComputedStyle(bodyEl).overflow !== "visible" ? bodyEl.getBoundingClientRect() : null;
|
|
14856
|
+
const meet = (a, b) => {
|
|
14857
|
+
const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
|
|
14858
|
+
const x1 = Math.min(a.x + a.width, b.x + b.width), y1 = Math.min(a.y + a.height, b.y + b.height);
|
|
14859
|
+
return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
14860
|
+
};
|
|
14861
|
+
const alpha = (el) => {
|
|
14862
|
+
let o = 1;
|
|
14863
|
+
for (let e = el; e && e !== document.body; e = e.parentElement) {
|
|
14864
|
+
const cs = getComputedStyle(e);
|
|
14865
|
+
if (cs.display === "none" || cs.visibility === "hidden") return 0;
|
|
14866
|
+
o *= Number(cs.opacity);
|
|
14867
|
+
}
|
|
14868
|
+
return o;
|
|
14869
|
+
};
|
|
14870
|
+
const name = (el) => el.id || (el.tagName.toLowerCase() + ":" + (el.textContent || "").trim().slice(0, 16));
|
|
14871
|
+
// What an element's box is after any clipping <svg> (overflow other than
|
|
14872
|
+
// visible) \u2014 a camera move zooms a viewBox, and what it leaves outside is
|
|
14873
|
+
// not drawn, so it is neither off the frame nor crossing anything.
|
|
14874
|
+
const clipOf = (el) => {
|
|
14875
|
+
const svg = el.ownerSVGElement || (el.closest && el.closest("svg"));
|
|
14876
|
+
let c = svg && getComputedStyle(svg).overflow !== "visible" ? svg.getBoundingClientRect() : null;
|
|
14877
|
+
if (bodyClip && bodyEl.contains(el)) c = c ? meet(c, bodyClip) : bodyClip;
|
|
14878
|
+
return c;
|
|
14879
|
+
};
|
|
14880
|
+
const boxOf = (el) => {
|
|
14881
|
+
const r = el.getBoundingClientRect();
|
|
14882
|
+
const c = clipOf(el);
|
|
14883
|
+
if (!c) return { x: r.x, y: r.y, width: r.width, height: r.height, clip: null };
|
|
14884
|
+
const x0 = Math.max(r.x, c.x), y0 = Math.max(r.y, c.y);
|
|
14885
|
+
const x1 = Math.min(r.x + r.width, c.x + c.width), y1 = Math.min(r.y + r.height, c.y + c.height);
|
|
14886
|
+
// >=, not >: a horizontal line has no height and is still drawn.
|
|
14887
|
+
return x1 >= x0 && y1 >= y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0, clip: c } : { x: 0, y: 0, width: 0, height: 0, clip: c };
|
|
14888
|
+
};
|
|
14889
|
+
// Text boxes: SVG <text>, a KaTeX run, or an HTML element with its own text.
|
|
14890
|
+
const texts = [];
|
|
14891
|
+
// Where a label sits, in the body box's own px (= the scene's svg units when it
|
|
14892
|
+
// has no camera move): a fix round can act on "x 1210-1330" where it cannot on
|
|
14893
|
+
// "these two overlap" \u2014 round 2's drafts failed on overlaps it could not see.
|
|
14894
|
+
const origin = document.getElementById(sid + "-g");
|
|
14895
|
+
const o0 = origin ? origin.getBoundingClientRect() : { x: 0, y: 0 };
|
|
14896
|
+
const at = (t) => t.id + " [x " + Math.round(t.x - o0.x) + "-" + Math.round(t.x + t.w - o0.x) + ", y " + Math.round(t.y - o0.y) + "-" + Math.round(t.y + t.h - o0.y) + "]";
|
|
14897
|
+
const walk = (el) => {
|
|
14898
|
+
const tag = el.tagName.toLowerCase();
|
|
14899
|
+
if (tag === "script" || tag === "style" || tag === "defs") return;
|
|
14900
|
+
const own = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim());
|
|
14901
|
+
const katex = el.classList && el.classList.contains("katex");
|
|
14902
|
+
if (tag === "text" || katex || (own && !(el instanceof SVGElement))) {
|
|
14903
|
+
const r = boxOf(el);
|
|
14904
|
+
const o = alpha(el);
|
|
14905
|
+
if (r.width > 0 && r.height > 0 && o > 0.15) {
|
|
14906
|
+
let fs = parseFloat(getComputedStyle(el).fontSize) || 0;
|
|
14907
|
+
if (el instanceof SVGGraphicsElement && el.getScreenCTM) {
|
|
14908
|
+
const m = el.getScreenCTM();
|
|
14909
|
+
if (m) fs *= Math.hypot(m.a, m.b);
|
|
14910
|
+
}
|
|
14911
|
+
texts.push({ el, id: name(el), x: r.x, y: r.y, w: r.width, h: r.height, fs, o });
|
|
14912
|
+
}
|
|
14913
|
+
if (tag === "text" || katex) return;
|
|
14914
|
+
}
|
|
14915
|
+
for (const c of el.children) walk(c);
|
|
14916
|
+
};
|
|
14917
|
+
walk(root);
|
|
14918
|
+
|
|
14919
|
+
const overlaps = [];
|
|
14920
|
+
for (let i = 0; i < texts.length; i++)
|
|
14921
|
+
for (let j = i + 1; j < texts.length; j++) {
|
|
14922
|
+
const a = texts[i], b = texts[j];
|
|
14923
|
+
if (a.el.contains(b.el) || b.el.contains(a.el)) continue;
|
|
14924
|
+
const ix = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
|
|
14925
|
+
const iy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
|
|
14926
|
+
if (ix > 4 && iy > 4) overlaps.push(at(a) + " \xD7 " + at(b));
|
|
14927
|
+
}
|
|
14928
|
+
const small = texts.filter((t) => t.fs > 0 && t.fs < ${TYPE_FLOOR_PX2}).map((t) => t.id + "@" + t.fs.toFixed(1) + "px");
|
|
14929
|
+
|
|
14930
|
+
// Strokes and fills. Every shape that paints something at this instant is a
|
|
14931
|
+
// LEAF: what the stage-fill and early-reveal measures below are made of.
|
|
14932
|
+
const crossings = [], occlusions = [], off = [], strays = [], leaves = [];
|
|
14933
|
+
// <image> is a bespoke scene's illustration: it paints its whole box.
|
|
14934
|
+
const shapes = root.querySelectorAll("path, line, polyline, polygon, circle, ellipse, rect, image");
|
|
14935
|
+
// Repair geometry (bespoke scenes, on request): stroke points, filled boxes.
|
|
14936
|
+
const geoPoints = [], geoBoxes = [];
|
|
14937
|
+
const inset = 4;
|
|
14938
|
+
const inside = (p, t) => p.x > t.x + inset && p.x < t.x + t.w - inset && p.y > t.y + inset && p.y < t.y + t.h - inset;
|
|
14939
|
+
for (const g of shapes) {
|
|
14940
|
+
if (g.closest("defs, marker, clipPath, mask, pattern, symbol")) continue;
|
|
14941
|
+
const o = alpha(g);
|
|
14942
|
+
if (o <= 0.15) continue;
|
|
14943
|
+
const cs = getComputedStyle(g);
|
|
14944
|
+
const r = boxOf(g);
|
|
14945
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
14946
|
+
if (r.x < -2 || r.y < -2 || r.x + r.width > W + 2 || r.y + r.height > H + 2) off.push(name(g));
|
|
14947
|
+
// A shape that wholly contains a text box is that label's plate, not a collision.
|
|
14948
|
+
const plate = (t) => r.x <= t.x + 1 && r.y <= t.y + 1 && r.x + r.width >= t.x + t.w - 1 && r.y + r.height >= t.y + t.h - 1;
|
|
14949
|
+
const sw = parseFloat(cs.strokeWidth) || 0;
|
|
14950
|
+
const stroked = cs.stroke && cs.stroke !== "none" && sw > 0 && Number(cs.strokeOpacity) * o > 0.15;
|
|
14951
|
+
// A <line> has no inside, and a shape scaled flat paints no area, whatever its fill says.
|
|
14952
|
+
const filled = cs.fill && cs.fill !== "none" && Number(cs.fillOpacity) * o > 0.15 &&
|
|
14953
|
+
g.tagName.toLowerCase() !== "line" && r.width >= 1 && r.height >= 1;
|
|
14954
|
+
// A marker paints with the element's opacity, not its stroke's, and DrawSVG
|
|
14955
|
+
// draws by dashing: an arrowhead stays where it is while its line is drawn
|
|
14956
|
+
// to 0%, or half way \u2014 the "stray arrowhead" of an undrawn line.
|
|
14957
|
+
const markerStart = cs.markerStart && cs.markerStart !== "none";
|
|
14958
|
+
const markerEnd = cs.markerEnd && cs.markerEnd !== "none";
|
|
14959
|
+
const picture = g.tagName.toLowerCase() === "image";
|
|
14960
|
+
let paints = filled;
|
|
14961
|
+
if (!stroked && (markerStart || markerEnd)) strays.push(name(g) + " (its arrowhead shows, its line does not)");
|
|
14962
|
+
if (stroked && typeof g.getTotalLength !== "function") paints = true;
|
|
14963
|
+
if (stroked && typeof g.getTotalLength === "function") {
|
|
14964
|
+
let len = 0;
|
|
14965
|
+
try { len = g.getTotalLength(); } catch (e) { len = 0; }
|
|
14966
|
+
const m = g.getScreenCTM();
|
|
14967
|
+
if (len > 0 && m) {
|
|
14968
|
+
const dash = (cs.strokeDasharray && cs.strokeDasharray !== "none")
|
|
14969
|
+
? cs.strokeDasharray.split(/[ ,]+/).map(parseFloat).filter((v) => v >= 0)
|
|
14970
|
+
: [];
|
|
14971
|
+
const pattern = dash.length % 2 ? dash.concat(dash) : dash;
|
|
14972
|
+
const period = pattern.reduce((a, b) => a + b, 0);
|
|
14973
|
+
const offset = parseFloat(cs.strokeDashoffset) || 0;
|
|
14974
|
+
const visible = (d) => {
|
|
14975
|
+
if (!period) return true;
|
|
14976
|
+
let pos = ((d + offset) % period + period) % period;
|
|
14977
|
+
for (let k = 0; k < pattern.length; k++) {
|
|
14978
|
+
if (pos < pattern[k]) return k % 2 === 0;
|
|
14979
|
+
pos -= pattern[k];
|
|
14980
|
+
}
|
|
14981
|
+
return true;
|
|
14982
|
+
};
|
|
14983
|
+
const step = Math.max(4, len / 400);
|
|
14984
|
+
const hit = new Set();
|
|
14985
|
+
let drawn = 0;
|
|
14986
|
+
let kept = -1e9;
|
|
14987
|
+
for (let d = 0; d <= len; d += step) {
|
|
14988
|
+
if (!visible(d)) continue;
|
|
14989
|
+
drawn += step;
|
|
14990
|
+
const q = g.getPointAtLength(d);
|
|
14991
|
+
const p = { x: m.a * q.x + m.c * q.y + m.e, y: m.b * q.x + m.d * q.y + m.f };
|
|
14992
|
+
if (r.clip && (p.x < r.clip.x || p.y < r.clip.y || p.x > r.clip.x + r.clip.width || p.y > r.clip.y + r.clip.height)) continue;
|
|
14993
|
+
if (wantGeo && d - kept >= 8 && geoPoints.length < 6000) { kept = d; geoPoints.push({ el: g, x: p.x, y: p.y }); }
|
|
14994
|
+
// No plate exemption here: a long curve's bounding box contains half
|
|
14995
|
+
// the labels on a chart, and its stroke still runs through them.
|
|
14996
|
+
for (const t of texts) if (!hit.has(t) && inside(p, t)) hit.add(t);
|
|
14997
|
+
}
|
|
14998
|
+
for (const t of hit) crossings.push(name(g) + " through " + at(t));
|
|
14999
|
+
if (drawn > 2) paints = true;
|
|
15000
|
+
const edge = Math.min(2, len / 100);
|
|
15001
|
+
if (markerEnd && !visible(len - edge)) strays.push(name(g) + " (an arrowhead at the end of a line not drawn that far)");
|
|
15002
|
+
else if (markerStart && !visible(edge)) strays.push(name(g) + " (an arrowhead at the start of a line not drawn from there)");
|
|
15003
|
+
} else if (len > 0) paints = true;
|
|
15004
|
+
}
|
|
15005
|
+
if (paints) leaves.push({ el: g, x: r.x, y: r.y, w: r.width, h: r.height, o });
|
|
15006
|
+
if (wantGeo && filled) geoBoxes.push({ el: g, x: r.x, y: r.y, w: r.width, h: r.height });
|
|
15007
|
+
if (filled) {
|
|
15008
|
+
for (const t of texts) {
|
|
15009
|
+
// A picture over a label is never its plate: it covers it.
|
|
15010
|
+
if (plate(t) && !picture) continue;
|
|
15011
|
+
// Only a shape painted AFTER the text can cover it.
|
|
15012
|
+
if (!(t.el.compareDocumentPosition(g) & Node.DOCUMENT_POSITION_FOLLOWING)) continue;
|
|
15013
|
+
const ix = Math.min(r.x + r.width, t.x + t.w) - Math.max(r.x, t.x);
|
|
15014
|
+
const iy = Math.min(r.y + r.height, t.y + t.h) - Math.max(r.y, t.y);
|
|
15015
|
+
if (ix > inset && iy > inset && ix * iy > 0.15 * t.w * t.h) occlusions.push(name(g) + " over " + at(t));
|
|
15016
|
+
}
|
|
15017
|
+
}
|
|
15018
|
+
}
|
|
15019
|
+
for (const t of texts)
|
|
15020
|
+
if (t.x < -1 || t.y < -1 || t.x + t.w > W + 1 || t.y + t.h > H + 1) off.push(t.id);
|
|
15021
|
+
|
|
15022
|
+
// The body box a bespoke scene draws in. Absent on an archetype's scene, and
|
|
15023
|
+
// then there is no stage to fill and no cue group to reveal.
|
|
15024
|
+
const box = document.getElementById(sid + "-g");
|
|
15025
|
+
let fill, maxType, cells, groups, revealed, dimmed;
|
|
15026
|
+
if (box) {
|
|
15027
|
+
const b = box.getBoundingClientRect();
|
|
15028
|
+
const mine = texts.filter((t) => box.contains(t.el)).map((t) => ({ el: t.el, x: t.x, y: t.y, w: t.w, h: t.h }))
|
|
15029
|
+
.concat(leaves.filter((l) => box.contains(l.el)));
|
|
15030
|
+
// A backdrop the size of the box fills it by being there; it is not content.
|
|
15031
|
+
// A picture is content however big it is: it is what the stage shows.
|
|
15032
|
+
const content = mine.filter((l) => l.w * l.h < 0.85 * b.width * b.height || l.el.tagName.toLowerCase() === "image");
|
|
15033
|
+
const clip = (l) => {
|
|
15034
|
+
const x0 = Math.max(b.x, l.x), y0 = Math.max(b.y, l.y);
|
|
15035
|
+
const x1 = Math.min(b.x + b.width, l.x + l.w), y1 = Math.min(b.y + b.height, l.y + l.h);
|
|
15036
|
+
return x1 > x0 && y1 > y0 ? { x0, y0, x1, y1 } : null;
|
|
15037
|
+
};
|
|
15038
|
+
const parts = content.map(clip).filter(Boolean);
|
|
15039
|
+
if (parts.length && b.width > 0 && b.height > 0) {
|
|
15040
|
+
const u = parts.reduce((a, p) => ({ x0: Math.min(a.x0, p.x0), y0: Math.min(a.y0, p.y0), x1: Math.max(a.x1, p.x1), y1: Math.max(a.y1, p.y1) }));
|
|
15041
|
+
fill = ((u.x1 - u.x0) * (u.y1 - u.y0)) / (b.width * b.height);
|
|
15042
|
+
// How many of a 6x4 grid's cells something is drawn in: a bbox can be
|
|
15043
|
+
// stretched by one stray dot, a grid cannot.
|
|
15044
|
+
let n = 0;
|
|
15045
|
+
for (let cx = 0; cx < 6; cx++)
|
|
15046
|
+
for (let cy = 0; cy < 4; cy++) {
|
|
15047
|
+
const gx0 = b.x + (cx * b.width) / 6, gx1 = b.x + ((cx + 1) * b.width) / 6;
|
|
15048
|
+
const gy0 = b.y + (cy * b.height) / 4, gy1 = b.y + ((cy + 1) * b.height) / 4;
|
|
15049
|
+
if (parts.some((p) => p.x0 < gx1 && p.x1 > gx0 && p.y0 < gy1 && p.y1 > gy0)) n++;
|
|
15050
|
+
}
|
|
15051
|
+
cells = n / 24;
|
|
15052
|
+
} else fill = 0;
|
|
15053
|
+
maxType = texts.filter((t) => box.contains(t.el)).reduce((a, t) => Math.max(a, t.fs), 0);
|
|
15054
|
+
// How much of what is drawn is drawn dimmed (focus): at the end, a summary
|
|
15055
|
+
// that is mostly ghosts is not one.
|
|
15056
|
+
const shown = texts.filter((t) => box.contains(t.el)).concat(leaves.filter((l) => box.contains(l.el)));
|
|
15057
|
+
dimmed = shown.length ? shown.filter((x) => x.o < 0.6).length / shown.length : 0;
|
|
15058
|
+
// Cue groups: what the scene says each cue introduces, and which are showing now.
|
|
15059
|
+
const tagged = [...box.querySelectorAll("[data-cue]")];
|
|
15060
|
+
groups = [...new Set(tagged.map((e) => Number(e.getAttribute("data-cue"))))].sort((a, c) => a - c);
|
|
15061
|
+
const showing = (e) => texts.some((t) => e === t.el || e.contains(t.el)) || leaves.some((l) => e === l.el || e.contains(l.el));
|
|
15062
|
+
revealed = tagged.filter(showing).map((e) => ({ id: name(e), cue: Number(e.getAttribute("data-cue")) }));
|
|
15063
|
+
}
|
|
15064
|
+
// Where the camera is, for \`camera_end\`: the shell's wrapper, and a viewBox camera.
|
|
15065
|
+
let camOff;
|
|
15066
|
+
const cam = document.getElementById(sid + "-cam");
|
|
15067
|
+
if (cam) {
|
|
15068
|
+
const t = getComputedStyle(cam).transform;
|
|
15069
|
+
if (t && t !== "none") {
|
|
15070
|
+
const v = t.slice(t.indexOf("(") + 1, t.lastIndexOf(")")).split(",").map(Number);
|
|
15071
|
+
if (v.length === 6 && (Math.abs(v[0] - 1) > 0.01 || Math.abs(v[4]) > 2 || Math.abs(v[5]) > 2))
|
|
15072
|
+
camOff = "#" + sid + "-cam at scale " + v[0].toFixed(2) + ", x " + Math.round(v[4]) + ", y " + Math.round(v[5]);
|
|
15073
|
+
}
|
|
15074
|
+
}
|
|
15075
|
+
// Round 4: the shell's camera (scale, x, y and the box it moves), the
|
|
15076
|
+
// subjects of an illustrated scene, and how its labels sit on them.
|
|
15077
|
+
let shot, subjects, anchors, onPicture;
|
|
15078
|
+
if (cam && box) {
|
|
15079
|
+
const t = getComputedStyle(cam).transform;
|
|
15080
|
+
const v = t && t !== "none" ? t.slice(t.indexOf("(") + 1, t.lastIndexOf(")")).split(",").map(Number) : [1, 0, 0, 1, 0, 0];
|
|
15081
|
+
const s = v.length === 6 ? v[0] : 1;
|
|
15082
|
+
shot = [Math.round(s * 1000) / 1000, Math.round(v[4] || 0), Math.round(v[5] || 0), cam.offsetWidth, cam.offsetHeight];
|
|
15083
|
+
const subs = [...cam.querySelectorAll("[data-ds-subject]")].map((e) => ({ k: Number(e.getAttribute("data-ds-subject")), r: e.getBoundingClientRect() }));
|
|
15084
|
+
subjects = subs.length;
|
|
15085
|
+
if (subs.length) {
|
|
15086
|
+
const area = (r) => Math.max(0, r.width) * Math.max(0, r.height);
|
|
15087
|
+
anchors = [];
|
|
15088
|
+
for (const g of box.querySelectorAll("[data-subject]")) {
|
|
15089
|
+
const k = Number(g.getAttribute("data-subject"));
|
|
15090
|
+
const mine = texts.filter((x) => g === x.el || g.contains(x.el));
|
|
15091
|
+
if (!mine.length) continue;
|
|
15092
|
+
const x0 = Math.min(...mine.map((x) => x.x)), y0 = Math.min(...mine.map((x) => x.y));
|
|
15093
|
+
const x1 = Math.max(...mine.map((x) => x.x + x.w)), y1 = Math.max(...mine.map((x) => x.y + x.h));
|
|
15094
|
+
const lab = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
15095
|
+
const own = subs.find((q) => q.k === k);
|
|
15096
|
+
// Gap between the label and its subject, in the box's own px (camera-free).
|
|
15097
|
+
const gap = own ? Math.hypot(Math.max(0, own.r.x - x1, x0 - (own.r.x + own.r.width)), Math.max(0, own.r.y - y1, y0 - (own.r.y + own.r.height))) / s : 1e9;
|
|
15098
|
+
// The most of the label any OTHER subject's box covers (the part not also its own).
|
|
15099
|
+
let over = 0;
|
|
15100
|
+
for (const q of subs) {
|
|
15101
|
+
if (q.k === k) continue;
|
|
15102
|
+
const m = meet(lab, q.r);
|
|
15103
|
+
let a = area(m);
|
|
15104
|
+
if (own) a -= area(meet(m, own.r));
|
|
15105
|
+
over = Math.max(over, a / Math.max(1, area(lab)));
|
|
15106
|
+
}
|
|
15107
|
+
anchors.push({ id: g.id || "g:" + k, k, d: Math.round(gap), o: Math.round(over * 100) / 100 });
|
|
15108
|
+
}
|
|
15109
|
+
}
|
|
15110
|
+
// A table or chart painted over the picture: numbers sitting on the illustration.
|
|
15111
|
+
const pic = box.querySelector("image[data-art]");
|
|
15112
|
+
if (pic && alpha(pic) > 0.15) {
|
|
15113
|
+
const pr = boxOf(pic);
|
|
15114
|
+
onPicture = texts.filter((x) => box.contains(x.el) && /[0-9]/.test(x.el.textContent || "") &&
|
|
15115
|
+
x.x + x.w / 2 > pr.x && x.x + x.w / 2 < pr.x + pr.width && x.y + x.h / 2 > pr.y && x.y + x.h / 2 < pr.y + pr.height).length;
|
|
15116
|
+
}
|
|
15117
|
+
}
|
|
15118
|
+
const svg0 = document.getElementById(sid + "-svg");
|
|
15119
|
+
if (svg0 && !camOff) {
|
|
15120
|
+
// The contract's viewBox is "0 0 W H" with W, H the svg's own size.
|
|
15121
|
+
const vb = (svg0.getAttribute("viewBox") || "").trim().split(/[ ,]+/).map(Number);
|
|
15122
|
+
const w0 = Number(svg0.getAttribute("width")), h0 = Number(svg0.getAttribute("height"));
|
|
15123
|
+
if (vb.length === 4 && w0 > 0 && h0 > 0 && (Math.abs(vb[0]) > 1 || Math.abs(vb[1]) > 1 || Math.abs(vb[2] - w0) > 1 || Math.abs(vb[3] - h0) > 1))
|
|
15124
|
+
camOff = "#" + sid + "-svg viewBox " + vb.join(" ");
|
|
15125
|
+
}
|
|
15126
|
+
|
|
15127
|
+
// What the repair pass needs (bespoke scenes only): every element of the body
|
|
15128
|
+
// addressed as tag:index in document order \u2014 the markup's own order, which
|
|
15129
|
+
// is how a fix finds it again in the fragment \u2014 the labels with the unit a
|
|
15130
|
+
// nudge would move (the text, or the smallest <g> that holds it and its
|
|
15131
|
+
// plate and no other label), what is painted, and each part's opacity.
|
|
15132
|
+
let geo;
|
|
15133
|
+
if (box && wantGeo) {
|
|
15134
|
+
const index = new Map(), order = new Map();
|
|
15135
|
+
const n = {};
|
|
15136
|
+
let k = 0;
|
|
15137
|
+
for (const e of box.querySelectorAll("*")) {
|
|
15138
|
+
// Not the markup's: KaTeX's rendered spans, and the shell's camera wrapper.
|
|
15139
|
+
if (e.closest(".katex") || e.id === sid + "-cam") continue;
|
|
15140
|
+
const t = e.tagName.toLowerCase();
|
|
15141
|
+
n[t] = n[t] || 0;
|
|
15142
|
+
index.set(e, t + ":" + n[t]++);
|
|
15143
|
+
order.set(e, k++);
|
|
15144
|
+
}
|
|
15145
|
+
const ob = box.getBoundingClientRect();
|
|
15146
|
+
const rel = (x, y, w, h) => [x - ob.x, y - ob.y, w, h].map((v) => Math.round(v * 10) / 10);
|
|
15147
|
+
const unitOf = (el) => {
|
|
15148
|
+
let u = el;
|
|
15149
|
+
const tb = el.getBoundingClientRect();
|
|
15150
|
+
const area = Math.max(1, tb.width * tb.height);
|
|
15151
|
+
for (let p = el.parentElement; p && p !== box && p.tagName.toLowerCase() === "g" && box.contains(p); p = p.parentElement) {
|
|
15152
|
+
if (p.querySelectorAll("text").length !== 1) break;
|
|
15153
|
+
const pb = p.getBoundingClientRect();
|
|
15154
|
+
if (pb.width * pb.height > 4 * area) break;
|
|
15155
|
+
u = p;
|
|
15156
|
+
}
|
|
15157
|
+
return u;
|
|
15158
|
+
};
|
|
15159
|
+
const scaleOf = (u) => {
|
|
15160
|
+
const par = u.parentElement;
|
|
15161
|
+
const m = par && par.getScreenCTM ? par.getScreenCTM() : null;
|
|
15162
|
+
return m ? Math.hypot(m.a, m.b) || 1 : 1;
|
|
15163
|
+
};
|
|
15164
|
+
const units = [];
|
|
15165
|
+
const labels = texts.filter((t) => box.contains(t.el)).map((t) => {
|
|
15166
|
+
// The shell's own labels (round 4) are not the scene's to move: obstacles.
|
|
15167
|
+
const movable = t.el.tagName.toLowerCase() === "text" && !t.el.closest(".ds-callouts");
|
|
15168
|
+
const u = movable ? unitOf(t.el) : null;
|
|
15169
|
+
if (u) units.push(u);
|
|
15170
|
+
return { a: index.get(t.el) || "", u: u ? index.get(u) : null, o: order.get(t.el) ?? -1, b: rel(t.x, t.y, t.w, t.h), s: u ? Math.round(scaleOf(u) * 1000) / 1000 : 1, fs: Math.round(t.fs) };
|
|
15171
|
+
});
|
|
15172
|
+
const ownerOf = (el) => { for (const u of units) if (u === el || u.contains(el)) return index.get(u); return null; };
|
|
15173
|
+
const points = geoPoints.filter((p) => box.contains(p.el)).map((p) => [Math.round(p.x - ob.x), Math.round(p.y - ob.y), ownerOf(p.el)]);
|
|
15174
|
+
const boxes = geoBoxes.filter((b) => box.contains(b.el)).map((b) => ({ a: index.get(b.el) || "", u: ownerOf(b.el), o: order.get(b.el) ?? -1, b: rel(b.x, b.y, b.w, b.h) }));
|
|
15175
|
+
// Each visible part's opacity, and the elements above it (itself included)
|
|
15176
|
+
// that dim it: what a relight would have to bring back.
|
|
15177
|
+
const own = (e) => Number(getComputedStyle(e).opacity);
|
|
15178
|
+
const parts = texts.filter((t) => box.contains(t.el)).map((t) => ({ el: t.el, o: t.o }))
|
|
15179
|
+
.concat(leaves.filter((l) => box.contains(l.el)).map((l) => ({ el: l.el, o: l.o })))
|
|
15180
|
+
.map((p) => {
|
|
15181
|
+
const dim = [];
|
|
15182
|
+
if (p.o < 0.95)
|
|
15183
|
+
for (let e = p.el; e && e !== box; e = e.parentElement)
|
|
15184
|
+
if (index.has(e) && own(e) < 0.95) dim.push(index.get(e) + (e.id ? "#" + e.id : ""));
|
|
15185
|
+
return { a: index.get(p.el) || "", al: Math.round(p.o * 100) / 100, dim };
|
|
15186
|
+
});
|
|
15187
|
+
geo = { w: Math.round(ob.width), h: Math.round(ob.height), labels, boxes, points, parts };
|
|
15188
|
+
}
|
|
15189
|
+
return { crossings, occlusions, overlaps, small, off, strays, fill, cells, maxType, groups, revealed, dimmed, camOff, geo, shot, subjects, anchors, onPicture };
|
|
15190
|
+
}`;
|
|
15191
|
+
|
|
14275
15192
|
// src/verify/drift.ts
|
|
14276
15193
|
import { execFile as execFile3 } from "node:child_process";
|
|
14277
15194
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -14295,12 +15212,12 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
14295
15212
|
}
|
|
14296
15213
|
|
|
14297
15214
|
// src/render/render.ts
|
|
14298
|
-
import { mkdir as
|
|
14299
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
15215
|
+
import { mkdir as mkdir9, readFile as readFile11, rename as rename3, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
|
|
15216
|
+
import { basename as basename4, dirname as dirname3, join as join13, resolve as resolve6 } from "node:path";
|
|
14300
15217
|
|
|
14301
15218
|
// src/render/captions.ts
|
|
14302
|
-
import { mkdir as
|
|
14303
|
-
import { join as
|
|
15219
|
+
import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
|
|
15220
|
+
import { join as join12 } from "node:path";
|
|
14304
15221
|
import { pathToFileURL } from "node:url";
|
|
14305
15222
|
var DECK_FONT_CSS = "assets/fonts/fonts.css";
|
|
14306
15223
|
function cssString(value) {
|
|
@@ -14391,11 +15308,11 @@ async function captionBlocker() {
|
|
|
14391
15308
|
}
|
|
14392
15309
|
async function renderCaptions(cues, style, deck, work2) {
|
|
14393
15310
|
if (cues.length === 0) throw new Error("renderCaptions was given no cues.");
|
|
14394
|
-
const fontCss =
|
|
15311
|
+
const fontCss = join12(deck, DECK_FONT_CSS);
|
|
14395
15312
|
const href = await import("node:fs/promises").then((fs) => fs.stat(fontCss).catch(() => null)) ? pathToFileURL(fontCss).href : null;
|
|
14396
|
-
const page =
|
|
14397
|
-
await
|
|
14398
|
-
await
|
|
15313
|
+
const page = join12(work2, "captions.html");
|
|
15314
|
+
await mkdir8(work2, { recursive: true });
|
|
15315
|
+
await writeFile10(page, captionPage(cues, style, href));
|
|
14399
15316
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
14400
15317
|
const executablePath = await chromePath(CAPTION_NEED);
|
|
14401
15318
|
const browser = await launchOwnPage(
|
|
@@ -14416,7 +15333,7 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
14416
15333
|
const name = `cap${String(i).padStart(4, "0")}.png`;
|
|
14417
15334
|
await tab.evaluate((id2) => document.getElementById(id2)?.classList.add("on"), `c${i}`);
|
|
14418
15335
|
await tab.screenshot({
|
|
14419
|
-
path:
|
|
15336
|
+
path: join12(work2, name),
|
|
14420
15337
|
type: "png",
|
|
14421
15338
|
omitBackground: true,
|
|
14422
15339
|
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
@@ -14457,9 +15374,9 @@ function subtitlePlan(mode) {
|
|
|
14457
15374
|
async function render(opts) {
|
|
14458
15375
|
const log = opts.log ?? (() => {
|
|
14459
15376
|
});
|
|
14460
|
-
const deck =
|
|
14461
|
-
const out =
|
|
14462
|
-
const work2 =
|
|
15377
|
+
const deck = resolve6(opts.deck);
|
|
15378
|
+
const out = resolve6(opts.out);
|
|
15379
|
+
const work2 = join13(dirname3(out), `.${basename4(out)}.parts`);
|
|
14463
15380
|
const timing = await readTiming(deck);
|
|
14464
15381
|
if (opts.targetSeconds && !opts.allowFastPlayback) {
|
|
14465
15382
|
const refusal = playbackRefusal(
|
|
@@ -14479,9 +15396,9 @@ async function render(opts) {
|
|
|
14479
15396
|
if (blocker) throw new Error(`Cannot burn in captions: ${blocker}`);
|
|
14480
15397
|
}
|
|
14481
15398
|
const burnable = plan0.burn;
|
|
14482
|
-
await
|
|
15399
|
+
await mkdir9(work2, { recursive: true });
|
|
14483
15400
|
try {
|
|
14484
|
-
const raw2 = opts.video ?
|
|
15401
|
+
const raw2 = opts.video ? resolve6(opts.video) : await capture(deck, join13(work2, "raw.mp4"), opts, log);
|
|
14485
15402
|
const shot = await probe(raw2);
|
|
14486
15403
|
log(
|
|
14487
15404
|
`render: ${shot.frames} frames, ${shot.width}\xD7${shot.height}, ${shot.fps.toFixed(3)} fps, ${shot.seconds.toFixed(2)}s`
|
|
@@ -14494,24 +15411,24 @@ async function render(opts) {
|
|
|
14494
15411
|
);
|
|
14495
15412
|
}
|
|
14496
15413
|
const retimed = await retime(raw2, plan, work2, log);
|
|
14497
|
-
const srtPath =
|
|
15414
|
+
const srtPath = join13(dirname3(out), SRT_NAME(out));
|
|
14498
15415
|
const playback = opts.targetSeconds ? playbackFactor(plan.frames / shot.fps, opts.targetSeconds) : 1;
|
|
14499
15416
|
const cues = playback > 1 ? plan.cues.map((c) => scaleCue(c, playback)) : plan.cues;
|
|
14500
15417
|
const srt = plan0.sidecar ? toSrt(cues) : "";
|
|
14501
|
-
if (srt) await
|
|
15418
|
+
if (srt) await writeFile11(srtPath, srt);
|
|
14502
15419
|
const burn = burnable && srt.length > 0;
|
|
14503
15420
|
await mux(retimed, timing, plan, deck, out, work2, burn ? plan.cues : void 0, shot.fps, log);
|
|
14504
15421
|
if (playback > 1) {
|
|
14505
15422
|
log(`render: speeding playback ${playback}\xD7 to reach ${opts.targetSeconds}s`);
|
|
14506
15423
|
const warning = playbackWarning(playback, p95CueRate(plan.cues));
|
|
14507
15424
|
if (warning) log(`render: ${warning}`);
|
|
14508
|
-
const fast =
|
|
15425
|
+
const fast = join13(work2, `fast.${basename4(out)}`);
|
|
14509
15426
|
const muxed = await probe(out);
|
|
14510
15427
|
await runTool(
|
|
14511
15428
|
"ffmpeg",
|
|
14512
15429
|
respeedArgs(out, playback, tempoChain(playback), shot.fps, muxed.hasAudio, fast)
|
|
14513
15430
|
);
|
|
14514
|
-
await
|
|
15431
|
+
await rename3(fast, out);
|
|
14515
15432
|
}
|
|
14516
15433
|
const final = await probe(out);
|
|
14517
15434
|
if (plan.audio.length > 0 && !final.hasAudio) {
|
|
@@ -14535,8 +15452,8 @@ async function render(opts) {
|
|
|
14535
15452
|
}
|
|
14536
15453
|
}
|
|
14537
15454
|
async function readTiming(deck) {
|
|
14538
|
-
const path2 =
|
|
14539
|
-
const text2 = await
|
|
15455
|
+
const path2 = join13(deck, TIMING_FILE);
|
|
15456
|
+
const text2 = await readFile11(path2, "utf8").catch(() => {
|
|
14540
15457
|
throw new Error(
|
|
14541
15458
|
`${path2} is missing. \`render\` needs the timing manifest \`build\` writes; rebuild the deck.`
|
|
14542
15459
|
);
|
|
@@ -14575,20 +15492,20 @@ async function retime(raw2, plan, work2, log) {
|
|
|
14575
15492
|
if (plan.pieces.every((p) => p.freeze === 0)) return raw2;
|
|
14576
15493
|
const list = [];
|
|
14577
15494
|
for (const [i, piece] of plan.pieces.entries()) {
|
|
14578
|
-
const file =
|
|
15495
|
+
const file = join13(work2, `p${String(i).padStart(4, "0")}.ts`);
|
|
14579
15496
|
await runTool("ffmpeg", pieceArgs(raw2, piece.from, piece.motion, piece.freeze, plan.fps, file));
|
|
14580
15497
|
list.push(file);
|
|
14581
15498
|
if ((i + 1) % 10 === 0 || i === plan.pieces.length - 1) {
|
|
14582
15499
|
log(`render: retimed ${i + 1}/${plan.pieces.length} pieces`);
|
|
14583
15500
|
}
|
|
14584
15501
|
}
|
|
14585
|
-
const listFile =
|
|
14586
|
-
await
|
|
15502
|
+
const listFile = join13(work2, "pieces.txt");
|
|
15503
|
+
await writeFile11(
|
|
14587
15504
|
listFile,
|
|
14588
15505
|
`${list.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n")}
|
|
14589
15506
|
`
|
|
14590
15507
|
);
|
|
14591
|
-
const joined =
|
|
15508
|
+
const joined = join13(work2, "retimed.mp4");
|
|
14592
15509
|
await runTool("ffmpeg", [
|
|
14593
15510
|
"-y",
|
|
14594
15511
|
"-hide_banner",
|
|
@@ -14610,7 +15527,7 @@ async function retime(raw2, plan, work2, log) {
|
|
|
14610
15527
|
}
|
|
14611
15528
|
async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
14612
15529
|
const inputs = plan.audio.map((a) => ({
|
|
14613
|
-
file:
|
|
15530
|
+
file: join13(deck, timing.audioDir, a.audio),
|
|
14614
15531
|
delayMs: a.delayMs
|
|
14615
15532
|
}));
|
|
14616
15533
|
const args = ["-y", "-hide_banner", "-loglevel", "error", "-i", video];
|
|
@@ -14631,8 +15548,8 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
14631
15548
|
graph.push(audioGraph(inputs, plan.frames / fps, 1 + (band?.files.length ?? 0)));
|
|
14632
15549
|
}
|
|
14633
15550
|
if (graph.length > 0) {
|
|
14634
|
-
const script =
|
|
14635
|
-
await
|
|
15551
|
+
const script = join13(work2, "mux.filter");
|
|
15552
|
+
await writeFile11(script, `${graph.join(";\n")}
|
|
14636
15553
|
`);
|
|
14637
15554
|
args.push("-filter_complex_script", script);
|
|
14638
15555
|
}
|
|
@@ -14647,12 +15564,12 @@ async function mux(video, timing, plan, deck, out, work2, burnCues, fps, log) {
|
|
|
14647
15564
|
log(
|
|
14648
15565
|
`render: muxing ${inputs.length} segment(s)${burnCues ? " and burning in the captions" : ""} \u2192 ${out}`
|
|
14649
15566
|
);
|
|
14650
|
-
await
|
|
15567
|
+
await mkdir9(dirname3(out), { recursive: true });
|
|
14651
15568
|
await runTool("ffmpeg", args, { cwd: work2 });
|
|
14652
15569
|
}
|
|
14653
15570
|
|
|
14654
15571
|
// src/pack/pack.ts
|
|
14655
|
-
import { mkdir as
|
|
15572
|
+
import { mkdir as mkdir10, readFile as readFile12, writeFile as writeFile12 } from "node:fs/promises";
|
|
14656
15573
|
import { dirname as dirname4, extname as extname3 } from "node:path";
|
|
14657
15574
|
import { unzipSync, zipSync } from "fflate";
|
|
14658
15575
|
var MTIME = Date.UTC(1980, 0, 2, 12);
|
|
@@ -14692,8 +15609,8 @@ async function writePack(pack3, files, out) {
|
|
|
14692
15609
|
entries[path2] = STORED.has(extname3(path2).toLowerCase()) ? [bytes, { level: 0 }] : bytes;
|
|
14693
15610
|
}
|
|
14694
15611
|
const zip = zipSync(entries, { mtime: MTIME });
|
|
14695
|
-
await
|
|
14696
|
-
await
|
|
15612
|
+
await mkdir10(dirname4(out), { recursive: true });
|
|
15613
|
+
await writeFile12(out, zip);
|
|
14697
15614
|
return zip.length;
|
|
14698
15615
|
}
|
|
14699
15616
|
function check2(label, path2 = label) {
|
|
@@ -14705,6 +15622,325 @@ function check2(label, path2 = label) {
|
|
|
14705
15622
|
var PREF_KEYS = Object.keys(prefsSchema.shape);
|
|
14706
15623
|
var NARRATION_KEYS = Object.keys(prefsSchema.shape.narration.unwrap().shape);
|
|
14707
15624
|
var IMAGES_KEYS = Object.keys(prefsSchema.shape.images.unwrap().shape);
|
|
15625
|
+
var BESPOKE_KEYS = Object.keys(prefsSchema.shape.bespoke.unwrap().shape);
|
|
15626
|
+
|
|
15627
|
+
// src/bespoke/references.ts
|
|
15628
|
+
var TOKEN_Y = [110, 240, 370, 500, 630];
|
|
15629
|
+
var EXPERT_Y = [30, 206, 382, 558];
|
|
15630
|
+
var SCORES = [0.62, 0.21, 0.71, 0.09];
|
|
15631
|
+
var route = {
|
|
15632
|
+
name: "route",
|
|
15633
|
+
shows: "a mechanism that RUNS: a queue of tokens streams particles into a router, wires draw out to four experts, scores grow, the two chosen paths light while the rest dim (focus) and particles flow along them, and a big counter lands the consequence",
|
|
15634
|
+
fits: ["pipeline", "stack", "grid", "process", "flow", "cycle", "split-compare"],
|
|
15635
|
+
cues: [
|
|
15636
|
+
{ t0: 1, t1: 4.6, text: "Every token arrives at the router." },
|
|
15637
|
+
{ t0: 4.6, t1: 9, text: "The router scores all four experts," },
|
|
15638
|
+
{ t0: 9, t1: 13.4, text: "and sends each token to its top two." },
|
|
15639
|
+
{ t0: 13.4, t1: 17.8, text: "Only those run, so compute drops by half." }
|
|
15640
|
+
],
|
|
15641
|
+
duration: 19,
|
|
15642
|
+
fragment: {
|
|
15643
|
+
markup: `<svg id="SCENEID-svg" width="1700" height="732" viewBox="0 0 1700 732" style="position:absolute;left:0;top:0;overflow:visible">
|
|
15644
|
+
<g id="SCENEID-tokens" data-cue="1">
|
|
15645
|
+
<text id="SCENEID-tokens-label" x="130" y="34" font-size="52" fill="{{muted}}" text-anchor="middle" dominant-baseline="middle">Tokens</text>
|
|
15646
|
+
${TOKEN_Y.map((y, i) => ` <circle id="SCENEID-tok${i}" cx="130" cy="${y}" r="40" fill="{{${"abcda"[i]}}}"/>`).join("\n")}
|
|
15647
|
+
</g>
|
|
15648
|
+
<g id="SCENEID-stream" data-cue="1">
|
|
15649
|
+
${TOKEN_Y.map((y, i) => ` <circle id="SCENEID-s${i}" cx="130" cy="${y}" r="16" fill="{{${"abcda"[i]}}}"/>`).join("\n")}
|
|
15650
|
+
</g>
|
|
15651
|
+
<g id="SCENEID-wires" data-cue="2">
|
|
15652
|
+
${EXPERT_Y.map((y, i) => ` <line id="SCENEID-wire${i}" x1="890" y1="370" x2="1176" y2="${y + 72}" stroke="{{rule}}" stroke-width="7" stroke-linecap="round"/>`).join("\n")}
|
|
15653
|
+
</g>
|
|
15654
|
+
<g id="SCENEID-flow" data-cue="3">
|
|
15655
|
+
${[0, 1, 2, 3, 4, 5].map((i) => ` <circle id="SCENEID-p${i}" cx="890" cy="370" r="15" fill="{{accent}}"/>`).join("\n")}
|
|
15656
|
+
</g>
|
|
15657
|
+
<g id="SCENEID-router" data-cue="1">
|
|
15658
|
+
<circle id="SCENEID-disc-fill" cx="700" cy="370" r="190" fill="{{bg}}"/>
|
|
15659
|
+
<circle id="SCENEID-disc" cx="700" cy="370" r="190" fill="{{accent}}" fill-opacity="0.22" stroke="{{accent}}" stroke-width="7"/>
|
|
15660
|
+
<text id="SCENEID-router-label" x="700" y="370" font-size="80" font-weight="700" fill="{{fg}}" text-anchor="middle" dominant-baseline="middle">Router</text>
|
|
15661
|
+
</g>
|
|
15662
|
+
${EXPERT_Y.map(
|
|
15663
|
+
(y, i) => ` <g id="SCENEID-expert${i}" data-cue="2">
|
|
15664
|
+
<rect id="SCENEID-box${i}" x="1180" y="${y}" width="480" height="144" rx="22" fill="{{${"abcd"[i]}}}" fill-opacity="0.16" stroke="{{rule}}" stroke-width="5"/>
|
|
15665
|
+
<text id="SCENEID-name${i}" x="1224" y="${y + 56}" font-size="60" font-weight="600" fill="{{fg}}" dominant-baseline="middle">Expert ${i + 1}</text>
|
|
15666
|
+
<rect id="SCENEID-bar${i}" x="1224" y="${y + 100}" width="${Math.round(380 * SCORES[i])}" height="22" rx="11" fill="{{${"abcd"[i]}}}"/>
|
|
15667
|
+
</g>`
|
|
15668
|
+
).join("\n")}
|
|
15669
|
+
<g id="SCENEID-saving" data-cue="4">
|
|
15670
|
+
<text id="SCENEID-count" x="770" y="660" font-size="120" font-weight="800" fill="{{accent}}" text-anchor="end" dominant-baseline="middle">100</text>
|
|
15671
|
+
<text id="SCENEID-count-unit" x="786" y="668" font-size="60" fill="{{fg}}" dominant-baseline="middle">% compute</text>
|
|
15672
|
+
</g>
|
|
15673
|
+
</svg>`,
|
|
15674
|
+
css: "",
|
|
15675
|
+
script: `// Layout (box 1700x732): tokens x 90-170, y 10-670 \xB7 router disc 510-890 x 180-560 \xB7
|
|
15676
|
+
// wires 890-1176 \xB7 experts 1180-1660 x 30-702 \xB7 counter 560-1090 x 600-700.
|
|
15677
|
+
// Particles are drawn BEFORE the router so they slide under its disc (absorbed).
|
|
15678
|
+
var tokens = [], stream = [], ps = [];
|
|
15679
|
+
var ys = [110, 240, 370, 500, 630];
|
|
15680
|
+
for (var i = 0; i < 5; i++) {
|
|
15681
|
+
tokens.push("#SCENEID-tok" + i);
|
|
15682
|
+
stream.push("#SCENEID-s" + i);
|
|
15683
|
+
}
|
|
15684
|
+
for (var j = 0; j < 6; j++) ps.push("#SCENEID-p" + j);
|
|
15685
|
+
var experts = ["#SCENEID-expert0", "#SCENEID-expert1", "#SCENEID-expert2", "#SCENEID-expert3"];
|
|
15686
|
+
var wires = ["#SCENEID-wire0", "#SCENEID-wire1", "#SCENEID-wire2", "#SCENEID-wire3"];
|
|
15687
|
+
var bars = ["#SCENEID-bar0", "#SCENEID-bar1", "#SCENEID-bar2", "#SCENEID-bar3"];
|
|
15688
|
+
|
|
15689
|
+
// Baselines: every animated thing's start state, set once, before any tween.
|
|
15690
|
+
gsap.set(tokens, { scale: 0, transformOrigin: "50% 50%" });
|
|
15691
|
+
gsap.set("#SCENEID-tokens-label", { opacity: 0 });
|
|
15692
|
+
gsap.set(stream, { opacity: 0, x: 0, y: 0 });
|
|
15693
|
+
gsap.set(["#SCENEID-disc", "#SCENEID-disc-fill"], { scale: 0, transformOrigin: "50% 50%" });
|
|
15694
|
+
gsap.set("#SCENEID-router-label", { opacity: 0 });
|
|
15695
|
+
gsap.set(experts, { opacity: 0, x: 60 });
|
|
15696
|
+
gsap.set(wires, { drawSVG: "0% 0%", opacity: 0 });
|
|
15697
|
+
gsap.set(bars, { scaleX: 0, transformOrigin: "0% 50%" });
|
|
15698
|
+
gsap.set(ps, { opacity: 0, x: 0, y: 0 });
|
|
15699
|
+
gsap.set("#SCENEID-saving", { opacity: 0, y: 30 });
|
|
15700
|
+
|
|
15701
|
+
// C1 1.0-4.6 "Every token arrives at the router." \u2014 the queue pops in, then streams into the router.
|
|
15702
|
+
tl.to("#SCENEID-tokens-label", { opacity: 1, duration: 0.4 }, 1.0);
|
|
15703
|
+
tl.to(tokens, { scale: 1, duration: 0.5, ease: "back.out(2)", stagger: 0.08 }, 1.05);
|
|
15704
|
+
tl.to(["#SCENEID-disc", "#SCENEID-disc-fill"], { scale: 1, duration: 0.7, ease: "expo.out" }, 1.4);
|
|
15705
|
+
tl.to("#SCENEID-router-label", { opacity: 1, duration: 0.4 }, 1.8);
|
|
15706
|
+
for (var k = 0; k < 5; k++) {
|
|
15707
|
+
tl.to(stream[k], { opacity: 1, duration: 0.1 }, 2.2 + k * 0.15);
|
|
15708
|
+
tl.to(stream[k], { x: 440, y: 370 - ys[k], duration: 0.9, ease: "power2.in", repeat: 2 }, 2.2 + k * 0.15);
|
|
15709
|
+
tl.to(stream[k], { opacity: 0, duration: 0.1 }, 4.9 + k * 0.15);
|
|
15710
|
+
}
|
|
15711
|
+
|
|
15712
|
+
// C2 4.6-9.0 "The router scores all four experts," \u2014 experts slide in, wires draw, scores grow.
|
|
15713
|
+
tl.to(experts, { opacity: 1, x: 0, duration: 0.6, ease: "power3.out", stagger: 0.15 }, 4.7);
|
|
15714
|
+
tl.to(wires, { opacity: 1, duration: 0.01, stagger: 0.2 }, 5.0);
|
|
15715
|
+
tl.to(wires, { drawSVG: "0% 100%", duration: 0.7, ease: "power2.inOut", stagger: 0.2 }, 5.0);
|
|
15716
|
+
tl.to(bars, { scaleX: 1, duration: 0.9, ease: "power2.out", stagger: 0.3 }, 6.4);
|
|
15717
|
+
tl.to("#SCENEID-disc", { scale: 1.05, duration: 0.35, yoyo: true, repeat: 3, ease: "sine.inOut" }, 7.6);
|
|
15718
|
+
|
|
15719
|
+
// C3 9.0-13.4 "and sends each token to its top two." \u2014 focus: the two winners light, the rest dim; particles flow.
|
|
15720
|
+
tl.to(["#SCENEID-expert1", "#SCENEID-expert3", "#SCENEID-wire1", "#SCENEID-wire3"], { opacity: 0.25, duration: 0.5 }, 9.1);
|
|
15721
|
+
tl.to(["#SCENEID-wire0", "#SCENEID-wire2"], { stroke: "{{accent}}", strokeWidth: 12, duration: 0.5 }, 9.1);
|
|
15722
|
+
var dest = [[286, -268], [286, 84]];
|
|
15723
|
+
for (var n = 0; n < 6; n++) {
|
|
15724
|
+
var d = dest[n % 2];
|
|
15725
|
+
tl.to(ps[n], { opacity: 1, duration: 0.15 }, 9.6 + n * 0.35);
|
|
15726
|
+
tl.to(ps[n], { x: d[0], y: d[1], duration: 0.9, ease: "none", repeat: 2 }, 9.6 + n * 0.35);
|
|
15727
|
+
tl.to(ps[n], { opacity: 0, duration: 0.2 }, 13.1);
|
|
15728
|
+
}
|
|
15729
|
+
|
|
15730
|
+
// C4 13.4-17.8 "Only those run, so compute drops by half." \u2014 the consequence, as a number that moves.
|
|
15731
|
+
tl.to("#SCENEID-saving", { opacity: 1, y: 0, duration: 0.5, ease: "power3.out" }, 13.5);
|
|
15732
|
+
tl.to("#SCENEID-count", { textContent: 50, snap: { textContent: 1 }, duration: 1.8, ease: "power2.inOut" }, 13.7);
|
|
15733
|
+
tl.to(["#SCENEID-box0", "#SCENEID-box2"], { stroke: "{{accent}}", fillOpacity: 0.32, duration: 0.5 }, 13.7);
|
|
15734
|
+
tl.to(["#SCENEID-bar0", "#SCENEID-bar2"], { opacity: 0.55, duration: 0.45, yoyo: true, repeat: 3, ease: "sine.inOut" }, 15.6);
|
|
15735
|
+
// The end frame is the summary: everything back at full strength; the winners keep their accent.
|
|
15736
|
+
tl.to(["#SCENEID-expert1", "#SCENEID-expert3", "#SCENEID-wire1", "#SCENEID-wire3"], { opacity: 1, duration: 0.5 }, 16.6);`
|
|
15737
|
+
}
|
|
15738
|
+
};
|
|
15739
|
+
var SLABS = Array.from({ length: 12 }, (_, i) => 70 + i * 132);
|
|
15740
|
+
var INSIDE = "translate(634.5 259.5) scale(0.5)";
|
|
15741
|
+
var zoom = {
|
|
15742
|
+
name: "zoom",
|
|
15743
|
+
shows: "structure then detail: a staggered build of the whole, a CAMERA zoom (viewBox tween) into one part whose inside is drawn at its own scale, a draw that explains it, and a zoom back out where a pulse runs through every part",
|
|
15744
|
+
fits: ["stack", "grid", "annotated-figure", "claim-figure", "layers", "pipeline"],
|
|
15745
|
+
cues: [
|
|
15746
|
+
{ t0: 1, t1: 4.8, text: "The model is a stack of twelve identical blocks." },
|
|
15747
|
+
{ t0: 4.8, t1: 9.2, text: "Inside each one, attention" },
|
|
15748
|
+
{ t0: 9.2, t1: 13.6, text: "lets every token read from every other." },
|
|
15749
|
+
{ t0: 13.6, t1: 18, text: "Then the next block does it again, twelve times over." }
|
|
15750
|
+
],
|
|
15751
|
+
duration: 19.2,
|
|
15752
|
+
fragment: {
|
|
15753
|
+
markup: `<svg id="SCENEID-svg" width="1700" height="732" viewBox="0 0 1700 732" style="position:absolute;left:0;top:0;overflow:hidden">
|
|
15754
|
+
<g id="SCENEID-stack" data-cue="1">
|
|
15755
|
+
${SLABS.map(
|
|
15756
|
+
(x, i) => ` <rect id="SCENEID-slab${i}" x="${x}" y="150" width="104" height="440" rx="18" fill="{{panel}}" stroke="{{${i === 5 ? "accent" : "rule"}}}" stroke-width="5"/>`
|
|
15757
|
+
).join("\n")}
|
|
15758
|
+
</g>
|
|
15759
|
+
<g id="SCENEID-count" data-cue="1">
|
|
15760
|
+
<text id="SCENEID-times" x="850" y="60" font-size="96" font-weight="700" fill="{{fg}}" text-anchor="middle" dominant-baseline="middle">\xD7 12</text>
|
|
15761
|
+
<text id="SCENEID-in" x="122" y="664" font-size="48" fill="{{muted}}" text-anchor="middle" dominant-baseline="middle">input</text>
|
|
15762
|
+
<text id="SCENEID-out" x="1574" y="664" font-size="48" fill="{{muted}}" text-anchor="middle" dominant-baseline="middle">output</text>
|
|
15763
|
+
</g>
|
|
15764
|
+
<g id="SCENEID-inside" data-cue="2" transform="${INSIDE}">
|
|
15765
|
+
<text id="SCENEID-att" x="295" y="40" font-size="64" font-weight="700" fill="{{accent}}" text-anchor="middle" dominant-baseline="middle">Attention</text>
|
|
15766
|
+
${[0, 1, 2, 3, 4].map((i) => ` <circle id="SCENEID-q${i}" cx="${55 + i * 120}" cy="150" r="34" fill="{{${"abcda"[i]}}}"/>`).join("\n")}
|
|
15767
|
+
${[0, 1, 2, 3, 4].map((i) => ` <circle id="SCENEID-k${i}" cx="${55 + i * 120}" cy="400" r="34" fill="{{${"abcda"[i]}}}"/>`).join("\n")}
|
|
15768
|
+
</g>
|
|
15769
|
+
<g id="SCENEID-links" data-cue="3" transform="${INSIDE}">
|
|
15770
|
+
${[0, 1, 2, 3, 4].flatMap(
|
|
15771
|
+
(a) => [0, 1, 2, 3, 4].map(
|
|
15772
|
+
(b) => ` <line id="SCENEID-l${a}${b}" x1="${55 + a * 120}" y1="186" x2="${55 + b * 120}" y2="364" stroke="{{accent}}" stroke-width="${a === 2 ? 7 : 4}" stroke-linecap="round"/>`
|
|
15773
|
+
)
|
|
15774
|
+
).join("\n")}
|
|
15775
|
+
</g>
|
|
15776
|
+
<g id="SCENEID-pulse" data-cue="4">
|
|
15777
|
+
${SLABS.map((x, i) => ` <rect id="SCENEID-glow${i}" x="${x}" y="150" width="104" height="440" rx="18" fill="{{accent}}"/>`).join("\n")}
|
|
15778
|
+
</g>
|
|
15779
|
+
</svg>`,
|
|
15780
|
+
css: "",
|
|
15781
|
+
script: `// Layout (box 1700x732): "\xD7 12" centre top \xB7 12 slabs x 70-1626, y 150-590 \xB7 input/output
|
|
15782
|
+
// under the ends. Block 6 (x 730-834) has an inside drawn in its own units at half scale
|
|
15783
|
+
// (x 645-919, y 263-477): only legible with the camera in (viewBox 482 240.8 600 258.4,
|
|
15784
|
+
// 2.83x), so it shows only then. overflow:hidden on the svg keeps the zoomed view in its box.
|
|
15785
|
+
var slabs = [], glows = [];
|
|
15786
|
+
for (var i = 0; i < 12; i++) {
|
|
15787
|
+
slabs.push("#SCENEID-slab" + i);
|
|
15788
|
+
glows.push("#SCENEID-glow" + i);
|
|
15789
|
+
}
|
|
15790
|
+
var links = root.querySelectorAll("#SCENEID-links line");
|
|
15791
|
+
var row3 = ["#SCENEID-l20", "#SCENEID-l21", "#SCENEID-l22", "#SCENEID-l23", "#SCENEID-l24"];
|
|
15792
|
+
var labels = ["#SCENEID-times", "#SCENEID-in", "#SCENEID-out"];
|
|
15793
|
+
var wide = "0 0 1700 732";
|
|
15794
|
+
var close = "482 240.8 600 258.4";
|
|
15795
|
+
|
|
15796
|
+
gsap.set("#SCENEID-svg", { attr: { viewBox: wide } });
|
|
15797
|
+
gsap.set(slabs, { scaleY: 0, transformOrigin: "50% 100%" });
|
|
15798
|
+
gsap.set(labels, { opacity: 0 });
|
|
15799
|
+
gsap.set("#SCENEID-inside", { opacity: 0 });
|
|
15800
|
+
gsap.set(links, { drawSVG: "0% 0%", opacity: 0 });
|
|
15801
|
+
gsap.set(glows, { opacity: 0 });
|
|
15802
|
+
|
|
15803
|
+
// C1 1.0-4.8 "a stack of twelve identical blocks." \u2014 a staggered build, then the count lands.
|
|
15804
|
+
tl.to(slabs, { scaleY: 1, duration: 0.6, ease: "expo.out", stagger: 0.12 }, 1.0);
|
|
15805
|
+
tl.to(["#SCENEID-in", "#SCENEID-out"], { opacity: 1, duration: 0.4 }, 2.4);
|
|
15806
|
+
tl.to("#SCENEID-times", { opacity: 1, duration: 0.5 }, 2.9);
|
|
15807
|
+
tl.to(slabs, { y: -10, duration: 0.3, yoyo: true, repeat: 1, ease: "sine.inOut", stagger: 0.08 }, 3.4);
|
|
15808
|
+
|
|
15809
|
+
// C2 4.8-9.2 "Inside each one, attention" \u2014 the camera flies into block 6; the others become ghosts.
|
|
15810
|
+
tl.to(labels, { opacity: 0, duration: 0.3 }, 4.85);
|
|
15811
|
+
tl.to("#SCENEID-svg", { attr: { viewBox: close }, duration: 1.4, ease: "power3.inOut" }, 4.9);
|
|
15812
|
+
tl.to(slabs, { opacity: 0.15, duration: 0.5 }, 5.9);
|
|
15813
|
+
tl.to("#SCENEID-inside", { opacity: 1, duration: 0.5 }, 6.2);
|
|
15814
|
+
tl.to("#SCENEID-att", { scale: 1.08, transformOrigin: "50% 50%", duration: 0.4, yoyo: true, repeat: 3, ease: "sine.inOut" }, 7.2);
|
|
15815
|
+
|
|
15816
|
+
// C3 9.2-13.6 "lets every token read from every other." \u2014 every link draws; one token's reads are the focus.
|
|
15817
|
+
tl.to(links, { opacity: 0.35, duration: 0.01 }, 9.25);
|
|
15818
|
+
tl.to(links, { drawSVG: "0% 100%", duration: 0.6, ease: "power2.out", stagger: 0.06 }, 9.3);
|
|
15819
|
+
tl.to(row3, { opacity: 1, duration: 0.4 }, 11.4);
|
|
15820
|
+
tl.to("#SCENEID-q2", { scale: 1.3, transformOrigin: "50% 50%", duration: 0.4, yoyo: true, repeat: 3 }, 11.6);
|
|
15821
|
+
|
|
15822
|
+
// C4 13.6-18.0 "the next block does it again, twelve times over." \u2014 out to the whole; block after block lights.
|
|
15823
|
+
tl.to(["#SCENEID-inside", "#SCENEID-links"], { opacity: 0, duration: 0.3 }, 13.65);
|
|
15824
|
+
tl.to(slabs, { opacity: 1, duration: 0.4 }, 13.9);
|
|
15825
|
+
tl.to("#SCENEID-svg", { attr: { viewBox: wide }, duration: 1.3, ease: "power3.inOut" }, 13.9);
|
|
15826
|
+
tl.to(labels, { opacity: 1, duration: 0.4 }, 15.0);
|
|
15827
|
+
tl.to(glows, { opacity: 0.5, duration: 0.3, ease: "power2.out", stagger: 0.14 }, 15.3);`
|
|
15828
|
+
}
|
|
15829
|
+
};
|
|
15830
|
+
var SPREAD = Array.from({ length: 24 }, (_, i) => {
|
|
15831
|
+
const h = (n3) => {
|
|
15832
|
+
const s = Math.sin(n3 * 12.9898) * 43758.5453;
|
|
15833
|
+
return s - Math.floor(s);
|
|
15834
|
+
};
|
|
15835
|
+
return { x: Math.round(160 + h(i + 1) * 1380), y: Math.round(210 + h(i + 31) * 380) };
|
|
15836
|
+
});
|
|
15837
|
+
var BELL = (() => {
|
|
15838
|
+
const counts = [1, 2, 3, 4, 4, 4, 3, 2, 1];
|
|
15839
|
+
const slots2 = [];
|
|
15840
|
+
counts.forEach((c, b) => {
|
|
15841
|
+
for (let k = 0; k < c; k++) slots2.push([850 + (b - 4) * 150, 596 - k * 68]);
|
|
15842
|
+
});
|
|
15843
|
+
const byX = SPREAD.map((p, i) => ({ i, x: p.x })).sort((a, b) => a.x - b.x);
|
|
15844
|
+
const to = new Array(24);
|
|
15845
|
+
byX.forEach((d, rank) => {
|
|
15846
|
+
to[d.i] = slots2[rank];
|
|
15847
|
+
});
|
|
15848
|
+
return to;
|
|
15849
|
+
})();
|
|
15850
|
+
var OUTLIERS = [0, 7, 14, 21];
|
|
15851
|
+
var gather = {
|
|
15852
|
+
name: "gather",
|
|
15853
|
+
shows: "one set of objects keeps its identity through a change of state: a scatter whose outliers are singled out, then every dot TRAVELS (staggered) into a bell on a shared axis, a band marks one standard deviation and a counter measures the spread shrinking",
|
|
15854
|
+
fits: ["split-compare", "bar-compare", "grid", "claim-figure", "definition", "concept"],
|
|
15855
|
+
cues: [
|
|
15856
|
+
{ t0: 1, t1: 4.8, text: "Without normalization, activations spread wide," },
|
|
15857
|
+
{ t0: 4.8, t1: 9, text: "and a few outliers dominate every update." },
|
|
15858
|
+
{ t0: 9, t1: 13.4, text: "Normalizing reshapes them onto one standard scale," },
|
|
15859
|
+
{ t0: 13.4, t1: 17.6, text: "cutting the spread from three point two to one." }
|
|
15860
|
+
],
|
|
15861
|
+
duration: 18.8,
|
|
15862
|
+
fragment: {
|
|
15863
|
+
markup: `<svg id="SCENEID-svg" width="1700" height="732" viewBox="0 0 1700 732" style="position:absolute;left:0;top:0;overflow:visible">
|
|
15864
|
+
<g id="SCENEID-scale" data-cue="1">
|
|
15865
|
+
<line id="SCENEID-axis" x1="120" y1="652" x2="1580" y2="652" stroke="{{muted}}" stroke-width="5" stroke-linecap="round"/>
|
|
15866
|
+
<text id="SCENEID-lo" x="120" y="702" font-size="48" fill="{{muted}}" text-anchor="middle" dominant-baseline="middle">\u22123\u03C3</text>
|
|
15867
|
+
<text id="SCENEID-mid" x="850" y="702" font-size="48" fill="{{muted}}" text-anchor="middle" dominant-baseline="middle">0</text>
|
|
15868
|
+
<text id="SCENEID-hi" x="1580" y="702" font-size="48" fill="{{muted}}" text-anchor="middle" dominant-baseline="middle">+3\u03C3</text>
|
|
15869
|
+
<text id="SCENEID-sigma" x="850" y="60" font-size="96" font-weight="700" fill="{{fg}}" text-anchor="end" dominant-baseline="middle">\u03C3 =</text>
|
|
15870
|
+
<text id="SCENEID-sigma-n" x="880" y="60" font-size="96" font-weight="700" fill="{{fg}}" dominant-baseline="middle">3.2</text>
|
|
15871
|
+
</g>
|
|
15872
|
+
<g id="SCENEID-band" data-cue="4">
|
|
15873
|
+
<rect id="SCENEID-band-fill" x="700" y="130" width="300" height="500" rx="16" fill="{{accent}}" fill-opacity="0.16"/>
|
|
15874
|
+
</g>
|
|
15875
|
+
<g id="SCENEID-dots" data-cue="1">
|
|
15876
|
+
${SPREAD.map((p, i) => ` <circle id="SCENEID-d${i}" cx="${p.x}" cy="${p.y}" r="${OUTLIERS.includes(i) ? 36 : 26}" fill="{{${OUTLIERS.includes(i) ? "d" : "a"}}}"/>`).join("\n")}
|
|
15877
|
+
</g>
|
|
15878
|
+
<g id="SCENEID-outliers" data-cue="2">
|
|
15879
|
+
${OUTLIERS.map((i, k) => ` <circle id="SCENEID-ring${k}" cx="${SPREAD[i]?.x}" cy="${SPREAD[i]?.y}" r="54" fill="none" stroke="{{d}}" stroke-width="6"/>`).join("\n")}
|
|
15880
|
+
<text id="SCENEID-outlier-label" x="1600" y="60" font-size="52" fill="{{d}}" text-anchor="end" dominant-baseline="middle">outliers</text>
|
|
15881
|
+
</g>
|
|
15882
|
+
<g id="SCENEID-norm" data-cue="3">
|
|
15883
|
+
<text id="SCENEID-norm-label" x="200" y="60" font-size="52" fill="{{accent}}" dominant-baseline="middle">normalized</text>
|
|
15884
|
+
</g>
|
|
15885
|
+
<g id="SCENEID-band-tag" data-cue="4">
|
|
15886
|
+
<text id="SCENEID-band-label" x="850" y="176" font-size="64" font-weight="700" fill="{{accent}}" text-anchor="middle" dominant-baseline="middle">\xB11\u03C3</text>
|
|
15887
|
+
</g>
|
|
15888
|
+
</svg>`,
|
|
15889
|
+
css: "",
|
|
15890
|
+
script: `// Layout (box 1700x732): \u03C3 readout top centre \xB7 scatter x 150-1550, y 200-600 \xB7 axis at
|
|
15891
|
+
// y 652 (x 120-1580) with \u22123\u03C3/0/+3\u03C3 under it. The same 24 dots travel into a bell of
|
|
15892
|
+
// 9 bins on that axis (x 250-1450, stacks up from y 600) \u2014 one set changing state,
|
|
15893
|
+
// not two pictures \u2014 and a \xB11\u03C3 band (x 700-1000) lands behind them.
|
|
15894
|
+
var dots = [], rest = [], rings = [];
|
|
15895
|
+
for (var i = 0; i < 24; i++) {
|
|
15896
|
+
dots.push("#SCENEID-d" + i);
|
|
15897
|
+
if (i % 7 !== 0) rest.push("#SCENEID-d" + i);
|
|
15898
|
+
}
|
|
15899
|
+
for (var k = 0; k < 4; k++) rings.push("#SCENEID-ring" + k);
|
|
15900
|
+
var from = ${JSON.stringify(SPREAD.map((p) => [p.x, p.y]))};
|
|
15901
|
+
var to = ${JSON.stringify(BELL)};
|
|
15902
|
+
|
|
15903
|
+
gsap.set(dots, { scale: 0, transformOrigin: "50% 50%", x: 0, y: 0 });
|
|
15904
|
+
gsap.set("#SCENEID-axis", { drawSVG: "50% 50%" });
|
|
15905
|
+
gsap.set(["#SCENEID-lo", "#SCENEID-mid", "#SCENEID-hi", "#SCENEID-sigma", "#SCENEID-sigma-n"], { opacity: 0 });
|
|
15906
|
+
gsap.set(rings, { drawSVG: "0% 0%", opacity: 0 });
|
|
15907
|
+
gsap.set("#SCENEID-outlier-label", { opacity: 0 });
|
|
15908
|
+
gsap.set("#SCENEID-norm-label", { opacity: 0, x: -40 });
|
|
15909
|
+
gsap.set("#SCENEID-band-fill", { scaleX: 0, transformOrigin: "50% 50%" });
|
|
15910
|
+
gsap.set("#SCENEID-band-label", { opacity: 0 });
|
|
15911
|
+
|
|
15912
|
+
// C1 1.0-4.8 "activations spread wide," \u2014 the scatter lands, the scale opens under it.
|
|
15913
|
+
tl.to(dots, { scale: 1, duration: 0.45, ease: "back.out(2)", stagger: 0.06 }, 1.0);
|
|
15914
|
+
tl.to("#SCENEID-axis", { drawSVG: "0% 100%", duration: 1.1, ease: "power3.out" }, 2.4);
|
|
15915
|
+
tl.to(["#SCENEID-lo", "#SCENEID-mid", "#SCENEID-hi"], { opacity: 1, duration: 0.4 }, 2.9);
|
|
15916
|
+
tl.to(["#SCENEID-sigma", "#SCENEID-sigma-n"], { opacity: 1, duration: 0.4 }, 3.1);
|
|
15917
|
+
tl.to(dots, { y: -8, duration: 0.5, yoyo: true, repeat: 1, ease: "sine.inOut", stagger: 0.03 }, 3.6);
|
|
15918
|
+
|
|
15919
|
+
// C2 4.8-9.0 "a few outliers dominate every update." \u2014 focus: rings draw on the four, the rest dim.
|
|
15920
|
+
tl.to(rings, { opacity: 1, duration: 0.01, stagger: 0.2 }, 4.9);
|
|
15921
|
+
tl.to(rings, { drawSVG: "0% 100%", duration: 0.6, ease: "power2.out", stagger: 0.2 }, 4.9);
|
|
15922
|
+
tl.to("#SCENEID-outlier-label", { opacity: 1, duration: 0.4 }, 5.6);
|
|
15923
|
+
tl.to(rest, { opacity: 0.3, duration: 0.5 }, 6.0);
|
|
15924
|
+
tl.to(rings, { scale: 1.15, transformOrigin: "50% 50%", duration: 0.4, yoyo: true, repeat: 3, ease: "sine.inOut" }, 6.8);
|
|
15925
|
+
|
|
15926
|
+
// C3 9.0-13.4 "Normalizing reshapes them onto one standard scale," \u2014 every dot travels into the bell.
|
|
15927
|
+
tl.to(rings.concat(["#SCENEID-outlier-label"]), { opacity: 0, duration: 0.4 }, 9.05);
|
|
15928
|
+
tl.to(rest, { opacity: 1, duration: 0.4 }, 9.1);
|
|
15929
|
+
tl.to("#SCENEID-norm-label", { opacity: 1, x: 0, duration: 0.5, ease: "power3.out" }, 9.2);
|
|
15930
|
+
for (var n = 0; n < 24; n++) {
|
|
15931
|
+
tl.to(dots[n], { x: to[n][0] - from[n][0], y: to[n][1] - from[n][1], attr: { r: 30 }, fill: "{{accent}}", duration: 1.2, ease: "power3.inOut" }, 9.5 + n * 0.07);
|
|
15932
|
+
}
|
|
15933
|
+
|
|
15934
|
+
// C4 13.4-17.6 "cutting the spread from three point two to one." \u2014 the band opens, the number follows it.
|
|
15935
|
+
tl.to("#SCENEID-band-fill", { scaleX: 1, duration: 0.9, ease: "power3.out" }, 13.5);
|
|
15936
|
+
tl.to("#SCENEID-band-label", { opacity: 1, duration: 0.4 }, 14.1);
|
|
15937
|
+
tl.to("#SCENEID-sigma-n", { textContent: 1, snap: { textContent: 0.1 }, fill: "{{accent}}", duration: 1.4, ease: "power3.inOut" }, 13.6);
|
|
15938
|
+
tl.to(dots, { scale: 1.12, duration: 0.35, yoyo: true, repeat: 3, ease: "sine.inOut", stagger: 0.02 }, 15.3);`
|
|
15939
|
+
}
|
|
15940
|
+
};
|
|
15941
|
+
|
|
15942
|
+
// src/bespoke/untangle.ts
|
|
15943
|
+
import { parse as parse2 } from "acorn";
|
|
14708
15944
|
|
|
14709
15945
|
// src/index.ts
|
|
14710
15946
|
async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
@@ -14712,8 +15948,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
14712
15948
|
if (!format) throw new Error("no deck-16x9 format");
|
|
14713
15949
|
const step = opts.onStep ?? (() => {
|
|
14714
15950
|
});
|
|
14715
|
-
const out =
|
|
14716
|
-
await
|
|
15951
|
+
const out = resolve7(outDir);
|
|
15952
|
+
await mkdir11(out, { recursive: true });
|
|
14717
15953
|
const speed = opts.speed ?? 1;
|
|
14718
15954
|
const design2 = opts.design ?? storyboard.design ?? "classic";
|
|
14719
15955
|
const theme = pickTheme(storyboard, source, format, {
|
|
@@ -14732,12 +15968,13 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
14732
15968
|
...opts.narration ? { narration: opts.narration } : {},
|
|
14733
15969
|
...opts.onBeatError ? { onBeatError: opts.onBeatError } : {},
|
|
14734
15970
|
...opts.onBeatWarning ? { onBeatWarning: opts.onBeatWarning } : {},
|
|
14735
|
-
...fontCss ? { fontCss } : {}
|
|
15971
|
+
...fontCss ? { fontCss } : {},
|
|
15972
|
+
...opts.bespoke ? { bespoke: opts.bespoke } : {}
|
|
14736
15973
|
});
|
|
14737
15974
|
const files = [];
|
|
14738
15975
|
const write = async (name, text2) => {
|
|
14739
|
-
await
|
|
14740
|
-
files.push(
|
|
15976
|
+
await writeFile13(join14(out, name), text2);
|
|
15977
|
+
files.push(join14(out, name));
|
|
14741
15978
|
};
|
|
14742
15979
|
await write("index.html", deck.composition);
|
|
14743
15980
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
@@ -14756,7 +15993,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
14756
15993
|
beats: deck.cut.kept,
|
|
14757
15994
|
speed,
|
|
14758
15995
|
...opts.narration ? { narration: opts.narration } : {},
|
|
14759
|
-
...opts.theme ? { theme: opts.theme } : {}
|
|
15996
|
+
...opts.theme ? { theme: opts.theme } : {},
|
|
15997
|
+
...opts.bespoke ? { bespoke: opts.bespoke } : {}
|
|
14760
15998
|
});
|
|
14761
15999
|
await write(TIMING_FILE, `${JSON.stringify(timing, null, 2)}
|
|
14762
16000
|
`);
|
|
@@ -14768,20 +16006,21 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
14768
16006
|
}
|
|
14769
16007
|
if (deck.page) {
|
|
14770
16008
|
await write(DECK_PAGE, deck.page);
|
|
14771
|
-
await cp2(playerBundle(),
|
|
14772
|
-
files.push(
|
|
16009
|
+
await cp2(playerBundle(), join14(out, PLAYER_FILE));
|
|
16010
|
+
files.push(join14(out, PLAYER_FILE));
|
|
14773
16011
|
}
|
|
14774
16012
|
files.push(...await vendorKatex(out));
|
|
14775
16013
|
files.push(...await vendorScripts(out, deck.composition));
|
|
14776
16014
|
if (opts.assetsFrom) {
|
|
14777
16015
|
files.push(...await copyAssets(opts.assetsFrom, out, source.figures, step));
|
|
14778
16016
|
}
|
|
16017
|
+
if (opts.bespoke) files.push(...await copyArt(Object.values(opts.bespoke), out));
|
|
14779
16018
|
if (opts.narration && opts.audioFrom) {
|
|
14780
16019
|
files.push(...await copyAudio(opts.audioFrom, opts.narration, out, step));
|
|
14781
16020
|
}
|
|
14782
16021
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
14783
16022
|
step(
|
|
14784
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
16023
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join14(out, "index.html")}`
|
|
14785
16024
|
);
|
|
14786
16025
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
14787
16026
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -14799,7 +16038,7 @@ var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
|
14799
16038
|
async function deckRuntime() {
|
|
14800
16039
|
const path2 = fileURLToPath2(new URL(`./${"deck-runtime.js"}`, import.meta.url));
|
|
14801
16040
|
try {
|
|
14802
|
-
return await
|
|
16041
|
+
return await readFile13(path2, "utf8");
|
|
14803
16042
|
} catch {
|
|
14804
16043
|
throw new Error(`Deck runtime missing at ${path2}. Run "npm run build" first.`);
|
|
14805
16044
|
}
|
|
@@ -14807,14 +16046,14 @@ async function deckRuntime() {
|
|
|
14807
16046
|
function playerBundle() {
|
|
14808
16047
|
const require2 = createRequire4(import.meta.url);
|
|
14809
16048
|
try {
|
|
14810
|
-
return
|
|
16049
|
+
return join14(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
14811
16050
|
} catch {
|
|
14812
16051
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
14813
16052
|
}
|
|
14814
16053
|
}
|
|
14815
16054
|
|
|
14816
16055
|
// src/server/upload.ts
|
|
14817
|
-
import { posix, sep as
|
|
16056
|
+
import { posix, sep as sep3 } from "node:path";
|
|
14818
16057
|
import { unzipSync as unzipSync2 } from "fflate";
|
|
14819
16058
|
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
14820
16059
|
var ZIP_LIMITS = {
|
|
@@ -14852,7 +16091,7 @@ function safeEntryPath(name) {
|
|
|
14852
16091
|
return path2;
|
|
14853
16092
|
}
|
|
14854
16093
|
function insideRoot2(root2, joined) {
|
|
14855
|
-
return joined === root2 || joined.startsWith(root2.endsWith(
|
|
16094
|
+
return joined === root2 || joined.startsWith(root2.endsWith(sep3) ? root2 : root2 + sep3);
|
|
14856
16095
|
}
|
|
14857
16096
|
function readZip(bytes, limits = ZIP_LIMITS) {
|
|
14858
16097
|
const seen = [];
|
|
@@ -15156,8 +16395,8 @@ function int(name, raw2) {
|
|
|
15156
16395
|
}
|
|
15157
16396
|
|
|
15158
16397
|
// src/server/pipeline.ts
|
|
15159
|
-
import { mkdir as
|
|
15160
|
-
import { dirname as dirname6, isAbsolute, join as
|
|
16398
|
+
import { mkdir as mkdir12, readFile as readFile14, stat as stat4, writeFile as writeFile14 } from "node:fs/promises";
|
|
16399
|
+
import { dirname as dirname6, isAbsolute, join as join15, relative, resolve as resolve8 } from "node:path";
|
|
15161
16400
|
var AUDIO_DIR = "audio";
|
|
15162
16401
|
var NARRATION_FILE = "narration.json";
|
|
15163
16402
|
var MAX_REMOTE_FIGURES = 40;
|
|
@@ -15180,10 +16419,10 @@ function stagesFor(options) {
|
|
|
15180
16419
|
async function runPipeline(job, input) {
|
|
15181
16420
|
const { options } = input;
|
|
15182
16421
|
const dirs = {
|
|
15183
|
-
upload:
|
|
15184
|
-
src:
|
|
15185
|
-
audio:
|
|
15186
|
-
deck:
|
|
16422
|
+
upload: join15(job.dir, "upload"),
|
|
16423
|
+
src: join15(job.dir, "src"),
|
|
16424
|
+
audio: join15(job.dir, AUDIO_DIR),
|
|
16425
|
+
deck: join15(job.dir, "deck")
|
|
15187
16426
|
};
|
|
15188
16427
|
const warnings = [...options.warnings];
|
|
15189
16428
|
const url = (rel) => `/d/${job.id}/${rel}`;
|
|
@@ -15207,21 +16446,21 @@ async function runPipeline(job, input) {
|
|
|
15207
16446
|
theme: options.stated.theme ? prefs.theme : planned.theme
|
|
15208
16447
|
};
|
|
15209
16448
|
assertRefsResolve(storyboard, source, { pending: options.images ? "allow" : "refuse" });
|
|
15210
|
-
await writeJson(
|
|
16449
|
+
await writeJson(join15(job.dir, "storyboard.json"), storyboard);
|
|
15211
16450
|
job.done("plan", `${storyboard.beats.length} beats`);
|
|
15212
16451
|
for (const f of scanBeatCount(storyboard, prefs)) warnings.push(f.message);
|
|
15213
16452
|
if (options.images) {
|
|
15214
16453
|
job.begin("illustrate");
|
|
15215
16454
|
const drawn = await illustrate(storyboard, source, {
|
|
15216
16455
|
prefs,
|
|
15217
|
-
assetsDir:
|
|
16456
|
+
assetsDir: join15(dirs.src, "assets"),
|
|
15218
16457
|
...input.imageChain ? { chain: input.imageChain } : {},
|
|
15219
16458
|
onStep: (line2) => job.log(line2)
|
|
15220
16459
|
});
|
|
15221
16460
|
storyboard = drawn.storyboard;
|
|
15222
16461
|
source = drawn.source;
|
|
15223
|
-
await writeJson(
|
|
15224
|
-
await writeJson(
|
|
16462
|
+
await writeJson(join15(dirs.src, "source.json"), source);
|
|
16463
|
+
await writeJson(join15(job.dir, "storyboard.json"), storyboard);
|
|
15225
16464
|
for (const p of drawn.illustrated) {
|
|
15226
16465
|
job.log(
|
|
15227
16466
|
`illustrate: ${p.beatId} \u2192 assets/${p.src} via ${p.provider}${p.cached ? " (cached)" : ""}`
|
|
@@ -15243,7 +16482,7 @@ async function runPipeline(job, input) {
|
|
|
15243
16482
|
warnings.push("the plan had nothing to say aloud, so the deck is silent");
|
|
15244
16483
|
} else {
|
|
15245
16484
|
job.log(`narrate: ${speaking} of ${storyboard.beats.length} beats speak`);
|
|
15246
|
-
await
|
|
16485
|
+
await mkdir12(dirs.audio, { recursive: true });
|
|
15247
16486
|
const staged = pickTheme(storyboard, source, options.format, {
|
|
15248
16487
|
stated: options.stated.theme ? prefs.theme : void 0,
|
|
15249
16488
|
design: prefs.design ?? storyboard.design ?? "classic",
|
|
@@ -15254,7 +16493,7 @@ async function runPipeline(job, input) {
|
|
|
15254
16493
|
dir: dirs.audio,
|
|
15255
16494
|
format: options.format
|
|
15256
16495
|
});
|
|
15257
|
-
await writeJson(
|
|
16496
|
+
await writeJson(join15(dirs.audio, NARRATION_FILE), spoken);
|
|
15258
16497
|
narration = { ...spoken, dir: AUDIO_DIR };
|
|
15259
16498
|
const segments2 = Object.values(spoken.beats).flat();
|
|
15260
16499
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
@@ -15312,7 +16551,7 @@ async function runPipeline(job, input) {
|
|
|
15312
16551
|
job.log("render: capturing frames \u2014 two minutes for a four-minute video");
|
|
15313
16552
|
const out = await render({
|
|
15314
16553
|
deck: dirs.deck,
|
|
15315
|
-
out:
|
|
16554
|
+
out: join15(dirs.deck, "video.mp4"),
|
|
15316
16555
|
subtitles: "sidecar",
|
|
15317
16556
|
log: (line2) => job.log(line2),
|
|
15318
16557
|
// The last of the three length levers, and the only one that acts on a
|
|
@@ -15361,8 +16600,8 @@ async function runPipeline(job, input) {
|
|
|
15361
16600
|
};
|
|
15362
16601
|
}
|
|
15363
16602
|
async function ingest(job, input, dirs, warnings) {
|
|
15364
|
-
await
|
|
15365
|
-
const root2 =
|
|
16603
|
+
await mkdir12(dirs.upload, { recursive: true });
|
|
16604
|
+
const root2 = resolve8(dirs.upload);
|
|
15366
16605
|
let docPath;
|
|
15367
16606
|
const upload = input.upload;
|
|
15368
16607
|
const bothOrNeither = new UploadError(
|
|
@@ -15372,13 +16611,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
15372
16611
|
);
|
|
15373
16612
|
if (input.url !== void 0) {
|
|
15374
16613
|
if (upload !== void 0) throw bothOrNeither;
|
|
15375
|
-
const harvested = await harvestBounded(input.url,
|
|
16614
|
+
const harvested = await harvestBounded(input.url, join15(root2, "assets"), {
|
|
15376
16615
|
...HARVEST_LIMITS,
|
|
15377
16616
|
...input.harvest
|
|
15378
16617
|
});
|
|
15379
16618
|
warnings.push(...harvested.warnings);
|
|
15380
|
-
docPath =
|
|
15381
|
-
await
|
|
16619
|
+
docPath = join15(root2, "document.md");
|
|
16620
|
+
await writeFile14(docPath, harvested.markdown);
|
|
15382
16621
|
job.log(
|
|
15383
16622
|
`ingest: harvested ${input.url} \u2014 "${harvested.title}", ${harvested.assets.length} asset(s)`
|
|
15384
16623
|
);
|
|
@@ -15394,13 +16633,13 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
15394
16633
|
const { files, warnings: zipWarnings } = readZip(upload.bytes);
|
|
15395
16634
|
warnings.push(...zipWarnings);
|
|
15396
16635
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
15397
|
-
const to =
|
|
16636
|
+
const to = resolve8(join15(root2, rel));
|
|
15398
16637
|
if (!insideRoot2(root2, to))
|
|
15399
16638
|
throw new UploadError(`Refusing to write ${rel}.`, "Re-zip from inside the folder.");
|
|
15400
|
-
await
|
|
15401
|
-
await
|
|
16639
|
+
await mkdir12(dirname6(to), { recursive: true });
|
|
16640
|
+
await writeFile14(to, bytes);
|
|
15402
16641
|
}
|
|
15403
|
-
docPath =
|
|
16642
|
+
docPath = join15(root2, pickMarkdown(files));
|
|
15404
16643
|
job.log(
|
|
15405
16644
|
`ingest: unpacked ${Object.keys(files).length} file(s), reading ${pickMarkdown(files)}`
|
|
15406
16645
|
);
|
|
@@ -15412,10 +16651,10 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
15412
16651
|
`DeckSmith reads ${MARKDOWN_EXTS.join(", ")} or a .zip containing one. Export the document to markdown first.`
|
|
15413
16652
|
);
|
|
15414
16653
|
}
|
|
15415
|
-
docPath =
|
|
15416
|
-
await
|
|
16654
|
+
docPath = join15(root2, "document.md");
|
|
16655
|
+
await writeFile14(docPath, upload.bytes);
|
|
15417
16656
|
}
|
|
15418
|
-
const text2 = await
|
|
16657
|
+
const text2 = await readFile14(docPath, "utf8");
|
|
15419
16658
|
const parsed = parseMarkdown(text2, {
|
|
15420
16659
|
lang: input.options.stated.lang ? input.options.prefs.lang : void 0
|
|
15421
16660
|
});
|
|
@@ -15435,8 +16674,8 @@ async function ingest(job, input, dirs, warnings) {
|
|
|
15435
16674
|
`ingest: ${parsed.sections.length} sections, ${parsed.figures.length} figures, ${parsed.equations.length} equations, ${parsed.tables.length} tables`
|
|
15436
16675
|
);
|
|
15437
16676
|
const guarded = await guardFigures(parsed, root2, input.fetchRemoteFigures, warnings);
|
|
15438
|
-
const source = await fetchFigures(guarded,
|
|
15439
|
-
await writeJson(
|
|
16677
|
+
const source = await fetchFigures(guarded, join15(resolve8(dirs.src), "assets"), warnings);
|
|
16678
|
+
await writeJson(join15(dirs.src, "source.json"), source);
|
|
15440
16679
|
return source;
|
|
15441
16680
|
}
|
|
15442
16681
|
async function harvestBounded(url, dir, opts) {
|
|
@@ -15512,12 +16751,12 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
15512
16751
|
warnings.push(`figure ${figure.id} was left out: "${scheme}:" figures are not read`);
|
|
15513
16752
|
continue;
|
|
15514
16753
|
}
|
|
15515
|
-
const abs = isAbsolute(src) ?
|
|
16754
|
+
const abs = isAbsolute(src) ? resolve8(src) : resolve8(join15(root2, src.replace(/^\.?\//, "")));
|
|
15516
16755
|
if (!insideRoot2(root2, abs)) {
|
|
15517
16756
|
warnings.push(`figure ${figure.id} was left out: "${src}" points outside the upload`);
|
|
15518
16757
|
continue;
|
|
15519
16758
|
}
|
|
15520
|
-
if (!(await
|
|
16759
|
+
if (!(await stat4(abs).catch(() => null))?.isFile()) {
|
|
15521
16760
|
warnings.push(`figure ${figure.id} was left out: "${src}" is not in the upload`);
|
|
15522
16761
|
continue;
|
|
15523
16762
|
}
|
|
@@ -15527,19 +16766,19 @@ async function guardFigures(source, root2, allowRemote, warnings) {
|
|
|
15527
16766
|
}
|
|
15528
16767
|
async function pack2(job, ctx) {
|
|
15529
16768
|
try {
|
|
15530
|
-
const assets =
|
|
16769
|
+
const assets = join15(resolve8(ctx.dirs.src), "assets");
|
|
15531
16770
|
const requests = ctx.source.figures.map((f) => ({
|
|
15532
16771
|
id: f.id,
|
|
15533
|
-
url:
|
|
16772
|
+
url: join15(assets, f.src),
|
|
15534
16773
|
prefer: "bake"
|
|
15535
16774
|
}));
|
|
15536
16775
|
const plan = await planMedia(requests, async (url) => ({
|
|
15537
|
-
bytes: new Uint8Array(await
|
|
16776
|
+
bytes: new Uint8Array(await readFile14(url))
|
|
15538
16777
|
}));
|
|
15539
16778
|
const files = { ...plan.files };
|
|
15540
16779
|
if (ctx.narration) {
|
|
15541
16780
|
for (const name of audioNames2(ctx.narration)) {
|
|
15542
|
-
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await
|
|
16781
|
+
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(await readFile14(join15(ctx.dirs.audio, name)));
|
|
15543
16782
|
}
|
|
15544
16783
|
}
|
|
15545
16784
|
const container = {
|
|
@@ -15556,10 +16795,10 @@ async function pack2(job, ctx) {
|
|
|
15556
16795
|
},
|
|
15557
16796
|
source: ctx.source,
|
|
15558
16797
|
storyboard: ctx.storyboard,
|
|
15559
|
-
...ctx.narration ? { narration: await readJson(
|
|
16798
|
+
...ctx.narration ? { narration: await readJson(join15(ctx.dirs.audio, NARRATION_FILE)) } : {},
|
|
15560
16799
|
media: plan.media
|
|
15561
16800
|
};
|
|
15562
|
-
const bytes = await writePack(container, files,
|
|
16801
|
+
const bytes = await writePack(container, files, join15(ctx.dirs.deck, "deck.deck"));
|
|
15563
16802
|
job.log(`pack: ${Math.round(bytes / 1024)} KB \u2192 deck.deck`);
|
|
15564
16803
|
return ctx.url("deck.deck");
|
|
15565
16804
|
} catch (err) {
|
|
@@ -15582,12 +16821,12 @@ function host(src) {
|
|
|
15582
16821
|
}
|
|
15583
16822
|
}
|
|
15584
16823
|
async function writeJson(path2, value) {
|
|
15585
|
-
await
|
|
15586
|
-
await
|
|
16824
|
+
await mkdir12(dirname6(path2), { recursive: true });
|
|
16825
|
+
await writeFile14(path2, `${JSON.stringify(value, null, 2)}
|
|
15587
16826
|
`);
|
|
15588
16827
|
}
|
|
15589
16828
|
async function readJson(path2) {
|
|
15590
|
-
return JSON.parse(await
|
|
16829
|
+
return JSON.parse(await readFile14(path2, "utf8"));
|
|
15591
16830
|
}
|
|
15592
16831
|
|
|
15593
16832
|
// src/server/errors.ts
|
|
@@ -15993,14 +17232,14 @@ var PAGE_BUDGET = {
|
|
|
15993
17232
|
refs: "relative"
|
|
15994
17233
|
};
|
|
15995
17234
|
async function readPage(url, work2) {
|
|
15996
|
-
await
|
|
15997
|
-
const dir = await mkdtemp3(
|
|
17235
|
+
await mkdir13(work2, { recursive: true });
|
|
17236
|
+
const dir = await mkdtemp3(join16(work2, "harvest-"));
|
|
15998
17237
|
try {
|
|
15999
17238
|
const page = await harvest(url, dir, PAGE_BUDGET);
|
|
16000
17239
|
const files = {
|
|
16001
17240
|
"document.md": new TextEncoder().encode(page.markdown)
|
|
16002
17241
|
};
|
|
16003
|
-
for (const asset of page.assets) files[basename5(asset)] = await
|
|
17242
|
+
for (const asset of page.assets) files[basename5(asset)] = await readFile15(asset);
|
|
16004
17243
|
const warnings = [...page.warnings];
|
|
16005
17244
|
if (page.clips.length > 0) {
|
|
16006
17245
|
warnings.push(
|
|
@@ -16034,7 +17273,7 @@ var statusSchema = z5.object({
|
|
|
16034
17273
|
var DEFAULT_WAIT = 45;
|
|
16035
17274
|
function deckTools(opts) {
|
|
16036
17275
|
const queue = new Queue({ maxQueued: 8 });
|
|
16037
|
-
const root2 =
|
|
17276
|
+
const root2 = resolve9(opts.root);
|
|
16038
17277
|
let cached2;
|
|
16039
17278
|
const found = async () => {
|
|
16040
17279
|
cached2 ??= await (opts.probe ?? prereqs)();
|
|
@@ -16042,8 +17281,8 @@ function deckTools(opts) {
|
|
|
16042
17281
|
};
|
|
16043
17282
|
const insideRoot3 = (p) => {
|
|
16044
17283
|
if (!isAbsolute2(p)) throw new Error(`document_path must be absolute; got "${p}".`);
|
|
16045
|
-
const full =
|
|
16046
|
-
if (full !== root2 && !full.startsWith(root2 +
|
|
17284
|
+
const full = resolve9(p);
|
|
17285
|
+
if (full !== root2 && !full.startsWith(root2 + sep4)) {
|
|
16047
17286
|
throw new Error(
|
|
16048
17287
|
`document_path is outside the server's root. It may only read files under ${root2}.`
|
|
16049
17288
|
);
|
|
@@ -16120,16 +17359,16 @@ function deckTools(opts) {
|
|
|
16120
17359
|
}
|
|
16121
17360
|
}
|
|
16122
17361
|
const page = input.document_url === void 0 ? void 0 : await readPage(input.document_url, opts.work);
|
|
16123
|
-
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(
|
|
16124
|
-
const bytes = page ? page.zip : file ? await
|
|
17362
|
+
const filename = page ? "page.zip" : input.document_path ? input.document_path.split(sep4).pop() : "document.md";
|
|
17363
|
+
const bytes = page ? page.zip : file ? await readFile15(file) : new TextEncoder().encode(input.document_text);
|
|
16125
17364
|
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
16126
17365
|
throw new Error(
|
|
16127
17366
|
`Document is ${(bytes.byteLength / 1e6).toFixed(1)} MB, over the ${(MAX_UPLOAD_BYTES / 1e6).toFixed(0)} MB cap.` + (page ? " Harvest fewer figures with a smaller maxAssets, or save the page and edit it." : "")
|
|
16128
17367
|
);
|
|
16129
17368
|
}
|
|
16130
17369
|
const id2 = randomBytes(16).toString("base64url");
|
|
16131
|
-
const dir =
|
|
16132
|
-
await
|
|
17370
|
+
const dir = join16(opts.work, id2);
|
|
17371
|
+
await mkdir13(dir, { recursive: true });
|
|
16133
17372
|
queue.submit({
|
|
16134
17373
|
id: id2,
|
|
16135
17374
|
dir,
|
|
@@ -16156,7 +17395,7 @@ function deckTools(opts) {
|
|
|
16156
17395
|
};
|
|
16157
17396
|
}
|
|
16158
17397
|
function dirOf(work2, id2) {
|
|
16159
|
-
return
|
|
17398
|
+
return join16(work2, id2);
|
|
16160
17399
|
}
|
|
16161
17400
|
function waitFor(queue, id2, seconds) {
|
|
16162
17401
|
const now = queue.view(id2);
|
|
@@ -16188,9 +17427,9 @@ function report(view, dir) {
|
|
|
16188
17427
|
// (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
|
|
16189
17428
|
// is the shape of the CLI's output directory and not the server's, and an
|
|
16190
17429
|
// agent told to read it would have found nothing there.
|
|
16191
|
-
storyboard_path:
|
|
17430
|
+
storyboard_path: join16(dir, "storyboard.json"),
|
|
16192
17431
|
...done && view.result ? {
|
|
16193
|
-
deck_path:
|
|
17432
|
+
deck_path: join16(dir, "deck"),
|
|
16194
17433
|
slides: view.result.slides,
|
|
16195
17434
|
duration_seconds: view.result.duration,
|
|
16196
17435
|
warnings: view.result.warnings
|
|
@@ -16199,7 +17438,7 @@ function report(view, dir) {
|
|
|
16199
17438
|
};
|
|
16200
17439
|
}
|
|
16201
17440
|
function defaultWork() {
|
|
16202
|
-
return
|
|
17441
|
+
return join16(tmpdir4(), "decksmith-mcp");
|
|
16203
17442
|
}
|
|
16204
17443
|
|
|
16205
17444
|
// src/mcp/main.ts
|