demobite 1.0.3 → 1.0.4

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.0.3",
3
+ "version": "1.0.4",
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"
@@ -87,6 +87,16 @@ const DEFAULT_CLICK_AFTER = 2000;
87
87
  const CLOSING_BEAT_MS = 2200;
88
88
  // LAW (hold your line, founder 2026-09-02): a beat is never shorter than its
89
89
  // own narration. Same estimate manifest.mjs uses (words / 2.6 s + 0.4 s).
90
+ // Storyboard durations (`dwell`, `after`, settle `ms`, scroll `ms`) are
91
+ // MILLISECONDS; an author who writes 3.4 means seconds. Anything under 60 is
92
+ // read as seconds (a 59 ms dwell is never intended).
93
+ // The video track starts up to ~2.5 s AFTER the wall clock (startup lead,
94
+ // measured per take by calibrate.mjs). trim.mjs cuts by record_from, so the
95
+ // opening beat loses that lead in the published file. The FIRST narrated beat
96
+ // after the stamp carries this slack so its line always has its full beat.
97
+ const STARTUP_LEAD_SLACK_S = 2.5;
98
+ let openingBeatPending = false;
99
+ const ms = (v, fallback) => (v == null ? fallback : v < 60 ? Math.round(v * 1000) : v);
90
100
  const estimateSpeech = (text) => String(text).trim().split(/\s+/).filter(Boolean).length / 2.6 + 0.4;
91
101
 
92
102
  const narrationOf = (s) => {
@@ -603,13 +613,14 @@ try {
603
613
  }
604
614
  await page.waitForTimeout(600);
605
615
  manifest.record_from = t();
616
+ openingBeatPending = true;
606
617
  }
607
618
  await page.evaluate(([a, b]) => window.__recSetCursor?.(a, b), [cx, cy]);
