b7f5c41da5
Three pictures on /arena — the chase camera behind the EV, the crow in flight, the
aircraft over the board — were the only imagery on the site with no generator
script anywhere in either repo. They were made by hand, and so they were a single
daylight frame served to dark-mode readers too, with no pair to choose from.
`shots.mjs` gains a `play` capability beside `aim`: a shot names a control mode,
the script clicks the app's own mode dock, and only then shoots — with the HUD
deliberately left in frame, because on these three the body and its readout ARE
the subject. `#mode-dock` and `#play-hud` are in FURNITURE.BARE rather than
CLUTTER for that reason, and there is a comment at the `hide()` call so a later
tidy-up does not crop the subject out.
Three guards, each verified by deliberately making it fire:
- the mode must be OFFERED, not merely present — asking for `drive` on the Bay
Area board now says `"drive" is not offered here — the dock is showing
[overview, actor]`;
- `aria-pressed="true"` AND `#play-hud` unhidden, the same pair
`performance-budget.mjs` waits on;
- the HUD's own body chip, so asking for `Explore` when an anonymous visitor
gets a crow says `the play HUD says "Crow", not "Explore"`.
The body guard runs twice, at the click and again at the shutter, because a
completed route or a bound ends a mode mid-wait and hands back an orbit wide shot
under the right filename.
`untilProgress` exists because the first delivery drifted: `run: 9000` gave 55% of
the leg in a 1x preview and 69% in the 2x render — a different stretch of road,
with a town's edge across the top of the frame. Road traffic runs from page load,
so a duration is really "boot plus a bit". Waiting on the app's own route
percentage lands preview and delivery on the same frame; overshoot throws rather
than shooting. The crow and the aircraft are built inert, so a duration is exact
for them and only the car needed the gate — measured, not assumed.
The run log now echoes the HUD (`[Drive] 56 mph · US-101 | assisted · 55% ·
chase`), because "shot drive-101-day" is equally true of a parked car, a finished
route and a correct frame.
The crow moved from the state board to the Bay Area. On California a one-metre
bird stands against ground drawn at 1,919 m to the unit with an oak the size of a
hill behind it, and the night frame would have been a black rectangle with a bird
in it. `crow-nav-v1`'s `source` said "Tera / California" and that was wrong on the
environment's own terms as well — `crowNav.ts` runs a ±120 m box with no terrain
at all.
One frame is honestly weak and is recorded rather than dressed up: `drive-101`
at night. From behind, on an unlit corridor, the EV is two tail lamps and a roof
strip. Four hours were shot between 21:15 and 20:05 and the corridor's ground is
equally black at all of them, so the hour is not the lever; both real fixes are in
the engine and are in TODO.md. It is still the honest night frame.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
498 lines
21 KiB
JavaScript
498 lines
21 KiB
JavaScript
/**
|
|
* Re-shoot everything the site shows of this engine, in one command.
|
|
*
|
|
* npm run refresh # build, stills, cards, films, manifests
|
|
* npm run refresh -- --dry-run # what it would shoot; no browser, no build
|
|
* npm run refresh -- --stills-only
|
|
* npm run refresh -- --cards-only
|
|
* npm run refresh -- --films-only
|
|
* npm run refresh -- --rough # films at 24 frames, for looking at
|
|
* npm run refresh -- --skip-build # you just built; do not build again
|
|
*
|
|
* ### Why this exists
|
|
*
|
|
* The imagery on lumbridgecorp.com is four artefacts produced by three scripts
|
|
* into two repos: product stills and a TypeScript manifest into `lumbridge-v4`,
|
|
* time-lapse films and a second manifest into the same place, and two share
|
|
* cards into this repo's `public/`. Re-shooting after an engine change was a
|
|
* *procedure* — build first, because the scripts photograph `dist/` and not
|
|
* `src/`; then three commands in an order that matters; then check the run
|
|
* actually got the GPU; then commit this repo before regenerating the manifest
|
|
* so it names a clean sha. A procedure that lives in somebody's head is a
|
|
* procedure that gets half-run, and the evidence is in the tree: the stills were
|
|
* re-shot at one commit, the films at another two weeks older, and nothing said
|
|
* so until a human compared two files.
|
|
*
|
|
* So: one command, and the parts you can skip are flags rather than the memory
|
|
* of which script does what.
|
|
*
|
|
* ### The three failures this must never report as success
|
|
*
|
|
* All three produce output that looks deliberate, which is why they are
|
|
* checked here rather than left to the eye:
|
|
*
|
|
* 1. **SwiftShader.** `launch()` in `harness.mjs` asks for `--use-angle=vulkan`
|
|
* and falls back to software when the card is not reachable, printing which
|
|
* it got. The pictures are the same pictures — measured, half a level out of
|
|
* 255 — but a film goes from ninety seconds to twenty-two minutes, so a run
|
|
* that has silently fallen back reads as a hang. Every child's output is
|
|
* scanned for that line and the run stops on it.
|
|
*
|
|
* 2. **A chapter `expect` guard firing.** Chapters are pack data and pack data
|
|
* gets reordered; the guard in `shots.mjs` refuses to shoot when the button at
|
|
* an index is not the chapter the shot list names. It has already earned its
|
|
* keep once, when the studios were rebuilt and chapter 0 became "Front Door".
|
|
* When it fires, the answer is to fix the shot list, not to re-run — so this
|
|
* surfaces it as its own headline rather than as one line of a stack trace
|
|
* fifty lines up the log.
|
|
*
|
|
* 3. **A `play` guard firing.** Three stills take control of a body before the
|
|
* shutter opens — the EV on US-101, the crow over the Bay, the aircraft on the
|
|
* corridor — and `enterPlay()` in `shots.mjs` refuses to shoot when the dock
|
|
* did not hand over the mode, when the HUD names a different body, or when the
|
|
* route ran out during the wait. Every one of those otherwise delivers an orbit
|
|
* wide shot under a filename that says an agent is driving. Same treatment as
|
|
* the chapter guard, and for the same reason: re-running cannot fix it.
|
|
*
|
|
* ### It re-stamps the provenance manifest, narrowly
|
|
*
|
|
* `PROVENANCE.json` records a SHA-256 for every tracked binary and
|
|
* `npm run provenance` fails on a mismatch, so every legitimate re-shoot of the
|
|
* share cards used to leave that gate red. This re-stamps an entry only when the
|
|
* file is one this run wrote, its `origin` is `repository-generated`, and its
|
|
* `generator` names a script this run actually executed. Every other mismatch is
|
|
* reported and left alone, because a hash that moved without this pipeline
|
|
* running is the thing the gate exists to catch.
|
|
*
|
|
* ### What "changed" means in the summary
|
|
*
|
|
* Every deliverable is hashed before and after, so the summary can say which
|
|
* pictures actually moved rather than which commands were run. Renders are never
|
|
* byte-identical — aircraft are crossing and cloud shadow is drifting between any
|
|
* two runs — so "changed" here means the bytes differ, not that the framing did.
|
|
* The size delta is the useful column: a WebP that halved or doubled is a frame
|
|
* worth opening.
|
|
*/
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, "..", "..");
|
|
|
|
// ---- Arguments --------------------------------------------------------------
|
|
|
|
function flag(name, fallback = null) {
|
|
const i = process.argv.indexOf(`--${name}`);
|
|
return i > -1 && process.argv[i + 1] && !process.argv[i + 1].startsWith("--")
|
|
? process.argv[i + 1]
|
|
: fallback;
|
|
}
|
|
const has = (name) => process.argv.includes(`--${name}`);
|
|
|
|
const dryRun = has("dry-run");
|
|
const rough = has("rough");
|
|
const skipBuild = has("skip-build");
|
|
const siteDir = resolve(flag("site", join(ROOT, "..", "lumbridge-v4")));
|
|
|
|
/**
|
|
* `--stills-only` and friends select; naming none of them selects all three.
|
|
*
|
|
* They compose, so `--stills-only --cards-only` is "stills and cards", which is
|
|
* the pair you want after a lighting change and before you are ready to pay for
|
|
* films.
|
|
*/
|
|
const picked = ["stills", "cards", "films"].filter((part) => has(`${part}-only`));
|
|
const parts = new Set(picked.length ? picked : ["stills", "cards", "films"]);
|
|
|
|
// ---- What each part writes ---------------------------------------------------
|
|
|
|
/**
|
|
* The deliverables, by part, so the summary can be computed rather than claimed.
|
|
*
|
|
* `optional` covers the normal case for anyone who cloned this repo on its own:
|
|
* there is no sibling `lumbridge-v4`, the scripts say so and skip the site step,
|
|
* and a missing directory here is a note rather than a failure.
|
|
*/
|
|
const OUTPUTS = {
|
|
stills: [
|
|
{ dir: join(siteDir, "apps", "web", "public", "shots"), match: /\.webp$/, optional: true },
|
|
{ dir: join(siteDir, "apps", "web", "src", "data"), match: /^shots\.ts$/, optional: true },
|
|
],
|
|
cards: [{ dir: join(ROOT, "public"), match: /^og-.*\.png$/ }],
|
|
films: [
|
|
{ dir: join(siteDir, "apps", "web", "public", "films"), match: /\.(mp4|webp|png|jpg)$/, optional: true },
|
|
{ dir: join(siteDir, "apps", "web", "src", "data"), match: /^films\.ts$/, optional: true },
|
|
],
|
|
};
|
|
|
|
async function fingerprint() {
|
|
const seen = new Map();
|
|
for (const part of parts) {
|
|
for (const { dir, match } of OUTPUTS[part]) {
|
|
let names;
|
|
try {
|
|
names = await readdir(dir);
|
|
} catch {
|
|
continue; // Not there yet, or no site checkout. `report` says so.
|
|
}
|
|
for (const name of names) {
|
|
if (!match.test(name)) continue;
|
|
const path = join(dir, name);
|
|
const body = await readFile(path);
|
|
const sha256 = createHash("sha256").update(body).digest("hex");
|
|
seen.set(path, { size: body.length, hash: sha256.slice(0, 12), sha256 });
|
|
}
|
|
}
|
|
}
|
|
return seen;
|
|
}
|
|
|
|
// ---- Running a step ----------------------------------------------------------
|
|
|
|
/**
|
|
* The two lines that mean "this run's output is not to be trusted".
|
|
*
|
|
* `launch()` prints exactly one of these per browser it opens, so matching the
|
|
* text it prints is matching the check it already does rather than repeating it.
|
|
*/
|
|
const SOFTWARE_RENDERER = /GPU:.*(SwiftShader|llvmpipe)|no GPU \(/i;
|
|
/** `chapter()` in `shots.mjs`, and the office picker's equivalent beside it. */
|
|
const PACK_REORDERED = /a city pack was reordered|the OFFICES table\s*\n?\s*changed|is "[^"]*", not "/;
|
|
/**
|
|
* `enterPlay()` in `shots.mjs` refusing to shoot a body it did not get.
|
|
*
|
|
* The third headline, and here for the same reason the second one is: three of
|
|
* the stills are pictures of an agent driving, flying and gliding, and every way
|
|
* that can go wrong produces a frame that still renders, still looks deliberate
|
|
* and is of the wrong thing — an overview wide shot with `drive-101-day` written
|
|
* on it. Re-running does not fix any of them. Either the dock stopped offering
|
|
* the mode, or the body is not the one the caption describes, or the leg ran out
|
|
* from under the wait.
|
|
*/
|
|
const BODY_REFUSED =
|
|
/is not offered here|the mode dock has no|the dock did not take|the play HUD says|did not survive the wait|was already \d+% done|carries no percentage/;
|
|
|
|
class StepFailure extends Error {
|
|
constructor(step, headline, detail) {
|
|
super(headline);
|
|
this.step = step;
|
|
this.headline = headline;
|
|
this.detail = detail;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run one child, streaming its output, and read that output as it goes.
|
|
*
|
|
* Streaming *and* buffering, because both are wanted: a film is ninety seconds
|
|
* of silence otherwise, and the two failures above have to be findable after the
|
|
* fact. The buffer is a few kilobytes of console output, not the frames.
|
|
*/
|
|
function run(step, command, args, { cwd = ROOT } = {}) {
|
|
return new Promise((resolveRun, rejectRun) => {
|
|
const started = Date.now();
|
|
const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
let log = "";
|
|
const watch = (stream, sink) => {
|
|
stream.setEncoding("utf8");
|
|
stream.on("data", (chunk) => {
|
|
log += chunk;
|
|
sink.write(chunk);
|
|
});
|
|
};
|
|
watch(child.stdout, process.stdout);
|
|
watch(child.stderr, process.stderr);
|
|
child.on("error", (error) => rejectRun(new StepFailure(step, `could not start ${command}`, String(error))));
|
|
child.on("close", (code) => {
|
|
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
if (SOFTWARE_RENDERER.test(log)) {
|
|
rejectRun(
|
|
new StepFailure(
|
|
step,
|
|
"the renderer came up as SwiftShader, not the card",
|
|
"Everything this run produced is software-rendered: same pictures, roughly fifteen times\n" +
|
|
"the wall clock, and a film that reads as a hang. Nothing has been published from it.\n" +
|
|
"Chrome reaches the Radeon with `--use-angle=vulkan` and needs read access to\n" +
|
|
"/dev/dri/renderD128, which group `render` grants — check `id` and `ls -l /dev/dri`,\n" +
|
|
"then re-run. `harness.mjs` explains why no other GL flag is a substitute.",
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
if (code !== 0 && PACK_REORDERED.test(log)) {
|
|
rejectRun(
|
|
new StepFailure(
|
|
step,
|
|
"a chapter guard fired — the shot list no longer matches the packs",
|
|
"A shot names a chapter by index and asserts its short label before the shutter opens,\n" +
|
|
"and the assertion failed. This is not a flake and re-running will not fix it: a pack was\n" +
|
|
"reordered or renamed underneath the shot list, so every camera and caption downstream of\n" +
|
|
"that index is now pointed at something else.\n" +
|
|
"Fix `SHOTS` in shots.mjs against the pack (`node scripts/brand-assets/shots.mjs --list`\n" +
|
|
"prints what it believes), then run this again. Do not weaken the guard.",
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
if (code !== 0 && BODY_REFUSED.test(log)) {
|
|
rejectRun(
|
|
new StepFailure(
|
|
step,
|
|
"a play guard fired — a shot did not get the body it asked for",
|
|
"Three stills take control of something before the shutter opens: the EV on US-101, the\n" +
|
|
"crow over the Bay, the aircraft on the California corridor. Each asks the mode dock for a\n" +
|
|
"mode, waits for `aria-pressed` and the play HUD, and asserts which body the HUD names.\n" +
|
|
"One of those checks refused, and re-running will not fix it: the mode is unavailable on\n" +
|
|
"that board, or the body changed, or the route ran out during the wait. A frame shot past\n" +
|
|
"any of those is an orbit wide shot with the right filename.\n" +
|
|
"`node scripts/brand-assets/shots.mjs --list` prints what each shot asks for. Do not\n" +
|
|
"weaken the guard.",
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
if (code !== 0) {
|
|
rejectRun(new StepFailure(step, `${step} failed (exit ${code})`, "The child's own output is above."));
|
|
return;
|
|
}
|
|
resolveRun({ seconds, log });
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---- The plan ----------------------------------------------------------------
|
|
|
|
const NODE = process.execPath;
|
|
const script = (name) => join(HERE, name);
|
|
|
|
/**
|
|
* The film list, read out of `films.mjs` rather than imported from it.
|
|
*
|
|
* `films.mjs` is a script with top-level effects — importing it opens a browser —
|
|
* and it belongs to a different workstream, so this reads its `FILMS` ids for the
|
|
* dry run and treats a miss as "cannot say" rather than as an error. The command
|
|
* printed underneath is the truth either way.
|
|
*/
|
|
async function filmIds() {
|
|
try {
|
|
const body = await readFile(script("films.mjs"), "utf8");
|
|
const from = body.indexOf("const FILMS = [");
|
|
if (from === -1) return null;
|
|
const ids = [...body.slice(from).matchAll(/^\s{4}id: "([^"]+)"/gm)].map((m) => m[1]);
|
|
return ids.length ? ids : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function plan() {
|
|
console.log(`refresh ${[...parts].join(", ")}${rough ? " (films at 24 frames)" : ""}`);
|
|
console.log(`site ${siteDir}`);
|
|
console.log("");
|
|
if (parts.has("stills") || parts.has("cards")) {
|
|
await run("list", NODE, [script("shots.mjs"), "--list"]);
|
|
console.log("");
|
|
}
|
|
if (parts.has("cards")) {
|
|
console.log("cards 2 renders of the running app, then og.html over each at 1200x630");
|
|
console.log(" og-tera.png tera.lumbridgecorp.com");
|
|
console.log(" og-office.png office.lumbridgecorp.com");
|
|
console.log("");
|
|
}
|
|
if (parts.has("films")) {
|
|
const ids = await filmIds();
|
|
console.log(`films ${ids ? `${ids.length} reels: ${ids.join(", ")}` : "reel list is films.mjs's own"}`);
|
|
console.log(` ${NODE} ${relative(ROOT, script("films.mjs"))}${rough ? " --frames 24" : ""}`);
|
|
console.log("");
|
|
}
|
|
console.log("nothing was rendered — this was --dry-run");
|
|
}
|
|
|
|
// ---- Provenance --------------------------------------------------------------
|
|
|
|
/**
|
|
* Re-stamp the SHA-256 of the artifacts this run generated, and nothing else.
|
|
*
|
|
* `PROVENANCE.json` records a hash for every tracked binary, and
|
|
* `npm run provenance` fails when one does not match. That is the right gate —
|
|
* it is what stops an art file arriving from somewhere nobody can name — but the
|
|
* two share cards are *outputs of this pipeline*, so every legitimate re-shoot
|
|
* left the gate red and a human had to work out which of two hashes went in
|
|
* which of two records. That is precisely the remembered procedure this script
|
|
* exists to delete.
|
|
*
|
|
* The rule is deliberately narrow, so this stays bookkeeping rather than a hole
|
|
* in the gate. An entry is re-stamped only when **all** of these hold:
|
|
*
|
|
* - the file is one this run wrote, in a directory this run owns;
|
|
* - `origin` is `repository-generated` — never a copied upstream item;
|
|
* - `generator` names a script this run actually executed.
|
|
*
|
|
* Anything else that has drifted is reported and left exactly as it was, because
|
|
* a hash that changed without this pipeline running is the thing the gate is for.
|
|
*/
|
|
async function restampProvenance(written, ranGenerators) {
|
|
const path = join(ROOT, "PROVENANCE.json");
|
|
let body;
|
|
let manifest;
|
|
try {
|
|
body = await readFile(path, "utf8");
|
|
manifest = JSON.parse(body);
|
|
} catch (error) {
|
|
console.log(`\nprovenance could not be read (${error.message}) — left alone`);
|
|
return;
|
|
}
|
|
const stamped = [];
|
|
const refused = [];
|
|
for (const entry of manifest.distributedArtifacts ?? []) {
|
|
const full = join(ROOT, entry.path);
|
|
const now = written.get(full);
|
|
if (!now || now.sha256 === entry.sha256) continue;
|
|
const mine =
|
|
entry.origin === "repository-generated" &&
|
|
ranGenerators.some((generator) => entry.generator === generator);
|
|
if (!mine) {
|
|
refused.push(entry.path);
|
|
continue;
|
|
}
|
|
/*
|
|
* Substitute the hash in the *text*, not by re-serialising the object.
|
|
* `JSON.stringify(…, null, 2)` reflows every array in the file, so a
|
|
* two-hash update arrived as a fourteen-line diff across records this run
|
|
* had nothing to do with — which is the last thing a licensing manifest
|
|
* should do. A SHA-256 is 64 hex characters and unique in this file, so a
|
|
* literal replacement is exact.
|
|
*/
|
|
if (!body.includes(entry.sha256)) {
|
|
refused.push(`${entry.path} (recorded hash not found verbatim)`);
|
|
continue;
|
|
}
|
|
body = body.replace(entry.sha256, now.sha256);
|
|
stamped.push(entry.path);
|
|
}
|
|
if (stamped.length) {
|
|
await writeFile(path, body, "utf8");
|
|
console.log(`\nprovenance re-stamped ${stamped.length}: ${stamped.join(", ")}`);
|
|
}
|
|
if (refused.length) {
|
|
console.log(
|
|
`\nprovenance ${refused.length} artifact(s) no longer match their recorded hash and were NOT` +
|
|
` touched:\n ${refused.join("\n ")}\n` +
|
|
" Nothing this run generated wrote them. Find out what did before committing.",
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---- The summary -------------------------------------------------------------
|
|
|
|
function report(before, after, timings) {
|
|
const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
|
|
const added = [];
|
|
const changed = [];
|
|
const same = [];
|
|
const gone = [];
|
|
for (const path of paths) {
|
|
const was = before.get(path);
|
|
const now = after.get(path);
|
|
if (!was) added.push({ path, now });
|
|
else if (!now) gone.push({ path, was });
|
|
else if (was.hash !== now.hash) changed.push({ path, was, now });
|
|
else same.push({ path });
|
|
}
|
|
|
|
const kb = (n) => `${Math.round(n / 1024)}kB`;
|
|
const show = (entries, label, line) => {
|
|
if (!entries.length) return;
|
|
console.log(`\n${label} (${entries.length})`);
|
|
for (const entry of entries) console.log(` ${line(entry)}`);
|
|
};
|
|
|
|
console.log("\n" + "─".repeat(72));
|
|
for (const [step, seconds] of timings) console.log(`${step.padEnd(10)} ${seconds}s`);
|
|
show(added, "new", ({ path, now }) => `${relative(ROOT, path).padEnd(58)} ${kb(now.size)}`);
|
|
show(
|
|
changed,
|
|
"changed",
|
|
({ path, was, now }) => {
|
|
const delta = now.size - was.size;
|
|
const sign = delta > 0 ? "+" : "";
|
|
return `${relative(ROOT, path).padEnd(58)} ${kb(now.size)} (${sign}${kb(delta)})`;
|
|
},
|
|
);
|
|
show(gone, "no longer written", ({ path }) => relative(ROOT, path));
|
|
if (same.length) console.log(`\nbyte-identical (${same.length}) — nothing about these moved`);
|
|
if (!added.length && !changed.length) {
|
|
console.log("\nnothing changed. If you expected a change, check that `npm run build` ran:");
|
|
console.log("these scripts photograph dist/, not src/, and --skip-build is a way to shoot a stale one.");
|
|
}
|
|
console.log("");
|
|
console.log("Commit tera first, then re-run the manifests so they name a clean sha:");
|
|
console.log(" node scripts/brand-assets/shots.mjs --manifest-only");
|
|
console.log(" node scripts/brand-assets/films.mjs --manifest-only");
|
|
console.log("Then commit lumbridge-v4 and deploy it.");
|
|
}
|
|
|
|
// ---- Run ---------------------------------------------------------------------
|
|
|
|
if (dryRun) {
|
|
await plan();
|
|
process.exit(0);
|
|
}
|
|
|
|
const before = await fingerprint();
|
|
const timings = [];
|
|
try {
|
|
if (!skipBuild) {
|
|
// First, and not optional without saying so: every script below photographs
|
|
// `dist/`, so skipping the build is how a "re-shoot" publishes the old engine.
|
|
const built = await run("build", "npm", ["run", "build"]);
|
|
timings.push(["build", built.seconds]);
|
|
} else {
|
|
console.log("build skipped (--skip-build) — the stills, cards and films will photograph the dist/ already on disk\n");
|
|
}
|
|
|
|
if (parts.has("stills")) {
|
|
const done = await run("stills", NODE, [script("shots.mjs"), "--site", siteDir]);
|
|
timings.push(["stills", done.seconds]);
|
|
}
|
|
if (parts.has("cards")) {
|
|
const done = await run("cards", NODE, [script("capture.mjs")]);
|
|
timings.push(["cards", done.seconds]);
|
|
}
|
|
if (parts.has("films")) {
|
|
const args = [script("films.mjs"), "--site", siteDir];
|
|
if (rough) args.push("--frames", "24");
|
|
const done = await run("films", NODE, args);
|
|
timings.push(["films", done.seconds]);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof StepFailure) {
|
|
console.error("\n" + "━".repeat(72));
|
|
console.error(`FAILED at ${error.step}: ${error.headline}`);
|
|
console.error("━".repeat(72));
|
|
console.error(error.detail);
|
|
console.error("");
|
|
process.exit(1);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
const after = await fingerprint();
|
|
report(before, after, timings);
|
|
await restampProvenance(
|
|
after,
|
|
timings.map(([step]) => step).flatMap((step) =>
|
|
step === "cards"
|
|
? ["scripts/brand-assets/capture.mjs"]
|
|
: step === "stills"
|
|
? ["scripts/brand-assets/shots.mjs"]
|
|
: step === "films"
|
|
? ["scripts/brand-assets/films.mjs"]
|
|
: [],
|
|
),
|
|
);
|