demobite 1.15.0 → 1.17.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/launcher/index.mjs
CHANGED
|
@@ -332,7 +332,7 @@ if (arg === "listen") {
|
|
|
332
332
|
await event(job.id, "started", { machine: name });
|
|
333
333
|
const takeDir = path.join(process.cwd(), ".recorder", "takes", job.id);
|
|
334
334
|
fs.mkdirSync(takeDir, { recursive: true });
|
|
335
|
-
fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model, review_storyboard: job.review_storyboard !== false }, null, 2) + "\n");
|
|
335
|
+
fs.writeFileSync(path.join(takeDir, "job.json"), JSON.stringify({ id: job.id, brief: job.brief, url: job.url ?? null, model, review_storyboard: job.review_storyboard !== false, ...(job.retake_of ? { retake_of: job.retake_of, retake_note: job.retake_note ?? null, storyboard: job.storyboard ?? null } : {}) }, null, 2) + "\n");
|
|
336
336
|
// Spawned async (1.14.1): a spawnSync here blocked the event loop, so the 5 s heartbeat never fired
|
|
337
337
|
// during a take and the rail called the machine offline mid-recording (founder 2026-10-15 17:39).
|
|
338
338
|
const child = (await import("node:child_process")).spawn("node", [path.join(dest, "scripts", "plan.mjs"), takeDir], { cwd: process.cwd(), stdio: "inherit", env: { ...process.env, RECORDER_JOB: job.id, RECORDER_BASE: base, RECORDER_LISTEN: "1" } });
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// THE CAMERA'S CLOCK (2026-10-15). What a storyboard step really costs on the camera, so the
|
|
2
|
+
// planner's total is the take's total. Numbers from record.mjs: estimateSpeech = words / 2.6 + 0.4;
|
|
3
|
+
// the hold is speech + 0.5 s breath measured from arrival; a click or hover spends ~1 s on cursor
|
|
4
|
+
// flight and dwell before arrival; the first narrated beat carries STARTUP_LEAD_SLACK_S = 2.5;
|
|
5
|
+
// the take closes on CLOSING_BEAT_MS = 2200. Keep the two in step.
|
|
6
|
+
export const CLOSING_S = 2.2;
|
|
7
|
+
const ms = (v, fallback) => (v == null ? fallback : v < 60 ? v * 1000 : v);
|
|
8
|
+
const speech = (text) => { const n = String(text ?? "").trim().split(/\s+/).filter(Boolean).length; return n ? n / 2.6 + 0.4 + 0.5 : 0; };
|
|
9
|
+
export const secondsOf = (s, firstNarrated) => {
|
|
10
|
+
const text = typeof s.narration === "string" ? s.narration : s.narration?.text ?? "";
|
|
11
|
+
const say = speech(text);
|
|
12
|
+
const slack = say && firstNarrated ? 2.5 : 0;
|
|
13
|
+
switch (s.action) {
|
|
14
|
+
case "goto": return 0.1;
|
|
15
|
+
case "settle": return Math.max(ms(s.ms, 1200) / 1000, say) + slack;
|
|
16
|
+
case "scroll": return Math.max(ms(s.ms, 1900) / 1000, say) + slack;
|
|
17
|
+
case "click":
|
|
18
|
+
case "hover": return 1.0 + Math.max(ms(s.after, 2000) / 1000, say) + slack;
|
|
19
|
+
case "type": return 1.0 + String(s.text ?? "").length * 0.05 + Math.max(ms(s.after, 1500) / 1000, say) + slack;
|
|
20
|
+
case "expect": return 0.3;
|
|
21
|
+
default: return 1 + say;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const estimateTake = (steps) => {
|
|
25
|
+
let first = true, total = CLOSING_S;
|
|
26
|
+
for (const s of steps ?? []) { total += secondsOf(s, first); if (s.narration) first = false; }
|
|
27
|
+
return total;
|
|
28
|
+
};
|
|
@@ -347,6 +347,10 @@ const wire = {
|
|
|
347
347
|
frame: { width: FW, height: FH },
|
|
348
348
|
// BROWSER HEADER (2026-09-16): true by default; DemoBites adds the dark macOS bar on top at ingest. A workspace rule "No browser header" sets it false.
|
|
349
349
|
...(typeof man.browserHeader === "boolean" ? { browserHeader: man.browserHeader } : {}),
|
|
350
|
+
// APPROVED STORY (founder 2026-10-15, bite 1228: "the storyteller is blabbing"): the lines came from the
|
|
351
|
+
// director and the person said Film it to THESE lines. DemoBites keeps them verbatim and never writes
|
|
352
|
+
// a filler into a silence or a longer line into a longer beat.
|
|
353
|
+
narration_approved: true,
|
|
350
354
|
duration,
|
|
351
355
|
steps,
|
|
352
356
|
camera,
|
package/skill/scripts/plan.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// reads is logged on the take (<takeDir>/code-shared.log) so the person can
|
|
19
19
|
// see exactly what left the machine.
|
|
20
20
|
import fs from "node:fs";
|
|
21
|
+
import { estimateTake } from "./clock.mjs";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import { spawnSync } from "node:child_process";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
@@ -150,6 +151,10 @@ const validate = (sb) => {
|
|
|
150
151
|
if (s.frame !== undefined && s.frame !== "close" && s.frame !== "wide") return `${at}: frame must be close or wide`;
|
|
151
152
|
}
|
|
152
153
|
if (sb.steps.length > 14) return "more than 14 steps; 6 to 10 is the law";
|
|
154
|
+
// THE CAMERA'S CLOCK (founder 2026-10-15): the take may not run past 45 s. The estimate is the
|
|
155
|
+
// camera's own (clock.mjs); the model gets the number and the cure.
|
|
156
|
+
const est = estimateTake(sb.steps);
|
|
157
|
+
if (est > 45) return `the camera would run this take about ${Math.round(est)} s; the law is 30 to 45. Cut words (6 to 11 a line) or steps (5 to 8 narrated); never pad, never add.`;
|
|
153
158
|
return null;
|
|
154
159
|
};
|
|
155
160
|
|
|
@@ -192,7 +197,11 @@ const firstMessage = () => {
|
|
|
192
197
|
`## The app's words on the starting screen\n${vocab}`,
|
|
193
198
|
`## Workspace rules (version ${rules.version || 0})\n${rules.lines?.length ? rules.lines.map((l, i) => `${i + 1}. ${l}`).join("\n") : "(none)"}`,
|
|
194
199
|
`## The project's files${codeOn ? " (this machine sits in the app's repository; read_file is on)" : " (no code here)"}\n${listFiles()}`,
|
|
195
|
-
|
|
200
|
+
// A RETAKE (founder 2026-10-15): the same demo again, because the app changed under it. The previous
|
|
201
|
+
// storyboard is the base; the note says what to keep an eye on; the probe finds what moved.
|
|
202
|
+
job.retake_of && job.storyboard
|
|
203
|
+
? `## This is a retake\nThe person wants the same demo filmed again, the same length and the same shots, because the app changed since. Their note: "${(job.retake_note || "no note").replace(/"/g, "'")}". ${job.storyboard.storyboard ? "Start from the previous storyboard below: keep its steps and its lines; probe it; fix only what no longer resolves or what the note names; then submit_storyboard. Never explore when the probe passes." : "Only the previous storyboard's lines survive (below): rebuild the same steps with the same lines, find the controls again, probe, then submit_storyboard."}\n\n## The previous storyboard\n${JSON.stringify(job.storyboard.storyboard ?? job.storyboard.steps ?? job.storyboard)}`
|
|
204
|
+
: codeOn ? `Write the storyboard, probe it, fix it, probe again, then submit_storyboard.` : `There is no code: explore first (explore from the starting address, looking_for = the brief's nouns), then write the storyboard, probe it, fix it, probe again, then submit_storyboard.`,
|
|
196
205
|
];
|
|
197
206
|
return parts.join("\n\n");
|
|
198
207
|
};
|
|
@@ -23,11 +23,13 @@ if (!mode || !file || !["print", "words", "await"].includes(mode)) {
|
|
|
23
23
|
}
|
|
24
24
|
const sb = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
25
25
|
const steps = Array.isArray(sb.steps) ? sb.steps : [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
26
|
+
// THE CAMERA'S CLOCK (founder 2026-10-15, bite 1228 said 0:36 and ran 0:51): a beat is held until its
|
|
27
|
+
// line would have finished (words / 2.6 s + 0.4 s, record.mjs estimateSpeech) plus half a second of
|
|
28
|
+
// breath; a click or hover adds about a second of cursor flight and dwell; the first narrated settle
|
|
29
|
+
// carries 2.5 s of startup slack; the take closes on a 2.2 s beat. Same numbers as record.mjs.
|
|
30
|
+
import { secondsOf as cameraSeconds, CLOSING_S } from "./clock.mjs";
|
|
31
|
+
let firstNarrated = true;
|
|
32
|
+
const secondsOf = (s) => { const r = cameraSeconds(s, firstNarrated); if (s.narration) firstNarrated = false; return r; };
|
|
31
33
|
const mmss = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
|
|
32
34
|
// A row per step that says or shows something; silent plumbing (a bare goto,
|
|
33
35
|
// an expect) rides inside the previous row's time.
|
|
@@ -39,7 +41,7 @@ for (const s of steps) {
|
|
|
39
41
|
if (text && !(s.action === "expect" && !s.narration)) rows.push({ at: clock, text: text.replace(/\s+/g, " "), pointed: s.pointed === true, step: s });
|
|
40
42
|
clock += dur;
|
|
41
43
|
}
|
|
42
|
-
const total = clock;
|
|
44
|
+
const total = clock + CLOSING_S;
|
|
43
45
|
const title = (sb.title ?? sb.app ?? "Demo").toString().trim();
|
|
44
46
|
|
|
45
47
|
if (mode === "words") {
|
|
@@ -61,7 +63,8 @@ if (mode === "await") {
|
|
|
61
63
|
rows.forEach((r, i) => boxOut.push(`│ ${`${String(i + 1).padStart(2)} ${mmss(r.at)} ${cap2(r.text + (r.pointed ? " — pointed at, not pressed" : ""), W2 - 14)}`.padEnd(W2 - 2)} │`));
|
|
62
64
|
boxOut.push(`│ ${"".padEnd(W2 - 2)} │`); boxOut.push(`└${"─".repeat(W2)}┘`);
|
|
63
65
|
if (!listening) { console.log(boxOut.join("\n")); console.log("film"); process.exit(0); }
|
|
64
|
-
|
|
66
|
+
// The whole storyboard rides along (1.17.0): a retake starts from it, selectors and all.
|
|
67
|
+
await postEvent("storyboard", { box: boxOut.join("\n"), total: mmss(total), steps: rows.map((r, i) => ({ n: i + 1, at: mmss(r.at), text: r.text, pointed: r.pointed })), storyboard: sb });
|
|
65
68
|
const started = Date.now();
|
|
66
69
|
while (Date.now() - started < 30 * 60 * 1000) {
|
|
67
70
|
const st = await poll();
|