608
619
  } else if (step.action === "settle") {
609
620
  // A settle can still carry the camera: `focus` names what to look at.
610
- const ms = step.ms ?? DEFAULT_SETTLE_MS;
621
+ const settleMs = ms(step.ms, DEFAULT_SETTLE_MS);
611
622
  const shotStart = t();
612
- await page.waitForTimeout(ms);
623
+ await page.waitForTimeout(settleMs);
613
624
  if (step.focus) {
614
625
  const box = await visibleBox(step.focus, step.minY ?? 0, 4000);
615
626
  pushShot(box, shotStart, t(), step.label, { n: rec.n });
@@ -617,7 +628,7 @@ try {
617
628
  } else if (step.action === "scroll") {
618
629
  // scrollTo distances are ALSO zoomed-space under CSS zoom (measured:
619
630
  // scrollTo(0,600) moves 300 design px) — storyboards speak design px.
620
- await smoothScroll(step.dy * SUPERSAMPLE, step.ms ?? 1400, step.within ?? null);
631
+ await smoothScroll(step.dy * SUPERSAMPLE, ms(step.ms, 1400), step.within ?? null);
621
632
  } else if (step.action === "click" || step.action === "hover") {
622
633
  const { box, el } = await visibleTarget(step.selector, step.minY ?? 0);
623
634
  if (!box) throw new Error("no visible target for " + step.selector);
@@ -665,7 +676,7 @@ try {
665
676
  };
666
677
 
667
678
  if (step.action === "hover") {
668
- await page.waitForTimeout(step.dwell ?? DEFAULT_HOVER_DWELL);
679
+ await page.waitForTimeout(ms(step.dwell, DEFAULT_HOVER_DWELL));
669
680
  // LAW (camera choreography, measured off the reference 2026-08-09):
670
681
  // the camera must NOT travel in lockstep with the cursor. When it
671
682
  // does, the cursor sits pinned near frame center while the page
@@ -675,7 +686,7 @@ try {
675
686
  // while the cursor sweeps across it, then the camera reframes.
676
687
  pushShot(step.focus ? await visibleBox(step.focus, 0, 3000) : box, Math.max(shotStart, arrivalT - 0.3), t(), step.label, { n: rec.n, glide: { t_start: Math.round(shotStart * 100) / 100, t_end: Math.round(arrivalT * 100) / 100 } });
677
688
  } else {
678
- await page.waitForTimeout(step.dwell ?? DEFAULT_CLICK_DWELL);
689
+ await page.waitForTimeout(ms(step.dwell, DEFAULT_CLICK_DWELL));
679
690
  await page.evaluate(([a, b]) => window.__recPulse?.(a, b), [x, y]);
680
691
  await page.waitForTimeout(220);
681
692
  rec.click_at = t();
@@ -699,7 +710,7 @@ try {
699
710
  // pointer. The recorder does not guess: it ASKS the page whether the
700
711
  // element it just clicked is still connected, visible, and under the
701
712
  // point. The verdict rides the click event as press:'full'|'down'.
702
- const afterMs = step.after ?? DEFAULT_CLICK_AFTER;
713
+ const afterMs = ms(step.after, DEFAULT_CLICK_AFTER);
703
714
  const early = Math.min(450, afterMs);
704
715
  await page.waitForTimeout(Math.min(200, early));
705
716
  // TIMING LAW (parity forensics 2026-08-09): this check must not eat
@@ -796,7 +807,7 @@ try {
796
807
  w: Math.round(box.width / SUPERSAMPLE), h: Math.round(box.height / SUPERSAMPLE),
797
808
  },
798
809
  };
799
- await page.waitForTimeout(step.dwell ?? DEFAULT_CLICK_DWELL);
810
+ await page.waitForTimeout(ms(step.dwell, DEFAULT_CLICK_DWELL));
800
811
  await page.evaluate(([a, b]) => window.__recPulse?.(a, b), [x, y]);
801
812
  await page.waitForTimeout(220);
802
813
  rec.click_at = t();
@@ -823,7 +834,7 @@ try {
823
834
  pushShot(box, Math.max(shotStart, arrivalT - 0.3), t() + 0.3, step.label ?? "type", { n: rec.n, glide: { t_start: Math.round(shotStart * 100) / 100, t_end: Math.round(arrivalT * 100) / 100 } });
824
835
  currentCursor = await cursorUnderPoint();
825
836
  pushMouse("move", cx, cy);
826
- await page.waitForTimeout(step.after ?? DEFAULT_CLICK_AFTER);
837
+ await page.waitForTimeout(ms(step.after, DEFAULT_CLICK_AFTER));
827
838
  }
828
839
  if (rec.narration && step.action !== "goto" && step.action !== "expect") {
829
840
  // Hold the shot until the line would have finished, plus a breath — the
@@ -831,7 +842,9 @@ try {
831
842
  // beat (founder 2026-09-02: the last track ran past the end).
832
843
  // Measured from ARRIVAL (where manifest.mjs anchors the line), not from
833
844
  // the cursor's departure — the glide is not speaking time.
834
- const need = estimateSpeech(rec.narration) + 0.5;
845
+ const narrText = typeof rec.narration === "string" ? rec.narration : rec.narration?.text ?? "";
846
+ let need = estimateSpeech(narrText) + 0.5;
847
+ if (openingBeatPending) { need += STARTUP_LEAD_SLACK_S; openingBeatPending = false; }
835
848
  const elapsed = t() - (rec.arrival ?? rec.t_start);
836
849
  if (elapsed < need) await page.waitForTimeout(Math.round((need - elapsed) * 1000));
837
850
  }
package/skill/SKILL.md CHANGED
@@ -130,7 +130,7 @@ Storyboard schema:
130
130
  }
131
131
  ```
132
132
 
133
- Step fields: `action` is one of `goto | settle | scroll | click | hover`. `goto` needs `url`. `settle` takes `ms` and an optional `focus` selector. `scroll` needs `dy` and takes `ms`. `click`/`hover` need `selector` and take `minY` (minimum Y for the visible instance pick), `dwell`, `after`, `waitLoad`. Every step takes `label` and `narration`.
133
+ Step fields: `action` is one of `goto | settle | scroll | click | hover | type | expect`. **Durations (`dwell`, `after`, settle `ms`, scroll `ms`) are milliseconds; a value under 60 is read as seconds** (write `"dwell": 3400` or `"dwell": 3.4`, never `"dwell": 3` meaning 3 ms). `goto` needs `url`. `settle` takes `ms` and an optional `focus` selector. `scroll` needs `dy` and takes `ms`. `click`/`hover` need `selector` and take `minY` (minimum Y for the visible instance pick), `dwell`, `after`, `waitLoad`. Every step takes `label` and `narration`.
134
134
 
135
135
  Two fields carry the whole advantage of this lane, so fill them in:
136
136