demowright 2.3.0 → 2.5.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.
@@ -34,6 +34,23 @@ declare function clickEl(page: Page, selector: string): Promise<void>;
34
34
  * If omitted, uses `document.activeElement`.
35
35
  */
36
36
  declare function typeKeys(page: Page, text: string, delay?: number, inputSelector?: string): Promise<void>;
37
+ /**
38
+ * Pre-fetch TTS audio for all narration texts in parallel.
39
+ * Call this at the start of a test to eliminate per-annotate wait times.
40
+ *
41
+ * ```ts
42
+ * const narrations = [
43
+ * "Welcome to the demo",
44
+ * "Now clicking the button",
45
+ * "That wraps up the tour",
46
+ * ];
47
+ * await prefetchTts(page, narrations);
48
+ *
49
+ * // All subsequent annotate/narrate calls hit the cache instantly
50
+ * await annotate(page, narrations[0], async () => { ... });
51
+ * ```
52
+ */
53
+ declare function prefetchTts(page: Page, texts: string[]): Promise<void>;
37
54
  /**
38
55
  * Speak `text` via the configured TTS provider, or fall back to
39
56
  * the browser's speechSynthesis API.
@@ -92,4 +109,4 @@ declare function annotate(page: Page, text: string, callbackOrOptions?: (() => P
92
109
  voice?: string;
93
110
  }, callback?: () => Promise<void>): Promise<void>;
94
111
  //#endregion
95
- export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys };
112
+ export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys };
package/dist/helpers.mjs CHANGED
@@ -129,16 +129,48 @@ async function typeKeys(page, text, delay = 65, inputSelector) {
129
129
  await page.waitForTimeout(delay);
130
130
  }
131
131
  }
132
- /** Fetch audio from a TTS provider. Returns a WAV Buffer. */
132
+ const ttsCache = /* @__PURE__ */ new Map();
133
+ /** Fetch audio from a TTS provider. Returns a WAV Buffer (cached). */
133
134
  async function fetchTtsAudio(text, provider) {
135
+ const cached = ttsCache.get(text);
136
+ if (cached) return cached;
137
+ let buf;
134
138
  if (typeof provider === "string") {
135
139
  const url = provider.replace(/%s/g, encodeURIComponent(text));
136
140
  const res = await fetch(url);
137
141
  if (!res.ok) throw new Error(`TTS fetch ${res.status}`);
138
- return Buffer.from(await res.arrayBuffer());
142
+ buf = Buffer.from(await res.arrayBuffer());
143
+ } else {
144
+ const result = await provider(text);
145
+ buf = Buffer.isBuffer(result) ? result : Buffer.from(result);
139
146
  }
140
- const result = await provider(text);
141
- return Buffer.isBuffer(result) ? result : Buffer.from(result);
147
+ ttsCache.set(text, buf);
148
+ return buf;
149
+ }
150
+ /**
151
+ * Pre-fetch TTS audio for all narration texts in parallel.
152
+ * Call this at the start of a test to eliminate per-annotate wait times.
153
+ *
154
+ * ```ts
155
+ * const narrations = [
156
+ * "Welcome to the demo",
157
+ * "Now clicking the button",
158
+ * "That wraps up the tour",
159
+ * ];
160
+ * await prefetchTts(page, narrations);
161
+ *
162
+ * // All subsequent annotate/narrate calls hit the cache instantly
163
+ * await annotate(page, narrations[0], async () => { ... });
164
+ * ```
165
+ */
166
+ async function prefetchTts(page, texts) {
167
+ const provider = getTtsProvider(page);
168
+ if (!provider) return;
169
+ const uncached = texts.filter((t) => !ttsCache.has(t));
170
+ if (uncached.length === 0) return;
171
+ console.log(`[demowright] Prefetching ${uncached.length} TTS segments...`);
172
+ const failed = (await Promise.allSettled(uncached.map((text) => fetchTtsAudio(text, provider)))).filter((r) => r.status === "rejected").length;
173
+ if (failed > 0) console.warn(`[demowright] ${failed}/${uncached.length} TTS prefetch failed`);
142
174
  }
143
175
  /** Parse WAV and return { float32, sampleRate, channels, durationMs }. */
144
176
  function parseWav(wavBuf) {
@@ -318,4 +350,4 @@ async function annotate(page, text, callbackOrOptions, callback) {
318
350
  }, cb)]);
319
351
  }
320
352
  //#endregion
321
- export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys };
353
+ export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { installAutoAnnotate } from "./auto-annotate.mjs";
2
2
  import { a as AudioWriter, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-8X-3F3M7.mjs";
3
- import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys } from "./helpers.mjs";
3
+ import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys } from "./helpers.mjs";
4
4
  import { OutroOptions, PaceFn, RenderOptions, TimelineEntry, TitleOptions, VideoScriptResult, buildFfmpegCommand, createVideoScript } from "./video-script.mjs";
5
5
  import { Page as Page$1, expect } from "@playwright/test";
6
6
  import * as _$electron from "electron";
