demobite 1.15.0 → 1.16.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.15.0",
3
+ "version": "1.16.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
  "demobite": "launcher/index.mjs"
@@ -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,
@@ -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
 
@@ -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
- const FLOOR = { goto: 1.5, settle: 2, click: 2.5, hover: 2, type: 3, scroll: 2, expect: 1 };
27
- const secondsOf = (s) => {
28
- const words = String(s.narration ?? "").trim().split(/\s+/).filter(Boolean).length;
29
- return Math.max(typeof s.dwell === "number" ? s.dwell : 0, words / 2.5, FLOOR[s.action] ?? 2);
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") {