demobite 1.0.7 → 1.0.9
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/package.json +1 -1
- package/scripts/calibrate.mjs +34 -7
- package/scripts/record.mjs +20 -15
- package/scripts/trim.mjs +40 -15
- package/skill/scripts/retake.mjs +1 -1
- package/skill/scripts/upload.mjs +6 -0
package/package.json
CHANGED
package/scripts/calibrate.mjs
CHANGED
|
@@ -131,10 +131,20 @@ console.log(`calibrate: map video = ${tb.a} * wall + ${tb.b.toFixed(3)} (${tb.me
|
|
|
131
131
|
console.log(`calibrate: ${anchors.length} hover anchor${anchors.length === 1 ? "" : "s"} usable`);
|
|
132
132
|
|
|
133
133
|
let anchored = false;
|
|
134
|
+
// BEACON OUTRANKS ANCHORS (2026-09-02, second prod bite-96 take): the head
|
|
135
|
+
// beacon had measured the cut to +-14 ms, yet ONE hover anchor locked onto a
|
|
136
|
+
// stray pixel change 2.66 s away and dragged the clock with it. When the
|
|
137
|
+
// beacon has spoken (trim stamped beaconDelta from >=3 matched flashes),
|
|
138
|
+
// anchors may only REFINE the clock inside a third of a second; they never
|
|
139
|
+
// re-decide it. Without a beacon the wide sweep stays (the pre-beacon world).
|
|
140
|
+
const beaconAnchored = typeof tb?.beaconDelta === "number";
|
|
141
|
+
const SWEEP_LO = beaconAnchored ? -0.35 : -3.0;
|
|
142
|
+
const SWEEP_HI = beaconAnchored ? 0.35 : 1.0;
|
|
143
|
+
if (beaconAnchored) console.log(`calibrate: beacon-anchored clock — hover anchors may refine by at most ${SWEEP_HI.toFixed(2)}s`);
|
|
134
144
|
if (anchors.length >= 2) {
|
|
135
145
|
const TOL = 0.067; // two frames
|
|
136
146
|
let best = { delta: 0, score: -1, resid: Infinity };
|
|
137
|
-
for (let d =
|
|
147
|
+
for (let d = SWEEP_LO; d <= SWEEP_HI + 0.0001; d += 1 / 60) {
|
|
138
148
|
let score = 0, resid = 0, hits = 0;
|
|
139
149
|
for (const a of anchors) {
|
|
140
150
|
const target = a.pred + d;
|
|
@@ -171,21 +181,27 @@ if (anchors.length >= 2) {
|
|
|
171
181
|
console.log(` anchor ${a.label}: no flip within tolerance at this offset (skipped)`);
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
if (residuals.length < 2) {
|
|
185
|
+
// One surviving anchor is an opinion, not a measurement.
|
|
186
|
+
console.log(`calibrate: only ${residuals.length} anchor survived the lock — clock left ${beaconAnchored ? "on the beacon" : "as stamped"}.`);
|
|
187
|
+
anchored = beaconAnchored;
|
|
188
|
+
best.delta = 0;
|
|
189
|
+
}
|
|
190
|
+
const rms = residuals.length >= 2 ? Math.sqrt(residuals.reduce((s, r) => s + r * r, 0) / residuals.length) : 0;
|
|
191
|
+
if (residuals.length >= 2 && rms > 0.08) {
|
|
176
192
|
console.error(`CALIBRATE FAILED: hover-anchor rms ${(rms * 1000).toFixed(0)}ms exceeds 80ms — footage disagrees with itself. Do not upload.`);
|
|
177
193
|
process.exit(1);
|
|
178
194
|
}
|
|
179
|
-
if (Math.abs(best.delta) > 0.005) {
|
|
195
|
+
if (residuals.length >= 2 && Math.abs(best.delta) > 0.005) {
|
|
180
196
|
tb.b += best.delta;
|
|
181
197
|
tb.videoRecordFrom = -tb.b;
|
|
182
198
|
tb.method += ` + hover-anchor ${best.delta.toFixed(3)}s (${best.hits} anchors, rms ${(rms * 1000).toFixed(0)}ms)`;
|
|
183
199
|
fs.writeFileSync(manPath, JSON.stringify(man, null, 2));
|
|
184
200
|
console.log(`calibrate: hover anchors moved the clock ${best.delta.toFixed(3)}s — b is now ${tb.b.toFixed(3)} (rms ${(rms * 1000).toFixed(0)}ms)`);
|
|
185
|
-
} else {
|
|
201
|
+
} else if (residuals.length >= 2) {
|
|
186
202
|
console.log(`calibrate: hover anchors confirm the stamp as-is (rms ${(rms * 1000).toFixed(0)}ms)`);
|
|
187
203
|
}
|
|
188
|
-
anchored = true;
|
|
204
|
+
if (residuals.length >= 2) anchored = true;
|
|
189
205
|
}
|
|
190
206
|
}
|
|
191
207
|
|
|
@@ -221,6 +237,8 @@ if (scenes.length === 0) {
|
|
|
221
237
|
process.exit(0);
|
|
222
238
|
}
|
|
223
239
|
|
|
240
|
+
const SWEEP_LO_FB = beaconAnchored ? -0.35 : -4;
|
|
241
|
+
const SWEEP_HI_FB = beaconAnchored ? 0.35 : 1.0;
|
|
224
242
|
if (!anchored) {
|
|
225
243
|
// Old plausible-latency search. Kept ONLY as the no-anchor fallback; its
|
|
226
244
|
// known failure mode (plausible != true, 0.237s off on a real take) is why
|
|
@@ -237,7 +255,7 @@ if (!anchored) {
|
|
|
237
255
|
return total;
|
|
238
256
|
};
|
|
239
257
|
let bestDelta = 0, bestScore = score(0);
|
|
240
|
-
for (let d =
|
|
258
|
+
for (let d = SWEEP_LO_FB; d <= SWEEP_HI_FB + 0.0001; d += 1 / 30) {
|
|
241
259
|
const sc = score(d);
|
|
242
260
|
if (sc < bestScore - 1e-9) { bestScore = sc; bestDelta = d; }
|
|
243
261
|
}
|
|
@@ -272,6 +290,15 @@ if (!anchored && (median < -0.15 || median > 0.9)) {
|
|
|
272
290
|
);
|
|
273
291
|
process.exit(1);
|
|
274
292
|
}
|
|
293
|
+
if (!beaconAnchored && anchored && (median < -0.6 || median > 2.5)) {
|
|
294
|
+
// INCIDENT 2026-09-02: +4.6 s "latency" on an anchored clock was the head
|
|
295
|
+
// miscut showing through (the anchors had locked onto the wrong beat).
|
|
296
|
+
console.error(`VERIFY FAILED: median click latency ${median.toFixed(3)}s on an anchor-measured clock is not an app delay, it is a broken timebase (the cut point or the anchor lock is wrong). Do not upload — film again.`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
if (beaconAnchored && (median < -0.6 || median > 2.5)) {
|
|
300
|
+
console.log(`WARNING: median click latency ${median.toFixed(3)}s on a beacon-measured clock — the clock is trusted, so the app genuinely answered late. The footage shows the wait; judge whether the beat reads as slow.`);
|
|
301
|
+
}
|
|
275
302
|
if (anchored && (median < -0.15 || median > 0.9)) {
|
|
276
303
|
console.log(`note: median click latency ${median.toFixed(3)}s is unusual, but the clock is anchor-measured — likely a genuinely slow app response. Judge the take on duration and feel.`);
|
|
277
304
|
}
|
package/scripts/record.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const DIR = path.resolve(outArg);
|
|
|
40
40
|
// RE-TAKE (founder 2026-09-02): the storyboard IS the bite's DNA. Keep a verbatim
|
|
41
41
|
// copy in the take dir so upload.mjs can stage it as the recording recipe —
|
|
42
42
|
// selectors, urls, typed text, hideCss — the wire manifest alone cannot re-film.
|
|
43
|
-
const ENGINE_VERSION = "1.0.
|
|
43
|
+
const ENGINE_VERSION = "1.0.9";
|
|
44
44
|
fs.mkdirSync(DIR, { recursive: true });
|
|
45
45
|
// The copy is the RECIPE, not the transport: cdpWsUrl / storageStatePath are
|
|
46
46
|
// per-take plumbing stamped by the cloud runner (a CDP url carries a session
|
|
@@ -615,6 +615,7 @@ async function smoothScroll(dy, ms = 1400, within = null) {
|
|
|
615
615
|
}), [dy, ms, within]);
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
const BEACON_GAPS_MS = [300, 450, 600, 750, 900, 1050];
|
|
618
619
|
// ── Head beacon ────────────────────────────────────────────────────────────
|
|
619
620
|
// The identity law (video = wall - record_from) assumes raw.webm's t=0 lines
|
|
620
621
|
// up with T0. That anchoring is otherwise UNMEASURED: on a cold remote-CDP
|
|
@@ -634,22 +635,26 @@ try {
|
|
|
634
635
|
// stamp error at half a CDP round trip — inside one frame co-located.
|
|
635
636
|
return { color, wall: (before + after) / 2 };
|
|
636
637
|
};
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
638
|
+
// GAP-CODED PATTERN (2026-09-02, after the prod bite-96 miscut): nothing
|
|
639
|
+
// inside this process can see whether the screencast is rolling yet (the
|
|
640
|
+
// video file does not grow while recording), and under load the first
|
|
641
|
+
// frame can land seconds after the page exists. So the beacon no longer
|
|
642
|
+
// relies on being seen from its first flash: seven flashes with strictly
|
|
643
|
+
// increasing gaps form a signature trim.mjs recognises from ANY three
|
|
644
|
+
// consecutive flashes it finds. A screencast that started late still
|
|
645
|
+
// yields the anchor; one that missed every flash stops the take in trim.
|
|
646
646
|
await page.evaluate(() => { document.documentElement.style.background = "#ffffff"; });
|
|
647
|
-
await page.waitForTimeout(
|
|
647
|
+
await page.waitForTimeout(1500); // let the page exist and the white state settle
|
|
648
648
|
const flips = [];
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
649
|
+
let color = "#ff00ff";
|
|
650
|
+
for (const gapMs of BEACON_GAPS_MS) {
|
|
651
|
+
flips.push(await flip(color));
|
|
652
|
+
color = color === "#ff00ff" ? "#ffffff" : "#ff00ff";
|
|
653
|
+
await page.waitForTimeout(gapMs);
|
|
654
|
+
}
|
|
655
|
+
flips.push(await flip(color)); // final onset closes the last gap
|
|
656
|
+
if (color === "#ff00ff") { await page.waitForTimeout(250); flips.push(await flip("#ffffff")); }
|
|
657
|
+
await page.waitForTimeout(300);
|
|
653
658
|
manifest.beacon = { flips };
|
|
654
659
|
process.stdout.write(`beacon: flips at ${flips.map((f) => f.wall.toFixed(3)).join("s, ")}s (wall)\n`);
|
|
655
660
|
} catch (e) {
|
package/scripts/trim.mjs
CHANGED
|
@@ -56,32 +56,57 @@ const r0 = man.record_from ?? 0;
|
|
|
56
56
|
// Shift the cut by it so clean.mp4 truly begins at the record_from moment.
|
|
57
57
|
let beaconDelta = 0;
|
|
58
58
|
let beaconMethod = "";
|
|
59
|
-
if ((man.beacon?.flips?.length ?? 0) >=
|
|
59
|
+
if ((man.beacon?.flips?.length ?? 0) >= 3) {
|
|
60
60
|
const flips = man.beacon.flips;
|
|
61
61
|
// The flashes are the first big whole-frame changes in the head. Search a
|
|
62
62
|
// window generous enough for seconds of anchor error in either direction.
|
|
63
|
-
const searchEnd = Math.min(Math.max(r0, flips[flips.length - 1].wall) +
|
|
63
|
+
const searchEnd = Math.min(Math.max(r0, flips[flips.length - 1].wall) + 10, 45);
|
|
64
64
|
const res = spawnSync("ffmpeg", [
|
|
65
65
|
"-loglevel", "info", "-t", String(searchEnd), "-i", raw,
|
|
66
66
|
"-vf", "select='gt(scene,0.3)',showinfo", "-f", "null", "-",
|
|
67
67
|
], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
68
68
|
const onsets = [...`${res.stderr ?? ""}`.matchAll(/pts_time:([0-9.]+)/g)]
|
|
69
69
|
.map((m) => parseFloat(m[1]));
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
// Align the KNOWN flash gaps against the DETECTED onset gaps: slide every
|
|
71
|
+
// flip index against every onset index and count consecutive gap matches
|
|
72
|
+
// (tolerance 70 ms). Three flashes in a row (two matching gaps) identify
|
|
73
|
+
// the pattern unambiguously because the gaps strictly increase; the
|
|
74
|
+
// alignment with the most matches wins. Missing leading flashes (a
|
|
75
|
+
// screencast that started late) cost nothing but the pairs they would
|
|
76
|
+
// have added.
|
|
77
|
+
const TOL = 0.07;
|
|
78
|
+
let best = null;
|
|
79
|
+
for (let i = 0; i < flips.length - 2; i++) {
|
|
80
|
+
for (let j = 0; j < onsets.length - 2; j++) {
|
|
81
|
+
let k = 0;
|
|
82
|
+
while (i + k + 1 < flips.length && j + k + 1 < onsets.length) {
|
|
83
|
+
const gf = flips[i + k + 1].wall - flips[i + k].wall;
|
|
84
|
+
const go = onsets[j + k + 1] - onsets[j + k];
|
|
85
|
+
if (Math.abs(gf - go) > TOL) break;
|
|
86
|
+
k++;
|
|
87
|
+
}
|
|
88
|
+
if (k >= 2 && (!best || k > best.k)) best = { i, j, k };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (best) {
|
|
92
|
+
const deltas = [];
|
|
93
|
+
for (let m = 0; m <= best.k; m++) deltas.push(flips[best.i + m].wall - onsets[best.j + m]);
|
|
94
|
+
const spread = Math.max(...deltas) - Math.min(...deltas);
|
|
95
|
+
beaconDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
|
|
96
|
+
const missed = best.i;
|
|
97
|
+
beaconMethod = ` + head-beacon anchor ${beaconDelta >= 0 ? "+" : ""}${beaconDelta.toFixed(3)}s (${deltas.length} flashes matched, ${missed} missed, spread ${(spread * 1000).toFixed(0)}ms)`;
|
|
98
|
+
console.log(`beacon: anchor error ${beaconDelta >= 0 ? "+" : ""}${beaconDelta.toFixed(3)}s measured from ${deltas.length} flashes (${missed} before the first frame, spread ${(spread * 1000).toFixed(0)}ms) — cut corrected`);
|
|
99
|
+
if (spread > 0.15) {
|
|
100
|
+
console.error(`BEACON FAILED: matched flashes disagree by ${(spread * 1000).toFixed(0)}ms. The cut point cannot be trusted — do not upload, film again.`);
|
|
101
|
+
process.exit(3);
|
|
82
102
|
}
|
|
83
103
|
} else {
|
|
84
|
-
|
|
104
|
+
// INCIDENT 2026-09-02: a missed beacon staged a take whose head was cut
|
|
105
|
+
// seconds late (screencast started after the flashes). The stamped cut
|
|
106
|
+
// is only right when the screencast began at wall 0, which nothing
|
|
107
|
+
// guarantees — so a recorded-but-unfound beacon is a hard stop.
|
|
108
|
+
console.error(`BEACON FAILED: no run of the flash pattern found in the raw head (${onsets.length} onsets seen). The startup lead is unmeasured and the cut point cannot be trusted. Do not upload — film again (a busy machine delays the screencast; let builds finish first).`);
|
|
109
|
+
process.exit(3);
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
112
|
|
package/skill/scripts/retake.mjs
CHANGED
|
@@ -72,5 +72,5 @@ r = run("trim.mjs", [takeDir]); if (r.status !== 0) process.exit(r.status ?? 1);
|
|
|
72
72
|
r = run("calibrate.mjs", [takeDir]);
|
|
73
73
|
if (r.status !== 0) { console.error("Calibration failed — do not stage this take. Investigate record_from / anchors and film again."); process.exit(3); }
|
|
74
74
|
r = run("manifest.mjs", [takeDir]); if (r.status !== 0) process.exit(r.status ?? 1);
|
|
75
|
-
r = run("upload.mjs", [takeDir, "--retake-of", String(biteId)]);
|
|
75
|
+
r = run("upload.mjs", [takeDir, "--retake-of", String(biteId), ...(note ? ["--note", note] : [])]);
|
|
76
76
|
process.exit(r.status ?? 0);
|
package/skill/scripts/upload.mjs
CHANGED
|
@@ -35,6 +35,11 @@ if (!dir) {
|
|
|
35
35
|
const retakeIdx = process.argv.indexOf("--retake-of");
|
|
36
36
|
const retakeOfBiteId = retakeIdx >= 0 ? Number(process.argv[retakeIdx + 1]) : null;
|
|
37
37
|
if (retakeIdx >= 0 && !(Number.isInteger(retakeOfBiteId) && retakeOfBiteId > 0)) { console.error("--retake-of needs a bite id"); process.exit(2); }
|
|
38
|
+
// `--note "<what changed>"` (device lane, founder 2026-09-02): the human's note
|
|
39
|
+
// rides in recipe.config.retake_note so the in-app preview and the recording
|
|
40
|
+
// history show why this version was filmed. Never a secret, never required.
|
|
41
|
+
const noteIdx = process.argv.indexOf("--note");
|
|
42
|
+
const retakeNote = noteIdx >= 0 ? String(process.argv[noteIdx + 1] ?? "").trim().slice(0, 600) : "";
|
|
38
43
|
const cfgPath = path.resolve(".recorder", "config.json");
|
|
39
44
|
let cfg = {};
|
|
40
45
|
try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {}
|
|
@@ -57,6 +62,7 @@ try {
|
|
|
57
62
|
const rc = fs.existsSync(rcPath) ? JSON.parse(fs.readFileSync(rcPath, "utf8")) : {};
|
|
58
63
|
recipe = { version: 1, lane: rc.lane ?? "skill", engine: rc.engine ?? null, storyboard: JSON.parse(fs.readFileSync(sbPath, "utf8")), config: rc.config ?? {} };
|
|
59
64
|
if (recipe.config && "api_key" in recipe.config) delete recipe.config.api_key;
|
|
65
|
+
if (retakeNote) recipe.config = { ...(recipe.config ?? {}), retake_note: retakeNote };
|
|
60
66
|
}
|
|
61
67
|
} catch (e) { console.error("recipe skipped:", e.message); }
|
|
62
68
|
if (!fs.existsSync(cleanPath)) { console.error(`${cleanPath} not found. Run: node scripts/trim.mjs ${dir}`); process.exit(1); }
|