demobites 1.33.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/calibrate.mjs +9 -4
- package/scripts/trim.mjs +45 -5
package/package.json
CHANGED
package/scripts/calibrate.mjs
CHANGED
|
@@ -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
|
}
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]",
|
|
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));
|