@@ -54041,4 +54041,4 @@ declare const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions & {
54041
54041
  */
54042
54042
  declare function getGlobalTtsProvider(): TtsProvider;
54043
54043
  //#endregion
54044
- export { AudioWriter, type OutroOptions, type PaceFn, type QaHudOptions, type RenderOptions, type TimelineEntry, type TitleOptions, type TtsProvider, type VideoScriptResult, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, subtitle, test, typeKeys };
54044
+ export { AudioWriter, type OutroOptions, type PaceFn, type QaHudOptions, type RenderOptions, type TimelineEntry, type TitleOptions, type TtsProvider, type VideoScriptResult, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, prefetchTts, subtitle, test, typeKeys };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { r as AudioWriter, t as applyHud } from "./setup-c_UpOQ7a.mjs";
2
2
  import { i as getGlobalTtsProvider, s as init_hud_registry } from "./hud-registry-Wfd4b4Nu.mjs";
3
3
  import { buildFfmpegCommand, createVideoScript, t as init_video_script } from "./video-script.mjs";
4
- import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys } from "./helpers.mjs";
4
+ import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys } from "./helpers.mjs";
5
5
  import { installAutoAnnotate } from "./auto-annotate.mjs";
6
6
  import { expect, test as test$1 } from "@playwright/test";
7
7
  //#region src/fixture.ts
@@ -26,4 +26,4 @@ const test = test$1.extend({
26
26
  init_video_script();
27
27
  init_hud_registry();
28
28
  //#endregion
29
- export { AudioWriter, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, subtitle, test, typeKeys };
29
+ export { AudioWriter, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, prefetchTts, subtitle, test, typeKeys };
@@ -20,6 +20,8 @@ interface SegmentStep {
20
20
  kind: "segment";
21
21
  text: string;
22
22
  action?: SegmentAction;
23
+ /** Runs BEFORE narration starts — use for navigation, page.goto(), scrolls, etc. */
24
+ setup?: () => Promise<void>;
23
25
  }
24
26
  interface TransitionStep {
25
27
  kind: "transition";
@@ -104,9 +106,18 @@ declare class VideoScriptImpl {
104
106
  title(text: string, opts?: TitleOptions): this;
105
107
  /**
106
108
  * Add a narrated segment — TTS audio drives timing, callback runs paced actions.
107
- * Same as NarrationPlan's `.annotate()` but named `.segment()` for clarity.
109
+ *
110
+ * Two signatures (backwards-compatible):
111
+ * .segment("narration", async (pace) => { ... })
112
+ * .segment("narration", { setup: async () => { ... }, action: async (pace) => { ... } })
113
+ *
114
+ * `setup` runs BEFORE narration starts — use for page.goto(), scrolling, etc.
115
+ * `action` runs DURING narration — use for visual actions like safeMove, hover.
108
116
  */
109
- segment(text: string, action?: SegmentAction): this;
117
+ segment(text: string, actionOrOpts?: SegmentAction | {
118
+ setup?: () => Promise<void>;
119
+ action?: SegmentAction;
120
+ }): this;
110
121
  /**
111
122
  * Add a transition between segments.
112
123
  * Applied as an ffmpeg filter during render.
@@ -282,13 +282,29 @@ var init_video_script = __esmMin((() => {
282
282
  }
283
283
  /**
284
284
  * Add a narrated segment — TTS audio drives timing, callback runs paced actions.
285
- * Same as NarrationPlan's `.annotate()` but named `.segment()` for clarity.
285
+ *
286
+ * Two signatures (backwards-compatible):
287
+ * .segment("narration", async (pace) => { ... })
288
+ * .segment("narration", { setup: async () => { ... }, action: async (pace) => { ... } })
289
+ *
290
+ * `setup` runs BEFORE narration starts — use for page.goto(), scrolling, etc.
291
+ * `action` runs DURING narration — use for visual actions like safeMove, hover.
286
292
  */
287
- segment(text, action) {
288
- this.steps.push({
293
+ segment(text, actionOrOpts) {
294
+ if (typeof actionOrOpts === "function") this.steps.push({
295
+ kind: "segment",
296
+ text,
297
+ action: actionOrOpts
298
+ });
299
+ else if (actionOrOpts && typeof actionOrOpts === "object") this.steps.push({
289
300
  kind: "segment",
290
301
  text,
291
- action
302
+ action: actionOrOpts.action,
303
+ setup: actionOrOpts.setup
304
+ });
305
+ else this.steps.push({
306
+ kind: "segment",
307
+ text
292
308
  });
293
309
  return this;
294
310
  }
@@ -459,7 +475,7 @@ var init_video_script = __esmMin((() => {
459
475
  let segmentIndex = 0;
460
476
  for (let i = 0; i < this.steps.length; i++) {
461
477
  const step = this.steps[i];
462
- const stepStartMs = Date.now();
478
+ let stepStartMs = Date.now();
463
479
  if (step.kind === "title" || step.kind === "outro") {
464
480
  const bg = step.background ?? DEFAULT_BG;
465
481
  const cardTts = step.narration ? this.prepared.get(`card-${i}`) : void 0;
@@ -499,6 +515,10 @@ var init_video_script = __esmMin((() => {
499
515
  overrunMs: 0
500
516
  });
501
517
  } else if (step.kind === "segment") {
518
+ if (step.setup) {
519
+ await step.setup();
520
+ stepStartMs = Date.now();
521
+ }
502
522
  const segId = `step-${segmentIndex}`;
503
523
  segmentIndex++;
504
524
  const segment = this.prepared.get(segId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demowright",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Playwright video production plugin — cursor overlay, keystroke badges, TTS narration, and narration-driven video scripts for test recordings",
5
5
  "license": "MIT",
6
6
  "repository": {