demobites 1.33.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -714,10 +714,24 @@ if (arg === "listen") {
714
714
  await event(job.id, "ready", { forgot: site, machine: name });
715
715
  noteState({ busy: false, job: null, last_error: null });
716
716
  };
717
+ // THE SESSION MOVES (2026-10-18, their consent in the app): this machine exports the sign-in its profile holds
718
+ // at the job's site and hands it to the vault with its own key. Counts come back; nothing else is said.
719
+ const runVault = async (job) => {
720
+ noteState({ busy: true, job: job.id });
721
+ await event(job.id, "started", { machine: name });
722
+ const env = { ...process.env, RECORDER_BASE: base, ...profileFor(job) };
723
+ const r = spawnSync(process.execPath, [path.join(dest, "scripts", "export-session.mjs"), env.RECORDER_PROFILE || path.join(process.cwd(), ".recorder", "profile"), job.url], { cwd: process.cwd(), encoding: "utf8", env });
724
+ const line = (r.stdout || "").trim().split("\n").filter((l) => l.startsWith("RESULT ")).pop();
725
+ let out = null; try { out = JSON.parse(line.slice(7)); } catch { /* none */ }
726
+ if (r.status === 0 && out) { console.log(` ○ Your sign-in at ${new URL(job.url).host} is in the vault for the cloud (${out.cookies} cookies).`); await event(job.id, "ready", { ...out, machine: name }); }
727
+ else { const why = (r.stderr || r.stdout || "").trim().split("\n").pop() || `exit ${r.status}`; console.log(` ! The sign-in could not be moved: ${why}`); await event(job.id, "failed", { message: r.status === 3 ? "No sign-in at that site on this machine yet" : `The sign-in could not be moved (${why.slice(0, 120)})` }); }
728
+ noteState({ busy: false, job: null, last_error: null });
729
+ };
717
730
  const runJob = async (job) => {
718
731
  waitOff();
719
732
  if (job.kind === "off") return runOff(job);
720
733
  if (job.kind === "forget") return runForget(job);
734
+ if (job.kind === "vault") return runVault(job);
721
735
  if (job.kind === "look" || job.kind === "signin" || job.kind === "signout") return runLookOrSignin(job);
722
736
  const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
723
737
  // Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
@@ -796,6 +810,13 @@ if (arg === "listen") {
796
810
  }
797
811
  }
798
812
 
813
+ // ── 2c. THE POOL (2026-10-18): a box of ours, many workspaces, no key of its own ──────────────
814
+ // demobite pool --base <url> --secret <pool secret> [--slots N] [--name <box>]
815
+ if (arg === "pool") {
816
+ const r = spawnSync(process.execPath, [path.join(dest, "scripts", "pool.mjs"), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd(), env: process.env });
817
+ process.exit(r.status ?? 0);
818
+ }
819
+
799
820
  // ── 3. Subcommands ─────────────────────────────────────────────────────────
800
821
  if (arg === "login" || arg === "logout") {
801
822
  // `login <base>` forwards the base (dev, a preview) to login.mjs, which
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobites",
3
- "version": "1.33.0",
3
+ "version": "1.35.0",
4
4
  "description": "The DemoBites agentic recorder — you prompt, it films a real browser, and DemoBites turns the take into an editable demo bite.",
5
5
  "bin": {
6
6
  "demobites": "launcher/index.mjs"
@@ -90,7 +90,7 @@ function bboxCrossing(step) {
90
90
  return cross;
91
91
  }
92
92
 
93
- function roiChanges(bb) {
93
+ function roiChanges(bb, around) {
94
94
  // ONSETS of visible change inside the padded bbox crop, in video time.
95
95
  // Not threshold crossings: hover styles animate (LinkedIn fades its row
96
96
  // background over ~150ms), and a threshold detector fires mid-fade —
@@ -101,14 +101,19 @@ function roiChanges(bb) {
101
101
  const pad = 6;
102
102
  const x0 = Math.max(0, bb.x - pad), y0 = Math.max(0, bb.y - pad);
103
103
  const w = Math.min(FRAME.w - x0, bb.w + 2 * pad), h = Math.min(FRAME.h - y0, bb.h + 2 * pad);
104
+ // ONLY THE WINDOW THAT MATTERS (2026-10-18: six anchors decoded the whole take six times, 48 s on the founder's
105
+ // Air). The caller keeps changes within [pred - 3.2, pred + 1.2]; the decode starts 3.6 s before the prediction and
106
+ // runs 5.2 s. Seeking the mp4 before the input is frame-accurate (decode from the keyframe, drop until the mark),
107
+ // and the reported pts restart at 0, so the window's start is added back.
108
+ const from = around == null ? 0 : Math.max(0, around - 3.6);
104
109
  const res = spawnSync(FFMPEG(), [
105
- "-loglevel", "info", "-i", clean,
110
+ "-loglevel", "info", ...(around == null ? [] : ["-ss", String(from), "-t", "5.2"]), "-i", clean,
106
111
  "-vf", `crop=${w}:${h}:${x0}:${y0},select='gte(scene,0)',metadata=print`, "-f", "null", "-",
107
112
  ], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
108
113
  const err = `${res.stderr ?? ""}`;
109
114
  const rows = [];
110
115
  const re = /pts_time:([0-9.]+)[\s\S]*?lavfi\.scene_score=([0-9.]+)/g;
111
- for (let m; (m = re.exec(err)); ) rows.push({ t: parseFloat(m[1]), s: parseFloat(m[2]) });
116
+ for (let m; (m = re.exec(err)); ) rows.push({ t: parseFloat(m[1]) + from, s: parseFloat(m[2]) });
112
117
  const onsets = [];
113
118
  for (let i = 1; i < rows.length; i++) {
114
119
  if (rows[i].s < 0.004) continue; // not a peak-worthy change
@@ -126,7 +131,7 @@ for (const s of man.steps ?? []) {
126
131
  const wallCross = bboxCrossing(s);
127
132
  if (wallCross == null) continue;
128
133
  const pred = tb.a * wallCross + tb.b;
129
- const changes = roiChanges(s.target.bbox).filter((c) => c >= pred - 3.2 && c <= pred + 1.2);
134
+ const changes = roiChanges(s.target.bbox, pred).filter((c) => c >= pred - 3.2 && c <= pred + 1.2);
130
135
  if (changes.length === 0) continue;
131
136
  anchors.push({ label: s.label ?? s.action, pred, changes, weight: 1 / Math.sqrt(changes.length) });
132
137
  }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // THE SESSION MOVES, NEVER THE CREDENTIALS (founder 2026-10-18, with the person's consent in the app): read the
3
+ // cookies and local storage the app set in the recorder's OWN Chrome profile — ours to drive; never the keychain,
4
+ // never a password — for the one site asked, and hand them to the vault with this machine's key. The vault answers
5
+ // with counts; nothing of the session is printed here.
6
+ // usage: node export-session.mjs <profileDir> <url> (base and key from <cwd>/.recorder/config.json)
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ const [profileDir, url] = process.argv.slice(2);
10
+ if (!profileDir || !url) { console.error("usage: export-session.mjs <profileDir> <url>"); process.exit(2); }
11
+ const cfg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), ".recorder", "config.json"), "utf8"));
12
+ const base = (process.env.RECORDER_BASE || cfg.base || "https://app.demobites.com").replace(/\/+$/, "");
13
+ const { chromium } = await import("playwright");
14
+ const origin = new URL(url).origin;
15
+ const host = new URL(url).hostname;
16
+ const site = host.split(".").slice(-2).join(".");
17
+ const ctx = await chromium.launchPersistentContext(path.resolve(profileDir), { channel: "chrome", headless: true, viewport: { width: 1280, height: 800 } });
18
+ let state;
19
+ try {
20
+ const page = await ctx.newPage();
21
+ await page.goto(origin, { waitUntil: "domcontentloaded", timeout: 30000 }).catch(() => {});
22
+ const cookies = (await ctx.cookies()).filter((c) => c.domain.replace(/^\./, "").endsWith(site));
23
+ const localStorage = await page.evaluate(() => Object.entries(window.localStorage).map(([name, value]) => ({ name, value }))).catch(() => []);
24
+ state = { cookies, origins: [{ origin, localStorage }] };
25
+ } finally { await ctx.close(); }
26
+ if (!state.cookies.length && !state.origins[0].localStorage.length) { console.error(`no session at ${origin} in this profile`); process.exit(3); }
27
+ const res = await fetch(`${base}/api/recorder/session`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.api_key}` }, body: JSON.stringify({ url: origin, state, label: "From my machine" }) });
28
+ const d = await res.json().catch(() => ({}));
29
+ if (!res.ok) { console.error(`the vault refused: ${d.error ?? res.status}`); process.exit(1); }
30
+ console.log(`RESULT ${JSON.stringify({ cookies: d.cookies ?? state.cookies.length, origins: d.origins ?? 1, profile_id: d.profile_id ?? null })}`);
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // THE POOL (founder 2026-10-18: "every user will install the inner one on his machine; the second one is a unique
3
+ // key he never sees… a pool that you're managing behind the scenes"). A box of OURS: it checks in with the pool
4
+ // secret, claims any workspace's cloud job, receives that job's cloud key and the person's session for the length
5
+ // of the take, runs the same chain the laptop runs (plan → record → trim → measure → pack → upload) in a folder of
6
+ // its own, and shreds the folder when the take is up. Ten identities on one box, none written where a neighbour
7
+ // could read them. A reclaim or a drain order stops new claims; running takes finish.
8
+ //
9
+ // node pool.mjs --base https://dev.demobites.com --secret <pool secret> [--slots 5] [--name <box name>]
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { spawn } from "node:child_process";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
17
+ const argv = process.argv.slice(2);
18
+ const flag = (f, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] ?? d : d);
19
+ const base = (flag("--base", process.env.RECORDER_BASE || "https://app.demobites.com")).replace(/\/+$/, "");
20
+ const secret = flag("--secret", process.env.POOL_SECRET || "");
21
+ if (!secret) { console.error("pool: --secret (or POOL_SECRET) is required"); process.exit(2); }
22
+ const HOME = process.env.DEMOBITE_POOL_HOME || path.join(os.homedir(), ".demobite", "pool");
23
+ fs.mkdirSync(HOME, { recursive: true });
24
+ const stamp = () => new Date().toISOString();
25
+ let version = "?"; try { version = JSON.parse(fs.readFileSync(path.join(HERE, "..", "package.json"), "utf8")).version; } catch { try { version = JSON.parse(fs.readFileSync(path.join(HERE, "package.json"), "utf8")).version; } catch { /* unknown */ } }
26
+
27
+ // WHO AM I: the instance's own id and type from the metadata service (IMDSv2), the hostname otherwise.
28
+ const imds = async (p) => {
29
+ try {
30
+ const t = await fetch("http://169.254.169.254/latest/api/token", { method: "PUT", headers: { "X-aws-ec2-metadata-token-ttl-seconds": "60" }, signal: AbortSignal.timeout(1500) });
31
+ if (!t.ok) return null;
32
+ const r = await fetch(`http://169.254.169.254/latest/meta-data/${p}`, { headers: { "X-aws-ec2-metadata-token": await t.text() }, signal: AbortSignal.timeout(1500) });
33
+ return r.ok ? (await r.text()).trim() : null;
34
+ } catch { return null; }
35
+ };
36
+ const instanceId = (await imds("instance-id")) || `local-${os.hostname()}`;
37
+ const instanceType = (await imds("instance-type")) || "local";
38
+ const name = flag("--name", instanceId);
39
+ // CAPACITY (the matrix, measured 2026-10-18 on c7i.2xlarge: 10 clean, 5 within ~90 s end to end): comfort slots by type.
40
+ const COMFORT = { "2xlarge": 5, "4xlarge": 10, "8xlarge": 20, "xlarge": 2, "large": 1 };
41
+ const sizeOf = (t) => Object.keys(COMFORT).find((k) => t.endsWith(`.${k}`));
42
+ const slots = Number(flag("--slots", "")) || COMFORT[sizeOf(instanceType) ?? ""] || Math.max(1, Math.floor(os.cpus().length * 0.6));
43
+
44
+ const api = async (body) => {
45
+ try {
46
+ const res = await fetch(`${base}/api/recorder/pool`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${secret}` }, body: JSON.stringify({ instance_id: instanceId, instance_type: instanceType, name, slots, version, ...body }), signal: AbortSignal.timeout(20000) });
47
+ if (res.status === 401) { console.error(` ${stamp()} the pool secret was refused; stopping`); process.exit(1); }
48
+ return res.ok ? await res.json() : null;
49
+ } catch (e) { console.error(` ${stamp()} pool: ${String(e?.message ?? e).split("\n")[0]}`); return null; }
50
+ };
51
+
52
+ const running = new Map(); // job id → child
53
+ let draining = false;
54
+ const shred = (dir) => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ } };
55
+
56
+ const runJob = async (claim) => {
57
+ const job = claim.job;
58
+ // The take's own door, with the job's cloud key (what a laptop's listener does with its own key).
59
+ const jobApi = async (body) => { try { const r = await fetch(`${base}/api/recorder/listen`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${claim.key}` }, body: JSON.stringify({ job_id: job.id, ...body }), signal: AbortSignal.timeout(20000) }); return r.ok ? await r.json() : null; } catch { return null; } };
60
+ const dir = path.join(HOME, job.id);
61
+ const rec = path.join(dir, ".recorder");
62
+ const profile = path.join(dir, "profile");
63
+ const ledger = path.join(dir, "ledger.json");
64
+ const takeDir = path.join(rec, "takes", job.id);
65
+ const t0 = Date.now();
66
+ try {
67
+ fs.mkdirSync(takeDir, { recursive: true });
68
+ // The job's cloud key: the person's authority to put the take in their workspace, for this folder only.
69
+ fs.writeFileSync(path.join(rec, "config.json"), JSON.stringify({ base, api_key: claim.key, workspace: claim.workspace ?? null, pool: true }), { mode: 0o600 });
70
+ fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model: job.model ?? null, review_storyboard: job.review_storyboard !== false, code_repo: job.code_repo ?? null, retake_of: job.retake_of ?? null, retake_note: job.retake_note ?? null, storyboard: job.storyboard ?? null, cloud: true }, null, 2));
71
+ // The person's session → a profile of this take's own; the ledger says the site is signed in so no window opens.
72
+ const origins = {};
73
+ for (const o of claim.session?.origins ?? []) if (o?.origin) origins[o.origin] = new Date().toISOString();
74
+ try { if (job.url) origins[new URL(job.url).origin] = origins[new URL(job.url).origin] ?? new Date().toISOString(); } catch { /* no url */ }
75
+ fs.writeFileSync(ledger, JSON.stringify({ origins, who: {} }, null, 2));
76
+ const sessionFile = path.join(dir, "session.json");
77
+ fs.writeFileSync(sessionFile, JSON.stringify(claim.session ?? { cookies: [], origins: [] }), { mode: 0o600 });
78
+ console.log(`\n ▶ ${stamp()} job ${job.id} (${claim.workspace?.name ?? "workspace"}): ${String(job.brief).slice(0, 90)}…`);
79
+ await new Promise((resolve) => { const s = spawn(process.execPath, [path.join(HERE, "seed-profile.mjs"), profile, sessionFile], { stdio: "inherit", cwd: dir }); s.on("close", resolve); });
80
+ shred(sessionFile);
81
+ const child = spawn(process.execPath, [path.join(HERE, "plan.mjs"), takeDir], {
82
+ cwd: dir, stdio: "inherit",
83
+ env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1", RECORDER_PROFILE: profile, RECORDER_LEDGER: ledger, DEMOBITES_NO_CODE: "1", RECORDER_POOL: instanceId },
84
+ detached: process.platform !== "win32",
85
+ });
86
+ running.set(job.id, child);
87
+ const code = await new Promise((resolve) => child.on("close", resolve));
88
+ const st = await jobApi({ action: "poll" });
89
+ if (st && st.status && !["ready", "failed", "cancelled"].includes(st.status)) await jobApi({ action: "event", kind: "failed", payload: { message: code === 0 ? "The demo did not come through — try again" : `The take stopped (exit ${code})` } });
90
+ console.log(` ■ ${stamp()} job ${job.id} ${st?.status ?? "ended"} in ${Math.round((Date.now() - t0) / 1000)} s\n`);
91
+ } catch (e) {
92
+ console.error(` ! ${stamp()} job ${job.id}: ${String(e?.message ?? e).split("\n")[0]}`);
93
+ await jobApi({ action: "event", kind: "failed", payload: { message: `The cloud recorder could not run the take (${String(e?.message ?? e).slice(0, 120)})` } });
94
+ } finally {
95
+ running.delete(job.id);
96
+ shred(dir); // the key, the session, the profile, the take: gone with the folder
97
+ await api({ action: "checkin", busy: [...running.keys()], draining });
98
+ }
99
+ };
100
+
101
+ const bye = () => { draining = true; for (const c of running.values()) { try { process.kill(process.platform === "win32" ? c.pid : -c.pid, "SIGTERM"); } catch { /* gone */ } } setTimeout(() => process.exit(0), 1500); };
102
+ process.on("SIGTERM", bye); process.on("SIGINT", bye);
103
+
104
+ console.log(`demobite pool ${version} — ${name} (${instanceType}), ${slots} cameras, ${base}`);
105
+ for (;;) {
106
+ const r = await api({ action: "checkin", busy: [...running.keys()], draining });
107
+ if (r?.drain && !draining) { draining = true; console.log(` ${stamp()} drain: no new takes; ${running.size} still running`); }
108
+ if (r?.stop && running.size === 0) { console.log(` ${stamp()} stop: the pool says goodbye`); process.exit(0); }
109
+ if (!draining && running.size < slots) {
110
+ const c = await api({ action: "claim" });
111
+ if (c?.job && c?.key) { void runJob(c); await new Promise((s) => setTimeout(s, 1500)); continue; }
112
+ }
113
+ await new Promise((s) => setTimeout(s, 5000));
114
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // SEED A PROFILE FROM A SESSION (the cloud pool, 2026-10-18): the person's session (a Playwright storageState:
3
+ // cookies and local storage, exported from their own machine with their consent) becomes a fresh Chrome profile
4
+ // the planner's look, probe and the camera all drive. Nothing else goes in; the folder is shredded after the take.
5
+ // usage: node seed-profile.mjs <profileDir> <session.json>
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ const [profileDir, sessionFile] = process.argv.slice(2);
9
+ if (!profileDir || !sessionFile) { console.error("usage: seed-profile.mjs <profileDir> <session.json>"); process.exit(2); }
10
+ const state = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
11
+ const { chromium } = await import("playwright");
12
+ fs.mkdirSync(profileDir, { recursive: true });
13
+ const ctx = await chromium.launchPersistentContext(path.resolve(profileDir), { channel: "chrome", headless: true, viewport: { width: 1280, height: 800 } });
14
+ try {
15
+ const cookies = (state.cookies ?? []).map((c) => ({ ...c, sameSite: ["Strict", "Lax", "None"].includes(c.sameSite) ? c.sameSite : "Lax" }));
16
+ if (cookies.length) await ctx.addCookies(cookies);
17
+ const page = await ctx.newPage();
18
+ for (const o of state.origins ?? []) {
19
+ if (!o?.origin || !o.localStorage?.length) continue;
20
+ try {
21
+ await page.goto(o.origin, { waitUntil: "domcontentloaded", timeout: 20000 });
22
+ await page.evaluate((items) => { for (const { name, value } of items) window.localStorage.setItem(name, value); }, o.localStorage);
23
+ } catch (e) { console.log(`seed: local storage for ${o.origin} skipped (${String(e?.message ?? e).split("\n")[0]})`); }
24
+ }
25
+ console.log(`seeded ${cookies.length} cookies and ${(state.origins ?? []).reduce((n, o) => n + (o.localStorage?.length ?? 0), 0)} local-storage keys`);
26
+ } finally { await ctx.close(); }
package/scripts/trim.mjs CHANGED
@@ -40,7 +40,7 @@ const clean = path.join(dir, "clean.mp4");
40
40
  // tail into a fake 1.027x, and a scene-change fit anchored on a modal that
41
41
  // took 0.49s to load tilted 0.897x. Both produced ramps of error that read as
42
42
  // a broken cursor.
43
- const r0 = man.record_from ?? 0;
43
+ let r0 = man.record_from ?? 0;
44
44
 
45
45
  // Head-beacon phase 0: the identity law above fixes the RATE; the beacon
46
46
  // fixes the ANCHOR. record.mjs flashed the blank page full-frame at stamped
@@ -105,7 +105,36 @@ if ((man.beacon?.flips?.length ?? 0) >= 3) {
105
105
  }
106
106
  }
107
107
 
108
- const cutAt = Math.max(0, r0 - beaconDelta);
108
+ let cutAt = Math.max(0, r0 - beaconDelta);
109
+
110
+ // THE FIRST FRAME IS A LOADED PAGE (founder 2026-10-18, bite 1236: "you started without the In-app Pulse loaded…
111
+ // after 3 seconds everything is loaded… use the trim at the start"). The camera's still-picture gate waits for the
112
+ // outer page; an inner panel can keep loading after it. So the head of the clean take is scanned for visible change
113
+ // in its first seconds, and when the picture kept changing, the cut moves to the last change plus a breath, and
114
+ // record_from moves with it (the timebase stays video = wall - record_from). Bounded at 3.5 s; a still head is
115
+ // untouched.
116
+ const HEAD_SCAN_S = 4.0;
117
+ const HEAD_MAX_SHIFT_S = 3.5;
118
+ try {
119
+ const res = spawnSync(FFMPEG(), ["-hide_banner", "-loglevel", "info", "-i", raw, "-vf", `trim=start=${cutAt}:end=${cutAt + HEAD_SCAN_S},setpts=PTS-STARTPTS,scale=480:-2,select='gt(scene,0.02)',showinfo`, "-f", "null", "-"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
120
+ const changes = [];
121
+ for (const m of `${res.stderr ?? ""}`.matchAll(/pts_time:([0-9.]+)/g)) changes.push(parseFloat(m[1]));
122
+ const late = changes.filter((t) => t > 0.3 && t < HEAD_SCAN_S - 0.3);
123
+ if (late.length) {
124
+ const shift = Math.min(HEAD_MAX_SHIFT_S, Math.round((late[late.length - 1] + 0.25) * 100) / 100);
125
+ console.log(`first frame: the picture kept changing for ${late[late.length - 1].toFixed(2)}s after the cut (${late.length} change${late.length === 1 ? "" : "s"}) — cutting ${shift.toFixed(2)}s later, to a loaded page`);
126
+ void postEvent("finishing", { text: "Cutting to the first loaded frame…" });
127
+ cutAt += shift;
128
+ r0 += shift;
129
+ man.record_from = r0;
130
+ man.first_frame_shift_s = shift;
131
+ // Nothing in the story happens before its first frame.
132
+ for (const st of man.steps ?? []) { if (Number(st.t_start) < r0) st.t_start = r0; if (Number(st.t_end) < r0) st.t_end = r0; }
133
+ for (const sh of man.shots ?? []) { if (Number(sh.t_start) < r0) sh.t_start = r0; }
134
+ } else console.log(`first frame: still (${changes.length} change${changes.length === 1 ? "" : "s"} in the first ${HEAD_SCAN_S}s, none after 0.3s)`);
135
+ } catch (e) {
136
+ console.log(`first frame: scan skipped (${String(e?.message ?? e).split("\n")[0]})`);
137
+ }
109
138
 
110
139
  // TAIL law (vanilla-Linux smoke 2026-09-09): raw.webm keeps receiving frames
111
140
  // while the browser context tears down — on a headless shell that was a
@@ -166,12 +195,23 @@ man.timebase = {
166
195
  fs.writeFileSync(manPath, JSON.stringify(man, null, 2));
167
196
  console.log(`timebase: video = (wall - ${r0.toFixed(3)})${speed === 1 ? "" : ` / ${speed}`}${beaconMethod ? ` (cut at raw ${cutAt.toFixed(3)}s)` : ""}`);
168
197
 
198
+ // THE MACHINE'S OWN ENCODER (founder 2026-10-18: "saving the take… this should be 5 seconds"; the trim took 39.6 s on
199
+ // his Air, most of it libx264 at preset medium). Apple's VideoToolbox encodes the same h264 several times faster;
200
+ // elsewhere libx264 at a faster preset. The ingest wants h264 at 30 fps in an mp4, which both give.
201
+ const ENCODER = (() => {
202
+ try {
203
+ const enc = execFileSync(FFMPEG(), ["-hide_banner", "-encoders"], { stdio: ["ignore", "pipe", "ignore"] }).toString();
204
+ if (process.platform === "darwin" && /\bh264_videotoolbox\b/.test(enc)) return ["-c:v", "h264_videotoolbox", "-b:v", "3M", "-maxrate", "4M", "-bufsize", "8M", "-profile:v", "high", "-allow_sw", "1", "-g", "60"];
205
+ } catch { /* libx264 */ }
206
+ return ["-c:v", "libx264", "-preset", "veryfast", "-crf", "19", "-g", "60"];
207
+ })();
208
+ console.log(`encoder: ${ENCODER[1]}`);
169
209
  execFileSync(FFMPEG(), [
170
210
  "-y", "-loglevel", "error",
171
211
  "-i", raw,
172
212
  "-filter_complex", `[0:v]trim=${tailCut ? `start=${cutAt}:end=${tailCut}` : `start=${cutAt}`},setpts=(PTS-STARTPTS)/${speed},fps=30,format=yuv420p[out]`,
173
213
  "-map", "[out]",
174
- "-c:v", "libx264", "-preset", "medium", "-crf", "19",
214
+ ...ENCODER,
175
215
  "-movflags", "+faststart",
176
216
  clean,
177
217
  ], { stdio: "inherit" });
@@ -184,7 +224,7 @@ execFileSync(FFMPEG(), [
184
224
  try {
185
225
  const filters = execFileSync(FFMPEG(), ["-hide_banner", "-filters"], { stdio: ["ignore", "pipe", "ignore"] }).toString();
186
226
  if (/\bfreezeframes\b/.test(filters) && /\bsignalstats\b/.test(filters)) {
187
- const stats = spawnSync(FFMPEG(), ["-hide_banner", "-loglevel", "error", "-i", clean, "-vf", "crop=iw/2:ih/2:iw/2:ih/2,signalstats,metadata=print:file=-", "-f", "null", "-"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
227
+ const stats = spawnSync(FFMPEG(), ["-hide_banner", "-loglevel", "error", "-i", clean, "-vf", "crop=iw/2:ih/2:iw/2:ih/2,scale=240:-2,signalstats,metadata=print:file=-", "-f", "null", "-"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
188
228
  const bad = [];
189
229
  let frame = -1, yavg = null, ymin = null, ymax = null;
190
230
  const flush = () => { if (frame >= 0 && yavg !== null && yavg >= 118 && yavg <= 138 && ymin !== null && ymax !== null && ymax - ymin <= 24) bad.push(frame); };
@@ -208,7 +248,7 @@ try {
208
248
  graph += `[a${chain.length}]format=yuv420p[out]`;
209
249
  const fixed = path.join(dir, "clean-fixed.mp4");
210
250
  void postEvent("finishing", { text: `Replacing ${bad.length} glitch frame${bad.length === 1 ? "" : "s"}…` });
211
- execFileSync(FFMPEG(), ["-y", "-loglevel", "error", "-i", clean, "-filter_complex", graph, "-map", "[out]", "-c:v", "libx264", "-preset", "medium", "-crf", "19", "-movflags", "+faststart", fixed], { stdio: "inherit" });
251
+ execFileSync(FFMPEG(), ["-y", "-loglevel", "error", "-i", clean, "-filter_complex", graph, "-map", "[out]", ...ENCODER, "-movflags", "+faststart", fixed], { stdio: "inherit" });
212
252
  fs.renameSync(fixed, clean);
213
253
  man.glitch = { frames: bad.length, windows: chain.map((r) => ({ first: r.first, last: r.last })) };
214
254
  fs.writeFileSync(manPath, JSON.stringify(man, null, 2));